Skip to content

Commit 3459368

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 3459368

File tree

6 files changed

+549
-19
lines changed

6 files changed

+549
-19
lines changed

CHANGELOG.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
66

77
## [Unreleased]
88

9+
### Added
10+
- add [`enforce-node-protocol-usage`] rule ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])
11+
912
### Changed
10-
- [Docs] `extensions`, `order`: improve documentation ([#3106], thanks [@Xunnamius])
13+
- [Docs] [`extensions`], [`order`]: improve documentation ([#3106], thanks [@Xunnamius])
1114

1215
## [2.31.0] - 2024-10-03
1316

@@ -1110,6 +1113,7 @@ for info on changes for earlier releases.
11101113
[`consistent-type-specifier-style`]: ./docs/rules/consistent-type-specifier-style.md
11111114
[`default`]: ./docs/rules/default.md
11121115
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
1116+
[`enforce-node-protocol-usage`]: ./docs/rules/enforce-node-protocol-usage.md
11131117
[`export`]: ./docs/rules/export.md
11141118
[`exports-last`]: ./docs/rules/exports-last.md
11151119
[`extensions`]: ./docs/rules/extensions.md
@@ -1169,6 +1173,7 @@ for info on changes for earlier releases.
11691173
[#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036
11701174
[#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033
11711175
[#3032]: https://github.com/import-js/eslint-plugin-import/pull/3032
1176+
[#3024]: https://github.com/import-js/eslint-plugin-import/pull/3024
11721177
[#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018
11731178
[#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012
11741179
[#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011
@@ -1788,7 +1793,6 @@ for info on changes for earlier releases.
17881793
[@bicstone]: https://github.com/bicstone
17891794
[@Blasz]: https://github.com/Blasz
17901795
[@bmish]: https://github.com/bmish
1791-
[@developer-bandi]: https://github.com/developer-bandi
17921796
[@borisyankov]: https://github.com/borisyankov
17931797
[@bradennapier]: https://github.com/bradennapier
17941798
[@bradzacher]: https://github.com/bradzacher
@@ -1808,6 +1812,7 @@ for info on changes for earlier releases.
18081812
[@darkartur]: https://github.com/darkartur
18091813
[@davidbonnet]: https://github.com/davidbonnet
18101814
[@dbrewer5]: https://github.com/dbrewer5
1815+
[@developer-bandi]: https://github.com/developer-bandi
18111816
[@devinrhode2]: https://github.com/devinrhode2
18121817
[@devongovett]: https://github.com/devongovett
18131818
[@dmnd]: https://github.com/dmnd
@@ -1842,6 +1847,7 @@ for info on changes for earlier releases.
18421847
[@georeith]: https://github.com/georeith
18431848
[@giodamelio]: https://github.com/giodamelio
18441849
[@gnprice]: https://github.com/gnprice
1850+
[@GoldStrikeArch]: https://github.com/GoldStrikeArch
18451851
[@golergka]: https://github.com/golergka
18461852
[@golopot]: https://github.com/golopot
18471853
[@GoodForOneFare]: https://github.com/GoodForOneFare
@@ -1901,9 +1907,9 @@ for info on changes for earlier releases.
19011907
[@Librazy]: https://github.com/Librazy
19021908
[@liby]: https://github.com/liby
19031909
[@lilling]: https://github.com/lilling
1910+
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
19041911
[@ljharb]: https://github.com/ljharb
19051912
[@ljqx]: https://github.com/ljqx
1906-
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
19071913
[@lo1tuma]: https://github.com/lo1tuma
19081914
[@loganfsmyth]: https://github.com/loganfsmyth
19091915
[@luczsoma]: https://github.com/luczsoma
@@ -1977,6 +1983,7 @@ for info on changes for earlier releases.
19771983
[@Schweinepriester]: https://github.com/Schweinepriester
19781984
[@scottnonnenberg]: https://github.com/scottnonnenberg
19791985
[@sergei-startsev]: https://github.com/sergei-startsev
1986+
[@sevenc-nanashi]: https://github.com/sevenc-nanashi
19801987
[@sharmilajesupaul]: https://github.com/sharmilajesupaul
19811988
[@sheepsteak]: https://github.com/sheepsteak
19821989
[@silverwind]: https://github.com/silverwind

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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
type: 'array',
101+
minItems: 1,
102+
maxItems: 1,
103+
items: [
104+
{
105+
enum: ['always', 'never'],
106+
},
107+
],
108+
},
109+
messages,
110+
},
111+
create(context) {
112+
return {
113+
CallExpression(node) {
114+
if (!isStaticRequireWith1Param(node)) { return; }
115+
116+
const arg = node.arguments[0];
117+
118+
return checkAndReport(arg, context);
119+
},
120+
ExportNamedDeclaration(node) {
121+
return checkAndReport(node.source, context);
122+
},
123+
ImportDeclaration(node) {
124+
return checkAndReport(node.source, context);
125+
},
126+
ImportExpression(node) {
127+
return checkAndReport(node.source, context);
128+
},
129+
};
130+
},
131+
};

0 commit comments

Comments
 (0)