Skip to content

feat(linter): introduce raw ast rules #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const {
const linter = createLinter(lintDryRun, disableRule);

const { loadFiles } = createMarkdownLoader();
const { parseApiDocs } = createMarkdownParser();
const { parseApiDocs } = createMarkdownParser(linter);

const apiDocFiles = await loadFiles(input, ignore);

Expand All @@ -124,9 +124,6 @@ const { runGenerators } = createGenerator(parsedApiDocs);
// Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file
const { getAllMajors } = createNodeReleases(changelog);

// Runs the Linter on the parsed API docs
linter.lintAll(parsedApiDocs);

if (target) {
await runGenerators({
// A list of target modes for the API docs parser
Expand Down
12 changes: 6 additions & 6 deletions src/linter/engine.mjs
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
'use strict';

/**
* Creates a linter engine instance to validate ApiDocMetadataEntry entries
* Creates a linter engine instance to validate mdast trees.
*
* @param {import('./types').LintRule[]} rules Lint rules to validate the entries against
*/
const createLinterEngine = rules => {
/**
* Validates an array of ApiDocMetadataEntry entries against all defined rules
* Validates an array of mdast trees against all defined rules
*
* @param {ApiDocMetadataEntry[]} entries
* @param {import('mdast').Root[]} ast
* @returns {import('./types').LintIssue[]}
*/
const lintAll = entries => {
const lint = ast => {
const issues = [];

for (const rule of rules) {
issues.push(...rule(entries));
issues.push(...rule(ast));
}

return issues;
};

return {
lintAll,
lint,
};
};

Expand Down
12 changes: 7 additions & 5 deletions src/linter/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import reporters from './reporters/index.mjs';
import rules from './rules/index.mjs';

/**
* Creates a linter instance to validate ApiDocMetadataEntry entries
* Creates a linter instance to validate mdast trees
*
* @param {boolean} dryRun Whether to run the engine in dry-run mode
* @param {string[]} disabledRules List of disabled rules names
* @returns {import('./types').Linter}
*/
const createLinter = (dryRun, disabledRules) => {
/**
Expand All @@ -34,10 +35,11 @@ const createLinter = (dryRun, disabledRules) => {
/**
* Lints all entries using the linter engine
*
* @param entries
* @param {import('mdast').Root} ast
* @returns {void}
*/
const lintAll = entries => {
issues.push(...engine.lintAll(entries));
const lint = ast => {
issues.push(...engine.lint(ast));
};

/**
Expand Down Expand Up @@ -68,7 +70,7 @@ const createLinter = (dryRun, disabledRules) => {
};

return {
lintAll,
lint,
report,
hasError,
};
Expand Down
12 changes: 6 additions & 6 deletions src/linter/rules/index.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
'use strict';

import { duplicateStabilityNodes } from './duplicate-stability-nodes.mjs';
import { invalidChangeVersion } from './invalid-change-version.mjs';
import { missingChangeVersion } from './missing-change-version.mjs';
// import { duplicateStabilityNodes } from './duplicate-stability-nodes.mjs';
// import { invalidChangeVersion } from './invalid-change-version.mjs';
// import { missingChangeVersion } from './missing-change-version.mjs';
import { missingIntroducedIn } from './missing-introduced-in.mjs';

/**
* @type {Record<string, import('../types').LintRule>}
*/
export default {
'duplicate-stability-nodes': duplicateStabilityNodes,
'invalid-change-version': invalidChangeVersion,
'missing-change-version': missingChangeVersion,
// 'duplicate-stability-nodes': duplicateStabilityNodes,
// 'invalid-change-version': invalidChangeVersion,
// 'missing-change-version': missingChangeVersion,
'missing-introduced-in': missingIntroducedIn,
};
35 changes: 21 additions & 14 deletions src/linter/rules/missing-introduced-in.mjs
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import { LINT_MESSAGES } from '../constants.mjs';

/**
* Checks if `introduced_in` field is missing
* Checks if `introduced_in` node is missing
*
* @param {ApiDocMetadataEntry[]} entries
* @param {import('mdast').Root} tree
* @returns {Array<import('../types.d.ts').LintIssue>}
*/
export const missingIntroducedIn = entries => {
const issues = [];
export const missingIntroducedIn = tree => {
const regex = /<!--introduced_in=.*-->/;

for (const entry of entries) {
// Early continue if not a top-level heading or if introduced_in exists
if (entry.heading.depth !== 1 || entry.introduced_in) continue;
const introduced_in = tree.children.find(
node => node.type === 'html' && regex.test(node.value)
);

issues.push({
level: 'info',
message: LINT_MESSAGES.missingIntroducedIn,
location: {
path: entry.api_doc_source,
if (!introduced_in) {
return [
{
level: 'info',
message: LINT_MESSAGES.missingIntroducedIn,
location: {
path: '?',
position: {
start: { line: 1, column: 1 },
end: { line: 1, column: 1 },
},
},
},
});
];
}

return issues;
return [];
};
9 changes: 8 additions & 1 deletion src/linter/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Root } from 'mdast';
import { Position } from 'unist';

export interface Linter {
lint: (ast: Root) => void;
report: (reporterName: keyof typeof reporters) => void;
hasError: () => boolean;
}

export type IssueLevel = 'info' | 'warn' | 'error';

export interface LintIssueLocation {
Expand All @@ -13,6 +20,6 @@ export interface LintIssue {
location: LintIssueLocation;
}

type LintRule = (input: ApiDocMetadataEntry[]) => LintIssue[];
type LintRule = (input: Root[]) => LintIssue[];

export type Reporter = (msg: LintIssue) => void;
6 changes: 4 additions & 2 deletions src/parsers/markdown.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { createNodeSlugger } from '../utils/slugger/index.mjs';
/**
* Creates an API doc parser for a given Markdown API doc file
*
* @param {import('./linter/index.mjs').Linter | undefined} linter
* @param {import('../linter/types').Linter} linter
*/
const createParser = () => {
const createParser = linter => {
// Creates an instance of the Remark processor with GFM support
// which is used for stringifying the AST tree back to Markdown
const remarkProcessor = getRemark();
Expand Down Expand Up @@ -63,6 +63,8 @@ const createParser = () => {
// Parses the API doc into an AST tree using `unified` and `remark`
const apiDocTree = remarkProcessor.parse(resolvedApiDoc);

linter.lint(apiDocTree);

// Get all Markdown Footnote definitions from the tree
const markdownDefinitions = selectAll('definition', apiDocTree);

Expand Down
Loading