Skip to content

Commit

Permalink
[New] add enforce-node-protocol-usage rule
Browse files Browse the repository at this point in the history
Co-authored-by: Mikhail Pertsev <mikhail.pertsev@brightpattern.com>
Co-authored-by: sevenc-nanashi <sevenc7c@sevenc7c.com>
  • Loading branch information
2 people authored and ljharb committed Jul 4, 2024
1 parent d5f2950 commit 165fb3b
Show file tree
Hide file tree
Showing 6 changed files with 549 additions and 19 deletions.
13 changes: 10 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 ([#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 @@ -1110,6 +1113,7 @@ for info on changes for earlier releases.
[`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 +1173,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 +1793,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 +1812,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 +1847,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 +1907,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 +1983,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
33 changes: 17 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
75 changes: 75 additions & 0 deletions docs/rules/enforce-node-protocol-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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.

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';
import * as test from 'node:test';
// require
const fs = require('fs/promises');
```

## 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
131 changes: 131 additions & 0 deletions src/rules/enforce-node-protocol-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
'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;
const end = Number.isInteger(relativeRangeEnd)
? relativeRangeEnd + firstCharacterIndex
: node.range[1] - 1;

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) {
const { value: moduleName } = src;
if (!isCoreModule(moduleName)) { return; }

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

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

context.report({
node: src,
messageId: 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(`node:${moduleName}`)
) {
return;
}

context.report({
node: src,
messageId: DO_PREFER_MESSAGE_ID,
data: { moduleName },
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix(fixer) {
return replaceStringLiteral(fixer, src, 'node:', 0, 0);
},
});
} else if (typeof context.options[0] === 'undefined') {
throw new Error('Missing option');
} else {
throw new Error(`Unexpected option: ${context.options[0]}`);
}
}

/** @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) {
return checkAndReport(node.source, context);
},
};
},
};
Loading

0 comments on commit 165fb3b

Please sign in to comment.