Skip to content

Commit 4efe60f

Browse files
authored
perf: improve prefer-node-protocol's performance (#406)
1 parent 86a5242 commit 4efe60f

File tree

1 file changed

+131
-135
lines changed

1 file changed

+131
-135
lines changed

lib/rules/prefer-node-protocol.js

+131-135
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
*/
55
"use strict"
66

7+
const { getStringIfConstant } = require("@eslint-community/eslint-utils")
8+
79
const { Range } = require("semver")
810

911
const getConfiguredNodeVersion = require("../util/get-configured-node-version")
10-
const visitImport = require("../util/visit-import")
11-
const visitRequire = require("../util/visit-require")
12-
const mergeVisitorsInPlace = require("../util/merge-visitors-in-place")
12+
const stripImportPathParams = require("../util/strip-import-path-params")
13+
1314
const {
1415
NodeBuiltinModules,
1516
} = require("../unsupported-features/node-builtins.js")
@@ -27,6 +28,104 @@ const messageId = "preferNodeProtocol"
2728
const supportedRangeForEsm = new Range("^12.20.0 || >= 14.13.1")
2829
const supportedRangeForCjs = new Range("^14.18.0 || >= 16.0.0")
2930

31+
/**
32+
* @param {import('estree').Node} [node]
33+
* @returns {node is import('estree').Literal}
34+
*/
35+
function isStringLiteral(node) {
36+
return node?.type === "Literal" && typeof node.type === "string"
37+
}
38+
39+
/**
40+
* @param {import('eslint').Rule.RuleContext} context
41+
* @param {import('../util/import-target.js').ModuleStyle} moduleStyle
42+
* @returns {boolean}
43+
*/
44+
function isEnablingThisRule(context, moduleStyle) {
45+
const version = getConfiguredNodeVersion(context)
46+
47+
// Only check Node.js version because this rule is meaningless if configured Node.js version doesn't match semver range.
48+
if (!version.intersects(supportedRangeForEsm)) {
49+
return false
50+
}
51+
52+
// Only check when using `require`
53+
if (
54+
moduleStyle === "require" &&
55+
!version.intersects(supportedRangeForCjs)
56+
) {
57+
return false
58+
}
59+
60+
return true
61+
}
62+
63+
/**
64+
* @param {import('estree').Node} node
65+
* @returns {boolean}
66+
**/
67+
function isValidRequireArgument(node) {
68+
const rawName = getStringIfConstant(node)
69+
if (typeof rawName !== "string") {
70+
return false
71+
}
72+
73+
const name = stripImportPathParams(rawName)
74+
if (!isBuiltin(name)) {
75+
return false
76+
}
77+
78+
return true
79+
}
80+
81+
/**
82+
* @param {import('estree').Node | null | undefined} node
83+
* @param {import('eslint').Rule.RuleContext} context
84+
* @param {import('../util/import-target.js').ModuleStyle} moduleStyle
85+
*/
86+
function validate(node, context, moduleStyle) {
87+
if (node == null) {
88+
return
89+
}
90+
91+
if (!isEnablingThisRule(context, moduleStyle)) {
92+
return
93+
}
94+
95+
if (!isStringLiteral(node)) {
96+
return
97+
}
98+
99+
if (moduleStyle === "require" && !isValidRequireArgument(node)) {
100+
return
101+
}
102+
103+
if (
104+
!("value" in node) ||
105+
typeof node.value !== "string" ||
106+
node.value.startsWith("node:") ||
107+
!isBuiltin(node.value) ||
108+
!isBuiltin(`node:${node.value}`)
109+
) {
110+
return
111+
}
112+
113+
context.report({
114+
node,
115+
messageId,
116+
data: {
117+
moduleName: node.value,
118+
},
119+
fix(fixer) {
120+
const firstCharacterIndex = (node?.range?.[0] ?? 0) + 1
121+
return fixer.replaceTextRange(
122+
[firstCharacterIndex, firstCharacterIndex],
123+
"node:"
124+
)
125+
},
126+
})
127+
}
128+
30129
/** @type {import('eslint').Rule.RuleModule} */
31130
module.exports = {
32131
meta: {
@@ -52,139 +151,36 @@ module.exports = {
52151
type: "suggestion",
53152
},
54153
create(context) {
55-
/**
56-
* @param {import('estree').Node} node
57-
* @param {object} options
58-
* @param {string} options.name
59-
* @param {number} options.argumentsLength
60-
* @returns {node is import('estree').CallExpression}
61-
*/
62-
function isCallExpression(node, { name, argumentsLength }) {
63-
if (node?.type !== "CallExpression") {
64-
return false
65-
}
66-
67-
if (node.optional) {
68-
return false
69-
}
70-
71-
if (node.arguments.length !== argumentsLength) {
72-
return false
73-
}
74-
75-
if (
76-
node.callee.type !== "Identifier" ||
77-
node.callee.name !== name
78-
) {
79-
return false
80-
}
81-
82-
return true
83-
}
84-
85-
/**
86-
* @param {import('estree').Node} [node]
87-
* @returns {node is import('estree').Literal}
88-
*/
89-
function isStringLiteral(node) {
90-
return node?.type === "Literal" && typeof node.type === "string"
91-
}
92-
93-
/**
94-
* @param {import('estree').Node | undefined} node
95-
* @returns {node is import('estree').CallExpression}
96-
*/
97-
function isStaticRequire(node) {
98-
return (
99-
node != null &&
100-
isCallExpression(node, {
101-
name: "require",
102-
argumentsLength: 1,
103-
}) &&
104-
isStringLiteral(node.arguments[0])
105-
)
106-
}
107-
108-
/**
109-
* @param {import('eslint').Rule.RuleContext} context
110-
* @param {import('../util/import-target.js').ModuleStyle} moduleStyle
111-
* @returns {boolean}
112-
*/
113-
function isEnablingThisRule(context, moduleStyle) {
114-
const version = getConfiguredNodeVersion(context)
115-
116-
// Only check Node.js version because this rule is meaningless if configured Node.js version doesn't match semver range.
117-
if (!version.intersects(supportedRangeForEsm)) {
118-
return false
119-
}
120-
121-
// Only check when using `require`
122-
if (
123-
moduleStyle === "require" &&
124-
!version.intersects(supportedRangeForCjs)
125-
) {
126-
return false
127-
}
128-
129-
return true
130-
}
154+
return {
155+
CallExpression(node) {
156+
if (node.type !== "CallExpression") {
157+
return
158+
}
159+
160+
if (
161+
node.optional ||
162+
node.arguments.length !== 1 ||
163+
node.callee.type !== "Identifier" ||
164+
node.callee.name !== "require"
165+
) {
166+
return
167+
}
168+
169+
return validate(node.arguments[0], context, "require")
170+
},
131171

132-
/** @type {import('../util/import-target.js')[]} */
133-
const targets = []
134-
return [
135-
visitImport(context, { includeCore: true }, importTargets => {
136-
targets.push(...importTargets)
137-
}),
138-
visitRequire(context, { includeCore: true }, requireTargets => {
139-
targets.push(
140-
...requireTargets.filter(target =>
141-
isStaticRequire(target.node.parent)
142-
)
143-
)
144-
}),
145-
{
146-
"Program:exit"() {
147-
for (const { node, moduleStyle } of targets) {
148-
if (!isEnablingThisRule(context, moduleStyle)) {
149-
continue
150-
}
151-
152-
if (node.type === "TemplateLiteral") {
153-
continue
154-
}
155-
156-
if (
157-
!("value" in node) ||
158-
typeof node.value !== "string" ||
159-
node.value.startsWith("node:") ||
160-
!isBuiltin(node.value) ||
161-
!isBuiltin(`node:${node.value}`)
162-
) {
163-
continue
164-
}
165-
166-
context.report({
167-
node,
168-
messageId,
169-
data: {
170-
moduleName: node.value,
171-
},
172-
fix(fixer) {
173-
const firstCharacterIndex =
174-
(node?.range?.[0] ?? 0) + 1
175-
return fixer.replaceTextRange(
176-
[firstCharacterIndex, firstCharacterIndex],
177-
"node:"
178-
)
179-
},
180-
})
181-
}
182-
},
172+
ExportAllDeclaration(node) {
173+
return validate(node.source, context, "import")
183174
},
184-
].reduce(
185-
(mergedVisitor, thisVisitor) =>
186-
mergeVisitorsInPlace(mergedVisitor, thisVisitor),
187-
{}
188-
)
175+
ExportNamedDeclaration(node) {
176+
return validate(node.source, context, "import")
177+
},
178+
ImportDeclaration(node) {
179+
return validate(node.source, context, "import")
180+
},
181+
ImportExpression(node) {
182+
return validate(node.source, context, "import")
183+
},
184+
}
189185
},
190186
}

0 commit comments

Comments
 (0)