10
10
11
11
import type { ValidationContext } from '../index' ;
12
12
import { GraphQLError } from '../../error' ;
13
- import type { SelectionSet , FragmentSpread } from '../../language/ast' ;
14
- import { FRAGMENT_DEFINITION } from '../../language/kinds' ;
15
- import { visit } from '../../language/visitor' ;
13
+ import { FRAGMENT_SPREAD } from '../../language/kinds' ;
14
+ import type {
15
+ SelectionSet ,
16
+ FragmentSpread ,
17
+ FragmentDefinition
18
+ } from '../../language/ast' ;
16
19
17
20
18
21
export function cycleErrorMessage (
@@ -24,83 +27,92 @@ export function cycleErrorMessage(
24
27
}
25
28
26
29
export function NoFragmentCycles ( context : ValidationContext ) : any {
30
+ var errors = [ ] ;
27
31
28
- // Gather all the fragment spreads ASTs for each fragment definition.
29
- // Importantly this does not include inline fragments.
30
- var definitions = context . getDocument ( ) . definitions ;
31
- var spreadsInFragment = definitions . reduce ( ( map , node ) => {
32
- if ( node . kind === FRAGMENT_DEFINITION ) {
33
- map [ node . name . value ] = gatherSpreads ( node ) ;
34
- }
35
- return map ;
36
- } , { } ) ;
32
+ // Tracks already visited fragments to maintain O(N) and to ensure that cycles
33
+ // are not redundantly reported.
34
+ var visitedFrags = Object . create ( null ) ;
35
+
36
+ // Array of AST nodes used to produce meaningful errors
37
+ var spreadPath = [ ] ;
37
38
38
- // Tracks spreads known to lead to cycles to ensure that cycles are not
39
- // redundantly reported.
40
- var knownToLeadToCycle = new Set ( ) ;
39
+ // Position in the spread path
40
+ var spreadPathIndexByName = Object . create ( null ) ;
41
41
42
42
return {
43
+ Document : {
44
+ leave ( ) {
45
+ if ( errors . length ) {
46
+ return errors ;
47
+ }
48
+ }
49
+ } ,
50
+ OperationDefinition : ( ) => false ,
43
51
FragmentDefinition ( node ) {
44
- var errors = [ ] ;
45
- var initialName = node . name . value ;
52
+ if ( ! visitedFrags [ node . name . value ] ) {
53
+ detectCycleRecursive ( node ) ;
54
+ }
55
+ return false ;
56
+ } ,
57
+ } ;
46
58
47
- // Array of AST nodes used to produce meaningful errors
48
- var spreadPath = [ ] ;
59
+ // This does a straight-forward DFS to find cycles.
60
+ // It does not terminate when a cycle was found but continues to explore
61
+ // the graph to find all possible cycles.
62
+ function detectCycleRecursive ( fragment : FragmentDefinition ) {
63
+ const fragmentName = fragment . name . value ;
64
+ visitedFrags [ fragmentName ] = true ;
49
65
50
- // This does a straight-forward DFS to find cycles.
51
- // It does not terminate when a cycle was found but continues to explore
52
- // the graph to find all possible cycles.
53
- function detectCycleRecursive ( fragmentName ) {
54
- var spreadNodes = spreadsInFragment [ fragmentName ] ;
55
- if ( spreadNodes ) {
56
- for ( var i = 0 ; i < spreadNodes . length ; ++ i ) {
57
- var spreadNode = spreadNodes [ i ] ;
58
- if ( knownToLeadToCycle . has ( spreadNode ) ) {
59
- continue ;
60
- }
61
- if ( spreadNode . name . value === initialName ) {
62
- var cyclePath = spreadPath . concat ( spreadNode ) ;
63
- cyclePath . forEach ( spread => knownToLeadToCycle . add ( spread ) ) ;
64
- errors . push ( new GraphQLError (
65
- cycleErrorMessage (
66
- initialName ,
67
- spreadPath . map ( s => s . name . value )
68
- ) ,
69
- cyclePath
70
- ) ) ;
71
- continue ;
72
- }
73
- if ( spreadPath . some ( spread => spread === spreadNode ) ) {
74
- continue ;
75
- }
66
+ const spreadNodes = [ ] ;
67
+ gatherSpreads ( spreadNodes , fragment . selectionSet ) ;
68
+ if ( spreadNodes . length === 0 ) {
69
+ return ;
70
+ }
71
+
72
+ spreadPathIndexByName [ fragmentName ] = spreadPath . length ;
76
73
77
- spreadPath . push ( spreadNode ) ;
78
- detectCycleRecursive ( spreadNode . name . value ) ;
79
- spreadPath . pop ( ) ;
74
+ for ( let i = 0 ; i < spreadNodes . length ; i ++ ) {
75
+ const spreadNode = spreadNodes [ i ] ;
76
+ const spreadName = spreadNode . name . value ;
77
+ const cycleIndex = spreadPathIndexByName [ spreadName ] ;
78
+
79
+ if ( cycleIndex === undefined ) {
80
+ spreadPath . push ( spreadNode ) ;
81
+ if ( ! visitedFrags [ spreadName ] ) {
82
+ const spreadFragment = context . getFragment ( spreadName ) ;
83
+ if ( spreadFragment ) {
84
+ detectCycleRecursive ( spreadFragment ) ;
80
85
}
81
86
}
87
+ spreadPath . pop ( ) ;
88
+ } else {
89
+ const cyclePath = spreadPath . slice ( cycleIndex ) ;
90
+ errors . push ( new GraphQLError (
91
+ cycleErrorMessage (
92
+ spreadName ,
93
+ cyclePath . map ( s => s . name . value )
94
+ ) ,
95
+ cyclePath . concat ( spreadNode )
96
+ ) ) ;
82
97
}
98
+ }
83
99
84
- detectCycleRecursive ( initialName ) ;
85
-
86
- if ( errors . length > 0 ) {
87
- return errors ;
88
- }
89
- } ,
90
- } ;
100
+ spreadPathIndexByName [ fragmentName ] = undefined ;
101
+ }
91
102
}
92
103
93
104
/**
94
105
* Given an operation or fragment AST node, gather all the
95
106
* named spreads defined within the scope of the fragment
96
107
* or operation
97
108
*/
98
- function gatherSpreads ( node : SelectionSet ) : Array < FragmentSpread > {
99
- var spreadNodes = [ ] ;
100
- visit ( node , {
101
- FragmentSpread ( spread ) {
102
- spreadNodes . push ( spread ) ;
109
+ function gatherSpreads ( spreads : Array < FragmentSpread > , node : SelectionSet ) {
110
+ for ( let i = 0 ; i < node . selections . length ; i ++ ) {
111
+ const selection = node . selections [ i ] ;
112
+ if ( selection . kind === FRAGMENT_SPREAD ) {
113
+ spreads . push ( selection ) ;
114
+ } else if ( selection . selectionSet ) {
115
+ gatherSpreads ( spreads , selection . selectionSet ) ;
103
116
}
104
- } ) ;
105
- return spreadNodes ;
117
+ }
106
118
}
0 commit comments