Skip to content

Commit a268fae

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

File tree

9 files changed

+159
-31
lines changed

9 files changed

+159
-31
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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,5 +426,8 @@ export const LINT_MESSAGES = {
426426
missingChangeVersion: 'Missing version field in the API doc entry',
427427
invalidChangeVersion: 'Invalid version number: {{version}}',
428428
malformedDeprecationHeader: 'Malformed deprecation header',
429-
outOfOrderDeprecationCode: "Deprecation code '{{code}}' out of order",
429+
outOfOrderDeprecationCode:
430+
"Deprecation code '{{code}}' out of order (expected {{expectedCode}})",
431+
invalidLinterDeclaration: "Invalid linter declaration '{{declaration}}'",
432+
malformedLinterDeclaration: 'Malformed linter declaration: {{message}}',
430433
};

src/linter/engine.mjs

Lines changed: 8 additions & 4 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,15 @@ 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

3842
for (const rule of rules) {
39-
const ruleIssues = rule(entries);
43+
const ruleIssues = rule(entries, declarations);
4044

4145
if (ruleIssues.length > 0) {
4246
issues.push(...ruleIssues);

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
};

src/linter/rules/deprecation-code-order.mjs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// @ts-check
21
'use strict';
32

43
import { LINT_MESSAGES } from '../../constants.mjs';
@@ -8,7 +7,7 @@ import getDeprecationEntries from './utils/getDeprecationEntries.mjs';
87

98
/**
109
* @param {ApiDocMetadataEntry} deprecation
11-
* @param {number} expectedCode
10+
* @param {number} expectedCode
1211
* @returns {Array<import('../types').LintIssue>}
1312
*/
1413
function lintDeprecation(deprecation, expectedCode) {
@@ -32,24 +31,28 @@ function lintDeprecation(deprecation, expectedCode) {
3231

3332
const code = Number(match[1]);
3433

35-
return code === expectedCode ? [] : [
36-
{
37-
level: 'error',
38-
location: {
39-
path: deprecation.api_doc_source,
40-
position: deprecation.yaml_position,
41-
},
42-
message: LINT_MESSAGES.outOfOrderDeprecationCode.replaceAll('{{code}}', match[1]),
43-
},
44-
];
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+
];
4548
}
4649

4750
/**
4851
* Checks if any deprecation codes are out of order
4952
*
5053
* @type {import('../types').LintRule}
5154
*/
52-
export const deprecationCodeOrder = entries => {
55+
export const deprecationCodeOrder = (entries, declarations) => {
5356
if (entries.length === 0 || entries[0].api !== 'deprecations') {
5457
// This is only relevant to doc/api/deprecations.md
5558
return [];
@@ -64,11 +67,15 @@ export const deprecationCodeOrder = entries => {
6467

6568
let expectedCode = 1;
6669

67-
deprecations?.forEach(deprecation => {
70+
for (const deprecation of deprecations || []) {
71+
while (declarations.skipDeprecation.includes(expectedCode)) {
72+
expectedCode++;
73+
}
74+
6875
issues.push(...lintDeprecation(deprecation, expectedCode));
6976

7077
expectedCode++;
71-
});
78+
}
7279
});
7380

7481
return issues;

src/linter/rules/index.mjs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
import { deprecationCodeOrder } from './deprecation-code-order.mjs';
44
import { invalidChangeVersion } from './invalid-change-version.mjs';
5-
import { malformedDeprecationHeader } from './malformed-deprecation-header.mjs';
65
import { missingChangeVersion } from './missing-change-version.mjs';
76
import { missingIntroducedIn } from './missing-introduced-in.mjs';
87

98
/**
109
* @type {Record<string, import('../types').LintRule>}
1110
*/
1211
export default {
13-
// 'invalid-change-version': invalidChangeVersion,
14-
// 'missing-change-version': missingChangeVersion,
15-
// 'missing-introduced-in': missingIntroducedIn,
16-
'deprecation-code-order': deprecationCodeOrder
17-
// 'malformed-deprecation-header': malformedDeprecationHeader,
12+
'invalid-change-version': invalidChangeVersion,
13+
'missing-change-version': missingChangeVersion,
14+
'missing-introduced-in': missingIntroducedIn,
15+
'deprecation-code-order': deprecationCodeOrder,
1816
};

src/linter/types.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export interface LintIssue {
1313
location: LintIssueLocation;
1414
}
1515

16-
type LintRule = (input: Array<ApiDocMetadataEntry>) => LintIssue[];
16+
export interface LintDeclarations {
17+
skipDeprecation: Array<number>;
18+
}
19+
20+
type LintRule = (
21+
input: Array<ApiDocMetadataEntry>,
22+
declarations: LintDeclarations
23+
) => LintIssue[];
1724

1825
export type Reporter = (msg: LintIssue) => void;

src/parsers/markdown.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { createNodeSlugger } from '../utils/slugger.mjs';
1515
/**
1616
* Creates an API doc parser for a given Markdown API doc file
1717
*
18-
* @param {import('./linter/index.mjs').Linter | undefined} linter
18+
* @param {ReturnType<import('../linter/index.mjs').default> | undefined} linter
1919
*/
20-
const createParser = () => {
20+
const createParser = linter => {
2121
// Creates an instance of the Remark processor with GFM support
2222
// which is used for stringifying the AST tree back to Markdown
2323
const remarkProcessor = getRemark();
@@ -142,6 +142,16 @@ const createParser = () => {
142142
addYAMLMetadata(node, apiEntryMetadata);
143143
});
144144

145+
// Visits all HTML nodes from the current subtree to check for linter declarations.
146+
// If there are, it gives them to the linter to parse and use.
147+
visit(subTree, createQueries.UNIST.isLinterComment, node => {
148+
if (linter) {
149+
linter.parseLinterDeclaration(
150+
node.value.match(createQueries.QUERIES.linterComment)[1]
151+
);
152+
}
153+
});
154+
145155
// Visits all Text nodes from the current subtree and if there's any that matches
146156
// any API doc type reference and then updates the type reference to be a Markdown link
147157
visit(subTree, createQueries.UNIST.isTextWithType, (node, _, parent) =>
@@ -150,6 +160,7 @@ const createParser = () => {
150160

151161
// Removes already parsed items from the subtree so that they aren't included in the final content
152162
remove(subTree, [createQueries.UNIST.isYamlNode]);
163+
remove(subTree, [createQueries.UNIST.isLinterComment]);
153164

154165
// Applies the AST transformations to the subtree based on the API doc entry Metadata
155166
// Note that running the transformation on the subtree isn't costly as it is a reduced tree

src/queries.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ createQueries.QUERIES = {
200200
stabilityIndexPrefix: /Stability: ([0-5])/,
201201
// ReGeX for retrieving the inner content from a YAML block
202202
yamlInnerContent: /^<!--[ ]?(?:YAML([\s\S]*?)|([ \S]*?))?[ ]?-->/,
203+
// ReGeX for retrieiving inline linting directives
204+
linterComment: /^<!--[ ]?md-lint (.+?)[ ]?-->$/,
203205
};
204206

205207
createQueries.UNIST = {
@@ -216,6 +218,12 @@ createQueries.UNIST = {
216218
*/
217219
isYamlNode: ({ type, value }) =>
218220
type === 'html' && createQueries.QUERIES.yamlInnerContent.test(value),
221+
/**
222+
* @param {import('@types/mdast').Html} html
223+
* @returns {boolean}
224+
*/
225+
isLinterComment: ({ type, value }) =>
226+
type === 'html' && createQueries.QUERIES.linterComment.test(value),
219227
/**
220228
* @param {import('@types/mdast').Text} text
221229
* @returns {boolean}

0 commit comments

Comments
 (0)