Skip to content

Commit 403051f

Browse files
committed
tools: add must-call-assert lint rule
1 parent cee9267 commit 403051f

File tree

6 files changed

+202
-9
lines changed

6 files changed

+202
-9
lines changed

test/eslint.config_partial.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default [
126126
'node-core/eslint-check': 'error',
127127
'node-core/async-iife-no-unused-result': 'error',
128128
'node-core/inspector-check': 'error',
129+
'node-core/must-call-assert': 'error',
129130
// `common` module is mandatory in tests.
130131
'node-core/required-modules': [
131132
'error',
@@ -152,6 +153,7 @@ export default [
152153
objects: 'only-multiline',
153154
},
154155
],
156+
'node-core/must-call-assert': 'off',
155157
},
156158
},
157159
{
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
const common = require('../common');
3+
if ((!common.hasCrypto) || (!common.hasIntl)) {
4+
common.skip('ESLint tests require crypto and Intl');
5+
}
6+
common.skipIfEslintMissing();
7+
8+
const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester;
9+
const rule = require('../../tools/eslint-rules/must-call-assert');
10+
11+
const message = 'Assertions must be wrapped into `common.mustCall` or `common.mustCallAtLeast`';
12+
13+
const tester = new RuleTester();
14+
tester.run('must-call-assert', rule, {
15+
valid: [
16+
'assert.strictEqual(2+2, 4)',
17+
'process.on("exit", common.mustCallAtLeast((code) => {assert.strictEqual(code, 0)}));',
18+
'process.once("exit", common.mustCall((code) => {assert.strictEqual(code, 0)}));',
19+
'process.once("exit", common.mustCall((code) => {if(2+2 === 5) { assert.strictEqual(code, 0)} }));',
20+
'process.once("exit", common.mustCall((code) => { (() => assert.strictEqual(code, 0))(); }));',
21+
'(async () => {await assert.rejects(fun())})().then()',
22+
'[1, true].forEach((val) => assert.strictEqual(fun(val), 0));',
23+
'const assert = require("node:assert")',
24+
'const assert = require("assert")',
25+
'const assert = require("assert/strict")',
26+
'const assert = require("node:assert/strict")',
27+
'import assert from "node:assert"',
28+
'import * as assert from "node:assert"',
29+
'import assert from "node:assert/strict"',
30+
'import * as assert from "node:assert/strict"',
31+
],
32+
invalid: [
33+
{
34+
code: 'process.on("exit", (code) => assert.strictEqual(code, 0))',
35+
errors: [{ message }],
36+
},
37+
{
38+
code: 'function test() { process.on("exit", (code) => assert.strictEqual(code, 0)) }',
39+
errors: [{ message }],
40+
},
41+
{
42+
code: 'process.once("exit", (code) => {if(2+2 === 5) { assert.strictEqual(code, 0)} });',
43+
errors: [{ message }],
44+
},
45+
{
46+
code: 'process.once("exit", (code) => { (() => { assert.strictEqual(code, 0)})(); });',
47+
errors: [{ message }],
48+
},
49+
{
50+
code: 'process.once("exit", common.mustCall((code) => {setImmediate(() => { assert.strictEqual(code, 0)}); }));',
51+
errors: [{ message }],
52+
},
53+
{
54+
code: 'require("node:assert").strictEqual(2+2, 5)',
55+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
56+
},
57+
{
58+
code: 'const { strictEqual } = require("node:assert")',
59+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
60+
},
61+
{
62+
code: 'const { strictEqual } = require("node:assert/strict")',
63+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
64+
},
65+
{
66+
code: 'const { strictEqual } = require("assert")',
67+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
68+
},
69+
{
70+
code: 'const { strictEqual } = require("assert/strict")',
71+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
72+
},
73+
{
74+
code: 'const someOtherName = require("assert")',
75+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
76+
},
77+
{
78+
code: 'import assert, { strictEqual } from "assert"',
79+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
80+
},
81+
{
82+
code: 'import * as someOtherName from "assert"',
83+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
84+
},
85+
{
86+
code: 'import someOtherName from "assert"',
87+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
88+
},
89+
{
90+
code: 'import "assert"',
91+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
92+
},
93+
{
94+
code: 'import { strictEqual } from "node:assert"',
95+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
96+
},
97+
{
98+
code: 'import assert, { strictEqual } from "node:assert"',
99+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
100+
},
101+
{
102+
code: 'import * as someOtherName from "node:assert"',
103+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
104+
},
105+
{
106+
code: 'import someOtherName from "node:assert"',
107+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
108+
},
109+
{
110+
code: 'import "node:assert"',
111+
errors: [{ message: 'Only assign `node:assert` to `assert`' }],
112+
},
113+
]
114+
});

test/v8-updates/test-trace-gc-flag.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
// This test verifies that `--trace-gc` flag is well integrated.
44
// We'll check here, that the console outputs gc events properly.
5-
require('../common');
5+
const common = require('../common');
66

77
const assert = require('assert');
88
const { spawnSync } = require('child_process');
@@ -21,10 +21,10 @@ const fixtures = require('../common/fixtures');
2121
const scavengeRegex = /\bScavenge\b/;
2222
const eofRegex = /\bMark-Compact\b/;
2323

24-
lines.forEach((line, index) => {
24+
lines.forEach(common.mustCallAtLeast((line, index) => {
2525
const expected = index !== lines.length - 1 ? scavengeRegex : eofRegex;
2626
assert.match(line, expected);
27-
});
27+
}));
2828
}
2929

