Skip to content

Commit ba6d311

Browse files
committed
refactor(rules): consolidate array method detection logic 🗂️
- Add common utility functions for array method detection - Extract for loop variable parsing to shared utilities - Simplify rule implementations by removing duplicate code - Update test formatting and structure
1 parent c5e7272 commit ba6d311

9 files changed

Lines changed: 346 additions & 564 deletions

File tree

src/rules/PreferArrayEvery.ts

Lines changed: 4 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,5 @@
1-
import type {
2-
BlockStatementNode,
3-
DenoASTNode,
4-
ForStatementNode,
5-
IfStatementNode,
6-
LintContext,
7-
LintFixer
8-
} from '@interfaces/index.ts'
9-
import {
10-
isBinaryExpression,
11-
isBlockStatement,
12-
isForStatement,
13-
isIdentifier,
14-
isIfStatement,
15-
isLiteral,
16-
isMemberExpression,
17-
isReturnStatement,
18-
isVariableDeclaration
19-
} from '@utils/index.ts'
20-
21-
/**
22-
* Checks if a for loop can be replaced with Array.every().
23-
* @param node - The for loop node
24-
* @returns True if the loop can be replaced, false otherwise
25-
*/
26-
function canReplaceWithArrayEvery(node: DenoASTNode): boolean {
27-
if (!isForStatement(node)) {
28-
return false
29-
}
30-
const forNode = node as ForStatementNode
31-
const body = forNode.body
32-
return containsReturnFalse(body)
33-
}
34-
35-
/**
36-
* Creates a fix that replaces for loops with Array.every().
37-
* @param context - The lint context
38-
* @param node - The for loop node
39-
* @returns A fix function
40-
*/
41-
function createArrayEveryFix(
42-
context: LintContext,
43-
node: DenoASTNode
44-
): (fixer: LintFixer) => unknown {
45-
return (fixer: LintFixer): unknown => {
46-
const forNode = node as ForStatementNode
47-
const init = forNode.init
48-
const test = forNode.test
49-
const body = forNode.body
50-
let arrayName = 'arr'
51-
let itemName = 'item'
52-
if (test && isBinaryExpression(test)) {
53-
if (test.right && isMemberExpression(test.right)) {
54-
const rightText = context.sourceCode.getText(test.right)
55-
if (rightText.includes('.length')) {
56-
arrayName = rightText.split('.')[0] || 'arr'
57-
}
58-
}
59-
}
60-
if (init && isVariableDeclaration(init)) {
61-
const declarations = init.declarations || []
62-
if (declarations.length > 0) {
63-
const declarator = declarations[0]
64-
if (declarator && declarator.id && isIdentifier(declarator.id)) {
65-
itemName = declarator.id.name
66-
}
67-
}
68-
}
69-
let callbackBody = 'item'
70-
if (isBlockStatement(body)) {
71-
const statements = body.body || []
72-
const ifStmt = statements.find((stmt) => {
73-
if (isIfStatement(stmt)) {
74-
const ifNode = stmt as IfStatementNode
75-
return (
76-
containsReturnFalse(ifNode.consequent) ||
77-
(ifNode.alternate && containsReturnFalse(ifNode.alternate))
78-
)
79-
}
80-
return false
81-
}) as IfStatementNode
82-
if (ifStmt) {
83-
const conditionText = context.sourceCode.getText(ifStmt.test)
84-
const paramName = arrayName.endsWith('s') ? arrayName.slice(0, -1) : 'item'
85-
let normalizedCondition = conditionText.replace(
86-
new RegExp(`${arrayName}\\[${itemName}\\]`, 'g'),
87-
paramName
88-
)
89-
normalizedCondition = normalizedCondition.replace(
90-
new RegExp(`\\b${itemName}\\b`, 'g'),
91-
paramName
92-
)
93-
callbackBody = `!(${normalizedCondition})`
94-
}
95-
}
96-
const paramName = arrayName.endsWith('s') ? arrayName.slice(0, -1) : 'item'
97-
const arrayEveryCall = `${arrayName}.every(${paramName} => ${callbackBody})`
98-
return fixer.replaceText(node, arrayEveryCall)
99-
}
100-
}
101-
102-
/**
103-
* Recursively checks if a statement contains a return false.
104-
* @param stmt - The statement to check
105-
* @returns True if the statement contains return false, false otherwise
106-
*/
107-
function containsReturnFalse(stmt: DenoASTNode): boolean {
108-
if (isReturnStatement(stmt)) {
109-
const returnArg = stmt.argument
110-
if (returnArg && isLiteral(returnArg) && returnArg.value === false) {
111-
return true
112-
}
113-
}
114-
if (isIfStatement(stmt)) {
115-
const ifStmt = stmt as IfStatementNode
116-
if (containsReturnFalse(ifStmt.consequent)) {
117-
return true
118-
}
119-
if (ifStmt.alternate && containsReturnFalse(ifStmt.alternate)) {
120-
return true
121-
}
122-
}
123-
if (isBlockStatement(stmt)) {
124-
const blockStmt = stmt as BlockStatementNode
125-
const statements = blockStmt.body || []
126-
return statements.some((s: DenoASTNode) => containsReturnFalse(s))
127-
}
128-
return false
129-
}
1+
import type { DenoASTNode, LintContext } from '@interfaces/index.ts'
2+
import { canReplaceWithArrayMethod, createArrayMethodFix, isForStatement } from '@utils/index.ts'
1303

