@@ -30,6 +30,168 @@ export default createRule<Options, MessageIds>({
3030 } ,
3131 defaultOptions : [ ] ,
3232 create ( context ) {
33+ const SKIPPED_IIFE_NODES = new Set <
34+ | TSESTree . ArrowFunctionExpression
35+ | TSESTree . FunctionDeclaration
36+ | TSESTree . FunctionExpression
37+ > ( ) ;
38+
39+ /**
40+ * Gets the containing loop node of a specified node.
41+ *
42+ * We don't need to check nested functions, so this ignores those.
43+ * `Scope.through` contains references of nested functions.
44+ *
45+ * @param node An AST node to get.
46+ * @returns The containing loop node of the specified node, or `null`.
47+ */
48+ function getContainingLoopNode ( node : TSESTree . Node ) : TSESTree . Node | null {
49+ for (
50+ let currentNode = node ;
51+ currentNode . parent ;
52+ currentNode = currentNode . parent
53+ ) {
54+ const parent = currentNode . parent ;
55+
56+ switch ( parent . type ) {
57+ case AST_NODE_TYPES . WhileStatement :
58+ case AST_NODE_TYPES . DoWhileStatement :
59+ return parent ;
60+
61+ case AST_NODE_TYPES . ForStatement :
62+ // `init` is outside of the loop.
63+ if ( parent . init !== currentNode ) {
64+ return parent ;
65+ }
66+ break ;
67+
68+ case AST_NODE_TYPES . ForInStatement :
69+ case AST_NODE_TYPES . ForOfStatement :
70+ // `right` is outside of the loop.
71+ if ( parent . right !== currentNode ) {
72+ return parent ;
73+ }
74+ break ;
75+
76+ case AST_NODE_TYPES . ArrowFunctionExpression :
77+ case AST_NODE_TYPES . FunctionExpression :
78+ case AST_NODE_TYPES . FunctionDeclaration :
79+ // We don't need to check nested functions.
80+
81+ // We need to check nested functions only in case of IIFE.
82+ if ( SKIPPED_IIFE_NODES . has ( parent ) ) {
83+ break ;
84+ }
85+ return null ;
86+
87+ default :
88+ break ;
89+ }
90+ }
91+
92+ return null ;
93+ }
94+
95+ /**
96+ * Gets the containing loop node of a given node.
97+ * If the loop was nested, this returns the most outer loop.
98+ * @param node A node to get. This is a loop node.
99+ * @param excludedNode A node that the result node should not include.
100+ * @returns The most outer loop node.
101+ */
102+ function getTopLoopNode (
103+ node : TSESTree . Node ,
104+ excludedNode : TSESTree . Node | null | undefined ,
105+ ) : TSESTree . Node {
106+ const border = excludedNode ? excludedNode . range [ 1 ] : 0 ;
107+ let retv = node ;
108+ let containingLoopNode : TSESTree . Node | null = node ;
109+
110+ while ( containingLoopNode && containingLoopNode . range [ 0 ] >= border ) {
111+ retv = containingLoopNode ;
112+ containingLoopNode = getContainingLoopNode ( containingLoopNode ) ;
113+ }
114+
115+ return retv ;
116+ }
117+
118+ /**
119+ * Checks whether a given reference which refers to an upper scope's variable is
120+ * safe or not.
121+ * @param loopNode A containing loop node.
122+ * @param reference A reference to check.
123+ * @returns `true` if the reference is safe or not.
124+ */
125+ function isSafe (
126+ loopNode : TSESTree . Node ,
127+ reference : TSESLint . Scope . Reference ,
128+ ) : boolean {
129+ const variable = reference . resolved ;
130+ const definition = variable ?. defs [ 0 ] ;
131+ const declaration = definition ?. parent ;
132+ const kind =
133+ declaration ?. type === AST_NODE_TYPES . VariableDeclaration
134+ ? declaration . kind
135+ : '' ;
136+
137+ // type references are all safe
138+ // this only really matters for global types that haven't been configured
139+ if ( reference . isTypeReference ) {
140+ return true ;
141+ }
142+
143+ // Variables which are declared by `const` is safe.
144+ if ( kind === 'const' ) {
145+ return true ;
146+ }
147+
148+ /*
149+ * Variables which are declared by `let` in the loop is safe.
150+ * It's a different instance from the next loop step's.
151+ */
152+ if (
153+ kind === 'let' &&
154+ declaration &&
155+ declaration . range [ 0 ] > loopNode . range [ 0 ] &&
156+ declaration . range [ 1 ] < loopNode . range [ 1 ]
157+ ) {
158+ return true ;
159+ }
160+
161+ /*
162+ * WriteReferences which exist after this border are unsafe because those
163+ * can modify the variable.
164+ */
165+ const border = getTopLoopNode (
166+ loopNode ,
167+ kind === 'let' ? declaration : null ,
168+ ) . range [ 0 ] ;
169+
170+ /**
171+ * Checks whether a given reference is safe or not.
172+ * The reference is every reference of the upper scope's variable we are
173+ * looking now.
174+ *
175+ * It's safe if the reference matches one of the following condition.
176+ * - is readonly.
177+ * - doesn't exist inside a local function and after the border.
178+ *
179+ * @param upperRef A reference to check.
180+ * @returns `true` if the reference is safe.
181+ */
182+ function isSafeReference ( upperRef : TSESLint . Scope . Reference ) : boolean {
183+ const id = upperRef . identifier ;
184+
185+ return (
186+ ! upperRef . isWrite ( ) ||
187+ ( variable ?. scope . variableScope === upperRef . from . variableScope &&
188+ id . range [ 0 ] < border )
189+ ) ;
190+ }
191+
192+ return variable ?. references . every ( isSafeReference ) ?? false ;
193+ }
194+
33195 /**
34196 * Reports functions which match the following condition:
35197 * - has a loop node in ancestors.
@@ -50,8 +212,25 @@ export default createRule<Options, MessageIds>({
50212 }
51213
52214 const references = context . sourceCode . getScope ( node ) . through ;
215+
216+ if ( ! ( node . async || node . generator ) && isIIFE ( node ) ) {
217+ const isFunctionExpression =
218+ node . type === AST_NODE_TYPES . FunctionExpression ;
219+
220+ // Check if the function is referenced elsewhere in the code
221+ const isFunctionReferenced =
222+ isFunctionExpression && node . id
223+ ? references . some ( r => r . identifier . name === node . id ?. name )
224+ : false ;
225+
226+ if ( ! isFunctionReferenced ) {
227+ SKIPPED_IIFE_NODES . add ( node ) ;
228+ return ;
229+ }
230+ }
231+
53232 const unsafeRefs = references
54- . filter ( r => ! isSafe ( loopNode , r ) )
233+ . filter ( r => r . resolved && ! isSafe ( loopNode , r ) )
55234 . map ( r => r . identifier . name ) ;
56235
57236 if ( unsafeRefs . length > 0 ) {
@@ -71,151 +250,14 @@ export default createRule<Options, MessageIds>({
71250 } ,
72251} ) ;
73252
74- /**
75- * Gets the containing loop node of a specified node.
76- *
77- * We don't need to check nested functions, so this ignores those.
78- * `Scope.through` contains references of nested functions.
79- *
80- * @param node An AST node to get.
81- * @returns The containing loop node of the specified node, or `null`.
82- */
83- function getContainingLoopNode ( node : TSESTree . Node ) : TSESTree . Node | null {
84- for (
85- let currentNode = node ;
86- currentNode . parent ;
87- currentNode = currentNode . parent
88- ) {
89- const parent = currentNode . parent ;
90-
91- switch ( parent . type ) {
92- case AST_NODE_TYPES . WhileStatement :
93- case AST_NODE_TYPES . DoWhileStatement :
94- return parent ;
95-
96- case AST_NODE_TYPES . ForStatement :
97- // `init` is outside of the loop.
98- if ( parent . init !== currentNode ) {
99- return parent ;
100- }
101- break ;
102-
103- case AST_NODE_TYPES . ForInStatement :
104- case AST_NODE_TYPES . ForOfStatement :
105- // `right` is outside of the loop.
106- if ( parent . right !== currentNode ) {
107- return parent ;
108- }
109- break ;
110-
111- case AST_NODE_TYPES . ArrowFunctionExpression :
112- case AST_NODE_TYPES . FunctionExpression :
113- case AST_NODE_TYPES . FunctionDeclaration :
114- // We don't need to check nested functions.
115- return null ;
116-
117- default :
118- break ;
119- }
120- }
121-
122- return null ;
123- }
124-
125- /**
126- * Gets the containing loop node of a given node.
127- * If the loop was nested, this returns the most outer loop.
128- * @param node A node to get. This is a loop node.
129- * @param excludedNode A node that the result node should not include.
130- * @returns The most outer loop node.
131- */
132- function getTopLoopNode (
133- node : TSESTree . Node ,
134- excludedNode : TSESTree . Node | null | undefined ,
135- ) : TSESTree . Node {
136- const border = excludedNode ? excludedNode . range [ 1 ] : 0 ;
137- let retv = node ;
138- let containingLoopNode : TSESTree . Node | null = node ;
139-
140- while ( containingLoopNode && containingLoopNode . range [ 0 ] >= border ) {
141- retv = containingLoopNode ;
142- containingLoopNode = getContainingLoopNode ( containingLoopNode ) ;
143- }
144-
145- return retv ;
146- }
147-
148- /**
149- * Checks whether a given reference which refers to an upper scope's variable is
150- * safe or not.
151- * @param loopNode A containing loop node.
152- * @param reference A reference to check.
153- * @returns `true` if the reference is safe or not.
154- */
155- function isSafe (
156- loopNode : TSESTree . Node ,
157- reference : TSESLint . Scope . Reference ,
253+ function isIIFE (
254+ node :
255+ | TSESTree . ArrowFunctionExpression
256+ | TSESTree . FunctionDeclaration
257+ | TSESTree . FunctionExpression ,
158258) : boolean {
159- const variable = reference . resolved ;
160- const definition = variable ?. defs [ 0 ] ;
161- const declaration = definition ?. parent ;
162- const kind =
163- declaration ?. type === AST_NODE_TYPES . VariableDeclaration
164- ? declaration . kind
165- : '' ;
166-
167- // type references are all safe
168- // this only really matters for global types that haven't been configured
169- if ( reference . isTypeReference ) {
170- return true ;
171- }
172-
173- // Variables which are declared by `const` is safe.
174- if ( kind === 'const' ) {
175- return true ;
176- }
177-
178- /*
179- * Variables which are declared by `let` in the loop is safe.
180- * It's a different instance from the next loop step's.
181- */
182- if (
183- kind === 'let' &&
184- declaration &&
185- declaration . range [ 0 ] > loopNode . range [ 0 ] &&
186- declaration . range [ 1 ] < loopNode . range [ 1 ]
187- ) {
188- return true ;
189- }
190-
191- /*
192- * WriteReferences which exist after this border are unsafe because those
193- * can modify the variable.
194- */
195- const border = getTopLoopNode ( loopNode , kind === 'let' ? declaration : null )
196- . range [ 0 ] ;
197-
198- /**
199- * Checks whether a given reference is safe or not.
200- * The reference is every reference of the upper scope's variable we are
201- * looking now.
202- *
203- * It's safe if the reference matches one of the following condition.
204- * - is readonly.
205- * - doesn't exist inside a local function and after the border.
206- *
207- * @param upperRef A reference to check.
208- * @returns `true` if the reference is safe.
209- */
210- function isSafeReference ( upperRef : TSESLint . Scope . Reference ) : boolean {
211- const id = upperRef . identifier ;
212-
213- return (
214- ! upperRef . isWrite ( ) ||
215- ( variable ?. scope . variableScope === upperRef . from . variableScope &&
216- id . range [ 0 ] < border )
217- ) ;
218- }
219-
220- return variable ?. references . every ( isSafeReference ) ?? false ;
259+ return (
260+ node . parent . type === AST_NODE_TYPES . CallExpression &&
261+ node . parent . callee === node
262+ ) ;
221263}
0 commit comments