From ad0d6f5688e36d78ab42865edbe289498c3ca85a Mon Sep 17 00:00:00 2001 From: Mikhail Pertsev Date: Thu, 4 Jul 2024 10:49:15 +0200 Subject: [PATCH] [New] add `enforce-node-protocol-usage` rule and `import/node-version` setting Co-authored-by: Mikhail Pertsev Co-authored-by: sevenc-nanashi --- CHANGELOG.md | 14 +- README.md | 46 ++- docs/rules/enforce-node-protocol-usage.md | 75 ++++ src/index.js | 1 + src/rules/enforce-node-protocol-usage.js | 142 ++++++++ .../src/rules/enforce-node-protocol-usage.js | 336 ++++++++++++++++++ 6 files changed, 595 insertions(+), 19 deletions(-) create mode 100644 docs/rules/enforce-node-protocol-usage.md create mode 100644 src/rules/enforce-node-protocol-usage.js create mode 100644 tests/src/rules/enforce-node-protocol-usage.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2174fc46b..b3a92d015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index 8cc723423..10ea28ce4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -495,6 +496,19 @@ 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 diff --git a/docs/rules/enforce-node-protocol-usage.md b/docs/rules/enforce-node-protocol-usage.md new file mode 100644 index 000000000..e4a30db62 --- /dev/null +++ b/docs/rules/enforce-node-protocol-usage.md @@ -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). + + + +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. diff --git a/src/index.js b/src/index.js index 0ab82ebee..625262060 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), diff --git a/src/rules/enforce-node-protocol-usage.js b/src/rules/enforce-node-protocol-usage.js new file mode 100644 index 000000000..eda2b9b3a --- /dev/null +++ b/src/rules/enforce-node-protocol-usage.js @@ -0,0 +1,142 @@ +'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; + const { settings } = context; + const nodeVersion = settings && settings['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)'); + } + + 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) + ) { + 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); + }, + }; + }, +}; diff --git a/tests/src/rules/enforce-node-protocol-usage.js b/tests/src/rules/enforce-node-protocol-usage.js new file mode 100644 index 000000000..bd3f396fa --- /dev/null +++ b/tests/src/rules/enforce-node-protocol-usage.js @@ -0,0 +1,336 @@ +import { RuleTester } from '../rule-tester'; +import flatMap from 'array.prototype.flatmap'; +import { satisfies } from 'semver'; + +import { test, testVersion } from '../utils'; + +const ruleTester = new RuleTester(); +const rule = require('rules/enforce-node-protocol-usage'); + +const preferUsingProtocol = ['always']; +const preferNotUsingProtocol = ['never']; +const useNewerParser = { ecmaVersion: 2021 }; + +const actualModules = ['fs', 'fs/promises', 'buffer', 'child_process', 'timers/promises']; + +const settings = { + 'import/node-version': '16.0.0', // the node: prefix is only available as of `^14.18 || >= 16` +}; + +const invalidTests = [].concat( + flatMap(actualModules, (moduleName) => [].concat( + { + code: `import x from "${moduleName}";`, + output: `import x from "node:${moduleName}";`, + options: preferUsingProtocol, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName } }, + ], + }, + { + code: `export {promises} from "${moduleName}";`, + output: `export {promises} from "node:${moduleName}";`, + options: preferUsingProtocol, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName } }, + ], + }, + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const x = await import('${moduleName}'); + } + `, + output: ` + async function foo() { + const x = await import('node:${moduleName}'); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName } }, + ], + })), + )), + + { + code: 'import fs from "fs/promises";', + output: 'import fs from "node:fs/promises";', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'fs/promises' }, + }, + ], + }, + { + code: 'export {default} from "fs/promises";', + output: 'export {default} from "node:fs/promises";', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'fs/promises' }, + }, + ], + }, + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const fs = await import('fs/promises'); + } + `, + output: ` + async function foo() { + const fs = await import('node:fs/promises'); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'fs/promises' }, + }, + ], + })), + { + code: 'import {promises} from "fs";', + output: 'import {promises} from "node:fs";', + options: preferUsingProtocol, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName: 'fs' } }, + ], + }, + { + code: 'export {default as promises} from "fs";', + output: 'export {default as promises} from "node:fs";', + options: preferUsingProtocol, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName: 'fs' } }, + ], + }, + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const fs = await import("fs/promises"); + } + `, + output: ` + async function foo() { + const fs = await import("node:fs/promises"); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'fs/promises' }, + }, + ], + })), + { + code: 'import "buffer";', + output: 'import "node:buffer";', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'buffer' }, + }, + ], + }, + { + code: 'import "child_process";', + output: 'import "node:child_process";', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'child_process' }, + }, + ], + }, + { + code: 'import "timers/promises";', + output: 'import "node:timers/promises";', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'timers/promises' }, + }, + ], + }, + { + code: 'const {promises} = require("fs")', + output: 'const {promises} = require("node:fs")', + options: preferUsingProtocol, + errors: [ + { messageId: 'requireNodeProtocol', data: { moduleName: 'fs' } }, + ], + }, + { + code: 'const fs = require("fs/promises")', + output: 'const fs = require("node:fs/promises")', + options: preferUsingProtocol, + errors: [ + { + messageId: 'requireNodeProtocol', + data: { moduleName: 'fs/promises' }, + }, + ], + }, +); + +ruleTester.run('enforce-node-protocol-usage', rule, { + valid: [].concat( + test({ + code: 'import unicorn from "unicorn";', + options: preferUsingProtocol, + }), + test({ + code: 'import fs from "./fs";', + options: preferUsingProtocol, + }), + test({ + code: 'import fs from "unknown-builtin-module";', + options: preferUsingProtocol, + }), + test({ + code: 'import fs from "node:fs";', + options: preferUsingProtocol, + }), + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const fs = await import(fs); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + })), + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const fs = await import(0); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + })), + testVersion('>= 7', () => ({ + code: ` + async function foo() { + const fs = await import(\`fs\`); + } + `, + options: preferUsingProtocol, + parserOptions: useNewerParser, + })), + test({ + code: 'import "punycode/";', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require("node:fs");', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require("node:fs/promises");', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require(fs);', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = notRequire("fs");', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = foo.require("fs");', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require.resolve("fs");', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require(`fs`);', + options: preferUsingProtocol, + }), + testVersion('>= 7', () => ({ + code: 'const fs = require?.("fs");', + parserOptions: useNewerParser, + options: preferUsingProtocol, + })), + test({ + code: 'const fs = require("fs", extra);', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require();', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require(...["fs"]);', + options: preferUsingProtocol, + }), + test({ + code: 'const fs = require("unicorn");', + options: preferUsingProtocol, + }), + test({ + code: 'import fs from "fs";', + options: preferNotUsingProtocol, + }), + test({ + code: 'const fs = require("fs");', + options: preferNotUsingProtocol, + }), + test({ + code: 'const fs = require("fs/promises");', + options: preferNotUsingProtocol, + }), + test({ + code: 'import "punycode/";', + options: preferNotUsingProtocol, + }), + + // should not report if the module requires `node:` protocol + test({ + code: 'const fs = require("node:test");', + options: preferNotUsingProtocol, + settings, + }), + ), + + invalid: [].concat( + // Prefer using the protocol + // in node versions without `node:`, the rule should not report + satisfies('^14.18 || >= 16') ? invalidTests.map((testCase) => test({ + ...testCase, + errors: testCase.errors.map(({ messageId, data, ...testCase }) => ({ + ...testCase, + message: rule.meta.messages[messageId].replace(/{{moduleName}}/g, data.moduleName), + })), + })) : [], + + // Prefer not using the protocol: flip the output and code + invalidTests.map((testCase) => test({ + ...testCase, + code: testCase.output, + options: preferNotUsingProtocol, + output: testCase.code, + // eslint-disable-next-line no-unused-vars + errors: testCase.errors.map(({ messageId, data, ...testCase }) => ({ + ...testCase, + message: rule.meta.messages.forbidNodeProtocol.replace(/{{moduleName}}/g, data.moduleName), + })), + })), + ), +});