1
- import { ESLintUtils , TSESTree } from "@typescript-eslint/utils" ;
1
+ import {
2
+ AST_NODE_TYPES ,
3
+ ESLintUtils ,
4
+ TSESTree ,
5
+ } from "@typescript-eslint/utils" ;
2
6
3
7
/**
4
8
* ESlint plugin to detect possible issues that can have impact on performance
@@ -10,7 +14,7 @@ const createRule = ESLintUtils.RuleCreator(
10
14
11
15
/**
12
16
* Rule: Array Initializer
13
- * avoid to use [] to init variable
17
+ * Avoid using [] to initialize variables
14
18
* [] will create a temporary object in data section.
15
19
* BAD
16
20
* let v: i32[] = [];
@@ -77,8 +81,8 @@ const arrayInitStyle: ESLintUtils.RuleModule<
77
81
78
82
/**
79
83
* Rule: Member Variable / Array Element Get
80
- * avoid to get member variable multiple- times in the same context.
81
- * it can significantly increase code size (8% in some proxy like usecase ) and waste CPU!! !
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!
82
86
*
83
87
* Implementation overview:
84
88
* - For each outermost MemberExpression (e.g. ctx.data.v1), extract the "object chain" part (e.g. ctx.data).
@@ -134,35 +138,114 @@ const noRepeatedMemberAccess: ESLintUtils.RuleModule<
134
138
let current = node ;
135
139
const path : string [ ] = [ ] ;
136
140
141
+ // Helper function to skip TSNonNullExpression nodes
142
+ function skipTSNonNullExpression (
143
+ node : TSESTree . Node
144
+ ) : TSESTree . Expression {
145
+ if ( node . type === "TSNonNullExpression" ) {
146
+ return skipTSNonNullExpression ( node . expression ) ;
147
+ }
148
+ return node as TSESTree . Expression ;
149
+ }
150
+
151
+ // First check if this is part of a switch-case statement
152
+ let parent = node ;
153
+ while ( parent && parent . parent ) {
154
+ parent = parent . parent ;
155
+ if (
156
+ parent . type === AST_NODE_TYPES . SwitchCase &&
157
+ parent . test &&
158
+ ( node === parent . test || isDescendant ( node , parent . test ) )
159
+ ) {
160
+ return null ; // Skip members used in switch-case statements
161
+ }
162
+ }
163
+
164
+ // Helper function to check if a node is a descendant of another node
165
+ function isDescendant (
166
+ node : TSESTree . Node ,
167
+ possibleAncestor : TSESTree . Node
168
+ ) : boolean {
169
+ let current = node . parent ;
170
+ while ( current ) {
171
+ if ( current === possibleAncestor ) return true ;
172
+ current = current . parent ;
173
+ }
174
+ return false ;
175
+ }
176
+
137
177
// Traverse up to the second last member (object chain)
138
- while ( current && current . type === "MemberExpression" ) {
178
+ while (
179
+ ( current && current . type === "MemberExpression" ) ||
180
+ current . type === "TSNonNullExpression"
181
+ ) {
182
+ // Skip non-null assertions
183
+ if ( current . type === "TSNonNullExpression" ) {
184
+ current = current . expression ;
185
+ continue ;
186
+ }
187
+
139
188
// Only handle static property or static index
140
189
if ( current . computed ) {
190
+ // true means access with "[]"
191
+ // false means access with "."
141
192
if ( current . property . type === "Literal" ) {
142
- path . unshift ( `[${ current . property . raw } ]` ) ;
193
+ // e.g. obj[1], obj["name"]
194
+ path . unshift ( `[${ current . property . value } ]` ) ;
143
195
} else {
144
196
// Ignore dynamic property access
197
+ // e.g. obj[var], obj[func()]
145
198
return null ;
146
199
}
147
200
} else {
201
+ // e.g. obj.prop
148
202
path . unshift ( `.${ current . property . name } ` ) ;
149
203
}
204
+
205
+ // Check if we've reached the base object
206
+ let objExpr = current . object ;
207
+ if ( objExpr ?. type === "TSNonNullExpression" ) {
208
+ objExpr = skipTSNonNullExpression ( objExpr ) ;
209
+ }
210
+
150
211
// If object is not MemberExpression, we've reached the base object
151
- if ( ! current . object || current . object . type !== "MemberExpression" ) {
212
+ if ( ! objExpr || objExpr . type !== "MemberExpression" ) {
213
+ // Handle "this" expressions
214
+ if ( objExpr && objExpr . type === "ThisExpression" ) {
215
+ path . unshift ( "this" ) ;
216
+
217
+ // Skip reporting if the chain is just 'this.property'
218
+ if ( path . length <= 2 ) {
219
+ return null ;
220
+ }
221
+
222
+ path . pop ( ) ; // Remove the last property
223
+ return path . join ( "" ) . replace ( / ^ \. / , "" ) ;
224
+ }
225
+
152
226
// If object is Identifier, add it to the path
153
- if ( current . object && current . object . type === "Identifier" ) {
154
- path . unshift ( current . object . name ) ;
227
+ if ( objExpr && objExpr . type === "Identifier" ) {
228
+ const baseName = objExpr . name ;
229
+
230
+ // Skip if the base looks like an enum/constant (starts with capital letter)
231
+ if (
232
+ baseName . length > 0 &&
233
+ baseName [ 0 ] === baseName [ 0 ] . toUpperCase ( )
234
+ ) {
235
+ return null ; // Likely an enum or static class
236
+ }
237
+
238
+ path . unshift ( baseName ) ;
155
239
// Remove the last property (keep only the object chain)
156
240
path . pop ( ) ;
157
241
return path . join ( "" ) . replace ( / ^ \. / , "" ) ;
158
242
}
159
243
return null ;
160
244
}
161
- current = current . object ;
245
+ current = objExpr ;
162
246
}
163
247
return null ;
164
248
}
165
-
166
249
const occurrences = new Map ( ) ;
167
250
const minOccurrences = context . options [ 0 ] ?. minOccurrences || 2 ;
168
251
@@ -174,8 +257,47 @@ const noRepeatedMemberAccess: ESLintUtils.RuleModule<
174
257
const objectChain = getObjectChain ( node ) ;
175
258
if ( ! objectChain ) return ;
176
259
260
+ const baseObjectName = objectChain . split ( / [ . [ ] / ) [ 0 ] ;
177
261
// Use scope range as part of the key
178
262
const scope = context . sourceCode . getScope ( node ) ;
263
+ if ( ! scope || ! scope . block || ! scope . block . range ) return ;
264
+
265
+ let variable = null ;
266
+ let currentScope = scope ;
267
+
268
+ while ( currentScope ) {
269
+ variable = currentScope . variables . find (
270
+ ( v ) => v . name === baseObjectName
271
+ ) ;
272
+ if ( variable ) break ;
273
+
274
+ const upperScope = currentScope . upper ;
275
+ if ( ! upperScope ) break ;
276
+ currentScope = upperScope ;
277
+ }
278
+
279
+ if ( variable ) {
280
+ // check for const/enum/import
281
+ const isConst = variable . defs . every (
282
+ ( def ) => def . node && "kind" in def . node && def . node . kind === "const"
283
+ ) ;
284
+ const isEnum = variable . defs . some (
285
+ ( def ) =>
286
+ ( def . parent as TSESTree . Node ) ?. type ===
287
+ AST_NODE_TYPES . TSEnumDeclaration
288
+ ) ;
289
+ const isImport = variable . defs . some (
290
+ ( def ) =>
291
+ def . type === "ImportBinding" ||
292
+ ( def . node &&
293
+ "type" in def . node &&
294
+ def . node . type === AST_NODE_TYPES . ImportDeclaration )
295
+ ) ;
296
+ if ( isConst || isEnum || isImport ) {
297
+ return ; // skip these types as extracting them won't be helpful
298
+ }
299
+ }
300
+
179
301
const key = `${ scope . block . range . join ( "-" ) } -${ objectChain } ` ;
180
302
181
303
const count = ( occurrences . get ( key ) || 0 ) + 1 ;
0 commit comments