Skip to content

Commit 8b62c8b

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 8b62c8b

File tree

5 files changed

+511
-16
lines changed

5 files changed

+511
-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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use strict';
2+
3+
const isCoreModule = require('is-core-module');
4+
const { default: docsUrl } = require('../docsUrl');
5+
6+
const DO_PREFER_MESSAGE_ID = 'preferNodeBuiltinImports';
7+
const NEVER_PREFER_MESSAGE_ID = 'neverPreferNodeBuiltinImports';
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+
&& node.arguments[0]
40+
&& isStringLiteral(node.arguments[0])
41+
// check for only 1 argument
42+
&& !node.arguments[1];
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 (moduleName.startsWith('node:')) { return; }
66+
67+
context.report({
68+
node: src,
69+
messageId: DO_PREFER_MESSAGE_ID,
70+
data: { moduleName },
71+
/** @param {import('eslint').Rule.RuleFixer} fixer */
72+
fix(fixer) {
73+
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
74+
},
75+
});
76+
} else if (typeof context.options[0] === 'undefined') {
77+
throw new Error('Missing option');
78+
} else {
79+
throw new Error(`Unexpected option: ${context.options[0]}`);
80+
}
81+
}
82+
83+
/** @type {import('eslint').Rule.RuleModule} */
84+
module.exports = {
85+
meta: {
86+
type: 'suggestion',
87+
docs: {
88+
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
89+
recommended: true,
90+
category: 'Static analysis',
91+
url: docsUrl('enforce-node-protocol-usage'),
92+
},
93+
fixable: 'code',
94+
schema: [
95+
{
96+
enum: ['always', 'never'],
97+
required: true,
98+
},
99+
],
100+
messages,
101+
},
102+
create(context) {
103+
return {
104+
CallExpression(node) {
105+
if (!isStaticRequireWith1Param(node)) { return; }
106+
107+
const arg = node.arguments[0];
108+
109+
if (!isStringLiteral(arg)) { return; }
110+
111+
return checkAndReport(arg, context);
112+
},
113+
ExportNamedDeclaration(node) {
114+
if (!isStringLiteral(node)) { return; }
115+
116+
return checkAndReport(node.source, context);
117+
},
118+
ImportDeclaration(node) {
119+
if (!isStringLiteral(node)) { return; }
120+
121+
return checkAndReport(node.source, context);
122+
},
123+
ImportExpression(node) {
124+
if (!isStringLiteral(node)) { return; }
125+
126+
return checkAndReport(node.source, context);
127+
},
128+
};
129+
},
130+
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy