11<?php
22/**
3- * Corrected Rule to detect and warn about overly nested GraphQL queries,
4- * with proper fragment handling.
3+ * Rule to detect and warn about overly nested GraphQL queries,
4+ * with proper fragment handling and recursion protection .
55 *
66 * @package WPGraphQL\Debug\Analysis\Rules
77 */
1515use GraphQL \Language \AST \FragmentDefinitionNode ;
1616use GraphQL \Language \AST \FragmentSpreadNode ;
1717use GraphQL \Language \AST \InlineFragmentNode ;
18- use GraphQL \Language \AST \Node ;
1918use GraphQL \Language \AST \OperationDefinitionNode ;
2019use GraphQL \Language \AST \SelectionSetNode ;
2120use GraphQL \Language \Parser ;
2524
2625class NestedQuery implements AnalyzerItemInterface {
2726
28- protected int $ maxDepth ;
29- protected ?string $ message = null ;
30-
31- /**
32- * @param int $maxDepth The maximum allowed nesting depth.
33- */
34- public function __construct ( int $ maxDepth = 8 ) {
35- $ this ->maxDepth = $ maxDepth ;
36- }
37-
38- /**
39- * @inheritDoc
40- */
41- public function analyze ( string $ query , array $ variables = [], ?Schema $ schema = null ): array {
42- $ triggered = false ;
43- $ maxDepthReached = 0 ;
44-
45- try {
46- /** @var DocumentNode $ast */
47- $ ast = Parser::parse ( $ query );
48- } catch ( SyntaxError $ error ) {
49- $ this ->message = 'Failed to analyze nesting depth due to GraphQL syntax error: ' . $ error ->getMessage ();
50- return [
51- 'triggered ' => false ,
52- 'message ' => $ this ->message ,
53- 'details ' => [ 'maxDepthReached ' => 0 ],
54- ];
55- }
56-
57- // Store fragment definitions in a map for easy lookup
58- $ fragments = [];
59- foreach ( $ ast ->definitions as $ definition ) {
60- if ( $ definition instanceof FragmentDefinitionNode ) {
61- $ fragments [ $ definition ->name ->value ] = $ definition ;
62- }
63- }
64-
65- // Find the operation definition and analyze its depth
66- foreach ( $ ast ->definitions as $ definition ) {
67- if ( $ definition instanceof OperationDefinitionNode ) {
68- // Start the recursion with a depth of 1, as the first field is the first level of nesting.
69- $ currentDepth = $ this ->getSelectionSetDepth ( $ definition ->selectionSet , $ fragments , 1 );
70- $ maxDepthReached = max ( $ maxDepthReached , $ currentDepth );
71- }
72- }
73-
74- // Use >= to trigger the rule when the max depth is reached or exceeded.
75- if ( $ maxDepthReached >= $ this ->maxDepth ) {
76- $ triggered = true ;
77- $ this ->message = sprintf (
78- 'Nested query depth of %d reached or exceeded the configured maximum of %d. ' ,
79- $ maxDepthReached ,
80- $ this ->maxDepth
81- );
82- } else {
83- $ this ->message = sprintf (
84- 'Nested query depth of %d is within the allowed limit of %d. ' ,
85- $ maxDepthReached ,
86- $ this ->maxDepth
87- );
88- }
89-
90- return [
91- 'triggered ' => $ triggered ,
92- 'message ' => $ this ->message ,
93- 'details ' => [
94- 'maxDepthReached ' => $ maxDepthReached ,
95- 'maxAllowed ' => $ this ->maxDepth ,
96- ],
97- ];
98- }
99-
100- /**
101- * Recursively calculates the maximum depth of a SelectionSet, including fragments.
102- *
103- * @param SelectionSetNode $selectionSet The selection set to analyze.
104- * @param array $fragments Map of fragment definitions.
105- * @param int $currentDepth The current depth of the traversal.
106- *
107- * @return int The maximum depth found.
108- */
109- protected function getSelectionSetDepth ( SelectionSetNode $ selectionSet , array $ fragments , int $ currentDepth = 0 ): int {
110- $ maxDepth = $ currentDepth ;
111-
112- foreach ( $ selectionSet ->selections as $ selection ) {
113- $ selectionMaxDepth = 0 ;
114-
115- if ( $ selection instanceof FieldNode && $ selection ->selectionSet instanceof SelectionSetNode ) {
116- $ selectionMaxDepth = $ this ->getSelectionSetDepth ( $ selection ->selectionSet , $ fragments , $ currentDepth + 1 );
117- }
118-
119- if ( $ selection instanceof FragmentSpreadNode ) {
120- // If a fragment spread is found, get the fragment definition and recurse.
121- if ( isset ( $ fragments [ $ selection ->name ->value ] ) ) {
122- /** @var FragmentDefinitionNode $fragmentDefinition */
123- $ fragmentDefinition = $ fragments [ $ selection ->name ->value ];
124- // The depth of the fragment is added to the current depth.
125- $ selectionMaxDepth = $ this ->getSelectionSetDepth ( $ fragmentDefinition ->selectionSet , $ fragments , $ currentDepth + 1 );
126- }
127- }
128-
129- if ( $ selection instanceof InlineFragmentNode && $ selection ->selectionSet instanceof SelectionSetNode ) {
130- // An inline fragment is also a new selection set, so recurse.
131- $ selectionMaxDepth = $ this ->getSelectionSetDepth ( $ selection ->selectionSet , $ fragments , $ currentDepth + 1 );
132- }
133-
134- $ maxDepth = max ( $ maxDepth , $ selectionMaxDepth );
135- }
136-
137- return $ maxDepth ;
138- }
139-
140- /**
141- * @inheritDoc
142- */
143- public function getKey (): string {
144- return 'nestedQuery ' ;
145- }
146- }
27+ protected int $ maxDepth ;
28+ protected ?string $ message = null ;
29+
30+ /**
31+ * @param int $maxDepth The maximum allowed nesting depth.
32+ */
33+ public function __construct ( int $ maxDepth = 8 ) {
34+ $ this ->maxDepth = $ maxDepth ;
35+ }
36+
37+ /**
38+ * @inheritDoc
39+ */
40+ public function analyze ( string $ query , array $ variables = [], ?Schema $ schema = null ): array {
41+ $ triggered = false ;
42+ $ maxDepthReached = 0 ;
43+
44+ try {
45+ /** @var DocumentNode $ast */
46+ $ ast = Parser::parse ( $ query );
47+ } catch ( SyntaxError $ error ) {
48+ $ this ->message = 'Failed to analyze nesting depth due to GraphQL syntax error: ' . $ error ->getMessage ();
49+ return [
50+ 'triggered ' => false ,
51+ 'message ' => $ this ->message ,
52+ 'details ' => [ 'maxDepthReached ' => 0 ],
53+ ];
54+ }
55+
56+ // Store fragment definitions for lookup
57+ $ fragments = [];
58+ foreach ( $ ast ->definitions as $ definition ) {
59+ if ( $ definition instanceof FragmentDefinitionNode ) {
60+ $ fragments [ $ definition ->name ->value ] = $ definition ;
61+ }
62+ }
63+
64+ // Analyze depth for each operation definition
65+ foreach ( $ ast ->definitions as $ definition ) {
66+ if ( $ definition instanceof OperationDefinitionNode ) {
67+ // Start depth at 1 because the first field is depth level 1.
68+ $ currentDepth = $ this ->getSelectionSetDepth ( $ definition ->selectionSet , $ fragments , 1 , [] );
69+ $ maxDepthReached = max ( $ maxDepthReached , $ currentDepth );
70+ }
71+ }
72+
73+ if ( $ maxDepthReached >= $ this ->maxDepth ) {
74+ $ triggered = true ;
75+ $ this ->message = sprintf (
76+ 'Nested query depth of %d reached or exceeded the configured maximum of %d. ' ,
77+ $ maxDepthReached ,
78+ $ this ->maxDepth
79+ );
80+ } else {
81+ $ this ->message = sprintf (
82+ 'Nested query depth of %d is within the allowed limit of %d. ' ,
83+ $ maxDepthReached ,
84+ $ this ->maxDepth
85+ );
86+ }
87+
88+ return [
89+ 'triggered ' => $ triggered ,
90+ 'message ' => $ this ->message ,
91+ 'details ' => [
92+ 'maxDepthReached ' => $ maxDepthReached ,
93+ 'maxAllowed ' => $ this ->maxDepth ,
94+ ],
95+ ];
96+ }
97+
98+ /**
99+ * Recursively calculates the maximum depth of a SelectionSet, including fragments.
100+ *
101+ * @param SelectionSetNode $selectionSet The selection set to analyze.
102+ * @param array $fragments Map of fragment definitions.
103+ * @param int $currentDepth The current depth of traversal.
104+ * @param array $visitedFragments Names of fragments already visited.
105+ *
106+ * @return int The maximum depth found.
107+ */
108+ protected function getSelectionSetDepth (
109+ SelectionSetNode $ selectionSet ,
110+ array $ fragments ,
111+ int $ currentDepth = 0 ,
112+ array $ visitedFragments = []
113+ ): int {
114+ $ maxDepth = $ currentDepth ;
115+
116+ foreach ( $ selectionSet ->selections as $ selection ) {
117+ $ selectionMaxDepth = $ currentDepth ;
118+
119+ // Field node
120+ if ( $ selection instanceof FieldNode && $ selection ->selectionSet instanceof SelectionSetNode ) {
121+ $ selectionMaxDepth = $ this ->getSelectionSetDepth (
122+ $ selection ->selectionSet ,
123+ $ fragments ,
124+ $ currentDepth + 1 ,
125+ $ visitedFragments
126+ );
127+ }
128+
129+ // Fragment spread
130+ if ( $ selection instanceof FragmentSpreadNode ) {
131+ $ fragName = $ selection ->name ->value ;
132+ if ( isset ( $ fragments [ $ fragName ] ) && ! in_array ( $ fragName , $ visitedFragments , true ) ) {
133+ $ visitedFragments [] = $ fragName ;
134+ /** @var FragmentDefinitionNode $fragmentDefinition */
135+ $ fragmentDefinition = $ fragments [ $ fragName ];
136+ // Inline fragment's selections at the current depth (no +1 here)
137+ $ selectionMaxDepth = $ this ->getSelectionSetDepth (
138+ $ fragmentDefinition ->selectionSet ,
139+ $ fragments ,
140+ $ currentDepth ,
141+ $ visitedFragments
142+ );
143+ }
144+ }
145+
146+ // Inline fragment
147+ if ( $ selection instanceof InlineFragmentNode && $ selection ->selectionSet instanceof SelectionSetNode ) {
148+ $ selectionMaxDepth = $ this ->getSelectionSetDepth (
149+ $ selection ->selectionSet ,
150+ $ fragments ,
151+ $ currentDepth + 1 ,
152+ $ visitedFragments
153+ );
154+ }
155+
156+ $ maxDepth = max ( $ maxDepth , $ selectionMaxDepth );
157+ }
158+
159+ return $ maxDepth ;
160+ }
161+
162+ /**
163+ * @inheritDoc
164+ */
165+ public function getKey (): string {
166+ return 'nestedQuery ' ;
167+ }
168+ }
0 commit comments