Skip to content

Commit 00f0549

Browse files
committed
second attempt; still trying to figure this out
1 parent 2a05148 commit 00f0549

File tree

1 file changed

+301
-21
lines changed

1 file changed

+301
-21
lines changed

plugins/rules/memberAccess.ts

Lines changed: 301 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,321 @@
1-
import {
2-
AST_NODE_TYPES,
3-
ESLintUtils,
4-
TSESTree,
5-
} from "@typescript-eslint/utils";
6-
import { RuleListener, RuleModule } from "@typescript-eslint/utils/ts-eslint";
1+
import { TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils";
2+
import { Scope } from "@typescript-eslint/utils/ts-eslint";
73
import createRule from "../utils/createRule.js";
84

9-
const noRepeatedMemberAccess: ESLintUtils.RuleModule<
10-
"repeatedAccess", // Message ID type
11-
unknown[],
12-
unknown,
13-
ESLintUtils.RuleListener // Listener type
14-
> = createRule({
5+
const noRepeatedMemberAccess = createRule({
156
name: "no-repeated-member-access",
16-
defaultOptions: [{ minOccurrences: 2 }], // Provide a default object matching the options structure
177
meta: {
188
type: "suggestion",
199
docs: {
2010
description:
21-
"Avoid getting member variable multiple-times in the same context",
11+
"Optimize repeated member access patterns by extracting variables",
2212
},
2313
fixable: "code",
24-
messages: {
25-
repeatedAccess:
26-
"Try refactor member access to a variable (e.g. 'const temp = {{ path }};') to avoid possible performance loss",
27-
},
2814
schema: [
2915
{
3016
type: "object",
3117
properties: {
32-
minOccurrences: { type: "number", minimum: 2 },
18+
minOccurrences: { type: "number", minimum: 2, default: 3 },
19+
maxChainDepth: { type: "number", minimum: 1, maximum: 5, default: 3 },
3320
},
3421
},
3522
],
23+
messages: {
24+
repeatedAccess:
25+
"Member chain '{{ chain }}' accessed {{ count }} times. Extract to variable.",
26+
modifiedChain:
27+
"Cannot optimize '{{ chain }}' due to potential modification.",
28+
},
3629
},
37-
create(context) {
38-
return {};
30+
defaultOptions: [{ minOccurrences: 3, maxChainDepth: 3 }],
31+
32+
create(context, [options]) {
33+
const sourceCode = context.sourceCode;
34+
const minOccurrences = options?.minOccurrences ?? 3;
35+
const maxChainDepth = options?.maxChainDepth ?? 3;
36+
37+
// ======================
38+
// Scope Management System
39+
// ======================
40+
type ScopeData = {
41+
chains: Map<
42+
string,
43+
{
44+
count: number;
45+
nodes: TSESTree.MemberExpression[];
46+
modified: boolean;
47+
}
48+
>;
49+
variables: Set<string>;
50+
};
51+
52+
const scopeDataMap = new WeakMap<Scope.Scope, ScopeData>();
53+
54+
function getScopeData(scope: Scope.Scope): ScopeData {
55+
if (!scopeDataMap.has(scope)) {
56+
scopeDataMap.set(scope, {
57+
chains: new Map(),
58+
variables: new Set(scope.variables.map((v) => v.name)),
59+
});
60+
}
61+
return scopeDataMap.get(scope)!;
62+
}
63+
64+
// ======================
65+
// Path Analysis System
66+
// ======================
67+
interface ChainInfo {
68+
hierarchy: string[]; // Chain hierarchy (e.g., ["a", "a.b", "a.b.c"])
69+
fullChain: string; // Complete path (e.g., "a.b.c")
70+
}
71+
72+
const chainCache = new WeakMap<
73+
TSESTree.MemberExpression,
74+
ChainInfo | null
75+
>();
76+
77+
function analyzeChain(node: TSESTree.MemberExpression): ChainInfo | null {
78+
if (chainCache.has(node)) return chainCache.get(node)!;
79+
80+
const parts: string[] = [];
81+
let current: TSESTree.Node = node;
82+
let isValid = true;
83+
84+
// Collect property chain (reverse order)
85+
while (current.type === AST_NODE_TYPES.MemberExpression) {
86+
if (current.computed) {
87+
if (current.property.type === AST_NODE_TYPES.Literal) {
88+
parts.push(`[${current.property.value}]`);
89+
} else {
90+
isValid = false;
91+
break;
92+
}
93+
} else {
94+
parts.push(current.property.name);
95+
}
96+
97+
current = current.object;
98+
}
99+
100+
// Handle base object
101+
if (current.type === AST_NODE_TYPES.Identifier) {
102+
parts.push(current.name);
103+
} else {
104+
isValid = false;
105+
}
106+
107+
if (!isValid || parts.length > maxChainDepth + 1) {
108+
chainCache.set(node, null);
109+
return null;
110+
}
111+
112+
// Generate hierarchy chain (forward order)
113+
parts.reverse();
114+
const reversedParts = parts;
115+
const hierarchy = reversedParts.reduce((acc, part, index) => {
116+
const chain = index === 0 ? part : `${acc[index - 1]}.${part}`;
117+
return [...acc, chain];
118+
}, [] as string[]);
119+
120+
const result = {
121+
hierarchy: hierarchy.slice(0, maxChainDepth + 1),
122+
fullChain: hierarchy.at(-1) ?? "", // Add default empty string
123+
};
124+
125+
chainCache.set(node, result);
126+
return result;
127+
}
128+
129+
// ======================
130+
// Modification Tracking System
131+
// ======================
132+
const modificationRegistry = new Map<string, TSESTree.Node[]>();
133+
134+
function trackModification(chain: string, node: TSESTree.Node) {
135+
if (!modificationRegistry.has(chain)) {
136+
modificationRegistry.set(chain, []);
137+
}
138+
modificationRegistry.get(chain)!.push(node);
139+
}
140+
141+
// ======================
142+
// Variable Naming System
143+
// ======================
144+
function generateVarName(parts: string[], scope: Scope.Scope): string {
145+
const base = parts
146+
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
147+
.join("")
148+
.replace(/[^a-zA-Z0-9]/g, "_");
149+
150+
let name = base;
151+
let suffix = 1;
152+
const scopeData = getScopeData(scope);
153+
154+
while (scopeData.variables.has(name)) {
155+
name = `${base}${suffix++}`;
156+
}
157+
158+
scopeData.variables.add(name);
159+
return name;
160+
}
161+
162+
// ======================
163+
// Core Processing Logic (Improved Hierarchy Tracking)
164+
// ======================
165+
function processMemberExpression(node: TSESTree.MemberExpression) {
166+
if (node.parent?.type === AST_NODE_TYPES.MemberExpression) return;
167+
168+
const chainInfo = analyzeChain(node);
169+
if (!chainInfo) return;
170+
171+
const scope = sourceCode.getScope(node);
172+
const scopeData = getScopeData(scope);
173+
174+
// Update hierarchy chain statistics
175+
for (const chain of chainInfo.hierarchy) {
176+
const record = scopeData.chains.get(chain) || {
177+
count: 0,
178+
nodes: [],
179+
modified: false,
180+
};
181+
record.count++;
182+
record.nodes.push(node);
183+
scopeData.chains.set(chain, record);
184+
}
185+
186+
// Check for optimal chain immediately
187+
const optimalChain = findOptimalChain(chainInfo.hierarchy, scopeData);
188+
if (
189+
optimalChain &&
190+
scopeData.chains.get(optimalChain)?.count === minOccurrences
191+
) {
192+
reportOptimization(optimalChain, scopeData, scope);
193+
}
194+
}
195+
196+
function findOptimalChain(
197+
hierarchy: string[],
198+
scopeData: ScopeData
199+
): string | null {
200+
for (let i = hierarchy.length - 1; i >= 0; i--) {
201+
const chain = hierarchy[i];
202+
const record = scopeData.chains.get(chain);
203+
if (record && record.count >= minOccurrences) {
204+
return chain;
205+
}
206+
}
207+
return null;
208+
}
209+
210+
// ======================
211+
// Optimization Reporting Logic
212+
// ======================
213+
function reportOptimization(
214+
chain: string,
215+
scopeData: ScopeData,
216+
scope: Scope.Scope
217+
) {
218+
const record = scopeData.chains.get(chain)!;
219+
const modifications = modificationRegistry.get(chain) || [];
220+
const lastModification = modifications.at(-1);
221+
222+
context.report({
223+
node: record.nodes[0],
224+
messageId: "repeatedAccess",
225+
data: {
226+
chain: chain,
227+
count: record.count,
228+
},
229+
*fix(fixer) {
230+
// Check for subsequent modifications
231+
if (
232+
lastModification &&
233+
lastModification.range[0] < record.nodes[0].range[0]
234+
) {
235+
return;
236+
}
237+
238+
// Generate variable name (based on chain hierarchy)
239+
const parts = chain.split(".");
240+
const varName = generateVarName(parts, scope);
241+
242+
// Insert variable declaration
243+
const insertPosition = findInsertPosition(scope);
244+
yield fixer.insertTextBefore(
245+
insertPosition.node,
246+
`const ${varName} = ${chain};\n`
247+
);
248+
249+
// Replace all matching nodes
250+
for (const memberNode of record.nodes) {
251+
const fullChain = analyzeChain(memberNode)?.fullChain;
252+
if (fullChain?.startsWith(chain)) {
253+
const remaining = fullChain.slice(chain.length + 1);
254+
yield fixer.replaceText(
255+
memberNode,
256+
`${varName}${remaining ? "." + remaining : ""}`
257+
);
258+
}
259+
}
260+
261+
// Update scope information
262+
scopeData.variables.add(varName);
263+
},
264+
});
265+
}
266+
267+
// ======================
268+
// Helper Functions
269+
// ======================
270+
function findInsertPosition(scope: Scope.Scope): {
271+
node: TSESTree.Node;
272+
isGlobal: boolean;
273+
} {
274+
if (scope.block.type === AST_NODE_TYPES.Program) {
275+
return { node: scope.block.body[0], isGlobal: true };
276+
}
277+
return {
278+
node: (scope.block as TSESTree.BlockStatement).body[0],
279+
isGlobal: false,
280+
};
281+
}
282+
283+
// ======================
284+
// Rule Listeners
285+
// ======================
286+
return {
287+
MemberExpression: (node) => processMemberExpression(node),
288+
289+
AssignmentExpression: (node) => {
290+
if (node.left.type === AST_NODE_TYPES.MemberExpression) {
291+
const chainInfo = analyzeChain(node.left);
292+
if (chainInfo) {
293+
for (const chain of chainInfo.hierarchy)
294+
trackModification(chain, node);
295+
}
296+
}
297+
},
298+
299+
UpdateExpression: (node) => {
300+
if (node.argument.type === AST_NODE_TYPES.MemberExpression) {
301+
const chainInfo = analyzeChain(node.argument);
302+
if (chainInfo) {
303+
for (const chain of chainInfo.hierarchy)
304+
trackModification(chain, node);
305+
}
306+
}
307+
},
308+
309+
CallExpression: (node) => {
310+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) {
311+
const chainInfo = analyzeChain(node.callee);
312+
if (chainInfo) {
313+
for (const chain of chainInfo.hierarchy)
314+
trackModification(chain, node);
315+
}
316+
}
317+
},
318+
};
39319
},
40320
});
41321

0 commit comments

Comments
 (0)