Skip to content

Commit 2e0f167

Browse files
committed
feat: introduce raw ast linter
1 parent 6f70de4 commit 2e0f167

27 files changed

+685
-386
lines changed

bin/commands/generate.mjs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { cpus } from 'node:os';
22
import { resolve } from 'node:path';
3-
import process from 'node:process';
43

54
import { coerce } from 'semver';
65

@@ -12,7 +11,8 @@ import createGenerator from '../../src/generators.mjs';
1211
import { publicGenerators } from '../../src/generators/index.mjs';
1312
import createNodeReleases from '../../src/releases.mjs';
1413
import { loadAndParse } from '../utils.mjs';
15-
import { runLint } from './lint.mjs';
14+
import createLinter from '../../src/linter/index.mjs';
15+
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';
1616

1717
const availableGenerators = Object.keys(publicGenerators);
1818

@@ -123,12 +123,12 @@ export default {
123123
* @returns {Promise<void>}
124124
*/
125125
async action(opts) {
126-
const docs = await loadAndParse(opts.input, opts.ignore);
126+
const linter = opts.skipLint
127+
? undefined
128+
: createLinter(getEnabledRules(opts.disableRule));
129+
const docs = await loadAndParse(opts.input, opts.ignore, linter);
127130

128-
if (!opts.skipLint && !runLint(docs)) {
129-
console.error('Lint failed; aborting generation.');
130-
process.exit(1);
131-
}
131+
linter.report();
132132

133133
const { runGenerators } = createGenerator(docs);
134134
const { getAllMajors } = createNodeReleases(opts.changelog);

bin/commands/lint.mjs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import createLinter from '../../src/linter/index.mjs';
44
import reporters from '../../src/linter/reporters/index.mjs';
55
import rules from '../../src/linter/rules/index.mjs';
66
import { loadAndParse } from '../utils.mjs';
7+
import { getEnabledRules } from '../../src/linter/utils/rules.mjs';
78

89
const availableRules = Object.keys(rules);
910
const availableReporters = Object.keys(reporters);
@@ -17,22 +18,6 @@ const availableReporters = Object.keys(reporters);
1718
* @property {keyof reporters} reporter - Reporter for linter output.
1819
*/
1920

20-
/**
21-
* Run the linter on parsed documentation.
22-
* @param {ApiDocMetadataEntry[]} docs - Parsed documentation objects.
23-
* @param {LinterOptions} options - Linter configuration options.
24-
* @returns {boolean} - True if no errors, false otherwise.
25-
*/
26-
export function runLint(
27-
docs,
28-
{ disableRule = [], dryRun = false, reporter = 'console' } = {}
29-
) {
30-
const linter = createLinter(dryRun, disableRule);
31-
linter.lintAll(docs);
32-
linter.report(reporter);
33-
return !linter.hasError();
34-
}
35-
3621
/**
3722
* @type {import('../utils.mjs').Command}
3823
*/
@@ -95,9 +80,14 @@ export default {
9580
*/
9681
async action(opts) {
9782
try {
98-
const docs = await loadAndParse(opts.input, opts.ignore);
99-
const success = runLint(docs, opts);
100-
process.exitCode = success ? 0 : 1;
83+
const rules = getEnabledRules(opts.disableRule);
84+
const linter = createLinter(rules, opts.dryRun);
85+
86+
await loadAndParse(opts.input, opts.ignore, linter);
87+
88+
linter.report();
89+
90+
process.exitCode = linter.hasError() ? 1 : 0;
10191
} catch (error) {
10292
console.error('Error running the linter:', error);
10393
process.exitCode = 1;

bin/utils.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import createMarkdownParser from '../src/parsers/markdown.mjs';
99
*/
1010
export const lazy = factory => {
1111
let instance;
12-
return () => (instance ??= factory());
12+
return args => (instance ??= factory(args));
1313
};
1414

1515
// Instantiate loader and parser once to reuse,
@@ -23,11 +23,12 @@ const parser = lazy(createMarkdownParser);
2323
* Load and parse markdown API docs.
2424
* @param {string[]} input - Glob patterns for input files.
2525
* @param {string[]} [ignore] - Glob patterns to ignore.
26+
* @param {import('../src/linter/types').Linter} [linter] - Linter instance
2627
* @returns {Promise<ApiDocMetadataEntry[]>} - Parsed documentation objects.
2728
*/
28-
export async function loadAndParse(input, ignore) {
29+
export async function loadAndParse(input, ignore, linter) {
2930
const files = await loader().loadFiles(input, ignore);
30-
return parser().parseApiDocs(files);
31+
return parser(linter).parseApiDocs(files);
3132
}
3233

3334
/**

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
"shiki": "^3.2.1",
5757
"unified": "^11.0.5",
5858
"unist-builder": "^4.0.0",
59+
"unist-util-find": "^3.0.0",
5960
"unist-util-find-after": "^5.0.0",
61+
"unist-util-find-before": "^4.0.1",
6062
"unist-util-position": "^5.0.0",
6163
"unist-util-remove": "^4.0.0",
6264
"unist-util-select": "^5.1.0",

src/linter/context.mjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
/**
4+
* Creates a linting context for a given file and AST tree.
5+
*
6+
* @param {import('vfile').VFile} file
7+
* @param {import('mdast').Root} tree
8+
* @returns {import('./types').LintContext}
9+
*/
10+
const createContext = (file, tree) => {
11+
/**
12+
* Lint issues reported during validation.
13+
*
14+
* @type {import('./types').LintIssue[]}
15+
*/
16+
const issues = [];
17+
18+
/**
19+
* Reports a lint issue.
20+
*
21+
* @param {import('./types').ReportIssueProps} issue
22+
* @returns {void}
23+
*/
24+
const report = ({ level, message, position }) => {
25+
/**
26+
* @type {import('./types').LintIssueLocation}
27+
*/
28+
const location = {
29+
path: file.path,
30+
};
31+
32+
if (position) {
33+
location.position = position;
34+
}
35+
36+
issues.push({
37+
level,
38+
message,
39+
location,
40+
});
41+
};
42+
43+
/**
44+
* Gets all reported issues.
45+
*
46+
* @returns {import('./types').LintIssue[]}
47+
*/
48+
const getIssues = () => {
49+
return issues;
50+
};
51+
52+
return {
53+
tree,
54+
report,
55+
getIssues,
56+
};
57+
};
58+
59+
export default createContext;

src/linter/engine.mjs

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/linter/index.mjs

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,48 @@
11
'use strict';
22

3-
import createLinterEngine from './engine.mjs';
3+
import createContext from './context.mjs';
44
import reporters from './reporters/index.mjs';
5-
import rules from './rules/index.mjs';
65

76
/**
8-
* Creates a linter instance to validate ApiDocMetadataEntry entries
7+
* Creates a linter instance to validate API documentation ASTs against a
8+
* defined set of rules.
99
*
10-
* @param {boolean} dryRun Whether to run the engine in dry-run mode
11-
* @param {string[]} disabledRules List of disabled rules names
10+
* @param {import('./types').LintRule[]} rules - Lint rules to apply
11+
* @param {boolean} [dryRun] - If true, the linter runs without reporting
12+
* @returns {import('./types').Linter}
1213
*/
13-
const createLinter = (dryRun, disabledRules) => {
14+
const createLinter = (rules, dryRun = false) => {
1415
/**
15-
* Retrieves all enabled rules
16-
*
17-
* @returns {import('./types').LintRule[]}
18-
*/
19-
const getEnabledRules = () => {
20-
return Object.entries(rules)
21-
.filter(([ruleName]) => !disabledRules.includes(ruleName))
22-
.map(([, rule]) => rule);
23-
};
24-
25-
const engine = createLinterEngine(getEnabledRules(disabledRules));
26-
27-
/**
28-
* Lint issues found during validations
16+
* Lint issues collected during validations.
2917
*
3018
* @type {Array<import('./types').LintIssue>}
3119
*/
3220
const issues = [];
3321

3422
/**
35-
* Lints all entries using the linter engine
23+
* Lints a API doc and collects issues.
3624
*
37-
* @param entries
25+
* @param {import('vfile').VFile} file
26+
* @param {import('mdast').Root} tree
27+
* @returns {void}
3828
*/
39-
const lintAll = entries => {
40-
issues.push(...engine.lintAll(entries));
29+
const lint = (file, tree) => {
30+
const context = createContext(file, tree);
31+
32+
for (const rule of rules) {
33+
rule(context);
34+
}
35+
36+
issues.push(...context.getIssues());
4137
};
4238

4339
/**
44-
* Reports found issues using the specified reporter
40+
* Reports collected issues using the specified reporter.
4541
*
46-
* @param {keyof typeof reporters} reporterName Reporter name
42+
* @param {keyof typeof reporters} [reporterName] Reporter name
4743
* @returns {void}
4844
*/
49-
const report = reporterName => {
45+
const report = (reporterName = 'console') => {
5046
if (dryRun) {
5147
return;
5248
}
@@ -59,7 +55,7 @@ const createLinter = (dryRun, disabledRules) => {
5955
};
6056

6157
/**
62-
* Checks if any error-level issues were found during linting
58+
* Checks if any error-level issues were collected.
6359
*
6460
* @returns {boolean}
6561
*/
@@ -68,7 +64,8 @@ const createLinter = (dryRun, disabledRules) => {
6864
};
6965

7066
return {
71-
lintAll,
67+
issues,
68+
lint,
7269
report,
7370
hasError,
7471
};

src/linter/rules/index.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
import { duplicateStabilityNodes } from './duplicate-stability-nodes.mjs';
3+
// import { duplicateStabilityNodes } from './duplicate-stability-nodes.mjs';
44
import { invalidChangeVersion } from './invalid-change-version.mjs';
55
import { missingIntroducedIn } from './missing-introduced-in.mjs';
66
import { missingLlmDescription } from './missing-llm-description.mjs';
@@ -9,7 +9,7 @@ import { missingLlmDescription } from './missing-llm-description.mjs';
99
* @type {Record<string, import('../types').LintRule>}
1010
*/
1111
export default {
12-
'duplicate-stability-nodes': duplicateStabilityNodes,
12+
// 'duplicate-stability-nodes': duplicateStabilityNodes,
1313
'invalid-change-version': invalidChangeVersion,
1414
'missing-introduced-in': missingIntroducedIn,
1515
'missing-llm-description': missingLlmDescription,

0 commit comments

Comments
 (0)