Skip to content

Commit

Permalink
tools: lint deprecation codes
Browse files Browse the repository at this point in the history
Add a rule to make sure deprecation codes are in order.

PR-URL: #41992
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
aduh95 authored and bengl committed Feb 21, 2022
1 parent 72cb44b commit dcb51b0
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 4 deletions.
12 changes: 8 additions & 4 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,10 @@ Type: End-of-Life
`runInAsyncIdScope` doesn't emit the `'before'` or `'after'` event and can thus
cause a lot of issues. See <https://github.com/nodejs/node/issues/14328>.

<!-- md-lint skip-deprecation DEP0087 -->

<!-- md-lint skip-deprecation DEP0088 -->

### DEP0089: `require('assert')`

<!-- YAML
Expand Down Expand Up @@ -2262,10 +2266,10 @@ Type: End-of-Life
The `crypto._toBuf()` function was not designed to be used by modules outside
of Node.js core and was removed.

### DEP0115: `crypto.prng()`, `crypto.pseudoRandomBytes()`, `crypto.rng()`

<!--lint disable nodejs-yaml-comments -->

### DEP0115: `crypto.prng()`, `crypto.pseudoRandomBytes()`, `crypto.rng()`

<!-- YAML
changes:
- version: v11.0.0
Expand All @@ -2276,10 +2280,10 @@ changes:
with `--pending-deprecation` support.
-->

<!--lint enable nodejs-yaml-comments -->

Type: Documentation-only (supports [`--pending-deprecation`][])

<!--lint enable nodejs-yaml-comments -->

In recent versions of Node.js, there is no difference between
[`crypto.randomBytes()`][] and `crypto.pseudoRandomBytes()`. The latter is
deprecated along with the undocumented aliases `crypto.prng()` and
Expand Down
28 changes: 28 additions & 0 deletions test/doctool/test-deprecation-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

require('../common');
const path = require('path');
const { spawn } = require('child_process');

const script = path.join(
__dirname,
'..',
'..',
'tools',
'doc',
'deprecationCodes.mjs'
);

const mdPath = path.join(
__dirname,
'..',
'..',
'doc',
'api',
'deprecations.md'
);

const cp = spawn(process.execPath, [script, mdPath], { encoding: 'utf-8', stdio: 'inherit' });

cp.on('error', (err) => { throw err; });
cp.on('exit', (code) => process.exit(code));
42 changes: 42 additions & 0 deletions test/parallel/test-eslint-documented-deprecation-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
if (!common.hasIntl)
common.skip('missing Intl');
common.skipIfEslintMissing();

const RuleTester = require('../../tools/node_modules/eslint').RuleTester;
const rule = require('../../tools/eslint-rules/documented-deprecation-codes');

const mdFile = 'doc/api/deprecations.md';

const invalidCode = 'UNDOCUMENTED INVALID CODE';

new RuleTester().run('documented-deprecation-codes', rule, {
valid: [
`
deprecate(function() {
return this.getHeaders();
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066')
`,
],
invalid: [
{
code: `
deprecate(function foo(){}, 'bar', '${invalidCode}');
`,
errors: [
{
message: `"${invalidCode}" does not match the expected pattern`,
line: 2
},
{
message: `"${invalidCode}" is not documented in ${mdFile}`,
line: 2
},
]
},
]
});
92 changes: 92 additions & 0 deletions tools/doc/deprecationCodes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'fs';
import { resolve } from 'path';
import assert from 'assert';

import { unified } from 'unified';
import remarkParse from 'remark-parse';

const source = resolve(process.argv[2]);

const skipDeprecationComment = /^<!-- md-lint skip-deprecation (DEP\d{4}) -->$/;

const generateDeprecationCode = (codeAsNumber) =>
`DEP${codeAsNumber.toString().padStart(4, '0')}`;

const addMarkdownPathToErrorStack = (error, node) => {
const { line, column } = node.position.start;
const [header, ...lines] = error.stack.split('\n');
error.stack =
header +
`\n at <anonymous> (${source}:${line}:${column})\n` +
lines.join('\n');
return error;
};

const testHeading = (headingNode, expectedDeprecationCode) => {
try {
assert.strictEqual(
headingNode?.children[0]?.value.substring(0, 9),
`${expectedDeprecationCode}: `,
'Ill-formed or out-of-order deprecation code.'
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, headingNode);
}
};

const testYAMLComment = (commentNode) => {
try {
assert.match(
commentNode?.value?.substring(0, 21),
/^<!-- YAML\r?\nchanges:\r?\n/,
'Missing or ill-formed YAML comment.'
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, commentNode);
}
};

const testDeprecationType = (paragraphNode) => {
try {
assert.strictEqual(
paragraphNode?.children[0]?.value?.substring(0, 6),
'Type: ',
'Missing deprecation type.'
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, paragraphNode);
}
};

const tree = unified()
.use(remarkParse)
.parse(fs.readFileSync(source));

let expectedDeprecationCodeNumber = 0;
for (let i = 0; i < tree.children.length; i++) {
const node = tree.children[i];
if (node.type === 'html' && skipDeprecationComment.test(node.value)) {
const expectedDeprecationCode =
generateDeprecationCode(++expectedDeprecationCodeNumber);
const deprecationCodeAsText = node.value.match(skipDeprecationComment)[1];

try {
assert.strictEqual(
deprecationCodeAsText,
expectedDeprecationCode,
'Deprecation codes are not ordered correctly.'
);
} catch (e) {
throw addMarkdownPathToErrorStack(e, node);
}
}
if (node.type === 'heading' && node.depth === 3) {
const expectedDeprecationCode =
generateDeprecationCode(++expectedDeprecationCodeNumber);

testHeading(node, expectedDeprecationCode);

testYAMLComment(tree.children[i + 1]);
testDeprecationType(tree.children[i + 2]);
}
}
37 changes: 37 additions & 0 deletions tools/eslint-rules/documented-deprecation-codes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const fs = require('fs');
const path = require('path');
const { isDefiningDeprecation } = require('./rules-utils.js');

const patternToMatch = /^DEP\d+$/;

const mdFile = 'doc/api/deprecations.md';
const doc = fs.readFileSync(path.resolve(__dirname, '../..', mdFile), 'utf8');

function isInDoc(code) {
return doc.includes(`### ${code}:`);
}

function getDeprecationCode(node) {
return node.expression.arguments[2].value;
}

module.exports = {
create: function(context) {
return {
ExpressionStatement: function(node) {
if (!isDefiningDeprecation(node) || !getDeprecationCode(node)) return;
const code = getDeprecationCode(node);
if (!patternToMatch.test(code)) {
const message = `"${code}" does not match the expected pattern`;
context.report({ node, message });
}
if (!isInDoc(code)) {
const message = `"${code}" is not documented in ${mdFile}`;
context.report({ node, message });
}
},
};
},
};
8 changes: 8 additions & 0 deletions tools/eslint-rules/rules-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ module.exports.isDefiningError = function(node) {
node.expression.arguments.length !== 0;
};

module.exports.isDefiningDeprecation = function(node) {
return node.expression &&
node.expression.type === 'CallExpression' &&
node.expression.callee &&
node.expression.callee.name.endsWith('deprecate') &&
node.expression.arguments.length !== 0;
};

/**
* Returns true if any of the passed in modules are used in
* require calls.
Expand Down

0 comments on commit dcb51b0

Please sign in to comment.