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 e897498
Show file tree
Hide file tree
Showing 5 changed files with 509 additions and 16 deletions.
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
73 changes: 73 additions & 0 deletions docs/rules/enforce-node-protocol-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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
130 changes: 130 additions & 0 deletions src/rules/enforce-node-protocol-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

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

const DO_PREFER_MESSAGE_ID = 'preferNodeBuiltinImports';
const NEVER_PREFER_MESSAGE_ID = 'neverPreferNodeBuiltinImports';
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'
&& node.arguments[0]
&& isStringLiteral(node.arguments[0])
// check for only 1 argument
&& !node.arguments[1];
}

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:')) { 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: [
{
enum: ['always', 'never'],
required: true,
},
],
messages,
},
create(context) {
return {
CallExpression(node) {
if (!isStaticRequireWith1Param(node)) { return; }

const arg = node.arguments[0];

if (!isStringLiteral(arg)) { return; }

return checkAndReport(arg, context);
},
ExportNamedDeclaration(node) {
if (!isStringLiteral(node)) { return; }

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

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

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

0 comments on commit e897498

Please sign in to comment.