Skip to content

Commit 3bdde2d

Browse files
authored
Change to handle variables listed in exported directive as unknown usage (#234)
1 parent 232ca08 commit 3bdde2d

13 files changed

+1433
-521
lines changed

lib/rules/no-unused-capturing-group.ts

Lines changed: 70 additions & 277 deletions
Large diffs are not rendered by default.

lib/rules/no-useless-flag.ts

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import { compositingVisitors, createRule, defineRegexpVisitor } from "../utils"
33
import type {
44
CallExpression,
55
Expression,
6-
Identifier,
76
NewExpression,
87
Node,
98
RegExpLiteral,
109
Statement,
1110
} from "estree"
1211
import type { KnownMethodCall } from "../utils/ast-utils"
13-
import { findVariable, isKnownMethodCall, getParent } from "../utils/ast-utils"
12+
import {
13+
isKnownMethodCall,
14+
extractExpressionReferences,
15+
} from "../utils/ast-utils"
1416
import { createTypeTracker } from "../utils/type-tracker"
1517
import type { RuleListener } from "../types"
1618
import type { Rule } from "eslint"
@@ -60,6 +62,7 @@ class RegExpReference {
6062
replace?: boolean
6163
replaceAll?: boolean
6264
}
65+
hasUnusedExpression?: boolean
6366
} = {
6467
usedIn: {},
6568
track: true,
@@ -154,26 +157,6 @@ class RegExpReference {
154157
}
155158
}
156159

