Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ For [Shareable Configs](https://eslint.org/docs/latest/developer-guide/shareable
| [prefer-global/url](docs/rules/prefer-global/url.md) | enforce either `URL` or `require("url").URL` | | | |
| [prefer-global/url-search-params](docs/rules/prefer-global/url-search-params.md) | enforce either `URLSearchParams` or `require("url").URLSearchParams` | | | |
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | enforce using the `node:` protocol when importing Node.js builtin modules. | | 🔧 | |
| [prefer-process-get-builtin-module](docs/rules/prefer-process-get-builtin-module.md) | enforce using `process.getBuiltinModule()` to load Node.js built-in modules | | | |
| [prefer-promises/dns](docs/rules/prefer-promises/dns.md) | enforce `require("dns").promises` | | | |
| [prefer-promises/fs](docs/rules/prefer-promises/fs.md) | enforce `require("fs").promises` | | | |
| [process-exit-as-throw](docs/rules/process-exit-as-throw.md) | require that `process.exit()` expressions use the same code path as `throw` | 🟢 ✅ | | |
Expand Down
76 changes: 76 additions & 0 deletions docs/rules/prefer-process-get-builtin-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# n/prefer-process-get-builtin-module

📝 Enforce using `process.getBuiltinModule()` to load Node.js built-in modules.

<!-- end auto-generated rule header -->

Node.js exposes built-in modules synchronously through `process.getBuiltinModule()`.
In ES modules, this avoids creating a `require` function solely to access a
built-in module. It also communicates that the requested module is built into
Node.js.

This API is available starting in Node.js 20.16.0 on the 20.x release line and
in Node.js 22.3.0 or later.

## 📖 Rule Details

This rule reports calls to `require()` for built-in modules and awaited dynamic
imports of built-in modules. It ignores non-built-in modules, dynamic imports
that are not awaited directly, arbitrary functions named `require`, and
references where `process` is shadowed. A local `require` created with
`createRequire()` from `node:module` is recognized.

This rule is not automatically fixable. An awaited dynamic import returns an
ES module namespace object, while `process.getBuiltinModule()` returns the
underlying built-in exports object. Review how the loaded module is used when
applying the suggested replacement.

👍 Examples of **correct** code for this rule:

```js
/*eslint n/prefer-process-get-builtin-module: error */

const fs = process.getBuiltinModule("node:fs")
const eslint = require("eslint")
const lazyFs = import("node:fs")
```

👎 Examples of **incorrect** code for this rule:

```js
/*eslint n/prefer-process-get-builtin-module: error */

const fs = require("node:fs")
const promises = await import("node:fs/promises")
```

### Configured Node.js version range

[Configured Node.js version range](../../README.md#configured-nodejs-version-range)

### Options

```json
{
"n/prefer-process-get-builtin-module": [
"error",
{
"version": ">=22.3.0"
}
]
}
```

Comment thread
aladdin-add marked this conversation as resolved.
#### version

This rule reads the [`engines`] field of `package.json`. You can override that
range with the `version` option, which accepts any valid
[`node-semver` range](https://github.com/npm/node-semver#range-grammar).

The rule does not report when the configured range includes Node.js versions
without `process.getBuiltinModule()`.

## 🔎 Implementation

- [Rule source](../../lib/rules/prefer-process-get-builtin-module.js)
- [Test source](../../tests/lib/rules/prefer-process-get-builtin-module.js)
2 changes: 2 additions & 0 deletions lib/all-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import preferGlobalUrlSearchParams from "./rules/prefer-global/url-search-params
import preferGlobalUrl from "./rules/prefer-global/url.js"
import preferGlobalTimers from "./rules/prefer-global/timers.js"
import preferNodeProtocol from "./rules/prefer-node-protocol.js"
import preferProcessGetBuiltinModule from "./rules/prefer-process-get-builtin-module.js"
import preferPromisesDns from "./rules/prefer-promises/dns.js"
import preferPromisesFs from "./rules/prefer-promises/fs.js"
import processExitAsThrow from "./rules/process-exit-as-throw.js"
Expand Down Expand Up @@ -87,6 +88,7 @@ const allRules = {
"prefer-global/url": preferGlobalUrl,
"prefer-global/timers": preferGlobalTimers,
"prefer-node-protocol": preferNodeProtocol,
"prefer-process-get-builtin-module": preferProcessGetBuiltinModule,
"prefer-promises/dns": preferPromisesDns,
"prefer-promises/fs": preferPromisesFs,
"process-exit-as-throw": processExitAsThrow,
Expand Down
164 changes: 164 additions & 0 deletions lib/rules/prefer-process-get-builtin-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* @author ColumbusLabs
* See LICENSE file in root directory for full license.
*/

import { isBuiltin } from "node:module"
import {
findVariable,
getStringIfConstant,
} from "@eslint-community/eslint-utils"
import { Range, subset } from "semver"
import { getConfiguredNodeVersion } from "../util/get-configured-node-version.js"
import { schema as configuredNodeVersionSchema } from "../util/get-configured-node-version.js"

const supportedRange = new Range("^20.16.0 || >=22.3.0")

/**
* @param {import("eslint").Rule.RuleContext} context
* @param {import("estree").Node} node
* @returns {boolean}
*/
function isProcessShadowed(context, node) {
const scope = context.sourceCode.getScope(node)
const variable = findVariable(scope, "process")
return Boolean(variable?.defs.length)
}

/**
* @param {import("eslint").Rule.RuleContext} context
* @param {import("estree").Identifier} node
* @returns {boolean}
*/
function isCreateRequireImport(context, node) {
const variable = findVariable(context.sourceCode.getScope(node), node)
return Boolean(
variable?.defs.some(
definition =>
definition.type === "ImportBinding" &&
definition.node.type === "ImportSpecifier" &&
definition.node.imported.type === "Identifier" &&
definition.node.imported.name === "createRequire" &&
(definition.parent.source.value === "node:module" ||
definition.parent.source.value === "module")
)
)
}

/**
* @param {import("eslint").Rule.RuleContext} context
* @param {import("estree").Identifier} node
* @returns {boolean}
*/
function isNodeRequire(context, node) {
const variable = findVariable(context.sourceCode.getScope(node), node)
if (variable == null || variable.defs.length === 0) {
return true
}

return variable.defs.some(definition => {
if (
definition.type !== "Variable" ||
definition.node.type !== "VariableDeclarator" ||
definition.parent.type !== "VariableDeclaration" ||
definition.parent.kind !== "const" ||
definition.node.init?.type !== "CallExpression" ||
definition.node.init.callee.type !== "Identifier"
) {
return false
}
return isCreateRequireImport(context, definition.node.init.callee)
})
}

/**
* @param {import("estree").Node | null | undefined} node
* @returns {boolean}
*/
function isBuiltinModuleName(node) {
if (node == null || node.type === "SpreadElement") {
return false
}
const name = getStringIfConstant(node)
return typeof name === "string" && isBuiltin(name)
}

/** @type {import("./rule-module.js").RuleModule} */
export default {
meta: {
docs: {
description:
"enforce using `process.getBuiltinModule()` to load Node.js built-in modules",
recommended: false,
url: "https://github.com/eslint-community/eslint-plugin-n/blob/HEAD/docs/rules/prefer-process-get-builtin-module.md",
},
messages: {
preferProcessGetBuiltinModule:
"Prefer `process.getBuiltinModule()` over `{{method}}()` for Node.js built-in modules.",
},
schema: [
{
type: "object",
properties: {
version: configuredNodeVersionSchema,
},
additionalProperties: false,
},
],
type: "suggestion",
},
create(context) {
if (!subset(getConfiguredNodeVersion(context), supportedRange)) {
return {}
}

/**
* @param {import("estree").CallExpression} node
*/
function reportRequire(node) {
if (
node.callee.type !== "Identifier" ||
node.callee.name !== "require" ||
!isNodeRequire(context, node.callee) ||
("optional" in node && node.optional) ||
node.arguments.length !== 1 ||
!isBuiltinModuleName(node.arguments[0]) ||
isProcessShadowed(context, node)
) {
return
}

context.report({
node,
messageId: "preferProcessGetBuiltinModule",
data: { method: "require" },
})
}

/**
* @param {import("estree").AwaitExpression} node
*/
function reportImport(node) {
const importExpression = node.argument
if (
importExpression.type !== "ImportExpression" ||
importExpression.options != null ||
!isBuiltinModuleName(importExpression.source) ||
isProcessShadowed(context, node)
) {
return
}

context.report({
node,
messageId: "preferProcessGetBuiltinModule",
data: { method: "import" },
})
}

return {
AwaitExpression: reportImport,
CallExpression: reportRequire,
}
},
}
Loading