1314
/**
1325
* Lint rule for preferring Array.every() over manual for loops.
@@ -147,11 +20,11 @@ export const preferArrayEveryRule = {
14720
if (!isForStatement(node)) {
14821
return
14922
}
150-
if (canReplaceWithArrayEvery(node)) {
23+
if (canReplaceWithArrayMethod(node, 'every')) {
15124
context.report({
15225
node,
15326
message: 'Prefer Array.every() over manual for loops that return false',
154-
fix: createArrayEveryFix(context, node)
27+
fix: createArrayMethodFix(context, node, 'every')
15528
})
15629
}
15730
}

src/rules/PreferArraySome.ts

Lines changed: 4 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,5 @@
1-
import type {
2-
BlockStatementNode,
3-
DenoASTNode,
4-
ForStatementNode,
5-
IfStatementNode,
6-
LintContext,
7-
LintFixer
8-
} from '@interfaces/index.ts'
9-
import {
10-
isBinaryExpression,
11-
isBlockStatement,
12-
isForStatement,
13-
isIdentifier,
14-
isIfStatement,
15-
isLiteral,
16-
isMemberExpression,
17-
isReturnStatement,
18-
isVariableDeclaration
19-
} from '@utils/index.ts'
20-
21-
/**
22-
* Checks if a for loop can be replaced with Array.some().
23-
* @param node - The for loop node
24-
* @param _context - The lint context for accessing source code
25-
* @returns True if the loop can be replaced, false otherwise
26-
*/
27-
function canReplaceWithArraySome(node: DenoASTNode, _context: LintContext): boolean {
28-
if (!isForStatement(node)) {
29-
return false
30-
}
31-
const forNode = node as ForStatementNode
32-
const body = forNode.body
33-
return containsReturnTrue(body)
34-
}
35-
36-
/**
37-
* Creates a fix that replaces for loops with Array.some().
38-
* @param context - The lint context
39-
* @param node - The for loop node
40-
* @returns A fix function
41-
*/
42-
function createArraySomeFix(
43-
context: LintContext,
44-
node: DenoASTNode
45-
): (fixer: LintFixer) => unknown {
46-
return (fixer: LintFixer): unknown => {
47-
const forNode = node as ForStatementNode
48-
const init = forNode.init
49-
const test = forNode.test
50-
const body = forNode.body
51-
let arrayName = 'arr'
52-
let itemName = 'item'
53-
if (test && isBinaryExpression(test)) {
54-
if (test.right && isMemberExpression(test.right)) {
55-
const rightText = context.sourceCode.getText(test.right)
56-
if (rightText.includes('.length')) {
57-
arrayName = rightText.split('.')[0] || 'arr'
58-
}
59-
}
60-
}
61-
if (init && isVariableDeclaration(init)) {
62-
const declarations = init.declarations || []
63-
if (declarations.length > 0) {
64-
const declarator = declarations[0]
65-
if (declarator && declarator.id && isIdentifier(declarator.id)) {
66-
itemName = declarator.id.name
67-
}
68-
}
69-
}
70-
let callbackBody = 'item'
71-
if (isBlockStatement(body)) {
72-
const statements = body.body || []
73-
const ifStmt = statements.find((stmt) => {
74-
if (isIfStatement(stmt)) {
75-
const ifNode = stmt as IfStatementNode
76-
return (
77-
containsReturnTrue(ifNode.consequent) ||
78-
(ifNode.alternate && containsReturnTrue(ifNode.alternate))
79-
)
80-
}
81-
return false
82-
}) as IfStatementNode
83-
if (ifStmt) {
84-
const conditionText = context.sourceCode.getText(ifStmt.test)
85-
const paramName = arrayName.endsWith('s') ? arrayName.slice(0, -1) : 'item'
86-
let normalizedCondition = conditionText.replace(
87-
new RegExp(`${arrayName}\\[${itemName}\\]`, 'g'),
88-
paramName
89-
)
90-
normalizedCondition = normalizedCondition.replace(
91-
new RegExp(`\\b${itemName}\\b`, 'g'),
92-
paramName
93-
)
94-
callbackBody = normalizedCondition
95-
}
96-
}
97-
const paramName = arrayName.endsWith('s') ? arrayName.slice(0, -1) : 'item'
98-
const arraySomeCall = `${arrayName}.some(${paramName} => ${callbackBody})`
99-
return fixer.replaceText(node, arraySomeCall)
100-
}
101-
}
102-
103-
/**
104-
* Recursively checks if a statement contains a return true.
105-
* @param stmt - The statement to check
106-
* @returns True if the statement contains return true, false otherwise
107-
*/
108-
function containsReturnTrue(stmt: DenoASTNode): boolean {
109-
if (isReturnStatement(stmt)) {
110-
const returnArg = stmt.argument
111-
if (returnArg && isLiteral(returnArg) && returnArg.value === true) {
112-
return true
113-
}
114-
}
115-
if (isIfStatement(stmt)) {
116-
const ifStmt = stmt as IfStatementNode
117-
if (containsReturnTrue(ifStmt.consequent)) {
118-
return true
119-
}
120-
if (ifStmt.alternate && containsReturnTrue(ifStmt.alternate)) {
121-
return true
122-
}
123-
}
124-
if (isBlockStatement(stmt)) {
125-
const blockStmt = stmt as BlockStatementNode
126-
const statements = blockStmt.body || []
127-
return statements.some((s: DenoASTNode) => containsReturnTrue(s))
128-
}
129-
return false
130-
}
1+
import type { DenoASTNode, LintContext } from '@interfaces/index.ts'
2+
import { canReplaceWithArrayMethod, createArrayMethodFix, isForStatement } from '@utils/index.ts'
1313

