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' ;
413import { asArray } from '@graphql-tools/utils' ;
14+ import { ARRAY_DEFAULT_OPTIONS , requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
15+ import { GraphQLESLintRule , OmitRecursively , ReportDescriptor } from '../types' ;
516import { getBaseType , GraphQLESTreeNode } from '../estree-parser' ;
617
718export type RequireIdWhenAvailableRuleConfig = { fieldName : string | string [ ] } ;
@@ -22,6 +33,7 @@ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunct
2233const rule : GraphQLESLintRule < [ RequireIdWhenAvailableRuleConfig ] , true > = {
2334 meta : {
2435 type : 'suggestion' ,
36+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
2537 hasSuggestions : true ,
2638 docs : {
2739 category : 'Operations' ,
@@ -93,82 +105,132 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
93105 } ,
94106 } ,
95107 create ( context ) {
96- requireGraphQLSchemaFromContext ( RULE_ID , context ) ;
108+ const schema = requireGraphQLSchemaFromContext ( RULE_ID , context ) ;
97109 const siblings = requireSiblingsOperations ( RULE_ID , context ) ;
98110 const { fieldName = DEFAULT_ID_FIELD_NAME } = context . options [ 0 ] || { } ;
99111 const idNames = asArray ( fieldName ) ;
100112
101113 // Check selections only in OperationDefinition,
102114 // skip selections of OperationDefinition and InlineFragment
103115 const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]' ;
116+ const typeInfo = new TypeInfo ( schema ) ;
104117
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 ;
116122 }
117123
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 ;
122127 }
123- const checkedFragmentSpreads = new Set < string > ( ) ;
124128
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 ) ;
129136 }
137+ } ,
138+ } ) ;
130139
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 ) ;
133184 }
185+ }
186+ return false ;
187+ } ) ;
188+ }
134189
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 ) ;
145191
146- if ( hasIdField ( node ) ) {
147- return ;
148- }
192+ checkFragments ( node as GraphQLESTreeNode < SelectionSetNode > ) ;
149193
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+ }
172234 } ,
173235 } ;
174236 } ,
0 commit comments