Skip to content

Commit 7dccf82

Browse files
MikhailPertsev1sevenc-nanashi
authored andcommitted
[New] add enforce-node-protocol-usage rule
Co-authored-by: Mikhail Pertsev <mikhail.pertsev@brightpattern.com> Co-authored-by: sevenc-nanashi <sevenc7c@sevenc7c.com>
1 parent d5f2950 commit 7dccf82

File tree

5 files changed

+542
-16
lines changed

5 files changed

+542
-16
lines changed

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,23 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
5151

5252
### Static analysis
5353

54-
| Name                       | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 ||
55-
| :--------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
56-
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
57-
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
58-
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
59-
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
60-
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
61-
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
62-
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
63-
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
64-
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
65-
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
66-
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
67-
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
68-
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
69-
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |
54+
| Name                        | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 ||
55+
| :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
56+
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
57+
| [enforce-node-protocol-usage](docs/rules/enforce-node-protocol-usage.md) | Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules. | | | | 🔧 | | |
58+
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
59+
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
60+
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
61+
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
62+
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
63+
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
64+
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
65+
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
66+
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
67+
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
68+
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
69+
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
70+
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |
7071

7172
### Style guide
7273

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# import/enforce-node-protocol-usage
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.
8+
9+
## Rule Details
10+
11+
This rule enforces that builtins node imports are using, or omitting, the `node:` protocol.
12+
13+
Reasons to prefer using the protocol include:
14+
15+
- the code is more explicitly and clearly referencing a Node.js built-in module
16+
17+
Reasons to prefer omitting the protocol include:
18+
19+
- some tools don't support the `node:` protocol
20+
- the code is more portable, because import maps and automatic polyfilling can be used
21+
22+
## Options
23+
24+
The rule requires a single string option which may be one of:
25+
26+
- `'always'` - enforces that builtins node imports are using the `node:` protocol.
27+
- `'never'` - enforces that builtins node imports are not using the `node:` protocol.
28+
29+
## Examples
30+
31+
### `'always'`
32+
33+
❌ Invalid
34+
35+
```js
36+
import fs from 'fs';
37+
export { promises } from 'fs';
38+
// require
39+
const fs = require('fs/promises');
40+
```
41+
42+
✅ Valid
43+
44+
```js
45+
import fs from 'node:fs';
46+
export { promises } from 'node:fs';
47+
import * as test from 'node:test';
48+
// require
49+
const fs = require('node:fs/promises');
50+
```
51+
52+
### `'never'`
53+
54+
❌ Invalid
55+
56+
```js
57+
import fs from 'node:fs';
58+
export { promises } from 'node:fs';
59+
// require
60+
const fs = require('node:fs/promises');
61+
```
62+
63+
✅ Valid
64+
65+
```js
66+
import fs from 'fs';
67+
export { promises } from 'fs';
68+
import * as test from 'node:test';
69+
// require
70+
const fs = require('fs/promises');
71+
```
72+
73+
## When Not To Use It
74+
75+
If you don't want to consistently enforce using, or omitting, the `node:` protocol when importing Node.js builtin modules.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const rules = {
4545
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
4646
'no-import-module-exports': require('./rules/no-import-module-exports'),
4747
'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
48+
'enforce-node-protocol-usage': require('./rules/enforce-node-protocol-usage'),
4849

