Skip to content

Commit 222d8b2

Browse files
committed
linter: add deprecation code check, linter declarations
Signed-off-by: flakey5 <[email protected]>
1 parent f51c583 commit 222d8b2

17 files changed

+256
-14
lines changed

bin/cli.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const {
106106
const linter = createLinter(lintDryRun, disableRule);
107107

108108
const { loadFiles } = createMarkdownLoader();
109-
const { parseApiDocs } = createMarkdownParser();
109+
const { parseApiDocs } = createMarkdownParser(linter);
110110

111111
const apiDocFiles = await loadFiles(input, ignore);
112112

src/constants.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,9 @@ export const LINT_MESSAGES = {
425425
missingIntroducedIn: "Missing 'introduced_in' field in the API doc entry",
426426
missingChangeVersion: 'Missing version field in the API doc entry',
427427
invalidChangeVersion: 'Invalid version number: {{version}}',
428+
malformedDeprecationHeader: 'Malformed deprecation header',
429+
outOfOrderDeprecationCode:
430+
"Deprecation code '{{code}}' out of order (expected {{expectedCode}})",
431+
invalidLinterDeclaration: "Invalid linter declaration '{{declaration}}'",
432+
malformedLinterDeclaration: 'Malformed linter declaration: {{message}}',
428433
};

src/generators/legacy-json/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface HierarchizedEntry extends ApiDocMetadataEntry {
88
/**
99
* List of child entries that are part of this entry's hierarchy.
1010
*/
11-
hierarchyChildren: ApiDocMetadataEntry[];
11+
hierarchyChildren: HierarchizedEntry[];
1212
}
1313

1414
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildHierarchy } from './buildHierarchy.mjs';
1+
import { buildHierarchy } from '../../../utils/buildHierarchy.mjs';
22
import { getRemarkRehype } from '../../../utils/remark.mjs';
33
import { transformNodesToString } from '../../../utils/unist.mjs';
44
import { parseList } from './parseList.mjs';

src/linter/constants.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
3+
// Validates a deprecation header from doc/api/deprecation.md and captures the
4+
// code
5+
// For example, `DEP0001: `http.OutgoingMessage.prototype.flush` captures `0001`
6+
export const DEPRECATION_HEADER_REGEX = /DEP(\d{4}): .+?/;

