Skip to content

Commit f3fc973

Browse files
axetroysindresorhusfisker
authoredJan 24, 2025··
Add consistent-assert rule (#2535)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: fisker <lionkay@gmail.com>
1 parent a3c6dab commit f3fc973

8 files changed

+850
-0
lines changed
 

‎docs/rules/consistent-assert.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Enforce consistent assertion style with `node:assert`
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
Prefer `assert.ok()` over `assert()` for its explicit intent and better readability. It aligns with other assert methods, ensuring consistency and making code easier to maintain and understand.
11+
12+
## Examples
13+
14+
```js
15+
import assert from 'node:assert/strict';
16+
17+
assert.strictEqual(actual, expected);
18+
assert.deepStrictEqual(actual, expected);
19+
20+
//
21+
assert(divide(10, 2) === 5); // Inconsistent with other API styles
22+
23+
//
24+
assert.ok(divide(10, 2) === 5);
25+
```
26+
27+
```js
28+
import assert from 'node:assert';
29+
30+
assert.strictEqual(actual, expected);
31+
assert.deepStrictEqual(actual, expected);
32+
33+
//
34+
assert(divide(10, 2) === 5); // Inconsistent with other API styles
35+
36+
//
37+
assert.ok(divide(10, 2) === 5);
38+
```
39+
40+
```js
41+
import {strict as assert} from 'node:assert';
42+
43+
assert.strictEqual(actual, expected);
44+
assert.deepStrictEqual(actual, expected);
45+
46+
//
47+
assert(divide(10, 2) === 5); // Inconsistent with other API styles
48+
49+
//
50+
assert.ok(divide(10, 2) === 5);
51+
```

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export default [
5858
| :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
5959
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | | 🔧 | |
6060
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. || 🔧 | |
61+
| [consistent-assert](docs/rules/consistent-assert.md) | Enforce consistent assertion style with `node:assert`. || 🔧 | |
6162
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | 🔧 | 💡 |
6263
| [consistent-empty-array-spread](docs/rules/consistent-empty-array-spread.md) | Prefer consistent types when spreading a ternary in an array literal. || 🔧 | |
6364
| [consistent-existence-index-check](docs/rules/consistent-existence-index-check.md) | Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`. || 🔧 | |

‎rules/consistent-assert.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const MESSAGE_ID_ERROR = 'consistent-assert/error';
2+
const messages = {
3+
[MESSAGE_ID_ERROR]: 'Prefer `{{name}}.ok(…)` over `{{name}}(…)`.',
4+
};
5+
6+
/**
7+
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier | import('estree').ImportSpecifier | import('estree').ImportDeclaration} node
8+
*/
9+
const isValueImport = node => !node.importKind || node.importKind === 'value';
10+
11+
/**
12+
Check if a specifier is `assert` function.
13+
14+
@param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier} specifier
15+
@param {string} moduleName
16+
*/
17+
const isAssertFunction = (specifier, moduleName) =>
18+
// `import assert from 'node:assert';`
19+
// `import assert from 'node:assert/strict';`
20+
specifier.type === 'ImportDefaultSpecifier'
21+
// `import {default as assert} from 'node:assert';`
22+
// `import {default as assert} from 'node:assert/strict';`
23+
|| (
24+
specifier.type === 'ImportSpecifier'
25+
&& specifier.imported.name === 'default'
26+
)
27+
// `import {strict as assert} from 'node:assert';`
28+
|| (
29+
moduleName === 'assert'
30+
&& specifier.type === 'ImportSpecifier'
31+
&& specifier.imported.name === 'strict'
32+
);
33+
34+
const NODE_PROTOCOL = 'node:';
35+
36+
/** @type {import('eslint').Rule.RuleModule['create']} */
37+
const create = context => ({
38+
* ImportDeclaration(importDeclaration) {
39+
if (!isValueImport(importDeclaration)) {
40+
return;
41+
}
42+
43+
let moduleName = importDeclaration.source.value;
44+
45+
if (moduleName.startsWith(NODE_PROTOCOL)) {
46+
moduleName = moduleName.slice(NODE_PROTOCOL.length);
47+
}
48+
49+
if (moduleName !== 'assert' && moduleName !== 'assert/strict') {
50+
return;
51+
}
52+
53+
for (const specifier of importDeclaration.specifiers) {
54+
if (!isValueImport(specifier) || !isAssertFunction(specifier, moduleName)) {
55+
continue;
56+
}
57+
58+
const variables = context.sourceCode.getDeclaredVariables(specifier);
59+
60+
/* c8 ignore next 3 */
61+
if (!Array.isArray(variables) && variables.length === 1) {
62+
continue;
63+
}
64+
65+
const [variable] = variables;
66+
67+
for (const {identifier} of variable.references) {
68+
if (!(identifier.parent.type === 'CallExpression' && identifier.parent.callee === identifier)) {
69+
continue;
70+
}
71+
72+
yield {
73+
node: identifier,
74+
messageId: MESSAGE_ID_ERROR,
75+
data: {name: identifier.name},
76+
/** @param {import('eslint').Rule.RuleFixer} fixer */
77+
fix: fixer => fixer.insertTextAfter(identifier, '.ok'),
78+
};
79+
}
80+
}
81+
},
82+
});
83+
84+
/** @type {import('eslint').Rule.RuleModule} */
85+
const config = {
86+
create,
87+
meta: {
88+
type: 'problem',
89+
docs: {
90+
description: 'Enforce consistent assertion style with `node:assert`.',
91+
recommended: true,
92+
},
93+
fixable: 'code',
94+
messages,
95+
},
96+
};
97+
98+
export default config;

‎rules/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {createRule} from './utils/rule.js';
33

44
import betterRegex from './better-regex.js';
55
import catchErrorName from './catch-error-name.js';
6+
import consistentAssert from './consistent-assert.js';
67
import consistentDestructuring from './consistent-destructuring.js';
78
import consistentEmptyArraySpread from './consistent-empty-array-spread.js';
89
import consistentExistenceIndexCheck from './consistent-existence-index-check.js';
@@ -128,6 +129,7 @@ import throwNewError from './throw-new-error.js';
128129
const rules = {
129130
'better-regex': createRule(betterRegex, 'better-regex'),
130131
'catch-error-name': createRule(catchErrorName, 'catch-error-name'),
132+
'consistent-assert': createRule(consistentAssert, 'consistent-assert'),
131133
'consistent-destructuring': createRule(consistentDestructuring, 'consistent-destructuring'),
132134
'consistent-empty-array-spread': createRule(consistentEmptyArraySpread, 'consistent-empty-array-spread'),
133135
'consistent-existence-index-check': createRule(consistentExistenceIndexCheck, 'consistent-existence-index-check'),

‎test/consistent-assert.js

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import outdent from 'outdent';
2+
import {getTester, parsers} from './utils/test.js';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'assert(foo)',
9+
'import assert from "assert";',
10+
// Import but not invoke
11+
outdent`
12+
import assert from 'node:assert';
13+
assert;
14+
`,
15+
outdent`
16+
import customAssert from 'node:assert';
17+
assert(foo);
18+
`,
19+
outdent`
20+
function foo (assert) {
21+
assert(bar);
22+
}
23+
`,
24+
outdent`
25+
import assert from 'node:assert';
26+
27+
function foo (assert) {
28+
assert(bar);
29+
}
30+
`,
31+
// Invalid named import
32+
outdent`
33+
import {strict} from 'node:assert/strict';
34+
35+
strict(foo);
36+
`,
37+
outdent`
38+
import * as assert from 'node:assert';
39+
assert(foo);
40+
`,
41+
outdent`
42+
export * as assert from 'node:assert';
43+
assert(foo);
44+
`,
45+
outdent`
46+
export {default as assert} from 'node:assert';
47+
export {assert as strict} from 'node:assert';
48+
assert(foo);
49+
`,
50+
outdent`
51+
import assert from 'node:assert/strict';
52+
console.log(assert)
53+
`,
54+
...[
55+
'import type assert from "node:assert/strict";',
56+
'import {type strict as assert} from "node:assert/strict";',
57+
'import type {strict as assert} from "node:assert/strict";',
58+
].flatMap(code => [code, `${code}\nassert();`]).map(code => ({code, languageOptions: {parser: parsers.typescript}})),
59+
],
60+
invalid: [
61+
// Default import
62+
outdent`
63+
import assert from 'assert';
64+
assert(foo)
65+
`,
66+
outdent`
67+
import assert from 'node:assert';
68+
assert(foo)
69+
`,
70+
outdent`
71+
import assert from 'assert/strict';
72+
assert(foo)
73+
`,
74+
outdent`
75+
import assert from 'node:assert/strict';
76+
assert(foo)
77+
`,
78+
outdent`
79+
import customAssert from 'assert';
80+
customAssert(foo)
81+
`,
82+
outdent`
83+
import customAssert from 'node:assert';
84+
customAssert(foo)
85+
`,
86+
// Multiple references
87+
outdent`
88+
import assert from 'assert';
89+
assert(foo)
90+
assert(bar)
91+
assert(baz)
92+
`,
93+
// Named import
94+
outdent`
95+
import {strict} from 'assert';
96+
strict(foo)
97+
`,
98+
// Named import with alias
99+
outdent`
100+
import {strict as assert} from 'assert';
101+
assert(foo)
102+
`,
103+
// All cases
104+
outdent`
105+
import a, {strict as b, default as c} from 'node:assert';
106+
import d, {strict as e, default as f} from 'assert';
107+
import g, {default as h} from 'node:assert/strict';
108+
import i, {default as j} from 'assert/strict';
109+
a(foo);
110+
b(foo);
111+
c(foo);
112+
d(foo);
113+
e(foo);
114+
f(foo);
115+
g(foo);
116+
h(foo);
117+
i(foo);
118+
j(foo);
119+
`,
120+
// Optional call, not really matters
121+
outdent`
122+
import assert from 'node:assert';
123+
assert?.(foo)
124+
`,
125+
outdent`
126+
import assert from 'assert';
127+
128+
((
129+
/* comment */ ((
130+
/* comment */
131+
assert
132+
/* comment */
133+
)) /* comment */
134+
(/* comment */ typeof foo === 'string', 'foo must be a string' /** after comment */)
135+
));
136+
`,
137+
],
138+
});

‎test/package.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
3434
'consistent-existence-index-check',
3535
'prefer-global-this',
3636
'no-instanceof-builtin-object',
37+
'consistent-assert',
3738
]);
3839

3940
test('Every rule is defined in index file in alphabetical order', t => {

‎test/snapshots/consistent-assert.js.md

+559
Large diffs are not rendered by default.
1.34 KB
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.