Skip to content

[New] add enforce-node-protocol-usage rule #3024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"line-length": false,
"ignore_case": true,
"no-duplicate-heading": {
"siblings_only": true
},
Expand Down
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

## [Unreleased]

### Added
- add [`enforce-node-protocol-usage`] rule and `import/node-version` setting ([#3024], thanks [@GoldStrikeArch] and [@sevenc-nanashi])

### Changed
- [Docs] `extensions`, `order`: improve documentation ([#3106], thanks [@Xunnamius])
- [Docs] [`extensions`], [`order`]: improve documentation ([#3106], thanks [@Xunnamius])

## [2.31.0] - 2024-10-03

Expand Down Expand Up @@ -1106,10 +1109,12 @@ for info on changes for earlier releases.
[`import/core-modules` setting]: ./README.md#importcore-modules
[`import/external-module-folders` setting]: ./README.md#importexternal-module-folders
[`internal-regex` setting]: ./README.md#importinternal-regex
[`import/node-version` setting]: ./README.md#importnode-version

[`consistent-type-specifier-style`]: ./docs/rules/consistent-type-specifier-style.md
[`default`]: ./docs/rules/default.md
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
[`enforce-node-protocol-usage`]: ./docs/rules/enforce-node-protocol-usage.md
[`export`]: ./docs/rules/export.md
[`exports-last`]: ./docs/rules/exports-last.md
[`extensions`]: ./docs/rules/extensions.md
Expand Down Expand Up @@ -1169,6 +1174,7 @@ for info on changes for earlier releases.
[#3036]: https://github.com/import-js/eslint-plugin-import/pull/3036
[#3033]: https://github.com/import-js/eslint-plugin-import/pull/3033
[#3032]: https://github.com/import-js/eslint-plugin-import/pull/3032
[#3024]: https://github.com/import-js/eslint-plugin-import/pull/3024
[#3018]: https://github.com/import-js/eslint-plugin-import/pull/3018
[#3012]: https://github.com/import-js/eslint-plugin-import/pull/3012
[#3011]: https://github.com/import-js/eslint-plugin-import/pull/3011
Expand Down Expand Up @@ -1788,7 +1794,6 @@ for info on changes for earlier releases.
[@bicstone]: https://github.com/bicstone
[@Blasz]: https://github.com/Blasz
[@bmish]: https://github.com/bmish
[@developer-bandi]: https://github.com/developer-bandi
[@borisyankov]: https://github.com/borisyankov
[@bradennapier]: https://github.com/bradennapier
[@bradzacher]: https://github.com/bradzacher
Expand All @@ -1808,6 +1813,7 @@ for info on changes for earlier releases.
[@darkartur]: https://github.com/darkartur
[@davidbonnet]: https://github.com/davidbonnet
[@dbrewer5]: https://github.com/dbrewer5
[@developer-bandi]: https://github.com/developer-bandi
[@devinrhode2]: https://github.com/devinrhode2
[@devongovett]: https://github.com/devongovett
[@dmnd]: https://github.com/dmnd
Expand Down Expand Up @@ -1842,6 +1848,7 @@ for info on changes for earlier releases.
[@georeith]: https://github.com/georeith
[@giodamelio]: https://github.com/giodamelio
[@gnprice]: https://github.com/gnprice
[@GoldStrikeArch]: https://github.com/GoldStrikeArch
[@golergka]: https://github.com/golergka
[@golopot]: https://github.com/golopot
[@GoodForOneFare]: https://github.com/GoodForOneFare
Expand Down Expand Up @@ -1901,9 +1908,9 @@ for info on changes for earlier releases.
[@Librazy]: https://github.com/Librazy
[@liby]: https://github.com/liby
[@lilling]: https://github.com/lilling
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
[@ljharb]: https://github.com/ljharb
[@ljqx]: https://github.com/ljqx
[@liuxingbaoyu]: https://github.com/liuxingbaoyu
[@lo1tuma]: https://github.com/lo1tuma
[@loganfsmyth]: https://github.com/loganfsmyth
[@luczsoma]: https://github.com/luczsoma
Expand Down Expand Up @@ -1977,6 +1984,7 @@ for info on changes for earlier releases.
[@Schweinepriester]: https://github.com/Schweinepriester
[@scottnonnenberg]: https://github.com/scottnonnenberg
[@sergei-startsev]: https://github.com/sergei-startsev
[@sevenc-nanashi]: https://github.com/sevenc-nanashi
[@sharmilajesupaul]: https://github.com/sharmilajesupaul
[@sheepsteak]: https://github.com/sheepsteak
[@silverwind]: https://github.com/silverwind
Expand Down
47 changes: 31 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,23 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a

### Static analysis

| Name                       | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | ❌ |
| :--------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |
| Name                        | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | ❌ |
| :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- | :--- | :- | :- | :- | :- | :- |
| [default](docs/rules/default.md) | Ensure a default export is present, given a default import. | ❗ ☑️ | | | | | |
| [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. | | | | 🔧 | | |
| [named](docs/rules/named.md) | Ensure named imports correspond to a named export in the remote file. | ❗ ☑️ | | ⌨️ | | | |
| [namespace](docs/rules/namespace.md) | Ensure imported namespaces contain dereferenced properties as they are dereferenced. | ❗ ☑️ | | | | | |
| [no-absolute-path](docs/rules/no-absolute-path.md) | Forbid import of modules using absolute paths. | | | | 🔧 | | |
| [no-cycle](docs/rules/no-cycle.md) | Forbid a module from importing a module with a dependency path back to itself. | | | | | | |
| [no-dynamic-require](docs/rules/no-dynamic-require.md) | Forbid `require()` calls with expressions. | | | | | | |
| [no-internal-modules](docs/rules/no-internal-modules.md) | Forbid importing the submodules of other modules. | | | | | | |
| [no-relative-packages](docs/rules/no-relative-packages.md) | Forbid importing packages through relative paths. | | | | 🔧 | | |
| [no-relative-parent-imports](docs/rules/no-relative-parent-imports.md) | Forbid importing modules from parent directories. | | | | | | |
| [no-restricted-paths](docs/rules/no-restricted-paths.md) | Enforce which files can be imported in a given folder. | | | | | | |
| [no-self-import](docs/rules/no-self-import.md) | Forbid a module from importing itself. | | | | | | |
| [no-unresolved](docs/rules/no-unresolved.md) | Ensure imports point to a file/module that can be resolved. | ❗ ☑️ | | | | | |
| [no-useless-path-segments](docs/rules/no-useless-path-segments.md) | Forbid unnecessary path segments in import and require statements. | | | | 🔧 | | |
| [no-webpack-loader-syntax](docs/rules/no-webpack-loader-syntax.md) | Forbid webpack loader syntax in imports. | | | | | | |

### Style guide

Expand Down Expand Up @@ -495,6 +496,20 @@ For example, if your packages in a monorepo are all in `@scope`, you can configu
}
```

### `import/node-version`

A string that represents the version of Node.js that you are using.
A falsy value will imply the version of Node.js that you are running ESLint with.

```jsonc
// .eslintrc
{
"settings": {
"import/node-version": "22.3.4",
},
}
```

## SublimeLinter-eslint

SublimeLinter-eslint introduced a change to support `.eslintignore` files
Expand Down
81 changes: 81 additions & 0 deletions docs/rules/enforce-node-protocol-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# import/enforce-node-protocol-usage

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.

## Rule Details

This rule enforces that builtins node imports are using, or omitting, the `node:` protocol.

Determining whether a specifier is a core module depends on the node version being used to run `eslint`.
This version can be specified in the configuration with the [`import/node-version` setting](../../README.md#importnode-version).

Reasons to prefer using the protocol include:

- the code is more explicitly and clearly referencing a Node.js built-in module

Reasons to prefer omitting the protocol include:

- some tools don't support the `node:` protocol
- the code is more portable, because import maps and automatic polyfilling can be used

## Options

The rule requires a single string option which may be one of:

- `'always'` - enforces that builtins node imports are using the `node:` protocol.
- `'never'` - enforces that builtins node imports are not using the `node:` protocol.

## Examples

### `'always'`

❌ Invalid

```js
import fs from 'fs';
export { promises } from 'fs';
// require
const fs = require('fs/promises');
```

✅ Valid

```js
import fs from 'node:fs';
export { promises } from 'node:fs';
import * as test from 'node:test';
// require
const fs = require('node:fs/promises');
```

### `'never'`

❌ Invalid

```js
import fs from 'node:fs';
export { promises } from 'node:fs';
// require
const fs = require('node:fs/promises');
```

✅ Valid

```js
import fs from 'fs';
export { promises } from 'fs';

// require
const fs = require('fs/promises');

// This rule will not enforce not using `node:` protocol when the module is only available under the `node:` protocol.
import * as test from 'node:test';
```

## When Not To Use It

If you don't want to consistently enforce using, or omitting, the `node:` protocol when importing Node.js builtin modules.
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const rules = {
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
'no-import-module-exports': require('./rules/no-import-module-exports'),
'no-empty-named-blocks': require('./rules/no-empty-named-blocks'),
'enforce-node-protocol-usage': require('./rules/enforce-node-protocol-usage'),

// export
'exports-last': require('./rules/exports-last'),
Expand Down
147 changes: 147 additions & 0 deletions src/rules/enforce-node-protocol-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict';

const isCoreModule = require('is-core-module');
const { default: docsUrl } = require('../docsUrl');

const DO_PREFER_MESSAGE_ID = 'requireNodeProtocol';
const NEVER_PREFER_MESSAGE_ID = 'forbidNodeProtocol';
const messages = {
[DO_PREFER_MESSAGE_ID]: 'Prefer `node:{{moduleName}}` over `{{moduleName}}`.',
[NEVER_PREFER_MESSAGE_ID]: 'Prefer `{{moduleName}}` over `node:{{moduleName}}`.',
};

function replaceStringLiteral(
fixer,
node,
text,
relativeRangeStart,
relativeRangeEnd,
) {
const firstCharacterIndex = node.range[0] + 1;
const start = Number.isInteger(relativeRangeEnd)
? relativeRangeStart + firstCharacterIndex
: firstCharacterIndex;

Check warning on line 23 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L23

Added line #L23 was not covered by tests
const end = Number.isInteger(relativeRangeEnd)
? relativeRangeEnd + firstCharacterIndex
: node.range[1] - 1;

Check warning on line 26 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L26

Added line #L26 was not covered by tests

return fixer.replaceTextRange([start, end], text);
}

function isStringLiteral(node) {
return node.type === 'Literal' && typeof node.value === 'string';
}

function isStaticRequireWith1Param(node) {
return !node.optional
&& node.callee.type === 'Identifier'
&& node.callee.name === 'require'
// check for only 1 argument
&& node.arguments.length === 1
&& node.arguments[0]
&& isStringLiteral(node.arguments[0]);
}

function checkAndReport(src, context) {
// TODO use src.quasis[0].value.raw
if (src.type === 'TemplateLiteral') { return; }
const moduleName = 'value' in src ? src.value : src.name;
if (typeof moduleName !== 'string') { console.log(src, moduleName); }
const { settings } = context;
const nodeVersion = settings && settings['import/node-version'];
if (
typeof nodeVersion !== 'undefined'
&& (
typeof nodeVersion !== 'string'
|| !(/^[0-9]+\.[0-9]+\.[0-9]+$/).test(nodeVersion)
)
) {
throw new TypeError('`import/node-version` setting must be a string in the format "10.23.45" (a semver version, with no leading zero)');

Check warning on line 59 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L59

Added line #L59 was not covered by tests
}

if (context.options[0] === 'never') {
if (!moduleName.startsWith('node:')) { return; }

const actualModuleName = moduleName.slice(5);
if (!isCoreModule(actualModuleName, nodeVersion || undefined)) { return; }

context.report({
node: src,
message: messages[NEVER_PREFER_MESSAGE_ID],
data: { moduleName: actualModuleName },
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix(fixer) {
return replaceStringLiteral(fixer, src, '', 0, 5);
},
});
} else if (context.options[0] === 'always') {
if (
moduleName.startsWith('node:')
|| !isCoreModule(moduleName, nodeVersion || undefined)
|| !isCoreModule(`node:${moduleName}`, nodeVersion || undefined)

Check warning on line 81 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L81

Added line #L81 was not covered by tests
) {
return;
}

context.report({
node: src,
message: messages[DO_PREFER_MESSAGE_ID],
data: { moduleName },
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix(fixer) {
return replaceStringLiteral(fixer, src, 'node:', 0, 0);

Check warning on line 92 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L92

Added line #L92 was not covered by tests
},
});
} else if (typeof context.options[0] === 'undefined') {
throw new Error('Missing option');

Check warning on line 96 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L96

Added line #L96 was not covered by tests
} else {
throw new Error(`Unexpected option: ${context.options[0]}`);

Check warning on line 98 in src/rules/enforce-node-protocol-usage.js

View check run for this annotation

Codecov / codecov/patch

src/rules/enforce-node-protocol-usage.js#L98

Added line #L98 was not covered by tests
}
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce either using, or omitting, the `node:` protocol when importing Node.js builtin modules.',
recommended: true,
category: 'Static analysis',
url: docsUrl('enforce-node-protocol-usage'),
},
fixable: 'code',
schema: {
type: 'array',
minItems: 1,
maxItems: 1,
items: [
{
enum: ['always', 'never'],
},
],
},
messages,
},
create(context) {
return {
CallExpression(node) {
if (!isStaticRequireWith1Param(node)) { return; }

const arg = node.arguments[0];

return checkAndReport(arg, context);
},
ExportNamedDeclaration(node) {
return checkAndReport(node.source, context);
},
ImportDeclaration(node) {
return checkAndReport(node.source, context);
},
ImportExpression(node) {
if (!isStringLiteral(node.source)) { return; }

return checkAndReport(node.source, context);
},
};
},
};
Loading
Loading