src/linter/engine.mjs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ const createLinterEngine = rules => {
1010
* Validates a ApiDocMetadataEntry entry against all defined rules
1111
*
1212
* @param {ApiDocMetadataEntry} entry
13+
* @param {import('./types').LintDeclarations}
14+
* @param declarations
1315
* @returns {import('./types').LintIssue[]}
1416
*/
15-
const lint = entry => {
17+
const lint = (entry, declarations) => {
1618
const issues = [];
1719

1820
for (const rule of rules) {
19-
const ruleIssues = rule(entry);
21+
const ruleIssues = rule([entry], declarations);
2022

2123
if (ruleIssues.length > 0) {
2224
issues.push(...ruleIssues);
@@ -30,13 +32,19 @@ const createLinterEngine = rules => {
3032
* Validates an array of ApiDocMetadataEntry entries against all defined rules
3133
*
3234
* @param {ApiDocMetadataEntry[]} entries
35+
* @param {import('./types').LintDeclarations}
36+
* @param declarations
3337
* @returns {import('./types').LintIssue[]}
3438
*/
35-
const lintAll = entries => {
39+
const lintAll = (entries, declarations) => {
3640
const issues = [];
3741

38-
for (const entry of entries) {
39-
issues.push(...lint(entry));
42+
for (const rule of rules) {
43+
const ruleIssues = rule(entries, declarations);
44+
45+
if (ruleIssues.length > 0) {
46+
issues.push(...ruleIssues);
47+
}
4048
}
4149

4250
return issues;

src/linter/index.mjs

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

3+
import { LINT_MESSAGES } from '../constants.mjs';
34
import createLinterEngine from './engine.mjs';
45
import reporters from './reporters/index.mjs';
56
import rules from './rules/index.mjs';
@@ -22,6 +23,13 @@ const createLinter = (dryRun, disabledRules) => {
2223
.map(([, rule]) => rule);
2324
};
2425

26+
/**
27+
* @type {import('./types').LintDeclarations}
28+
*/
29+
const declarations = {
30+
skipDeprecation: [],
31+
};
32+
2533
const engine = createLinterEngine(getEnabledRules(disabledRules));
2634

2735
/**
@@ -37,7 +45,7 @@ const createLinter = (dryRun, disabledRules) => {
3745
* @param entries
3846
*/
3947
const lintAll = entries => {
40-
issues.push(...engine.lintAll(entries));
48+
issues.push(...engine.lintAll(entries, declarations));
4149
};
4250

4351
/**
@@ -58,6 +66,87 @@ const createLinter = (dryRun, disabledRules) => {
5866
}
5967
};
6068

69+
/**
70+
* Parse an inline-declaration found in the markdown input
71+
*
72+
* @param {string} declaration
73+
*/
74+
const parseLinterDeclaration = declaration => {
75+
// Trim off any excess spaces from the beginning & end
76+
declaration = declaration.trim();
77+
78+
// Extract the name for the declaration
79+
const [name, ...value] = declaration.split(' ');
80+
81+
switch (name) {
82+
case 'skip-deprecation': {
83+
if (value.length !== 1) {
84+
issues.push({
85+
level: 'error',
86+
location: {
87+
// TODO,
88+
path: '',
89+
position: 0,
90+
},
91+
message: LINT_MESSAGES.malformedLinterDeclaration.replace(
92+
'{{message}}',
93+
`Expected 1 argument, got ${value.length}`
94+
),
95+
});
96+
97+
break;
98+
}
99+
100+
// Get the deprecation code. This should be something like DEP0001.
101+
const deprecation = value[0];
102+
103+
// Extract the number from the code
104+
const deprecationCode = Number(deprecation.substring('DEP'.length));
105+
106+
// Make sure this is a valid deprecation code, output an error otherwise
107+
if (
108+
deprecation.length !== 7 ||
109+
!deprecation.startsWith('DEP') ||
110+
isNaN(deprecationCode)
111+
) {
112+
issues.push({
113+
level: 'error',
114+
location: {
115+
// TODO,
116+
path: '',
117+
position: 0,
118+
},
119+
message: LINT_MESSAGES.malformedLinterDeclaration.replace(
120+
'{{message}}',
121+
`Invalid deprecation code ${deprecation}`
122+
),
123+
});
124+
125+
break;
126+
}
127+
128+
declarations.skipDeprecation.push(deprecationCode);
129+
130+
break;
131+
}
132+
default: {
133+
issues.push({
134+
level: 'error',
135+
location: {
136+
// TODO
137+
path: '',
138+
position: 0,
139+
},
140+
message: LINT_MESSAGES.invalidLinterDeclaration.replace(
141+
'{{declaration}}',
142+
name
143+
),
144+
});
145+
break;
146+
}
147+
}
148+
};
149+
61150
/**
62151
* Checks if any error-level issues were found during linting
63152
*
@@ -70,6 +159,7 @@ const createLinter = (dryRun, disabledRules) => {
70159
return {
71160
lintAll,
72161
report,
162+
parseLinterDeclaration,
73163
hasError,
74164
};
75165
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
import { LINT_MESSAGES } from '../../constants.mjs';
4+
import { buildHierarchy } from '../../utils/buildHierarchy.mjs';
5+
import { DEPRECATION_HEADER_REGEX } from '../constants.mjs';
6+
import getDeprecationEntries from './utils/getDeprecationEntries.mjs';
7+
8+
/**
9+
* @param {ApiDocMetadataEntry} deprecation
10+
* @param {number} expectedCode
11+
* @returns {Array<import('../types').LintIssue>}
12+
*/
13+
function lintDeprecation(deprecation, expectedCode) {
14+
// Try validating the header (`DEPXXXX: ...`) and extract the code for us to
15+
// look at
16+
const match = deprecation.heading.data.text.match(DEPRECATION_HEADER_REGEX);
17+
18+
if (!match) {
19+
// Malformed header
20+
return [
21+
{
22+
level: 'error',
23+
location: {
24+
path: deprecation.api_doc_source,
25+
position: deprecation.yaml_position,
26+
},
27+
message: LINT_MESSAGES.malformedDeprecationHeader,
28+
},
29+
];
30+
}
31+
32+
const code = Number(match[1]);
33+
34+
return code === expectedCode
35+
? []
36+
: [
37+
{
38+
level: 'error',
39+
location: {
40+
path: deprecation.api_doc_source,
41+
position: deprecation.yaml_position,
42+
},
43+
message: LINT_MESSAGES.outOfOrderDeprecationCode
44+
.replaceAll('{{code}}', match[1])
45+
.replace('{{expectedCode}}', `${expectedCode}`.padStart(4, '0')),
46+
},
47+
];
48+
}
49+
50+
/**
51+
* Checks if any deprecation codes are out of order
52+
*
53+
* @type {import('../types').LintRule}
54+
*/
55+
export const deprecationCodeOrder = (entries, declarations) => {
56+
if (entries.length === 0 || entries[0].api !== 'deprecations') {
57+
// This is only relevant to doc/api/deprecations.md
58+
return [];
59+
}
60+
61+
const issues = [];
62+
63+
const hierarchy = buildHierarchy(entries);
64+
65+
hierarchy.forEach(root => {
66+
const deprecations = getDeprecationEntries(root.hierarchyChildren);
67+
68+
let expectedCode = 1;
69+
70+
for (const deprecation of deprecations || []) {
71+
while (declarations.skipDeprecation.includes(expectedCode)) {
72+
expectedCode++;
73+
}
74+
75+
issues.push(...lintDeprecation(deprecation, expectedCode));
76+
77+
expectedCode++;
78+
}
79+
});
80+
81+
return issues;
82+
};

src/linter/rules/index.mjs

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

3+
import { deprecationCodeOrder } from './deprecation-code-order.mjs';
34
import { invalidChangeVersion } from './invalid-change-version.mjs';
45
import { missingChangeVersion } from './missing-change-version.mjs';
56
import { missingIntroducedIn } from './missing-introduced-in.mjs';
@@ -11,4 +12,5 @@ export default {
1112
'invalid-change-version': invalidChangeVersion,
1213
'missing-change-version': missingChangeVersion,
1314
'missing-introduced-in': missingIntroducedIn,
15+
'deprecation-code-order': deprecationCodeOrder,
1416
};

src/linter/rules/invalid-change-version.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use strict';
2+
13
import { LINT_MESSAGES } from '../../constants.mjs';
24
import { valid } from 'semver';
35

0 commit comments

Comments
 (0)