diff --git a/.changeset/six-pianos-eat.md b/.changeset/six-pianos-eat.md new file mode 100644 index 00000000000..36f0164c64e --- /dev/null +++ b/.changeset/six-pianos-eat.md @@ -0,0 +1,5 @@ +--- +'@graphql-eslint/eslint-plugin': patch +--- + +fix comments in files that contains import comments diff --git a/packages/plugin/src/parser.ts b/packages/plugin/src/parser.ts index 4598cb8ea3e..1ed883bd589 100644 --- a/packages/plugin/src/parser.ts +++ b/packages/plugin/src/parser.ts @@ -1,8 +1,7 @@ -import { convertToESTree } from './estree-parser'; -import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { parseGraphQLSDL } from '@graphql-tools/utils'; import { GraphQLError, TypeInfo } from 'graphql'; import { Linter } from 'eslint'; -import fs from 'fs'; +import { convertToESTree } from './estree-parser'; import { GraphQLESLintParseResult, ParserOptions } from './types'; import { extractTokens } from './utils'; import { getSchema } from './schema'; @@ -28,13 +27,10 @@ export function parseForESLint(code: string, options?: ParserOptions): GraphQLES try { const filePath = options.filePath || ''; - const isVirtualFile = !fs.existsSync(options.filePath); - const fileLoader = new GraphQLFileLoader(); - const graphqlAst = fileLoader.handleFileContent(code, filePath, { + const graphqlAst = parseGraphQLSDL(filePath, code, { ...options.graphQLParserOptions, noLocation: false, - skipGraphQLImport: isVirtualFile, }); const { rootTree, comments } = convertToESTree(graphqlAst.document, schema ? new TypeInfo(schema) : null); diff --git a/packages/plugin/src/rules/graphql-js-validation.ts b/packages/plugin/src/rules/graphql-js-validation.ts index 5f584b7b31c..25a521183eb 100644 --- a/packages/plugin/src/rules/graphql-js-validation.ts +++ b/packages/plugin/src/rules/graphql-js-validation.ts @@ -1,8 +1,9 @@ -import { GraphQLESLintRule, GraphQLESlintRuleContext } from '../types'; -import { requireGraphQLSchemaFromContext } from '../utils'; - import { validate, GraphQLSchema, DocumentNode, ASTNode, ValidationRule } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import fs from 'fs'; +import { GraphQLESLintRule, GraphQLESlintRuleContext } from '../types'; +import { requireGraphQLSchemaFromContext } from '../utils'; import { GraphQLESTreeNode } from '../estree-parser'; function extractRuleName(stack: string | undefined): string | null { @@ -44,11 +45,16 @@ export function validateDoc( } } +const isGraphQLImportFile = rawSDL => { + const trimmedRawSDL = rawSDL.trim(); + return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import'); +}; + const validationToRule = ( name: string, ruleName: string, - meta: GraphQLESLintRule['meta'], - skipSchema = false + docs: GraphQLESLintRule['meta']['docs'], + getDocumentNode?: (context: GraphQLESlintRuleContext) => DocumentNode | null ): Record> => { let ruleFn: null | ValidationRule = null; @@ -62,22 +68,23 @@ const validationToRule = ( } } + const requiresSchema = docs.requiresSchema ?? true; + return { [name]: { meta: { - ...meta, docs: { category: 'Validation', - requiresSchema: !skipSchema, + requiresSchema, requiresSiblings: false, url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`, - ...meta.docs, + ...docs, description: - meta.docs.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).`, }, }, - create: context => { + create(context) { return { Document(node) { if (!ruleFn) { @@ -85,12 +92,18 @@ const validationToRule = ( console.warn( `You rule "${name}" depends on a GraphQL validation rule ("${ruleName}") but it's not available in the "graphql-js" version you are using. Skipping...` ); - return; } - const schema = skipSchema ? null : requireGraphQLSchemaFromContext(name, context); - validateDoc(node, context, schema, node.rawNode(), [ruleFn], ruleName); + const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null; + + let documentNode: DocumentNode; + const filePath = context.getFilename(); + const isVirtualFile = !fs.existsSync(filePath); + if (!isVirtualFile && getDocumentNode) { + documentNode = getDocumentNode(context); + } + validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName); }, }; }, @@ -101,32 +114,24 @@ const validationToRule = ( export const GRAPHQL_JS_VALIDATIONS = Object.assign( {}, validationToRule('executable-definitions', 'ExecutableDefinitions', { - docs: { - description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`, - }, + description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`, }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', { - docs: { - description: `A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as __typename.`, - }, + description: `A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as __typename.`, }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', { - docs: { - description: `Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.`, - }, + description: `Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.`, }), validationToRule('known-argument-names', 'KnownArgumentNames', { - docs: { - description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`, - }, + description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`, }), validationToRule('known-directives', 'KnownDirectives', { - docs: { - description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`, - }, + description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`, }), - validationToRule('known-fragment-names', 'KnownFragmentNames', { - docs: { + validationToRule( + 'known-fragment-names', + 'KnownFragmentNames', + { description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`, examples: [ { @@ -171,170 +176,98 @@ export const GRAPHQL_JS_VALIDATIONS = Object.assign( }, ], }, - }), + context => { + const code = context.getSourceCode().text; + if (!isGraphQLImportFile(code)) { + return null; + } + // Import documents if file contains '#import' comments + const fileLoader = new GraphQLFileLoader(); + const graphqlAst = fileLoader.handleFileContent(code, context.getFilename(), { noLocation: true }); + return graphqlAst.document; + } + ), validationToRule('known-type-names', 'KnownTypeNames', { - docs: { - description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`, - }, + description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`, }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', { - docs: { - description: `A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.`, - }, + description: `A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.`, + }), + validationToRule('lone-schema-definition', 'LoneSchemaDefinition', { + description: `A GraphQL document is only valid if it contains only one schema definition.`, + requiresSchema: false, }), - validationToRule( - 'lone-schema-definition', - 'LoneSchemaDefinition', - { - docs: { - description: `A GraphQL document is only valid if it contains only one schema definition.`, - }, - }, - true - ), validationToRule('no-fragment-cycles', 'NoFragmentCycles', { - docs: { - description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`, - }, + description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`, }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', { - docs: { - description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`, - }, + description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`, }), validationToRule('no-unused-fragments', 'NoUnusedFragments', { - docs: { - description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`, - }, + description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`, }), validationToRule('no-unused-variables', 'NoUnusedVariables', { - docs: { - description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`, - }, + description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`, }), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', { - docs: { - description: `A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.`, - }, + description: `A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.`, }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', { - docs: { - description: `A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.`, - }, + description: `A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.`, + }), + validationToRule('possible-type-extension', 'PossibleTypeExtensions', { + description: `A type extension is only valid if the type is defined and has the same kind.`, + requiresSchema: false, }), - validationToRule( - 'possible-type-extension', - 'PossibleTypeExtensions', - { - docs: { - description: `A type extension is only valid if the type is defined and has the same kind.`, - }, - }, - true - ), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', { - docs: { - description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`, - }, + description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`, }), validationToRule('scalar-leafs', 'ScalarLeafs', { - docs: { - description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`, - }, + description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`, }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', { - docs: { - description: `A GraphQL subscription is valid only if it contains a single root field.`, - }, + description: `A GraphQL subscription is valid only if it contains a single root field.`, }), validationToRule('unique-argument-names', 'UniqueArgumentNames', { - docs: { - description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`, - }, + description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`, + }), + validationToRule('unique-directive-names', 'UniqueDirectiveNames', { + description: `A GraphQL document is only valid if all defined directives have unique names.`, + requiresSchema: false, }), - validationToRule( - 'unique-directive-names', - 'UniqueDirectiveNames', - { - docs: { - description: `A GraphQL document is only valid if all defined directives have unique names.`, - }, - }, - true - ), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', { - docs: { - description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`, - }, + description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`, + }), + validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', { + description: `A GraphQL enum type is only valid if all its values are uniquely named.`, + requiresSchema: false, + }), + validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', { + description: `A GraphQL complex type is only valid if all its fields are uniquely named.`, + requiresSchema: false, + }), + validationToRule('unique-input-field-names', 'UniqueInputFieldNames', { + description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`, + requiresSchema: false, + }), + validationToRule('unique-operation-types', 'UniqueOperationTypes', { + description: `A GraphQL document is only valid if it has only one type per operation.`, + requiresSchema: false, + }), + validationToRule('unique-type-names', 'UniqueTypeNames', { + description: `A GraphQL document is only valid if all defined types have unique names.`, + requiresSchema: false, }), - validationToRule( - 'unique-enum-value-names', - 'UniqueEnumValueNames', - { - docs: { - description: `A GraphQL enum type is only valid if all its values are uniquely named.`, - }, - }, - true - ), - validationToRule( - 'unique-field-definition-names', - 'UniqueFieldDefinitionNames', - { - docs: { - description: `A GraphQL complex type is only valid if all its fields are uniquely named.`, - }, - }, - true - ), - validationToRule( - 'unique-input-field-names', - 'UniqueInputFieldNames', - { - docs: { - description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`, - }, - }, - true - ), - validationToRule( - 'unique-operation-types', - 'UniqueOperationTypes', - { - docs: { - description: `A GraphQL document is only valid if it has only one type per operation.`, - }, - }, - true - ), - validationToRule( - 'unique-type-names', - 'UniqueTypeNames', - { - docs: { - description: `A GraphQL document is only valid if all defined types have unique names.`, - }, - }, - true - ), validationToRule('unique-variable-names', 'UniqueVariableNames', { - docs: { - description: `A GraphQL operation is only valid if all its variables are uniquely named.`, - }, + description: `A GraphQL operation is only valid if all its variables are uniquely named.`, }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', { - docs: { - description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`, - }, + description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`, }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', { - docs: { - description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`, - }, + description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`, }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', { - docs: { - description: `Variables passed to field arguments conform to type.`, - }, + description: `Variables passed to field arguments conform to type.`, }) ) as Record; diff --git a/packages/plugin/tests/eslint-directives.spec.ts b/packages/plugin/tests/eslint-directives.spec.ts index bc78bc27f5d..348fd05669d 100644 --- a/packages/plugin/tests/eslint-directives.spec.ts +++ b/packages/plugin/tests/eslint-directives.spec.ts @@ -1,4 +1,5 @@ -import { GraphQLRuleTester } from '../src/testkit'; +import { join } from 'path'; +import { GraphQLRuleTester } from '../src'; import rule from '../src/rules/no-anonymous-operations'; const ruleTester = new GraphQLRuleTester(); @@ -44,12 +45,8 @@ ruleTester.runGraphQLTests('test-directives', rule, { `, }, { - code: /* GraphQL */ ` - # eslint-disable - query { - a - } - `, + filename: join(__dirname, 'mocks/test-directives-with-import.graphql'), + code: ruleTester.fromMockFile('test-directives-with-import.graphql'), }, ], invalid: [ diff --git a/packages/plugin/tests/mocks/test-directives-with-import.graphql b/packages/plugin/tests/mocks/test-directives-with-import.graphql new file mode 100644 index 00000000000..c4594891479 --- /dev/null +++ b/packages/plugin/tests/mocks/test-directives-with-import.graphql @@ -0,0 +1,6 @@ +#import './user-fields.graphql' + +# eslint-disable-next-line +query { + a +}