Skip to content

Commit 79cc21c

Browse files
araujoguiflakey5
andauthored
feat: create linter system (#191)
* feat: linter Signed-off-by: flakey5 <[email protected]> * feat: wip * fix: optional location * fix: create hasError getter * fix: optional location * test: update expected metadata * refactor: some changes * fix: missing reporter option * fix: reporter node position * refactor: some improvements * refactor: better docs * refactor: remove useless directive * refactor: remove semver regex * refactor: add yaml_position description * test: create rules tests * test: create reporters tests * fix: missing change version issue location * feat: add disable rule cli option * test: create engine test * feat: create cli option * test: oops * chore: add FORCE_COLOR env --------- Signed-off-by: flakey5 <[email protected]> Co-authored-by: flakey5 <[email protected]>
1 parent c428037 commit 79cc21c

27 files changed

+766
-7
lines changed

.github/workflows/pr.yml

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ on:
99
permissions:
1010
contents: read
1111

12+
env:
13+
FORCE_COLOR: 1
14+
1215
jobs:
1316
build:
1417
runs-on: ubuntu-latest

bin/cli.mjs

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22

33
import { resolve } from 'node:path';
4-
import { argv } from 'node:process';
4+
import { argv, exit } from 'node:process';
55

66
import { Command, Option } from 'commander';
77

@@ -12,6 +12,9 @@ import generators from '../src/generators/index.mjs';
1212
import createMarkdownLoader from '../src/loaders/markdown.mjs';
1313
import createMarkdownParser from '../src/parsers/markdown.mjs';
1414
import createNodeReleases from '../src/releases.mjs';
15+
import createLinter from '../src/linter/index.mjs';
16+
import reporters from '../src/linter/reporters/index.mjs';
17+
import rules from '../src/linter/rules/index.mjs';
1518

1619
const availableGenerators = Object.keys(generators);
1720

@@ -50,6 +53,19 @@ program
5053
'Set the processing target modes'
5154
).choices(availableGenerators)
5255
)
56+
.addOption(
57+
new Option('--disable-rule [rule...]', 'Disable a specific linter rule')
58+
.choices(Object.keys(rules))
59+
.default([])
60+
)
61+
.addOption(
62+
new Option('--lint-dry-run', 'Run linter in dry-run mode').default(false)
63+
)
64+
.addOption(
65+
new Option('-r, --reporter [reporter]', 'Specify the linter reporter')
66+
.choices(Object.keys(reporters))
67+
.default('console')
68+
)
5369
.parse(argv);
5470

5571
/**
@@ -60,13 +76,27 @@ program
6076
* @property {string} output Specifies the directory where output files will be saved.
6177
* @property {Target[]} target Specifies the generator target mode.
6278
* @property {string} version Specifies the target Node.js version.
63-
* @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file
79+
* @property {string} changelog Specifies the path to the Node.js CHANGELOG.md file.
80+
* @property {string[]} disableRule Specifies the linter rules to disable.
81+
* @property {boolean} lintDryRun Specifies whether the linter should run in dry-run mode.
82+
* @property {keyof reporters} reporter Specifies the linter reporter.
6483
*
6584
* @name ProgramOptions
6685
* @type {Options}
6786
* @description The return type for values sent to the program from the CLI.
6887
*/
69-
const { input, output, target = [], version, changelog } = program.opts();
88+
const {
89+
input,
90+
output,
91+
target = [],
92+
version,
93+
changelog,
94+
disableRule,
95+
lintDryRun,
96+
reporter,
97+
} = program.opts();
98+
99+
const linter = createLinter(lintDryRun, disableRule);
70100

71101
const { loadFiles } = createMarkdownLoader();
72102
const { parseApiDocs } = createMarkdownParser();
@@ -80,6 +110,8 @@ const { runGenerators } = createGenerator(parsedApiDocs);
80110
// Retrieves Node.js release metadata from a given Node.js version and CHANGELOG.md file
81111
const { getAllMajors } = createNodeReleases(changelog);
82112

