11import { validate , GraphQLSchema , DocumentNode , ASTNode , ValidationRule } from 'graphql' ;
22import { 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' ;
56import { GraphQLESLintRule , GraphQLESLintRuleContext } from '../types' ;
6- import { requireGraphQLSchemaFromContext } from '../utils' ;
7+ import { requireGraphQLSchemaFromContext , requireSiblingsOperations } from '../utils' ;
78import { GraphQLESTreeNode } from '../estree-parser' ;
89
910function extractRuleName ( stack : string | undefined ) : string | null {
@@ -24,7 +25,7 @@ export function validateDoc(
2425 rules : ReadonlyArray < ValidationRule > ,
2526 ruleName : string | null = null
2627) : void {
27- if ( documentNode && documentNode . definitions && documentNode . definitions . length > 0 ) {
28+ if ( documentNode ? .definitions ? .length > 0 ) {
2829 try {
2930 const validationErrors = schema ? validate ( schema , documentNode , rules ) : validateSDL ( documentNode , null , rules ) ;
3031
@@ -69,15 +70,15 @@ const validationToRule = (
6970 }
7071
7172 const requiresSchema = docs . requiresSchema ?? true ;
72-
73+ const requiresSiblings = docs . requiresSiblings ?? false ;
7374 return {
7475 [ name ] : {
7576 meta : {
7677 docs : {
7778 ...docs ,
7879 category : 'Validation' ,
7980 requiresSchema,
80- requiresSiblings : false ,
81+ requiresSiblings,
8182 url : `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ name } .md` ,
8283 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).` ,
8384 } ,
@@ -96,9 +97,8 @@ const validationToRule = (
9697 const schema = requiresSchema ? requireGraphQLSchemaFromContext ( name , context ) : null ;
9798
9899 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 ) {
102102 documentNode = getDocumentNode ( context ) ;
103103 }
104104 validateDoc ( node , context , schema , documentNode || node . rawNode ( ) , [ ruleFn ] , ruleName ) ;
@@ -180,10 +180,8 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
180180 if ( ! isGraphQLImportFile ( code ) ) {
181181 return null ;
182182 }
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 ( ) ) ;
187185 }
188186 ) ,
189187 validationToRule ( 'known-type-names' , 'KnownTypeNames' , {
@@ -202,9 +200,45 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign(
202200 validationToRule ( 'no-undefined-variables' , 'NoUndefinedVariables' , {
203201 description : `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.` ,
204202 } ) ,
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+ ) ,
208242 validationToRule ( 'no-unused-variables' , 'NoUnusedVariables' , {
209243 description : `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.` ,
210244 } ) ,
0 commit comments