Skip to content

Commit dcc1cdb

Browse files
committed
fix repeated-member detection; (TODO) need to add fixer; also current implementatio made some assumption about constant and enums
1 parent e21df95 commit dcc1cdb

File tree

3 files changed

+179
-12
lines changed

3 files changed

+179
-12
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
代码部分请仅使用英文,其他部分使用中文,尽量在说明的同时也给一些实例方便理解

plugins/perf-plugin.ts

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
1+
import {
2+
AST_NODE_TYPES,
3+
ESLintUtils,
4+
TSESTree,
5+
} from "@typescript-eslint/utils";
26

37
/**
48
* ESlint plugin to detect possible issues that can have impact on performance
@@ -10,7 +14,7 @@ const createRule = ESLintUtils.RuleCreator(
1014

1115
/**
1216
* Rule: Array Initializer
13-
* avoid to use [] to init variable
17+
* Avoid using [] to initialize variables
1418
* [] will create a temporary object in data section.
1519
* BAD
1620
* let v: i32[] = [];
@@ -77,8 +81,8 @@ const arrayInitStyle: ESLintUtils.RuleModule<
7781

7882
/**
7983
* Rule: Member Variable / Array Element Get
80-
* avoid to get member variable multiple-times in the same context.
81-
* it can significantly increase code size (8% in some proxy like usecase) and waste CPU!!!
84+
* Avoid accessing member variables multiple times in the same context.
85+
* This can significantly increase code size (8% in some proxy-like usecases) and waste CPU!
8286
*
8387
* Implementation overview:
8488
* - For each outermost MemberExpression (e.g. ctx.data.v1), extract the "object chain" part (e.g. ctx.data).
@@ -134,35 +138,114 @@ const noRepeatedMemberAccess: ESLintUtils.RuleModule<
134138
let current = node;
135139
const path: string[] = [];
136140

141+
// Helper function to skip TSNonNullExpression nodes
142+
function skipTSNonNullExpression(
143+
node: TSESTree.Node
144+
): TSESTree.Expression {
145+
if (node.type === "TSNonNullExpression") {
146+
return skipTSNonNullExpression(node.expression);
147+
}
148+
return node as TSESTree.Expression;
149+
}
150+
151+
// First check if this is part of a switch-case statement
152+
let parent = node;
153+
while (parent && parent.parent) {
154+
parent = parent.parent;
155+
if (
156+
parent.type === AST_NODE_TYPES.SwitchCase &&
157+
parent.test &&
158+
(node === parent.test || isDescendant(node, parent.test))
159+
) {
160+
return null; // Skip members used in switch-case statements
161+
}
162+
}
163+
164+
// Helper function to check if a node is a descendant of another node
165+
function isDescendant(
166+
node: TSESTree.Node,
167+
possibleAncestor: TSESTree.Node
168+
): boolean {
169+
let current = node.parent;
170+
while (current) {
171+
if (current === possibleAncestor) return true;
172+
current = current.parent;
173+
}
174+
return false;
175+
}
176+
137177
// Traverse up to the second last member (object chain)
138-
while (current && current.type === "MemberExpression") {
178+
while (
179+
(current && current.type === "MemberExpression") ||
180+
current.type === "TSNonNullExpression"
181+
) {
182+
// Skip non-null assertions
183+
if (current.type === "TSNonNullExpression") {
184+
current = current.expression;
185+
continue;
186+
}
187+
139188
// Only handle static property or static index
140189
if (current.computed) {
190+
// true means access with "[]"
191+
// false means access with "."
141192
if (current.property.type === "Literal") {
142-
path.unshift(`[${current.property.raw}]`);
193+
// e.g. obj[1], obj["name"]
194+
path.unshift(`[${current.property.value}]`);
143195
} else {
144196
// Ignore dynamic property access
197+
// e.g. obj[var], obj[func()]
145198
return null;
146199
}
147200
} else {
201+
// e.g. obj.prop
148202
path.unshift(`.${current.property.name}`);
149203
}
204+
205+
// Check if we've reached the base object
206+
let objExpr = current.object;
207+
if (objExpr?.type === "TSNonNullExpression") {
208+
objExpr = skipTSNonNullExpression(objExpr);
209+
}
210+
150211
// If object is not MemberExpression, we've reached the base object
151-
if (!current.object || current.object.type !== "MemberExpression") {
212+
if (!objExpr || objExpr.type !== "MemberExpression") {
213+
// Handle "this" expressions
214+
if (objExpr && objExpr.type === "ThisExpression") {
215+
path.unshift("this");
216+
217+
// Skip reporting if the chain is just 'this.property'
218+
if (path.length <= 2) {
219+
return null;
220+
}
221+
222+
path.pop(); // Remove the last property
223+
return path.join("").replace(/^\./, "");
224+
}
225+
152226
// If object is Identifier, add it to the path
153-
if (current.object && current.object.type === "Identifier") {
154-
path.unshift(current.object.name);
227+
if (objExpr && objExpr.type === "Identifier") {
228+
const baseName = objExpr.name;
229+
230+
// Skip if the base looks like an enum/constant (starts with capital letter)
231+
if (
232+
baseName.length > 0 &&
233+
baseName[0] === baseName[0].toUpperCase()
234+
) {
235+
return null; // Likely an enum or static class
236+
}
237+
238+
path.unshift(baseName);
155239
// Remove the last property (keep only the object chain)
156240
path.pop();
157241
return path.join("").replace(/^\./, "");
158242
}
159243
return null;
160244
}
161-
current = current.object;
245+
current = objExpr;
162246
}
163247
return null;
164248
}
165-
166249
const occurrences = new Map();
167250
const minOccurrences = context.options[0]?.minOccurrences || 2;
168251

@@ -174,8 +257,47 @@ const noRepeatedMemberAccess: ESLintUtils.RuleModule<
174257
const objectChain = getObjectChain(node);
175258
if (!objectChain) return;
176259

260+
const baseObjectName = objectChain.split(/[.[]/)[0];
177261
// Use scope range as part of the key
178262
const scope = context.sourceCode.getScope(node);
263+
if (!scope || !scope.block || !scope.block.range) return;
264+
265+
let variable = null;
266+
let currentScope = scope;
267+
268+
while (currentScope) {
269+
variable = currentScope.variables.find(
270+
(v) => v.name === baseObjectName
271+
);
272+
if (variable) break;
273+
274+
const upperScope = currentScope.upper;
275+
if (!upperScope) break;
276+
currentScope = upperScope;
277+
}
278+
279+
if (variable) {
280+
// check for const/enum/import
281+
const isConst = variable.defs.every(
282+
(def) => def.node && "kind" in def.node && def.node.kind === "const"
283+
);
284+
const isEnum = variable.defs.some(
285+
(def) =>
286+
(def.parent as TSESTree.Node)?.type ===
287+
AST_NODE_TYPES.TSEnumDeclaration
288+
);
289+
const isImport = variable.defs.some(
290+
(def) =>
291+
def.type === "ImportBinding" ||
292+
(def.node &&
293+
"type" in def.node &&
294+
def.node.type === AST_NODE_TYPES.ImportDeclaration)
295+
);
296+
if (isConst || isEnum || isImport) {
297+
return; // skip these types as extracting them won't be helpful
298+
}
299+
}
300+
179301
const key = `${scope.block.range.join("-")}-${objectChain}`;
180302

181303
const count = (occurrences.get(key) || 0) + 1;

tests/perf-plugin.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,46 @@ RuleTester.itOnly = test.it.only;
1010

1111
const ruleTester = new RuleTester();
1212

13+
// ruleTester.run(
14+
// "no-repeated-member-access",
15+
// perfPlugin.rules["no-repeated-member-access"],
16+
// {
17+
// valid: [
18+
// `
19+
// switch (reason) {
20+
// case Test.STARTUP: {
21+
// return "STARTUP"
22+
// }
23+
// case Test.DEBOUNCE: {
24+
// return "DEBOUNCE"
25+
// }
26+
// case Test.INSTANT: {
27+
// return "INSTANT"
28+
// }
29+
// case Test.SHUTDOWN: {
30+
// return "SHUTDOWN"
31+
// }
32+
// default: {
33+
// return "UNKNOWN"
34+
// }
35+
// }
36+
// `,
37+
// ],
38+
39+
// invalid: [
40+
// {
41+
// code: `
42+
// this.vehicleSys!.automobile = new TransportCore(new TransportBlueprint());
43+
// this.vehicleSys!.automobile!.underframe = new ChassisAssembly(new ChassisSchema());
44+
// this.vehicleSys!.automobile!.underframe!.propulsionCover = new EngineEnclosure(new EnclosureSpec());
45+
// this.vehicleSys!.automobile!.underframe!.logisticsBay = new CargoModule(new ModuleTemplate());
46+
// `,
47+
// errors: [{ messageId: "repeatedAccess" }],
48+
// },
49+
// ],
50+
// }
51+
// );
52+
1353
// Throws error if the tests in ruleTester.run() do not pass
1454
ruleTester.run(
1555
"array-init-style", // rule name
@@ -154,7 +194,11 @@ ruleTester.run(
154194
this.vehicleSys!.automobile!.underframe!.propulsionCover = new EngineEnclosure(new EnclosureSpec());
155195
this.vehicleSys!.automobile!.underframe!.logisticsBay = new CargoModule(new ModuleTemplate());
156196
`,
157-
errors: [{ messageId: "repeatedAccess" }],
197+
errors: [
198+
{ messageId: "repeatedAccess" },
199+
{ messageId: "repeatedAccess" },
200+
{ messageId: "repeatedAccess" },
201+
],
158202
},
159203
],
160204
}

0 commit comments

Comments
 (0)