-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New] add
enforce-node-protocol-usage
rule
Co-authored-by: Mikhail Pertsev <mikhail.pertsev@brightpattern.com> Co-authored-by: sevenc-nanashi <sevenc7c@sevenc7c.com>
- Loading branch information
Showing
6 changed files
with
563 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
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(`node:${moduleName}`) | ||
) { | ||
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); | ||
}, | ||
}); | ||
} 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); | ||
}, | ||
}; | ||
}, | ||
}; |
Oops, something went wrong.