1324
/**
1335
* Lint rule for preferring Array.some() over manual for loops.
@@ -148,11 +20,11 @@ export const preferArraySomeRule = {
14820
if (!isForStatement(node)) {
14921
return
15022
}
151-
if (canReplaceWithArraySome(node, context)) {
23+
if (canReplaceWithArrayMethod(node, 'some')) {
15224
context.report({
15325
node,
15426
message: 'Prefer Array.some() over manual for loops that return true',
155-
fix: createArraySomeFix(context, node)
27+
fix: createArrayMethodFix(context, node, 'some')
15628
})
15729
}
15830
}

src/rules/PreferArrowCallback.ts

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import type {
77
LintFixer
88
} from '@interfaces/index.ts'
99
import {
10+
containsArgumentsUsage,
11+
containsThisExpression,
1012
isBlockStatement,
1113
isCallExpression,
1214
isFunctionExpression,
1315
isIdentifier,
14-
isMemberExpression,
15-
isThisExpression
16+
isMemberExpression
1617
} from '@utils/index.ts'
1718

1819
/**
@@ -72,8 +73,9 @@ function createArrowFunctionFix(
7273
} else if (params.length === 1 && params[0] && isIdentifier(params[0])) {
7374
paramsText = context.sourceCode.getText(params[0])
7475
} else {
75-
paramsText =
76-
'(' + params.map((param: DenoASTNode) => context.sourceCode.getText(param)).join(', ') + ')'
76+
paramsText = '(' + params.map((param: DenoASTNode) =>
77+
context.sourceCode.getText(param)
78+
).join(', ') + ')'
7779
}
7880
let bodyText = ''
7981
if (isBlockStatement(body)) {
@@ -86,70 +88,6 @@ function createArrowFunctionFix(
8688
}
8789
}
8890

89-
/**
90-
* Recursively checks if a statement contains 'arguments' usage.
91-
* @param stmt - The statement to check
92-
* @returns True if the statement contains 'arguments', false otherwise
93-
*/
94-
function containsArgumentsUsage(stmt: DenoASTNode): boolean {
95-
if (isIdentifier(stmt) && stmt.name === 'arguments') {
96-
return true
97-
}
98-
for (const key in stmt) {
99-
if (Object.prototype.hasOwnProperty.call(stmt, key)) {
100-
const value = (stmt as unknown as Record<string, unknown>)[key]
101-
if (value && typeof value === 'object') {
102-
if (Array.isArray(value)) {
103-
for (const item of value) {
104-
if (item && typeof item === 'object' && 'type' in item) {
105-
if (containsArgumentsUsage(item as DenoASTNode)) {
106-
return true
107-
}
108-
}
109-
}
110-
} else if ('type' in value) {
111-
if (containsArgumentsUsage(value as DenoASTNode)) {
112-
return true
113-
}
114-
}
115-
}
116-
}
117-
}
118-
return false
119-
}
120-
121-
/**
122-
* Recursively checks if a statement contains a 'this' expression.
123-
* @param stmt - The statement to check
124-
* @returns True if the statement contains 'this', false otherwise
125-
*/
126-
function containsThisExpression(stmt: DenoASTNode): boolean {
127-
if (isThisExpression(stmt)) {
128-
return true
129-
}
130-
for (const key in stmt) {
131-
if (Object.prototype.hasOwnProperty.call(stmt, key)) {
132-
const value = (stmt as unknown as Record<string, unknown>)[key]
133-
if (value && typeof value === 'object') {
134-
if (Array.isArray(value)) {
135-
for (const item of value) {
136-
if (item && typeof item === 'object' && 'type' in item) {
137-
if (containsThisExpression(item as DenoASTNode)) {
138-
return true
139-
}
140-
}
141-
}
142-
} else if ('type' in value) {
143-
if (containsThisExpression(value as DenoASTNode)) {
144-
return true
145-
}
146-
}
147-
}
148-
}
149-
}
150-
return false
151-
}
152-
15391
/**
15492
* Checks if a call expression uses a callback method.
15593
* @param node - The call expression node

0 commit comments

Comments
 (0)