Repository: import-js/eslint-plugin-import (js)

Issue:
Suggestion: new rule to enforce usage of `node:` prefix for Node.js built-ins
I searched a bit in the existing issues and didn't see someone asking for anything similar, also couldn't find in the existing rules. I think this could belong to this package.

Benefits (copied this from 3rd party resources, and added a few personal touches):
- More explicit protocol makes it more readable and also makes it clear that a built-in module is being imported. Since there are a lot of built-in ones, this extra information is useful for beginners or for someone who might not know about one of them. I once had a senior engineer with 10 years of experience install [assert](https://www.npmjs.com/package/assert) without knowing that Node provided it.
- It reduces the risk of a module present in node_modules overriding the newer imports.
- If the prefix is used, the built-in module is always returned, completely bypassing the require cache. For instance, require('node:http') will always return the built in HTTP module, even if there is require.cache entry by that name.
- New core modules in the future may only be available if the prefix is used, so enforcing it for everything will keep everything consistent. For instance, the new built-in test module introduced in Node.js 18 can only be used via `node:test`.

Pull request description:
[New] add `enforce-node-protocol-usage` rule
Fixes #2717
Added a new rule `prefer-node-builtin-imports`.
All test cases are taken from [eslint-plugin-unicorn_test](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/test/prefer-node-protocol.mjs)

Patch:
diff --git a/.markdownlint.json b/.markdownlint.json
index 02a06673..f434832d 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -1,5 +1,6 @@
 {
   "line-length": false,
+  "ignore_case": true,
   "no-duplicate-heading": {
     "siblings_only": true
   },
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2174fc46..b3a92d01 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 8cc72342..81dedf65 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,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
diff --git a/docs/rules/enforce-node-protocol-usage.md b/docs/rules/enforce-node-protocol-usage.md
new file mode 100644
index 00000000..8d023b4e
--- /dev/null
+++ b/docs/rules/enforce-node-protocol-usage.md
@@ -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.
diff --git a/src/index.js b/src/index.js
index 0ab82ebe..62526206 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 00000000..88c31fc7
--- /dev/null
+++ b/src/rules/enforce-node-protocol-usage.js
@@ -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;
+  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) {
+  // 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)');
+  }
+
+  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) {
+        if (!isStringLiteral(node.source)) { return; }
+
+        return checkAndReport(node.source, context);
+      },
+    };
+  },
+};


Test patch:
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 00000000..f9bbcbad
--- /dev/null
+++ b/tests/src/rules/enforce-node-protocol-usage.js
@@ -0,0 +1,345 @@
+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' },
+      },
+    ],
+    settings,
+  },
+  {
+    code: 'export {default} from "fs/promises";',
+    output: 'export {default} from "node:fs/promises";',
+    options: preferUsingProtocol,
+    errors: [
+      {
+        messageId: 'requireNodeProtocol',
+        data: { moduleName: 'fs/promises' },
+      },
+    ],
+    settings,
+  },
+  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' },
+      },
+    ],
+    settings,
+  })),
+  {
+    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' },
+      },
+    ],
+    settings,
+  })),
+  {
+    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' },
+      },
+    ],
+    settings,
+  },
+  {
+    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' },
+      },
+    ],
+    settings,
+  },
+);
+
+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,
+      settings,
+    }),
+    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,
+      settings,
+    }),
+    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),
+      })),
+      settings,
+    })),
+  ),
+});