113+
linter.lintAll(parsedApiDocs);
114+
83115
await runGenerators({
84116
// A list of target modes for the API docs parser
85117
generators: target,
@@ -92,3 +124,7 @@ await runGenerators({
92124
// A list of all Node.js major versions with LTS status
93125
releases: await getAllMajors(),
94126
});
127+
128+
linter.report(reporter);
129+
130+
exit(Number(linter.hasError()));

package-lock.json

+66
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"dependencies": {
3232
"acorn": "^8.14.0",
33+
"@actions/core": "^1.11.1",
3334
"commander": "^13.1.0",
3435
"estree-util-visit": "^2.0.0",
3536
"dedent": "^1.5.3",

src/constants.mjs

+6
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,9 @@ export const DOC_TYPES_MAPPING_OTHER = {
420420
Response: `${DOC_MDN_BASE_URL}/API/Response`,
421421
Request: `${DOC_MDN_BASE_URL}/API/Request`,
422422
};
423+
424+
export const LINT_MESSAGES = {
425+
missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry",
426+
missingChangeVersion: 'Missing version field in the API doc entry',
427+
invalidChangeVersion: 'Invalid version number: {{version}}',
428+
};

src/linter/engine.mjs

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
/**
4+
* Creates a linter engine instance to validate ApiDocMetadataEntry entries
5+
*
6+
* @param {import('./types').LintRule} rules Lint rules to validate the entries against
7+
*/
8+
const createLinterEngine = rules => {
9+
/**
10+
* Validates a ApiDocMetadataEntry entry against all defined rules
11+
*
12+
* @param {ApiDocMetadataEntry} entry
13+
* @returns {import('./types').LintIssue[]}
14+
*/
15+
const lint = entry => {
16+
const issues = [];
17+
18+
for (const rule of rules) {
19+
const ruleIssues = rule(entry);
20+
21+
if (ruleIssues.length > 0) {
22+
issues.push(...ruleIssues);
23+
}
24+
}
25+
26+
return issues;
27+
};
28+
29+
/**
30+
* Validates an array of ApiDocMetadataEntry entries against all defined rules
31+
*
32+
* @param {ApiDocMetadataEntry[]} entries
33+
* @returns {import('./types').LintIssue[]}
34+
*/
35+
const lintAll = entries => {
36+
const issues = [];
37+
38+
for (const entry of entries) {
39+
issues.push(...lint(entry));
40+
}
41+
42+
return issues;
43+
};
44+
45+
return {
46+
lint,
47+
lintAll,
48+
};
49+
};
50+
51+
export default createLinterEngine;

src/linter/index.mjs

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
import createLinterEngine from './engine.mjs';
4+
import reporters from './reporters/index.mjs';
5+
import rules from './rules/index.mjs';
6+
7+
/**
8+
* Creates a linter instance to validate ApiDocMetadataEntry entries
9+
*
10+
* @param {boolean} dryRun Whether to run the engine in dry-run mode
11+
* @param {string[]} disabledRules List of disabled rules names
12+
*/
13+
const createLinter = (dryRun, disabledRules) => {
14+
const engine = createLinterEngine(getEnabledRules(disabledRules));
15+
16+
/**
17+
* Lint issues found during validations
18+
*
19+
* @type {Array<import('./types').LintIssue>}
20+
*/
21+
const issues = [];
22+
23+
/**
24+
* Lints all entries using the linter engine
25+
*
26+
* @param entries
27+
*/
28+
const lintAll = entries => {
29+
issues.push(...engine.lintAll(entries));
30+
};
31+
32+
/**
33+
* Reports found issues using the specified reporter
34+
*
35+
* @param {keyof typeof reporters} reporterName Reporter name
36+
* @returns {void}
37+
*/
38+
const report = reporterName => {
39+
if (dryRun) {
40+
return;
41+
}
42+
43+
const reporter = reporters[reporterName];
44+
45+
for (const issue of issues) {
46+
reporter(issue);
47+
}
48+
};
49+
50+
/**
51+
* Checks if any error-level issues were found during linting
52+
*
53+
* @returns {boolean}
54+
*/
55+
const hasError = () => {
56+
return issues.some(issue => issue.level === 'error');
57+
};
58+
59+
/**
60+
* Retrieves all enabled rules
61+
*
62+
* @returns {import('./types').LintRule[]}
63+
*/
64+
const getEnabledRules = () => {
65+
return Object.entries(rules)
66+
.filter(([ruleName]) => !disabledRules.includes(ruleName))
67+
.map(([, rule]) => rule);
68+
};
69+
70+
return {
71+
lintAll,
72+
report,
73+
hasError,
74+
};
75+
};
76+
77+
export default createLinter;

src/linter/reporters/console.mjs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
import { styleText } from 'node:util';
4+
5+
/**
6+
* @type {Record<import('../types.d.ts').IssueLevel, string>}
7+
*/
8+
const levelToColorMap = {
9+
info: 'gray',
10+
warn: 'yellow',
11+
error: 'red',
12+
};
13+
14+
/**
15+
* Console reporter
16+
*
17+
* @type {import('../types.d.ts').Reporter}
18+
*/
19+
export default issue => {
20+
const position = issue.location.position
21+
? ` (${issue.location.position.start.line}:${issue.location.position.end.line})`
22+
: '';
23+
24+
process.stdout.write(
25+
styleText(
26+
levelToColorMap[issue.level],
27+
`${issue.message} at ${issue.location.path}${position}`
28+
) + '\n'
29+
);
30+
};

0 commit comments

Comments
 (0)