1
1
import { validate , GraphQLSchema , DocumentNode , ASTNode , ValidationRule } from 'graphql' ;
2
2
import { validateSDL } from 'graphql/validation/validate' ;
3
- import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader' ;
4
- import fs from 'fs' ;
3
+ import { parseImportLine , processImport } from '@graphql-tools/import' ;
4
+ import { existsSync } from 'fs' ;
5
+ import { join , dirname } from 'path' ;
5
6
import { GraphQLESLintRule , GraphQLESLintRuleContext } from '../types' ;
6
- import { requireGraphQLSchemaFromContext } from '../utils' ;
7
+ import { requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
7
8
import { GraphQLESTreeNode } from '../estree-parser' ;
8
9
9
10
function extractRuleName ( stack : string | undefined ) : string | null {
@@ -24,7 +25,7 @@ export function validateDoc(
24
25
rules : ReadonlyArray < ValidationRule > ,
25
26
ruleName : string | null = null
26
27
) : void {
27
- if ( documentNode && documentNode . definitions && documentNode . definitions . length > 0 ) {
28
+ if ( documentNode ? .definitions ? .length > 0 ) {
28
29
try {
29
30
const validationErrors = schema ? validate ( schema , documentNode , rules ) : validateSDL ( documentNode , null , rules ) ;
30
31
@@ -69,15 +70,15 @@ const validationToRule = (
69
70
}
70
71
71
72
const requiresSchema = docs . requiresSchema ?? true ;
72
-
73
+ const requiresSiblings = docs . requiresSiblings ?? false ;
73
74
return {
74
75
[ name ] : {
75
76
meta : {
76
77
docs : {
77
78
...docs ,
78
79
category : 'Validation' ,
79
80
requiresSchema,
80
- requiresSiblings : false ,
81
+ requiresSiblings,
81
82
url : `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ name } .md` ,
82
83
description : `${ docs . description } \n\n> This rule is a wrapper around a \`graphql-js\` validation function. [You can find it's source code here](https://github.com/graphql/graphql-js/blob/main/src/validation/rules/${ ruleName } Rule.ts).` ,
83
84
} ,
@@ -96,9 +97,8 @@ const validationToRule = (
96
97
const schema = requiresSchema ? requireGraphQLSchemaFromContext ( name , context ) : null ;
97
98
98
99
let documentNode : DocumentNode ;
99
- const filePath = context . getFilename ( ) ;
100
- const isVirtualFile = ! fs . existsSync ( filePath ) ;
101
- if ( ! isVirtualFile && getDocumentNode ) {
100
+ const isRealFile = existsSync ( context . getFilename ( ) ) ;
101
+ if ( isRealFile && getDocumentNode ) {
102
102
documentNode = getDocumentNode ( context ) ;
103
103
}
104
104
validateDoc ( node , context , schema , documentNode || node . rawNode ( ) , [ ruleFn ] , ruleName ) ;
@@ -180,10 +180,8 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
180
180
if ( ! isGraphQLImportFile ( code ) ) {
181
181
return null ;
182
182
}
183
- // Import documents if file contains '#import' comments
184
- const fileLoader = new GraphQLFileLoader ( ) ;
185
- const graphqlAst = fileLoader . handleFileContent ( code , context . getFilename ( ) , { noLocation : true } ) ;
186
- return graphqlAst . document ;
183
+ // Import documents because file contains '#import' comments
184
+ return processImport ( context . getFilename ( ) ) ;
187
185
}
188
186
) ,
189
187
validationToRule ( 'known-type-names' , 'KnownTypeNames' , {
@@ -202,9 +200,45 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
202
200
validationToRule ( 'no-undefined-variables' , 'NoUndefinedVariables' , {
203
201
description : `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.` ,
204
202
} ) ,
205
- validationToRule ( 'no-unused-fragments' , 'NoUnusedFragments' , {
206
- description : `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.` ,
207
- } ) ,
203
+ validationToRule (
204
+ 'no-unused-fragments' ,
205
+ 'NoUnusedFragments' ,
206
+ {
207
+ description : `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.` ,
208
+ requiresSiblings : true ,
209
+ } ,
210
+ context => {
211
+ const siblings = requireSiblingsOperations ( 'no-unused-fragments' , context ) ;
212
+ const documents = [ ...siblings . getOperations ( ) , ...siblings . getFragments ( ) ]
213
+ . filter ( ( { document } ) => isGraphQLImportFile ( document . loc . source . body ) )
214
+ . map ( ( { filePath, document } ) => ( {
215
+ filePath,
216
+ code : document . loc . source . body ,
217
+ } ) ) ;
218
+
219
+ const getParentNode = ( filePath : string ) : DocumentNode | null => {
220
+ for ( const { filePath : docFilePath , code } of documents ) {
221
+ const isFileImported = code
222
+ . split ( '\n' )
223
+ . filter ( isGraphQLImportFile )
224
+ . map ( line => parseImportLine ( line . replace ( '#' , '' ) ) )
225
+ . some ( o => filePath === join ( dirname ( docFilePath ) , o . from ) ) ;
226
+
227
+ if ( ! isFileImported ) {
228
+ continue ;
229
+ }
230
+ // Import first file that import this file
231
+ const document = processImport ( docFilePath ) ;
232
+ // Import most top file that import this file
233
+ return getParentNode ( docFilePath ) || document ;
234
+ }
235
+
236
+ return null ;
237
+ } ;
238
+
239
+ return getParentNode ( context . getFilename ( ) ) ;
240
+ }
241
+ ) ,
208
242
validationToRule ( 'no-unused-variables' , 'NoUnusedVariables' , {
209
243
description : `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.` ,
210
244
} ) ,
0 commit comments