3
3
* @author Eduardo San Martin Morote
4
4
*/
5
5
6
- import { ESLintUtils , type TSESTree } from '@typescript-eslint/utils'
6
+ import {
7
+ ESLintUtils ,
8
+ type TSESTree ,
9
+ type TSESLint ,
10
+ } from '@typescript-eslint/utils'
7
11
import {
8
12
isDefineStoreCall ,
9
13
isSetupStore ,
@@ -34,13 +38,14 @@ export const noCircularStoreDependencies = createRule({
34
38
circularDependency :
35
39
'Potential circular dependency detected: store "{{currentStore}}" uses "{{usedStore}}"' ,
36
40
setupCircularDependency :
37
- 'Avoid using other stores directly in setup function body. Use them in computed properties or actions instead.' ,
41
+ 'Avoid using other stores directly in setup function body. Use them in actions instead.' ,
38
42
} ,
39
43
} ,
40
44
defaultOptions : [ ] ,
41
45
create ( context ) {
42
46
const storeUsages = new Map < string , string [ ] > ( ) // currentStore -> [usedStores]
43
- let currentStoreName : string | null = null
47
+ const usageGraph = new Map < string , Map < string , TSESTree . Node [ ] > > ( ) // currentStore -> usedStore -> nodes
48
+ const storeStack : string [ ] = [ ]
44
49
45
50
return {
46
51
CallExpression ( node : TSESTree . CallExpression ) {
@@ -52,28 +57,29 @@ export const noCircularStoreDependencies = createRule({
52
57
parent ?. type === 'VariableDeclarator' &&
53
58
parent . id . type === 'Identifier'
54
59
) {
55
- currentStoreName = parent . id . name
60
+ const currentStoreName = parent . id . name
61
+ storeStack . push ( currentStoreName )
56
62
57
63
// Initialize usage tracking for this store
58
64
if ( ! storeUsages . has ( currentStoreName ) ) {
59
65
storeUsages . set ( currentStoreName , [ ] )
60
66
}
67
+ if ( ! usageGraph . has ( currentStoreName ) ) {
68
+ usageGraph . set ( currentStoreName , new Map ( ) )
69
+ }
61
70
62
71
// Check for store usage in setup function
63
72
if ( isSetupStore ( node ) ) {
64
73
const setupFunction = getSetupFunction ( node )
65
74
if ( setupFunction ) {
66
- checkSetupFunctionForStoreUsage (
67
- setupFunction ,
68
- currentStoreName ,
69
- context
70
- )
75
+ checkSetupFunctionForStoreUsage ( setupFunction , context )
71
76
}
72
77
}
73
78
}
74
79
}
75
80
76
81
// Track store usage calls
82
+ const currentStoreName = storeStack [ storeStack . length - 1 ]
77
83
if ( isStoreUsage ( node ) && currentStoreName ) {
78
84
const usedStoreName = getStoreNameFromUsage ( node )
79
85
if ( usedStoreName && usedStoreName !== currentStoreName ) {
@@ -82,6 +88,11 @@ export const noCircularStoreDependencies = createRule({
82
88
usages . push ( usedStoreName )
83
89
storeUsages . set ( currentStoreName , usages )
84
90
}
91
+ // record node for later reporting
92
+ const edges = usageGraph . get ( currentStoreName ) !
93
+ const nodes = edges . get ( usedStoreName ) ?? [ ]
94
+ nodes . push ( node )
95
+ edges . set ( usedStoreName , nodes )
85
96
86
97
// Check for immediate circular dependency
87
98
const usedStoreUsages = storeUsages . get ( usedStoreName ) || [ ]
@@ -99,9 +110,15 @@ export const noCircularStoreDependencies = createRule({
99
110
}
100
111
} ,
101
112
113
+ 'CallExpression:exit' ( node : TSESTree . CallExpression ) {
114
+ if ( isDefineStoreCall ( node ) ) {
115
+ storeStack . pop ( )
116
+ }
117
+ } ,
118
+
102
119
'Program:exit' ( ) {
103
120
// Check for indirect circular dependencies
104
- checkIndirectCircularDependencies ( storeUsages , context )
121
+ checkIndirectCircularDependencies ( usageGraph , context )
105
122
} ,
106
123
}
107
124
} ,
@@ -112,8 +129,10 @@ export const noCircularStoreDependencies = createRule({
112
129
*/
113
130
function checkSetupFunctionForStoreUsage (
114
131
setupFunction : TSESTree . FunctionExpression | TSESTree . ArrowFunctionExpression ,
115
- currentStoreName : string ,
116
- context : any
132
+ context : TSESLint . RuleContext <
133
+ 'circularDependency' | 'setupCircularDependency' ,
134
+ [ ]
135
+ >
117
136
) {
118
137
if ( setupFunction . body . type !== 'BlockStatement' ) {
119
138
return
@@ -150,40 +169,47 @@ function checkSetupFunctionForStoreUsage(
150
169
* Checks for indirect circular dependencies (A -> B -> C -> A)
151
170
*/
152
171
function checkIndirectCircularDependencies (
153
- storeUsages : Map < string , string [ ] > ,
154
- context : any
172
+ usageGraph : Map < string , Map < string , TSESTree . Node [ ] > > ,
173
+ context : TSESLint . RuleContext <
174
+ 'circularDependency' | 'setupCircularDependency' ,
175
+ [ ]
176
+ >
155
177
) {
156
178
const visited = new Set < string > ( )
157
- const recursionStack = new Set < string > ( )
158
-
159
- function hasCycle ( store : string , path : string [ ] = [ ] ) : boolean {
160
- if ( recursionStack . has ( store ) ) {
161
- // Found a cycle
162
- return true
163
- }
164
-
165
- if ( visited . has ( store ) ) {
166
- return false
167
- }
179
+ const inPath = new Set < string > ( )
180
+ const path : string [ ] = [ ]
181
+ const reported = new Set < string > ( ) // "A->B"
168
182
183
+ const dfs = ( store : string ) => {
169
184
visited . add ( store )
170
- recursionStack . add ( store )
171
-
172
- const dependencies = storeUsages . get ( store ) || [ ]
173
- for ( const dependency of dependencies ) {
174
- if ( hasCycle ( dependency , [ ...path , store ] ) ) {
175
- return true
185
+ inPath . add ( store )
186
+ path . push ( store )
187
+ const deps = usageGraph . get ( store ) ?? new Map ( )
188
+ for ( const [ dep ] of deps ) {
189
+ if ( ! visited . has ( dep ) ) dfs ( dep )
190
+ if ( inPath . has ( dep ) ) {
191
+ // report the edge(s) participating in the cycle at least once
192
+ const from = store
193
+ const to = dep
194
+ const key = `${ from } ->${ to } `
195
+ if ( ! reported . has ( key ) ) {
196
+ reported . add ( key )
197
+ const node = usageGraph . get ( from ) ?. get ( to ) ?. [ 0 ]
198
+ if ( node ) {
199
+ context . report ( {
200
+ node,
201
+ messageId : 'circularDependency' ,
202
+ data : { currentStore : from , usedStore : to } ,
203
+ } )
204
+ }
205
+ }
176
206
}
177
207
}
178
-
179
- recursionStack . delete ( store )
180
- return false
208
+ path . pop ( )
209
+ inPath . delete ( store )
181
210
}
182
211
183
- // Check each store for cycles
184
- for ( const store of storeUsages . keys ( ) ) {
185
- if ( ! visited . has ( store ) ) {
186
- hasCycle ( store )
187
- }
212
+ for ( const store of usageGraph . keys ( ) ) {
213
+ if ( ! visited . has ( store ) ) dfs ( store )
188
214
}
189
215
}
0 commit comments