@@ -10,42 +10,64 @@ const noRepeatedMemberAccess = createRule({
10
10
description :
11
11
"Optimize repeated member access patterns by extracting variables" ,
12
12
} ,
13
+ schema : [ ] ,
13
14
fixable : "code" ,
14
- schema : [
15
- {
16
- type : "object" ,
17
- properties : {
18
- minOccurrences : { type : "number" , minimum : 2 , default : 3 } ,
19
- } ,
20
- } ,
21
- ] ,
22
15
messages : {
23
16
repeatedAccess :
24
- "Member chain '{{ chain }}' accessed {{ count }} times. Extract to variable." ,
17
+ "Member chain '{{ chain }}' is accessed multiple times. Extract to variable." ,
25
18
} ,
26
19
} ,
27
- defaultOptions : [ { minOccurrences : 3 } ] ,
20
+ defaultOptions : [ ] ,
28
21
29
- create ( context , [ options ] ) {
22
+ create ( context ) {
30
23
const sourceCode = context . sourceCode ;
31
- const minOccurrences = options . minOccurrences ;
32
24
33
25
// Track which chains have already been reported to avoid duplicate reports
34
26
const reportedChains = new Set < string > ( ) ;
35
27
36
28
// Tree-based approach for storing member access chains
37
29
// Each node represents a property in the chain (e.g., a -> b -> c for a.b.c)
38
30
class ChainNode {
39
- name : string ;
40
- count : number = 0 ;
41
- modified : boolean = false ;
42
- parent ?: ChainNode ;
43
- children : Map < string , ChainNode > = new Map ( ) ;
31
+ private name : string ;
32
+ private count : number = 0 ;
33
+ private modified : boolean = false ;
34
+ private parent ?: ChainNode ;
35
+ private children : Map < string , ChainNode > = new Map ( ) ;
44
36
45
37
constructor ( name : string ) {
46
38
this . name = name ;
47
39
}
48
40
41
+ // Getter methods for private properties
42
+ get getName ( ) : string {
43
+ return this . name ;
44
+ }
45
+
46
+ get getCount ( ) : number {
47
+ return this . count ;
48
+ }
49
+
50
+ get isModified ( ) : boolean {
51
+ return this . modified ;
52
+ }
53
+
54
+ get getParent ( ) : ChainNode | undefined {
55
+ return this . parent ;
56
+ }
57
+
58
+ get getChildren ( ) : Map < string , ChainNode > {
59
+ return this . children ;
60
+ }
61
+
62
+ // Setter methods for private properties
63
+ set setParent ( parent : ChainNode | undefined ) {
64
+ this . parent = parent ;
65
+ }
66
+
67
+ incrementCount ( ) : void {
68
+ this . count ++ ;
69
+ }
70
+
49
71
// Get or create child node
50
72
getOrCreateChild ( childName : string ) : ChainNode {
51
73
if ( ! this . children . has ( childName ) ) {
@@ -56,27 +78,33 @@ const noRepeatedMemberAccess = createRule({
56
78
57
79
// Get the full chain path from root to this node
58
80
getChainPath ( ) : string {
81
+ // Build path from child to root, then reverse at the end
59
82
const path : string [ ] = [ ] ;
60
83
let current = this as ChainNode | undefined ;
61
- while ( current && current . name !== "__root__" ) {
62
- path . unshift ( current . name ) ;
63
- current = current . parent ;
84
+ while ( current && current . getName !== "__root__" ) {
85
+ path . push ( current . getName ) ;
86
+ current = current . getParent ;
64
87
}
88
+
89
+ // Reverse the array once at the end
90
+ path . reverse ( ) ;
65
91
return path . join ( "." ) ;
66
92
}
67
93
68
94
// Mark this node and all its descendants as modified
69
95
markAsModified ( ) : void {
70
96
this . modified = true ;
71
- for ( const child of this . children . values ( ) ) {
97
+ for ( const child of this . getChildren . values ( ) ) {
72
98
child . markAsModified ( ) ;
73
99
}
74
100
}
75
101
}
76
102
77
103
// Root node for the tree (per scope)
78
104
class ChainTree {
79
- root : ChainNode = new ChainNode ( "__root__" ) ;
105
+ private root : ChainNode = new ChainNode ( "__root__" ) ;
106
+ private validChainsCache : Array < { chain : string } > = [ ] ;
107
+ private cacheValid : boolean = false ;
80
108
81
109
// Insert a chain path into the tree and increment counts
82
110
insertChain ( properties : string [ ] ) : void {
@@ -85,14 +113,15 @@ const noRepeatedMemberAccess = createRule({
85
113
// Navigate/create path in tree
86
114
for ( const prop of properties ) {
87
115
const child = current . getOrCreateChild ( prop ) ;
88
- child . parent = current ;
116
+ child . setParent = current ;
89
117
current = child ;
90
118
91
119
// Only increment count for non-single properties (chains with dots)
92
120
if ( properties . length > 1 ) {
93
- current . count ++ ;
121
+ current . incrementCount ( ) ;
94
122
}
95
123
}
124
+ this . cacheValid = false ;
96
125
}
97
126
98
127
// Mark a chain and its descendants as modified
@@ -101,52 +130,47 @@ const noRepeatedMemberAccess = createRule({
101
130
102
131
// Navigate to the target node, creating nodes if they don't exist
103
132
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
- const newChild = current . getOrCreateChild ( prop ) ;
110
- newChild . parent = current ;
111
- current = newChild ;
112
- }
133
+ const newChild = current . getOrCreateChild ( prop ) ;
134
+ newChild . setParent = current ;
135
+ current = newChild ;
113
136
}
114
137
115
138
// Mark this node and all descendants as modified
116
139
current . markAsModified ( ) ;
140
+ this . cacheValid = false ;
117
141
}
118
142
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 ;
143
+ // Find any valid chain that meets the minimum occurrence threshold
144
+ findValidChains ( ) {
145
+ if ( this . cacheValid ) {
146
+ return this . validChainsCache ;
147
+ }
148
+ const validChains : Array < { chain : string } > = [ ] ;
125
149
126
150
const traverse = ( node : ChainNode , depth : number ) => {
127
151
// Only consider chains with more than one segment (has dots)
128
- if ( depth > 1 && ! node . modified && node . count >= minOccurrences ) {
152
+ if ( depth > 1 && ! node . isModified && node . getCount >= 2 ) {
129
153
const chainPath = node . getChainPath ( ) ;
130
- if ( chainPath . length > ( bestChain ?. length || 0 ) ) {
131
- bestChain = chainPath ;
132
- bestCount = node . count ;
133
- }
154
+ validChains . push ( {
155
+ chain : chainPath ,
156
+ } ) ;
134
157
}
135
158
136
159
// Stop traversing if this node is modified
137
- if ( node . modified ) {
160
+ if ( node . isModified ) {
138
161
return ;
139
162
}
140
163
141
164
// Recursively traverse children
142
- for ( const child of node . children . values ( ) ) {
165
+ for ( const child of node . getChildren . values ( ) ) {
143
166
traverse ( child , depth + 1 ) ;
144
167
}
145
168
} ;
146
169
147
170
traverse ( this . root , 0 ) ;
148
-
149
- return bestChain ? { chain : bestChain , count : bestCount } : null ;
171
+ this . cacheValid = true ;
172
+ this . validChainsCache = validChains ;
173
+ return validChains ;
150
174
}
151
175
}
152
176
@@ -224,15 +248,17 @@ const noRepeatedMemberAccess = createRule({
224
248
// Insert the chain into the tree (this will increment counts automatically)
225
249
chainTree . insertChain ( properties ) ;
226
250
227
- // Find the longest valid chain to report
228
- const result = chainTree . findLongestValidChain ( minOccurrences ) ;
229
- if ( result && ! reportedChains . has ( result . chain ) ) {
230
- context . report ( {
231
- node : node ,
232
- messageId : "repeatedAccess" ,
233
- data : { chain : result . chain , count : result . count } ,
234
- } ) ;
235
- reportedChains . add ( result . chain ) ;
251
+ // Find all valid chains to report
252
+ const validChains = chainTree . findValidChains ( ) ;
253
+ for ( const result of validChains ) {
254
+ if ( ! reportedChains . has ( result . chain ) ) {
255
+ context . report ( {
256
+ node : node ,
257
+ messageId : "repeatedAccess" ,
258
+ data : { chain : result . chain } ,
259
+ } ) ;
260
+ reportedChains . add ( result . chain ) ;
261
+ }
236
262
}
237
263
}
238
264
0 commit comments