157-
/**
158-
* If the given expression node is assigned with a variable declaration, it returns the variable name node.
159-
*/
160-
function getVariableId(node: Expression) {
161-
const parent = getParent(node)
162-
if (
163-
!parent ||
164-
parent.type !== "VariableDeclarator" ||
165-
parent.init !== node ||
166-
parent.id.type !== "Identifier"
167-
) {
168-
return null
169-
}
170-
const decl = getParent(parent)
171-
if (decl && decl.type === "VariableDeclaration" && decl.kind === "const") {
172-
return parent.id
173-
}
174-
return null
175-
}
176-
177160
/**
178161
* Gets the location for reporting the flag.
179162
*/
@@ -493,29 +476,6 @@ function createRegExpReferenceExtractVisitor(
493476
const regExpReferenceMap = new Map<Node, RegExpReference>()
494477
const regExpReferenceList: RegExpReference[] = []
495478

496-
/**
497-
* Extract read references
498-
*/
499-
function extractReadReferences(node: Identifier): Identifier[] {
500-
const references: Identifier[] = []
501-
const variable = findVariable(context, node)
502-
if (!variable) {
503-
return references
504-
}
505-
for (const reference of variable.references) {
506-
if (reference.isRead()) {
507-
const id = getVariableId(reference.identifier)
508-
if (id) {
509-
references.push(...extractReadReferences(id))
510-
} else {
511-
references.push(reference.identifier)
512-
}
513-
}
514-
}
515-
516-
return references
517-
}
518-
519479
/** Verify for String.prototype.search() or String.prototype.split() */
520480
function verifyForSearchOrSplit(
521481
node: KnownMethodCall,
@@ -568,15 +528,16 @@ function createRegExpReferenceExtractVisitor(
568528
const regExpReference = new RegExpReference(regexpNode)
569529
regExpReferenceList.push(regExpReference)
570530
regExpReferenceMap.set(regexpNode, regExpReference)
571-
const id = getVariableId(regexpNode)
572-
if (id) {
573-
const readReferences = extractReadReferences(id)
574-
for (const ref of readReferences) {
575-
regExpReferenceMap.set(ref, regExpReference)
576-
regExpReference.addReadNode(ref)
531+
for (const ref of extractExpressionReferences(
532+
regexpNode,
533+
context,
534+
)) {
535+
if (ref.type === "argument" || ref.type === "member") {
536+
regExpReferenceMap.set(ref.node, regExpReference)
537+
regExpReference.addReadNode(ref.node)
538+
} else {
539+
regExpReference.markAsCannotTrack()
577540
}
578-
} else {
579-
regExpReference.addReadNode(regexpNode)
580541
}
581542
}
582543
return {} // not visit RegExpNodes
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import type { Rule } from "eslint"
2+
import type { Variable } from "eslint-scope"
3+
import type {
4+
ArrayPattern,
5+
ArrowFunctionExpression,
6+
CallExpression,
7+
Expression,
8+
ForOfStatement,
9+
FunctionDeclaration,
10+
FunctionExpression,
11+
Identifier,
12+
MemberExpression,
13+
ObjectPattern,
14+
Pattern,
15+
} from "estree"
16+
import { findFunction, findVariable, getParent } from "./utils"
17+
18+
export type ExpressionReference =
19+
| {
20+
// The result of the expression is not referenced.
21+
type: "unused"
22+
node: Expression
23+
}
24+
| {
25+
// Unknown what the expression was referenced for.
26+
type: "unknown"
27+
node: Expression
28+
}
29+
| {
30+
// The expression is exported.
31+
type: "exported"
32+
node: Expression
33+
}
34+
| {
35+
// The expression is referenced for member access.
36+
type: "member"
37+
node: Expression
38+
memberExpression: MemberExpression
39+
}
40+
| {
41+
// The expression is referenced for destructuring.
42+
type: "destructuring"
43+
node: Expression
44+
pattern: ObjectPattern | ArrayPattern
45+
}
46+
| {
47+
// The expression is referenced to give as an argument.
48+
type: "argument"
49+
node: Expression
50+
callExpression: CallExpression
51+
}
52+
| {
53+
// The expression is referenced to call.
54+
type: "call"
55+
node: Expression
56+
}
57+
| {
58+
// The expression is referenced for iteration.
59+
type: "iteration"
60+
node: Expression
61+
for: ForOfStatement
62+
}
63+
64+
type AlreadyChecked = {
65+
variables: Set<Variable>
66+
functions: Map<
67+
FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
68+
Set<number>
69+
>
70+
}
71+
/** Extract references from the given expression */
72+
export function* extractExpressionReferences(
73+
node: Expression,
74+
context: Rule.RuleContext,
75+
): Iterable<ExpressionReference> {
76+
yield* iterateReferencesForExpression(node, context, {
77+
variables: new Set(),
78+
functions: new Map(),
79+
})
80+
}
81+
82+
/** Extract references from the given identifier */
83+
export function* extractExpressionReferencesForVariable(
84+
node: Identifier,
85+
context: Rule.RuleContext,
86+
): Iterable<ExpressionReference> {
87+
yield* iterateReferencesForVariable(node, context, {
88+
variables: new Set(),
89+
functions: new Map(),
90+
})
91+
}
92+
93+
/* eslint-disable complexity -- ignore */
94+
/** Iterate references from the given expression */
95+
function* iterateReferencesForExpression(
96+
/* eslint-enable complexity -- ignore */
97+
expression: Expression,
98+
context: Rule.RuleContext,
99+
alreadyChecked: AlreadyChecked,
100+
): Iterable<ExpressionReference> {
101+
let node = expression
102+
let parent = getParent(node)
103+
while (parent?.type === "ChainExpression") {
104+
node = parent
105+
parent = getParent(node)
106+
}
107+
if (!parent || parent.type === "ExpressionStatement") {
108+
yield { node, type: "unused" }
109+
return
110+
}
111+
if (parent.type === "MemberExpression") {
112+
if (parent.object === node) {
113+
yield { node, type: "member", memberExpression: parent }
114+
} else {
115+
yield { node, type: "unknown" }
116+
}
117+
} else if (parent.type === "AssignmentExpression") {
118+
if (parent.right === node && parent.operator === "=") {
119+
yield* iterateReferencesForESPattern(
120+
node,
121+
parent.left,
122+
context,
123+
alreadyChecked,
124+
)
125+
} else {
126+
yield { node, type: "unknown" }
127+
}
128+
} else if (parent.type === "VariableDeclarator") {
129+
if (parent.init === node) {
130+
const pp = getParent(getParent(parent))
131+
if (pp?.type === "ExportNamedDeclaration") {
132+
yield { node, type: "exported" }
133+
}
134+
yield* iterateReferencesForESPattern(
135+
node,
136+
parent.id,
137+
context,
138+
alreadyChecked,
139+
)
140+
} else {
141+
yield { node, type: "unknown" }
142+
}
143+
} else if (parent.type === "CallExpression") {
144+
const argIndex = parent.arguments.indexOf(node)
145+
if (argIndex > -1) {
146+
// `foo(regexp)`
147+
if (parent.callee.type === "Identifier") {
148+
const fn = findFunction(context, parent.callee)
149+
if (fn) {
150+
yield* iterateReferencesForFunctionArgument(
151+
node,
152+
fn,
153+
argIndex,
154+
context,
155+
alreadyChecked,
156+
)
157+
return
158+
}
159+
}
160+
yield { node, type: "argument", callExpression: parent }
161+
} else {
162+
yield { node, type: "call" }
163+
}
164+
} else if (
165+
parent.type === "ExportSpecifier" ||
166+
parent.type === "ExportDefaultDeclaration"
167+
) {
168+
yield { node, type: "exported" }
169+
} else if (parent.type === "ForOfStatement") {
170+
if (parent.right === node) {
171+
yield { node, type: "iteration", for: parent }
172+
} else {
173+
yield { node, type: "unknown" }
174+
}
175+
} else {
176+
yield { node, type: "unknown" }
177+
}
178+
}
179+
180+
/** Iterate references for the given pattern node. */
181+
function* iterateReferencesForESPattern(
182+
expression: Expression,
183+
pattern: Pattern,
184+
context: Rule.RuleContext,
185+
alreadyChecked: AlreadyChecked,
186+
): Iterable<ExpressionReference> {
187+
let target = pattern
188+
while (target.type === "AssignmentPattern") {
189+
target = target.left
190+
}
191+
if (target.type === "Identifier") {
192+
// e.g. const foo = expr
193+
yield* iterateReferencesForVariable(target, context, alreadyChecked)
194+
} else if (
195+
target.type === "ObjectPattern" ||
196+
target.type === "ArrayPattern"
197+
) {
198+
yield { node: expression, type: "destructuring", pattern: target }
199+
} else {
200+
yield { node: expression, type: "unknown" }
201+
}
202+
}
203+
204+
/** Iterate references for the given variable id node. */
205+
function* iterateReferencesForVariable(
206+
identifier: Identifier,
207+
context: Rule.RuleContext,
208+
alreadyChecked: AlreadyChecked,
209+
): Iterable<ExpressionReference> {
210+
const variable = findVariable(context, identifier)
211+
if (!variable) {
212+
yield { node: identifier, type: "unknown" }
213+
return
214+
}
215+
if (alreadyChecked.variables.has(variable)) {
216+
return
217+
}
218+
alreadyChecked.variables.add(variable)
219+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- expect
220+
if ((variable as any).eslintUsed) {
221+
yield { node: identifier, type: "exported" }
222+
}
223+
const readReferences = variable.references.filter((ref) => ref.isRead())
224+
if (!readReferences.length) {
225+
yield { node: identifier, type: "unused" }
226+
return
227+
}
228+
for (const reference of readReferences) {
229+
yield* iterateReferencesForExpression(
230+
reference.identifier,
231+
context,
232+
alreadyChecked,
233+
)
234+
}
235+
}
236+
237+
/** Iterate references for the given function argument. */
238+
function* iterateReferencesForFunctionArgument(
239+
expression: Expression,
240+
fn: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
241+
argIndex: number,
242+
context: Rule.RuleContext,
243+
alreadyChecked: AlreadyChecked,
244+
): Iterable<ExpressionReference> {
245+
let alreadyIndexes = alreadyChecked.functions.get(fn)
246+
if (!alreadyIndexes) {
247+
alreadyIndexes = new Set()
248+
alreadyChecked.functions.set(fn, alreadyIndexes)
249+
}
250+
if (alreadyIndexes.has(argIndex)) {
251+
// cannot check
252+
return
253+
}
254+
alreadyIndexes.add(argIndex)
255+
const params = fn.params.slice(0, argIndex + 1)
256+
const argNode = params[argIndex]
257+
if (!argNode || params.some((param) => param?.type === "RestElement")) {
258+
yield { node: expression, type: "unknown" }
259+
return
260+
}
261+
yield* iterateReferencesForESPattern(
262+
expression,
263+
argNode,
264+
context,
265+
alreadyChecked,
266+
)
267+
}

0 commit comments

Comments
 (0)