4950
// export
5051
'exports-last': require('./rules/exports-last'),
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use strict';
2+
3+
const isCoreModule = require('is-core-module');
4+
const { default: docsUrl } = require('../docsUrl');
5+
6+
const DO_PREFER_MESSAGE_ID = 'requireNodeProtocol';
7+
const NEVER_PREFER_MESSAGE_ID = 'forbidNodeProtocol';
8+
const messages = {
9+
[DO_PREFER_MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.',
10+
[NEVER_PREFER_MESSAGE_ID]: 'Prefer `{{moduleName}}` over `node:{{moduleName}}`.',
11+
};
12+
13+
function replaceStringLiteral(
14+
fixer,
15+
node,
16+
text,
17+
relativeRangeStart,
18+
relativeRangeEnd,
19+
) {
20+
const firstCharacterIndex = node.range[0] + 1;
21+
const start = Number.isInteger(relativeRangeEnd)
22+
? relativeRangeStart + firstCharacterIndex
23+
: firstCharacterIndex;
24+
const end = Number.isInteger(relativeRangeEnd)
25+
? relativeRangeEnd + firstCharacterIndex
26+
: node.range[1] - 1;
27+
28+
return fixer.replaceTextRange([start, end], text);
29+
}
30+
31+
function isStringLiteral(node) {
32+
return node.type === 'Literal' && typeof node.value === 'string';
33+
}
34+
35+
function isStaticRequireWith1Param(node) {
36+
return !node.optional
37+
&& node.callee.type === 'Identifier'
38+
&& node.callee.name === 'require'
39+
// check for only 1 argument
40+
&& node.arguments.length === 1
41+
&& node.arguments[0]
42+
&& isStringLiteral(node.arguments[0]);
43+
}
44+
45+
function checkAndReport(src, context) {
46+
const { value: moduleName } = src;
47+
if (!isCoreModule(moduleName)) { return; }
48+
49+
if (context.options[0] === 'never') {
50+
if (!moduleName.startsWith('node:')) { return; }
51+
52+
const actualModuleName = moduleName.slice(5);
53+
if (!isCoreModule(actualModuleName)) { return; }
54+
55+
context.report({
56+
node: src,
57+
messageId: NEVER_PREFER_MESSAGE_ID,
58+
data: { moduleName: actualModuleName },
59+
/** @param {import('eslint').Rule.RuleFixer} fixer */
60+
fix(fixer) {
61+
return replaceStringLiteral(fixer, src, '', 0, 5);
62+
},
63+
});
64+
} else if (context.options[0] === 'always') {
65+
if (
66+
moduleName.startsWith('node:')
67+
|| !isCoreModule(`node:${moduleName}`)
68+
) {
69+
return;
70+
}
71+
72+
context.report({
73+
node: src,
74+
messageId: DO_PREFER_MESSAGE_ID,
75+
data: { moduleName },
76+
/** @param {import('eslint').Rule.RuleFixer} fixer */
77+
fix(fixer) {
78+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
79+
},
80+
});
81+
} else if (typeof context.options[0] === 'undefined') {
82+
throw new Error('Missing option');
83+
} else {
84+
throw new Error(`Unexpected option: ${context.options[0]}`);
85+
}
86+
}
87+
88+
/** @type {import('eslint').Rule.RuleModule} */
89+
module.exports = {
90+
meta: {
91+
type: 'suggestion',
92+
docs: {
93+
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
94+
recommended: true,
95+
category: 'Static analysis',
96+
url: docsUrl('enforce-node-protocol-usage'),
97+
},
98+
fixable: 'code',
99+
schema: [
100+
{
101+
enum: ['always', 'never'],
102+
},
103+
],
104+
messages,
105+
},
106+
create(context) {
107+
return {
108+
CallExpression(node) {
109+
if (!isStaticRequireWith1Param(node)) { return; }
110+
111+
const arg = node.arguments[0];
112+
113+
if (!isStringLiteral(arg)) { return; }
114+
115+
return checkAndReport(arg, context);
116+
},
117+
ExportNamedDeclaration(node) {
118+
if (!isStringLiteral(node)) { return; }
119+
120+
return checkAndReport(node.source, context);
121+
},
122+
ImportDeclaration(node) {
123+
if (!isStringLiteral(node)) { return; }
124+
125+
return checkAndReport(node.source, context);
126+
},
127+
ImportExpression(node) {
128+
if (!isStringLiteral(node)) { return; }
129+
130+
return checkAndReport(node.source, context);
131+
},
132+
};
133+
},
134+
};

0 commit comments

Comments
 (0)