@@ -33,37 +33,135 @@ const noRepeatedMemberAccess = createRule({
33
33
// Track which chains have already been reported to avoid duplicate reports
34
34
const reportedChains = new Set < string > ( ) ;
35
35
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 ( ) ;
38
44
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 ;
45
47
}
46
- > ;
47
- // Stores mapping of scope to ChainMap
48
- const scopeDataMap = new WeakMap < Scope . Scope , ChainMap > ( ) ;
49
48
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 ) ;
58
144
}
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 ( ) ) ;
61
159
}
62
160
return scopeDataMap . get ( scope ) ! ;
63
161
}
64
162
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
67
165
// eslint-disable-next-line unicorn/consistent-function-scoping
68
166
function analyzeChain ( node : TSESTree . MemberExpression ) : string [ ] {
69
167
const properties : string [ ] = [ ] ; // AST is iterated in reverse order
@@ -96,44 +194,15 @@ const noRepeatedMemberAccess = createRule({
96
194
properties . push ( "this" ) ;
97
195
} // ignore other patterns
98
196
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"]
103
198
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 ;
115
200
}
116
201
117
- function setModifiedFlag ( chain : string , node : TSESTree . Node ) {
202
+ function setModifiedFlag ( chain : string [ ] , node : TSESTree . Node ) {
118
203
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 ) ;
137
206
}
138
207
139
208
function processMemberExpression ( node : TSESTree . MemberExpression ) {
@@ -144,53 +213,26 @@ const noRepeatedMemberAccess = createRule({
144
213
return ;
145
214
}
146
215
147
- const chainInfo = analyzeChain ( node ) ;
148
- if ( ! chainInfo ) {
216
+ const properties = analyzeChain ( node ) ;
217
+ if ( ! properties || properties . length === 0 ) {
149
218
return ;
150
219
}
151
220
152
221
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 ) ;
175
223
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 ) ;
184
226
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 ) ) {
188
230
context . report ( {
189
231
node : node ,
190
232
messageId : "repeatedAccess" ,
191
- data : { chain : longestValidChain , count : chainInfo . count } ,
233
+ data : { chain : result . chain , count : result . count } ,
192
234
} ) ;
193
- reportedChains . add ( longestValidChain ) ;
235
+ reportedChains . add ( result . chain ) ;
194
236
}
195
237
}
196
238
@@ -200,32 +242,26 @@ const noRepeatedMemberAccess = createRule({
200
242
// This prevents us from extracting chains that are modified
201
243
AssignmentExpression : ( node ) => {
202
244
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 ) ;
207
247
}
208
248
} ,
209
249
210
250
// Track increment/decrement operations
211
251
// Example: obj.prop.counter++ modifies "obj.prop.counter"
212
252
UpdateExpression : ( node ) => {
213
253
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 ) ;
218
256
}
219
257
} ,
220
258
221
259
// Track function calls that might modify their arguments
222
260
// Example: obj.methods.update() might modify the "obj.methods" chain
223
261
CallExpression : ( node ) => {
224
262
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 ) ;
229
265
}
230
266
} ,
231
267
0 commit comments