1
- import { ARRAY_DEFAULT_OPTIONS , requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
2
- import { GraphQLESLintRule , OmitRecursively } from '../types' ;
3
- import { GraphQLInterfaceType , GraphQLObjectType , Kind , SelectionSetNode } from 'graphql' ;
1
+ import {
2
+ ASTNode ,
3
+ GraphQLInterfaceType ,
4
+ GraphQLObjectType ,
5
+ GraphQLOutputType ,
6
+ Kind ,
7
+ SelectionSetNode ,
8
+ TypeInfo ,
9
+ visit ,
10
+ visitWithTypeInfo ,
11
+ } from 'graphql' ;
12
+ import type * as ESTree from 'estree' ;
4
13
import { asArray } from '@graphql-tools/utils' ;
14
+ import { ARRAY_DEFAULT_OPTIONS , requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
15
+ import { GraphQLESLintRule , OmitRecursively , ReportDescriptor } from '../types' ;
5
16
import { getBaseType , GraphQLESTreeNode } from '../estree-parser' ;
6
17
7
18
export type RequireIdWhenAvailableRuleConfig = { fieldName : string | string [ ] } ;
@@ -22,6 +33,7 @@ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunct
22
33
const rule : GraphQLESLintRule < [ RequireIdWhenAvailableRuleConfig ] , true > = {
23
34
meta : {
24
35
type : 'suggestion' ,
36
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
25
37
hasSuggestions : true ,
26
38
docs : {
27
39
category : 'Operations' ,
@@ -93,82 +105,132 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
93
105
} ,
94
106
} ,
95
107
create ( context ) {
96
- requireGraphQLSchemaFromContext ( RULE_ID , context ) ;
108
+ const schema = requireGraphQLSchemaFromContext ( RULE_ID , context ) ;
97
109
const siblings = requireSiblingsOperations ( RULE_ID , context ) ;
98
110
const { fieldName = DEFAULT_ID_FIELD_NAME } = context . options [ 0 ] || { } ;
99
111
const idNames = asArray ( fieldName ) ;
100
112
101
113
// Check selections only in OperationDefinition,
102
114
// skip selections of OperationDefinition and InlineFragment
103
115
const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]' ;
116
+ const typeInfo = new TypeInfo ( schema ) ;
104
117
105
- return {
106
- [ selector ] ( node : GraphQLESTreeNode < SelectionSetNode , true > ) {
107
- const typeInfo = node . typeInfo ( ) ;
108
- if ( ! typeInfo . gqlType ) {
109
- return ;
110
- }
111
- const rawType = getBaseType ( typeInfo . gqlType ) ;
112
- const isObjectType = rawType instanceof GraphQLObjectType ;
113
- const isInterfaceType = rawType instanceof GraphQLInterfaceType ;
114
- if ( ! isObjectType && ! isInterfaceType ) {
115
- return ;
118
+ function checkFragments ( node : GraphQLESTreeNode < SelectionSetNode > ) : void {
119
+ for ( const selection of node . selections ) {
120
+ if ( selection . kind !== Kind . FRAGMENT_SPREAD ) {
121
+ continue ;
116
122
}
117
123
118
- const fields = rawType . getFields ( ) ;
119
- const hasIdFieldInType = idNames . some ( name => fields [ name ] ) ;
120
- if ( ! hasIdFieldInType ) {
121
- return ;
124
+ const [ foundSpread ] = siblings . getFragment ( selection . name . value ) ;
125
+ if ( ! foundSpread ) {
126
+ continue ;
122
127
}
123
- const checkedFragmentSpreads = new Set < string > ( ) ;
124
128
125
- const hasIdField = ( { selections } : OmitRecursively < SelectionSetNode , 'loc' > ) : boolean =>
126
- selections . some ( selection => {
127
- if ( selection . kind === Kind . FIELD ) {
128
- return idNames . includes ( selection . name . value ) ;
129
+ const checkedFragmentSpreads = new Set < string > ( ) ;
130
+ const visitor = visitWithTypeInfo ( typeInfo , {
131
+ SelectionSet ( node , key , parent : ASTNode ) {
132
+ if ( parent . kind === Kind . FRAGMENT_DEFINITION ) {
133
+ checkedFragmentSpreads . add ( parent . name . value ) ;
134
+ } else if ( parent . kind !== Kind . INLINE_FRAGMENT ) {
135
+ checkSelections ( node , typeInfo . getType ( ) , selection . loc . start , parent , checkedFragmentSpreads ) ;
129
136
}
137
+ } ,
138
+ } ) ;
130
139
131
- if ( selection . kind === Kind . INLINE_FRAGMENT ) {
132
- return hasIdField ( selection . selectionSet ) ;
140
+ visit ( foundSpread . document , visitor ) ;
141
+ }
142
+ }
143
+
144
+ function checkSelections (
145
+ node : OmitRecursively < SelectionSetNode , 'loc' > ,
146
+ type : GraphQLOutputType ,
147
+ // Fragment can be placed in separate file
148
+ // Provide actual fragment spread location instead of location in fragment
149
+ loc : ESTree . Position ,
150
+ // Can't access to node.parent in GraphQL AST.Node, so pass as argument
151
+ parent : any ,
152
+ checkedFragmentSpreads = new Set < string > ( )
153
+ ) : void {
154
+ const rawType = getBaseType ( type ) ;
155
+ const isObjectType = rawType instanceof GraphQLObjectType ;
156
+ const isInterfaceType = rawType instanceof GraphQLInterfaceType ;
157
+
158
+ if ( ! isObjectType && ! isInterfaceType ) {
159
+ return ;
160
+ }
161
+ const fields = rawType . getFields ( ) ;
162
+ const hasIdFieldInType = idNames . some ( name => fields [ name ] ) ;
163
+
164
+ if ( ! hasIdFieldInType ) {
165
+ return ;
166
+ }
167
+
168
+ function hasIdField ( { selections } : typeof node ) : boolean {
169
+ return selections . some ( selection => {
170
+ if ( selection . kind === Kind . FIELD ) {
171
+ return idNames . includes ( selection . name . value ) ;
172
+ }
173
+
174
+ if ( selection . kind === Kind . INLINE_FRAGMENT ) {
175
+ return hasIdField ( selection . selectionSet ) ;
176
+ }
177
+
178
+ if ( selection . kind === Kind . FRAGMENT_SPREAD ) {
179
+ const [ foundSpread ] = siblings . getFragment ( selection . name . value ) ;
180
+ if ( foundSpread ) {
181
+ const fragmentSpread = foundSpread . document ;
182
+ checkedFragmentSpreads . add ( fragmentSpread . name . value ) ;
183
+ return hasIdField ( fragmentSpread . selectionSet ) ;
133
184
}
185
+ }
186
+ return false ;
187
+ } ) ;
188
+ }
134
189
135
- if ( selection . kind === Kind . FRAGMENT_SPREAD ) {
136
- const [ foundSpread ] = siblings . getFragment ( selection . name . value ) ;
137
- if ( foundSpread ) {
138
- const fragmentSpread = foundSpread . document ;
139
- checkedFragmentSpreads . add ( fragmentSpread . name . value ) ;
140
- return hasIdField ( fragmentSpread . selectionSet ) ;
141
- }
142
- }
143
- return false ;
144
- } ) ;
190
+ const hasId = hasIdField ( node ) ;
145
191
146
- if ( hasIdField ( node ) ) {
147
- return ;
148
- }
192
+ checkFragments ( node as GraphQLESTreeNode < SelectionSetNode > ) ;
149
193
150
- const pluralSuffix = idNames . length > 1 ? 's' : '' ;
151
- const fieldName = englishJoinWords ( idNames . map ( name => `\`${ name } \`` ) ) ;
152
- const addition =
153
- checkedFragmentSpreads . size === 0
154
- ? ''
155
- : ` or add to used fragment${ checkedFragmentSpreads . size > 1 ? 's' : '' } ${ englishJoinWords (
156
- [ ...checkedFragmentSpreads ] . map ( name => `\`${ name } \`` )
157
- ) } `;
158
-
159
- context . report ( {
160
- loc : node . loc . start ,
161
- messageId : RULE_ID ,
162
- data : {
163
- pluralSuffix,
164
- fieldName,
165
- addition,
166
- } ,
167
- suggest : idNames . map ( idName => ( {
168
- desc : `Add \`${ idName } \` selection` ,
169
- fix : fixer => fixer . insertTextBefore ( ( node as any ) . selections [ 0 ] , `${ idName } ` ) ,
170
- } ) ) ,
171
- } ) ;
194
+ if ( hasId ) {
195
+ return ;
196
+ }
197
+
198
+ const pluralSuffix = idNames . length > 1 ? 's' : '' ;
199
+ const fieldName = englishJoinWords ( idNames . map ( name => `\`${ ( parent . alias || parent . name ) . value } .${ name } \`` ) ) ;
200
+
201
+ const addition =
202
+ checkedFragmentSpreads . size === 0
203
+ ? ''
204
+ : ` or add to used fragment${ checkedFragmentSpreads . size > 1 ? 's' : '' } ${ englishJoinWords (
205
+ [ ...checkedFragmentSpreads ] . map ( name => `\`${ name } \`` )
206
+ ) } `;
207
+
208
+ const problem : ReportDescriptor = {
209
+ loc,
210
+ messageId : RULE_ID ,
211
+ data : {
212
+ pluralSuffix,
213
+ fieldName,
214
+ addition,
215
+ } ,
216
+ } ;
217
+
218
+ // Don't provide suggestions for selections in fragments as fragment can be in a separate file
219
+ if ( 'type' in node ) {
220
+ problem . suggest = idNames . map ( idName => ( {
221
+ desc : `Add \`${ idName } \` selection` ,
222
+ fix : fixer => fixer . insertTextBefore ( ( node as any ) . selections [ 0 ] , `${ idName } ` ) ,
223
+ } ) ) ;
224
+ }
225
+ context . report ( problem ) ;
226
+ }
227
+
228
+ return {
229
+ [ selector ] ( node : GraphQLESTreeNode < SelectionSetNode , true > ) {
230
+ const typeInfo = node . typeInfo ( ) ;
231
+ if ( typeInfo . gqlType ) {
232
+ checkSelections ( node , typeInfo . gqlType , node . loc . start , ( node as any ) . parent ) ;
233
+ }
172
234
} ,
173
235
} ;
174
236
} ,
0 commit comments