Skip to content

Commit 0e27580

Browse files
authored
feat: introduce raw mdast linter (#275)
1 parent 32834c4 commit 0e27580

34 files changed

+958
-520
lines changed

bin/commands/generate.mjs

Lines changed: 9 additions & 4 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,9 +123,14 @@ export default {
123123
* @returns {Promise<void>}
124124
*/
125125
async action(opts) {
126-
const docs = await loadAndParse(opts.input, opts.ignore);
126+
const rules = getEnabledRules(opts.disableRule);
127+
const linter = opts.skipLint ? undefined : createLinter(rules);
127128

128-
if (!opts.skipLint && !runLint(docs)) {
129+
const docs = await loadAndParse(opts.input, opts.ignore, linter);
130+
131+
linter?.report();
132+
133+
if (linter?.hasError()) {
129134
console.error('Lint failed; aborting generation.');
130135
process.exit(1);
131136
}

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();
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
@@ -59,7 +59,9 @@
5959
"shiki": "^3.4.2",
6060
"unified": "^11.0.5",
6161
"unist-builder": "^4.0.0",
62+
"unist-util-find": "^3.0.0",
6263
"unist-util-find-after": "^5.0.0",
64+
"unist-util-find-before": "^4.0.1",
6365
"unist-util-position": "^5.0.0",
6466
"unist-util-remove": "^4.0.0",
6567
"unist-util-select": "^5.1.0",

src/generators/legacy-json/utils/buildSection.mjs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@ import { getRemarkRehype } from '../../../utils/remark.mjs';
33
import { transformNodesToString } from '../../../utils/unist.mjs';
44
import { parseList } from './parseList.mjs';
55
import { SECTION_TYPE_PLURALS, UNPROMOTED_KEYS } from '../constants.mjs';
6-
7-
/**
8-
* Converts a value to an array.
9-
* @template T
10-
* @param {T | T[]} val - The value to convert.
11-
* @returns {T[]} The value as an array.
12-
*/
13-
const enforceArray = val => (Array.isArray(val) ? val : [val]);
6+
import { enforceArray } from '../../../utils/array.mjs';
147

158
/**
169
*

src/linter/constants.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict';
22

3+
export const INTRDOCUED_IN_REGEX = /<!--\s?introduced_in=.*-->/;
4+
5+
export const LLM_DESCRIPTION_REGEX = /<!--\s?llm_description=.*-->/;
6+
37
export const LINT_MESSAGES = {
48
missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry",
59
missingChangeVersion: 'Missing version field in the API doc entry',

src/linter/context.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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').IssueDescriptor} descriptor
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+
position,
31+
};
32+
33+
issues.push({
34+
level,
35+
message,
36+
location,
37+
});
38+
};
39+
40+
/**
41+
* Gets all reported issues.
42+
*
43+
* @returns {import('./types').LintIssue[]}
44+
*/
45+
const getIssues = () => {
46+
return issues;
47+
};
48+
49+
return {
50+
tree,
51+
report,
52+
getIssues,
53+
};
54+
};
55+
56+
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
};

0 commit comments

Comments
 (0)