Skip to content

Commit 2c569fa

Browse files
committed
refactor memberaccess using tree structures
1 parent dd0221a commit 2c569fa

File tree

1 file changed

+142
-106
lines changed

1 file changed

+142
-106
lines changed

plugins/rules/memberAccess.ts

Lines changed: 142 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,135 @@ const noRepeatedMemberAccess = createRule({
3333
// Track which chains have already been reported to avoid duplicate reports
3434
const reportedChains = new Set<string>();
3535

36-
// We have got two map types, chainMap and scopeDataMap
37-
// it works like: scopeDataMap -> chainMap -> chainInfo
36+
// Tree-based approach for storing member access chains
37+
// Each node represents a property in the chain (e.g., a -> b -> c for a.b.c)
38+
class ChainNode {
39+
name: string;
40+
count: number = 0;
41+
modified: boolean = false;
42+
parent?: ChainNode;
43+
children: Map<string, ChainNode> = new Map();
3844

39-
// Stores info to decide if a extraction is necessary
40-
type ChainMap = Map<
41-
string,
42-
{
43-
count: number; // Number of times this chain is accessed
44-
modified: boolean; // Whether this chain is modified (written to)
45+
constructor(name: string) {
46+
this.name = name;
4547
}
46-
>;
47-
// Stores mapping of scope to ChainMap
48-
const scopeDataMap = new WeakMap<Scope.Scope, ChainMap>();
4948

50-
function getChainMap(scope: Scope.Scope): ChainMap {
51-
if (!scopeDataMap.has(scope)) {
52-
// Create new info map if not already present
53-
const newChainMap = new Map<
54-
string,
55-
{
56-
count: number;
57-
modified: boolean;
49+
// Get or create child node
50+
getOrCreateChild(childName: string): ChainNode {
51+
if (!this.children.has(childName)) {
52+
this.children.set(childName, new ChainNode(childName));
53+
}
54+
return this.children.get(childName)!;
55+
}
56+
57+
// Get the full chain path from root to this node
58+
getChainPath(): string {
59+
const path: string[] = [];
60+
let current = this as ChainNode | undefined;
61+
while (current && current.name !== "__root__") {
62+
path.unshift(current.name);
63+
current = current.parent;
64+
}
65+
return path.join(".");
66+
}
67+
68+
// Mark this node and all its descendants as modified
69+
markAsModified(): void {
70+
this.modified = true;
71+
for (const child of this.children.values()) {
72+
child.markAsModified();
73+
}
74+
}
75+
}
76+
77+
// Root node for the tree (per scope)
78+
class ChainTree {
79+
root: ChainNode = new ChainNode("__root__");
80+
81+
// Insert a chain path into the tree and increment counts
82+
insertChain(properties: string[]): void {
83+
let current = this.root;
84+
85+
// Navigate/create path in tree
86+
for (const prop of properties) {
87+
const child = current.getOrCreateChild(prop);
88+
child.parent = current;
89+
current = child;
90+
91+
// Only increment count for non-single properties (chains with dots)
92+
if (properties.length > 1) {
93+
current.count++;
94+
}
95+
}
96+
}
97+
98+
// Mark a chain and its descendants as modified
99+
markChainAsModified(properties: string[]): void {
100+
let current = this.root;
101+
102+
// Navigate to the target node
103+
for (const prop of properties) {
104+
const child = current.children.get(prop);
105+
if (child) {
106+
current = child;
107+
} else {
108+
// Create the chain if it doesn't exist
109+
current = current.getOrCreateChild(prop);
110+
current.parent = current;
111+
current.modified = true;
112+
}
113+
}
114+
115+
// Mark this node and all descendants as modified
116+
current.markAsModified();
117+
}
118+
119+
// Find the longest valid chain that meets the minimum occurrence threshold
120+
findLongestValidChain(
121+
minOccurrences: number
122+
): { chain: string; count: number } | null {
123+
let bestChain: string | null = null;
124+
let bestCount = 0;
125+
126+
const traverse = (node: ChainNode, depth: number) => {
127+
// Only consider chains with more than one segment (has dots)
128+
if (depth > 1 && !node.modified && node.count >= minOccurrences) {
129+
const chainPath = node.getChainPath();
130+
if (chainPath.length > (bestChain?.length || 0)) {
131+
bestChain = chainPath;
132+
bestCount = node.count;
133+
}
134+
}
135+
136+
// Stop traversing if this node is modified
137+
if (node.modified) {
138+
return;
139+
}
140+
141+
// Recursively traverse children
142+
for (const child of node.children.values()) {
143+
traverse(child, depth + 1);
58144
}
59-
>();
60-
scopeDataMap.set(scope, newChainMap);
145+
};
146+
147+
traverse(this.root, 0);
148+
149+
return bestChain ? { chain: bestChain, count: bestCount } : null;
150+
}
151+
}
152+
153+
// Stores mapping of scope to ChainTree
154+
const scopeDataMap = new WeakMap<Scope.Scope, ChainTree>();
155+
156+
function getChainTree(scope: Scope.Scope): ChainTree {
157+
if (!scopeDataMap.has(scope)) {
158+
scopeDataMap.set(scope, new ChainTree());
61159
}
62160
return scopeDataMap.get(scope)!;
63161
}
64162

65-
// This function generates ["a", "a.b", "a.b.c"] from a.b.c
66-
// We will further add [count, modified] info to them in ChainMap, and use them as an indication for extraction
163+
// This function generates ["a", "b", "c"] from a.b.c (just the property names)
164+
// The tree structure will handle the hierarchy automatically
67165
// eslint-disable-next-line unicorn/consistent-function-scoping
68166
function analyzeChain(node: TSESTree.MemberExpression): string[] {
69167
const properties: string[] = []; // AST is iterated in reverse order
@@ -96,44 +194,15 @@ const noRepeatedMemberAccess = createRule({
96194
properties.push("this");
97195
} // ignore other patterns
98196

99-
// Generate hierarchy chain (forward order)
100-
// Example:
101-
// Input is "a.b.c"
102-
// For property ["c", "b", "a"], we reverse it to ["a", "b", "c"]
197+
// Reverse to get forward order: ["a", "b", "c"]
103198
properties.reverse();
104-
105-
// and build chain of object ["a", "a.b", "a.b.c"]
106-
const result: string[] = [];
107-
let currentChain = "";
108-
for (let i = 0; i < properties.length; i++) {
109-
currentChain =
110-
i === 0 ? properties[0] : `${currentChain}.${properties[i]}`;
111-
result.push(currentChain);
112-
}
113-
114-
return result;
199+
return properties;
115200
}
116201

117-
function setModifiedFlag(chain: string, node: TSESTree.Node) {
202+
function setModifiedFlag(chain: string[], node: TSESTree.Node) {
118203
const scope = sourceCode.getScope(node);
119-
const scopeData = getChainMap(scope);
120-
121-
for (const [existingChain, chainInfo] of scopeData) {
122-
// Check if the existing chain starts with the modified chain followed by a dot or bracket, and if so, marks them as modified
123-
if (
124-
existingChain === chain ||
125-
existingChain.startsWith(chain + ".") ||
126-
existingChain.startsWith(chain + "[")
127-
) {
128-
chainInfo.modified = true;
129-
}
130-
}
131-
if (!scopeData.has(chain)) {
132-
scopeData.set(chain, {
133-
count: 0,
134-
modified: true,
135-
});
136-
}
204+
const chainTree = getChainTree(scope);
205+
chainTree.markChainAsModified(chain);
137206
}
138207

139208
function processMemberExpression(node: TSESTree.MemberExpression) {
@@ -144,53 +213,26 @@ const noRepeatedMemberAccess = createRule({
144213
return;
145214
}
146215

147-
const chainInfo = analyzeChain(node);
148-
if (!chainInfo) {
216+
const properties = analyzeChain(node);
217+
if (!properties || properties.length === 0) {
149218
return;
150219
}
151220

152221
const scope = sourceCode.getScope(node);
153-
const chainMap = getChainMap(scope);
154-
155-
// keeps record of the longest valid chain, and only report it instead of shorter ones (to avoid repeated reports)
156-
let longestValidChain = "";
157-
158-
// Update chain statistics for each part of the hierarchy
159-
for (const chain of chainInfo) {
160-
// Skip single-level chains
161-
if (!chain.includes(".")) {
162-
continue;
163-
}
164-
165-
const chainInfo = chainMap.get(chain) || {
166-
count: 0,
167-
modified: false,
168-
};
169-
if (chainInfo.modified) {
170-
break;
171-
}
172-
173-
chainInfo.count++;
174-
chainMap.set(chain, chainInfo);
222+
const chainTree = getChainTree(scope);
175223

176-
// record longest extractable chain
177-
if (
178-
chainInfo.count >= minOccurrences &&
179-
chain.length > longestValidChain.length
180-
) {
181-
longestValidChain = chain;
182-
}
183-
}
224+
// Insert the chain into the tree (this will increment counts automatically)
225+
chainTree.insertChain(properties);
184226

185-
// report the longest chain
186-
if (longestValidChain && !reportedChains.has(longestValidChain)) {
187-
const chainInfo = chainMap.get(longestValidChain)!;
227+
// Find the longest valid chain to report
228+
const result = chainTree.findLongestValidChain(minOccurrences);
229+
if (result && !reportedChains.has(result.chain)) {
188230
context.report({
189231
node: node,
190232
messageId: "repeatedAccess",
191-
data: { chain: longestValidChain, count: chainInfo.count },
233+
data: { chain: result.chain, count: result.count },
192234
});
193-
reportedChains.add(longestValidChain);
235+
reportedChains.add(result.chain);
194236
}
195237
}
196238

@@ -200,32 +242,26 @@ const noRepeatedMemberAccess = createRule({
200242
// This prevents us from extracting chains that are modified
201243
AssignmentExpression: (node) => {
202244
if (node.left.type === AST_NODE_TYPES.MemberExpression) {
203-
const chainInfo = analyzeChain(node.left);
204-
for (const chain of chainInfo) {
205-
setModifiedFlag(chain, node);
206-
}
245+
const properties = analyzeChain(node.left);
246+
setModifiedFlag(properties, node);
207247
}
208248
},
209249

210250
// Track increment/decrement operations
211251
// Example: obj.prop.counter++ modifies "obj.prop.counter"
212252
UpdateExpression: (node) => {
213253
if (node.argument.type === AST_NODE_TYPES.MemberExpression) {
214-
const chainInfo = analyzeChain(node.argument);
215-
for (const chain of chainInfo) {
216-
setModifiedFlag(chain, node);
217-
}
254+
const properties = analyzeChain(node.argument);
255+
setModifiedFlag(properties, node);
218256
}
219257
},
220258

221259
// Track function calls that might modify their arguments
222260
// Example: obj.methods.update() might modify the "obj.methods" chain
223261
CallExpression: (node) => {
224262
if (node.callee.type === AST_NODE_TYPES.MemberExpression) {
225-
const chainInfo = analyzeChain(node.callee);
226-
for (const chain of chainInfo) {
227-
setModifiedFlag(chain, node);
228-
}
263+
const properties = analyzeChain(node.callee);
264+
setModifiedFlag(properties, node);
229265
}
230266
},
231267

0 commit comments

Comments
 (0)