|
2 | 2 | AST_NODE_TYPES,
|
3 | 3 | ESLintUtils,
|
4 | 4 | TSESTree,
|
| 5 | + TSESLint, |
5 | 6 | } from "@typescript-eslint/utils";
|
6 | 7 |
|
7 | 8 | /**
|
@@ -80,21 +81,34 @@ const arrayInitStyle: ESLintUtils.RuleModule<
|
80 | 81 | });
|
81 | 82 |
|
82 | 83 | /**
|
83 |
| - * Rule: Member Variable / Array Element Get |
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! |
| 84 | + * Rule: No Repeated Member Access |
86 | 85 | *
|
87 |
| - * Implementation overview: |
88 |
| - * - For each outermost MemberExpression (e.g. ctx.data.v1), extract the "object chain" part (e.g. ctx.data). |
89 |
| - * - Only static properties and static indices are supported; dynamic properties are ignored. |
90 |
| - * - Use the current scope's range and the object chain as a unique key to count occurrences. |
91 |
| - * - When the same object chain is accessed more than once in the same scope (default threshold: 2), report a warning to suggest refactoring. |
92 |
| - * - This helps avoid repeated property lookups and encourages caching the result in a local variable. |
| 86 | + * Description: |
| 87 | + * Detects and warns when member variables or properties are accessed multiple times |
| 88 | + * in the same scope. Repeated property access can significantly increase code size |
| 89 | + * (up to 8% in proxy-like use cases) and waste CPU cycles. |
| 90 | + * |
| 91 | + * Implementation: |
| 92 | + * 1. Identifies outermost MemberExpression nodes (e.g., ctx.data.value) |
| 93 | + * 2. Extracts the "object chain" part (e.g., ctx.data) |
| 94 | + * 3. Counts occurrences of the same chain in each scope |
| 95 | + * 4. Reports when occurrences exceed the threshold (default: 2) |
| 96 | + * 5. Suggests extracting the chain into a local variable |
| 97 | + * |
| 98 | + * Limitations: |
| 99 | + * - Only static properties and indices are supported |
| 100 | + * - Dynamic properties (obj[variable]) are ignored |
| 101 | + * - Constants, enums, and imports are skipped |
93 | 102 | *
|
94 | 103 | * Example:
|
| 104 | + * // Bad - repeated access |
95 | 105 | * const v1 = ctx.data.v1;
|
96 | 106 | * const v2 = ctx.data.v2;
|
97 |
| - * The rule will suggest extracting 'ctx.data' into a variable if accessed multiple times. |
| 107 | + * |
| 108 | + * // Good - extracted common chain |
| 109 | + * const ctxData = ctx.data; |
| 110 | + * const v1 = ctxData.v1; |
| 111 | + * const v2 = ctxData.v2; |
98 | 112 | */
|
99 | 113 |
|
100 | 114 | // Define the type for the rule options
|
@@ -265,40 +279,54 @@ const noRepeatedMemberAccess: ESLintUtils.RuleModule<
|
265 | 279 | const scope = context.sourceCode.getScope(node);
|
266 | 280 | if (!scope || !scope.block || !scope.block.range) return;
|
267 | 281 |
|
268 |
| - let variable = null; |
269 |
| - let currentScope = scope; |
| 282 | + // Find variable in scope chain |
| 283 | + const variable = findVariableInScopeChain(scope, baseObjectName); |
270 | 284 |
|
271 |
| - while (currentScope) { |
272 |
| - variable = currentScope.variables.find( |
273 |
| - (v) => v.name === baseObjectName |
274 |
| - ); |
275 |
| - if (variable) break; |
| 285 | + // Skip certain variable types that shouldn't be extracted |
| 286 | + if ( |
| 287 | + variable && |
| 288 | + (isConstVariable(variable) || |
| 289 | + isEnumVariable(variable) || |
| 290 | + isImportVariable(variable)) |
| 291 | + ) { |
| 292 | + return; |
| 293 | + } |
276 | 294 |
|
277 |
| - const upperScope = currentScope.upper; |
278 |
| - if (!upperScope) break; |
279 |
| - currentScope = upperScope; |
| 295 | + // Helper functions |
| 296 | + function findVariableInScopeChain(scope: TSESLint.Scope.Scope, name: string) { |
| 297 | + let currentScope: TSESLint.Scope.Scope | null = scope; |
| 298 | + while (currentScope) { |
| 299 | + const variable = currentScope.variables.find( |
| 300 | + (v) => v.name === name |
| 301 | + ); |
| 302 | + if (variable) return variable; |
| 303 | + currentScope = currentScope.upper; |
| 304 | + } |
| 305 | + return null; |
280 | 306 | }
|
281 | 307 |
|
282 |
| - if (variable) { |
283 |
| - // check for const/enum/import |
284 |
| - const isConst = variable.defs.every( |
| 308 | + function isConstVariable(variable: TSESLint.Scope.Variable) { |
| 309 | + return variable.defs.every( |
285 | 310 | (def) => def.node && "kind" in def.node && def.node.kind === "const"
|
286 | 311 | );
|
287 |
| - const isEnum = variable.defs.some( |
| 312 | + } |
| 313 | + |
| 314 | + function isEnumVariable(variable: TSESLint.Scope.Variable) { |
| 315 | + return variable.defs.some( |
288 | 316 | (def) =>
|
289 | 317 | (def.parent as TSESTree.Node)?.type ===
|
290 | 318 | AST_NODE_TYPES.TSEnumDeclaration
|
291 | 319 | );
|
292 |
| - const isImport = variable.defs.some( |
| 320 | + } |
| 321 | + |
| 322 | + function isImportVariable(variable: TSESLint.Scope.Variable) { |
| 323 | + return variable.defs.some( |
293 | 324 | (def) =>
|
294 | 325 | def.type === "ImportBinding" ||
|
295 | 326 | (def.node &&
|
296 | 327 | "type" in def.node &&
|
297 | 328 | def.node.type === AST_NODE_TYPES.ImportDeclaration)
|
298 | 329 | );
|
299 |
| - if (isConst || isEnum || isImport) { |
300 |
| - return; // skip these types as extracting them won't be helpful |
301 |
| - } |
302 | 330 | }
|
303 | 331 |
|
304 | 332 | const key = `${scope.block.range.join("-")}-${objectChain}`;
|
|
0 commit comments