Skip to content

Commit fe159c7

Browse files
committed
tools: lint deprecation codes
Add a rule to make sure deprecation codes are in order.
1 parent daf8a46 commit fe159c7

File tree

6 files changed

+226
-4
lines changed

6 files changed

+226
-4
lines changed

doc/api/deprecations.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,10 @@ Type: End-of-Life
18151815
`runInAsyncIdScope` doesn't emit the `'before'` or `'after'` event and can thus
18161816
cause a lot of issues. See <https://github.com/nodejs/node/issues/14328>.
18171817

1818+
<!-- md-lint skip-deprecation DEP0087 -->
1819+
1820+
<!-- md-lint skip-deprecation DEP0088 -->
1821+
18181822
### DEP0089: `require('assert')`
18191823

18201824
<!-- YAML
@@ -2255,10 +2259,10 @@ Type: End-of-Life
22552259
The `crypto._toBuf()` function was not designed to be used by modules outside
22562260
of Node.js core and was removed.
22572261

2258-
### DEP0115: `crypto.prng()`, `crypto.pseudoRandomBytes()`, `crypto.rng()`
2259-
22602262
<!--lint disable nodejs-yaml-comments -->
22612263

2264+
### DEP0115: `crypto.prng()`, `crypto.pseudoRandomBytes()`, `crypto.rng()`
2265+
22622266
<!-- YAML
22632267
changes:
22642268
- version: v11.0.0
@@ -2269,10 +2273,10 @@ changes:
22692273
with `--pending-deprecation` support.
22702274
-->
22712275

2272-
<!--lint enable nodejs-yaml-comments -->
2273-
22742276
Type: Documentation-only (supports [`--pending-deprecation`][])
22752277

2278+
<!--lint enable nodejs-yaml-comments -->
2279+
22762280
In recent versions of Node.js, there is no difference between
22772281
[`crypto.randomBytes()`][] and `crypto.pseudoRandomBytes()`. The latter is
22782282
deprecated along with the undocumented aliases `crypto.prng()` and
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
require('../common');
4+
const path = require('path');
5+
const { spawn } = require('child_process');
6+
7+
const script = path.join(
8+
__dirname,
9+
'..',
10+
'..',
11+
'tools',
12+
'doc',
13+
'deprecationCodes.mjs'
14+
);
15+
16+
const mdPath = path.join(
17+
__dirname,
18+
'..',
19+
'..',
20+
'doc',
21+
'api',
22+
'deprecations.md'
23+
);
24+
25+
const cp = spawn(process.execPath, [script, mdPath], { encoding: 'utf-8', stdio: 'inherit' });
26+
27+
cp.on('error', (err) => { throw err; });
28+
cp.on('exit', (code) => process.exit(code));
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
common.skipIfEslintMissing();
7+
8+
const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
9+
const rule = require('../../tools/eslint-rules/documented-deprecation-codes');
10+
11+
const mdFile = 'doc/api/deprecations.md';
12+
13+
const invalidCode = 'UNDOCUMENTED INVALID CODE';
14+
15+
new RuleTester().run('documented-deprecation-codes', rule, {
16+
valid: [
17+
`
18+
deprecate(function() {
19+
return this.getHeaders();
20+
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066')
21+
`,
22+
],
23+
invalid: [
24+
{
25+
code: `
26+
deprecate(function foo(){}, 'bar', '${invalidCode}');
27+
`,
28+
errors: [
29+
{
30+
message: `"${invalidCode}" does not match the expected pattern`,
31+
line: 2
32+
},
33+
{
34+
message: `"${invalidCode}" is not documented in ${mdFile}`,
35+
line: 2
36+
},
37+
{
38+
message:
39+
`${mdFile} does not have an anchor for "${invalidCode}"`,
40+
line: 2
41+
},
42+
]
43+
},
44+
]
45+
});