3030
/**

test/wasi/test-wasi-stdio.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22
const common = require('../common');
33
const tmpdir = require('../common/tmpdir');
4-
const { strictEqual } = require('assert');
4+
const assert = require('assert');
55
const { closeSync, openSync, readFileSync, writeFileSync } = require('fs');
66
const { join } = require('path');
77
const { WASI } = require('wasi');
@@ -24,10 +24,10 @@ const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
2424
(async () => {
2525
const { instance } = await WebAssembly.instantiate(buffer, importObject);
2626

27-
strictEqual(wasi.start(instance), 0);
27+
assert.strictEqual(wasi.start(instance), 0);
2828
closeSync(stdin);
2929
closeSync(stdout);
3030
closeSync(stderr);
31-
strictEqual(readFileSync(stdoutFile, 'utf8').trim(), 'x'.repeat(31));
32-
strictEqual(readFileSync(stderrFile, 'utf8').trim(), '');
31+
assert.strictEqual(readFileSync(stdoutFile, 'utf8').trim(), 'x'.repeat(31));
32+
assert.strictEqual(readFileSync(stderrFile, 'utf8').trim(), '');
3333
})().then(common.mustCall());

test/wasi/test-wasi-worker-terminate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ const bytecode = new Uint8Array([
2525
if (!process.env.HAS_STARTED_WORKER) {
2626
process.env.HAS_STARTED_WORKER = 1;
2727
const worker = new Worker(__filename);
28-
worker.once('message', (message) => {
28+
worker.once('message', common.mustCall((message) => {
2929
assert.strictEqual(message, 'start');
3030
setTimeout(() => worker.terminate(), common.platformTimeout(50));
31-
});
31+
}));
3232
} else {
3333
go();
3434
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
const message =
4+
'Assertions must be wrapped into `common.mustCall` or `common.mustCallAtLeast`';
5+
6+
7+
const requireCall = 'CallExpression[callee.name="require"]';
8+
const assertModuleSpecifier = '/^(node:)?assert(.strict)?$/';
9+
10+
function findEnclosingFunction(node) {
11+
while (node) {
12+
node = node.parent;
13+
if (!node) break;
14+
15+
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
16+
// We want to exit the loop only if it's not an IIFE nor a `[].forEach` call.
17+
if (node.parent.type !== 'CallExpression' || (
18+
node.parent.callee !== node && // IIFE
19+
!(
20+
node.parent.callee.type === 'MemberExpression' &&
21+
node.parent.callee.object.type === 'ArrayExpression' &&
22+
node.parent.callee.property.name === 'forEach'
23+
)
24+
)) {
25+
break;
26+
}
27+
}
28+
}
29+
return node;
30+
}
31+
32+
function isMustCallOrMustCallAtLeast(str) {
33+
return str === 'mustCall' || str === 'mustCallAtLeast';
34+
}
35+
36+
function isInMustCallFunction(node) {
37+
const parent = findEnclosingFunction(node)?.parent;
38+
if (!parent) return true;
39+
return (
40+
parent.type === 'CallExpression' &&
41+
(
42+
parent.callee.type === 'MemberExpression' ?
43+
parent.callee.object.name === 'common' && isMustCallOrMustCallAtLeast(parent.callee.property.name) :
44+
parent.callee.type === 'Identifier' && isMustCallOrMustCallAtLeast(parent.callee.name)
45+
)
46+
);
47+
}
48+
49+
module.exports = {
50+
meta: {
51+
fixable: 'code',
52+
},
53+
create: function(context) {
54+
return {
55+
':function CallExpression[callee.object.name="assert"]': (node) => {
56+
if (!isInMustCallFunction(node)) {
57+
context.report({
58+
node,
59+
message,
60+
});
61+
}
62+
},
63+
64+
[[
65+
`:not(VariableDeclarator[id.name="assert"])>${requireCall}[arguments.0.value=${assertModuleSpecifier}]`,
66+
`ImportDeclaration[source.value=${assertModuleSpecifier}]:not(
67+
[specifiers.length=1][specifiers.0.type=/^Import(Default|Namespace)Specifier$/][specifiers.0.local.name="assert"]
68+
)`,
69+
].join(',')]: (node) => {
70+
context.report({
71+
node,
72+
message: 'Only assign `node:assert` to `assert`',
73+
});
74+
},
75+
};
76+
},
77+
};

0 commit comments

Comments
 (0)