tools/doc/deprecationCodes.mjs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import fs from 'fs';
2+
import { resolve } from 'path';
3+
import assert from 'assert';
4+
5+
import { unified } from 'unified';
6+
import remarkParse from 'remark-parse';
7+
8+
const source = resolve(process.argv[2]);
9+
10+
const skipDeprecationComment = /^<!-- md-lint skip-deprecation (DEP\d{4}) -->$/;
11+
12+
const generateDeprecationCode = (codeAsNumber) =>
13+
`DEP${codeAsNumber.toString().padStart(4, '0')}`;
14+
15+
const addMarkdownPathToErrorStack = (error, node) => {
16+
const { line, column } = node.position.start;
17+
const [header, ...lines] = error.stack.split('\n');
18+
error.stack =
19+
header +
20+
`\n at <anonymous> (${source}:${line}:${column})\n` +
21+
lines.join('\n');
22+
return error;
23+
};
24+
25+
const testHeading = (headingNode, expectedDeprecationCode) => {
26+
try {
27+
assert.strictEqual(
28+
headingNode?.children[0]?.value.substring(0, 9),
29+
`${expectedDeprecationCode}: `,
30+
'Ill-formed or out-of-order deprecation code.'
31+
);
32+
} catch (e) {
33+
throw addMarkdownPathToErrorStack(e, headingNode);
34+
}
35+
};
36+
37+
const testYAMLComment = (commentNode) => {
38+
try {
39+
assert.strictEqual(
40+
commentNode?.value?.substring(0, 19),
41+
'<!-- YAML\nchanges:\n',
42+
'Missing or ill-formed YAML comment.'
43+
);
44+
} catch (e) {
45+
throw addMarkdownPathToErrorStack(e, commentNode);
46+
}
47+
};
48+
49+
const testDeprecationType = (paragraphNode) => {
50+
try {
51+
assert.strictEqual(
52+
paragraphNode?.children[0]?.value?.substring(0, 6),
53+
'Type: ',
54+
'Missing deprecation type.'
55+
);
56+
} catch (e) {
57+
throw addMarkdownPathToErrorStack(e, paragraphNode);
58+
}
59+
};
60+
61+
const tree = unified()
62+
.use(remarkParse)
63+
.parse(fs.readFileSync(source));
64+
65+
let expectedDeprecationCodeNumber = 0;
66+
for (let i = 0; i < tree.children.length; i++) {
67+
const node = tree.children[i];
68+
if (node.type === 'html' && skipDeprecationComment.test(node.value)) {
69+
const expectedDeprecationCode =
70+
generateDeprecationCode(++expectedDeprecationCodeNumber);
71+
const deprecationCodeAsText = node.value.match(skipDeprecationComment)[1];
72+
73+
try {
74+
assert.strictEqual(
75+
deprecationCodeAsText,
76+
expectedDeprecationCode,
77+
'Deprecation codes are not ordered correctly.'
78+
);
79+
} catch (e) {
80+
throw addMarkdownPathToErrorStack(e, node);
81+
}
82+
}
83+
if (node.type === 'heading' && node.depth === 3) {
84+
const expectedDeprecationCode =
85+
generateDeprecationCode(++expectedDeprecationCodeNumber);
86+
87+
testHeading(node, expectedDeprecationCode);
88+
89+
testYAMLComment(tree.children[i + 1]);
90+
testDeprecationType(tree.children[i + 2]);
91+
}
92+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { isDefiningDeprecation } = require('./rules-utils.js');
6+
7+
const patternToMatch = /^DEP\d+$/;
8+
9+
const mdFile = 'doc/api/deprecations.md';
10+
const doc = fs.readFileSync(path.resolve(__dirname, '../..', mdFile), 'utf8');
11+
12+
function isInDoc(code) {
13+
return doc.includes(`### ${code}:`);
14+
}
15+
16+
function includesAnchor(code) {
17+
return doc.includes(`<a id="${code}"></a>`);
18+
}
19+
20+
function getDeprecationCode(node) {
21+
return node.expression.arguments[2].value;
22+
}
23+
24+
module.exports = {
25+
create: function(context) {
26+
return {
27+
ExpressionStatement: function(node) {
28+
if (!isDefiningDeprecation(node) || !getDeprecationCode(node)) return;
29+
const code = getDeprecationCode(node);
30+
if (!patternToMatch.test(code)) {
31+
const message = `"${code}" does not match the expected pattern`;
32+
context.report({ node, message });
33+
}
34+
if (!isInDoc(code)) {
35+
const message = `"${code}" is not documented in ${mdFile}`;
36+
context.report({ node, message });
37+
}
38+
if (!includesAnchor(code)) {
39+
const message = `${mdFile} does not have an anchor for "${code}"`;
40+
context.report({ node, message });
41+
}
42+
},
43+
};
44+
},
45+
};

tools/eslint-rules/rules-utils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ module.exports.isDefiningError = function(node) {
2020
node.expression.arguments.length !== 0;
2121
};
2222

23+
module.exports.isDefiningDeprecation = function(node) {
24+
return node.expression &&
25+
node.expression.type === 'CallExpression' &&
26+
node.expression.callee &&
27+
node.expression.callee.name.endsWith('deprecate') &&
28+
node.expression.arguments.length !== 0;
29+
};
30+
2331
/**
2432
* Returns true if any of the passed in modules are used in
2533
* require calls.

0 commit comments

Comments
 (0)