UNPKG

@graphql-eslint/eslint-plugin

Version:
670 lines (667 loc) • 20.3 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var graphql_js_validation_exports = {}; __export(graphql_js_validation_exports, { GRAPHQL_JS_VALIDATIONS: () => GRAPHQL_JS_VALIDATIONS }); module.exports = __toCommonJS(graphql_js_validation_exports); var import_graphql = require("graphql"); var import_validate = require("graphql/validation/validate.js"); var import_utils = require("../utils.js"); function validateDocument({ context, schema = null, documentNode, rule, hasDidYouMeanSuggestions }) { var _a; if (documentNode.definitions.length === 0) { return; } try { const validationErrors = schema ? (0, import_graphql.validate)(schema, documentNode, [rule]) : (0, import_validate.validateSDL)(documentNode, null, [rule]); for (const error of validationErrors) { const { line, column } = error.locations[0]; const sourceCode = context.getSourceCode(); const { tokens } = sourceCode.ast; const token = tokens.find( (token2) => token2.loc.start.line === line && token2.loc.start.column === column - 1 ); let loc = { line, column: column - 1 }; if (token) { loc = // if cursor on `@` symbol than use next node token.type === "@" ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc; } const didYouMeanContent = (_a = error.message.match(/Did you mean (?<content>.*)\?$/)) == null ? void 0 : _a.groups.content; const matches = didYouMeanContent ? [...didYouMeanContent.matchAll(/"(?<name>[^"]*)"/g)] : []; context.report({ loc, message: error.message, suggest: hasDidYouMeanSuggestions ? matches.map((match) => { const { name } = match.groups; return { desc: `Rename to \`${name}\``, fix: (fixer) => fixer.replaceText(token, name) }; }) : [] }); } } catch (error) { context.report({ loc: import_utils.REPORT_ON_FIRST_CHARACTER, message: error.message }); } } const getFragmentDefsAndFragmentSpreads = (node) => { const fragmentDefs = /* @__PURE__ */ new Set(); const fragmentSpreads = /* @__PURE__ */ new Set(); const visitor = { FragmentDefinition(node2) { fragmentDefs.add(node2.name.value); }, FragmentSpread(node2) { fragmentSpreads.add(node2.name.value); } }; (0, import_graphql.visit)(node, visitor); return { fragmentDefs, fragmentSpreads }; }; const getMissingFragments = (node) => { const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(node); return [...fragmentSpreads].filter((name) => !fragmentDefs.has(name)); }; const handleMissingFragments = ({ ruleId, context, node }) => { const missingFragments = getMissingFragments(node); if (missingFragments.length > 0) { const siblings = (0, import_utils.requireSiblingsOperations)(ruleId, context); const fragmentsToAdd = []; for (const fragmentName of missingFragments) { const [foundFragment] = siblings.getFragment(fragmentName).map((source) => source.document); if (foundFragment) { fragmentsToAdd.push(foundFragment); } } if (fragmentsToAdd.length > 0) { return handleMissingFragments({ ruleId, context, node: { kind: import_graphql.Kind.DOCUMENT, definitions: [...node.definitions, ...fragmentsToAdd] } }); } } return node; }; const validationToRule = ({ ruleId, ruleName, getDocumentNode, schema = [], hasDidYouMeanSuggestions }, docs) => { let ruleFn = null; try { ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`]; } catch { try { ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`]; } catch { ruleFn = require("graphql/validation")[`${ruleName}Rule`]; } } return { [ruleId]: { meta: { docs: { recommended: true, ...docs, graphQLJSRuleName: ruleName, url: `https://the-guild.dev/graphql/eslint/rules/${ruleId}`, description: `${docs.description} > This rule is a wrapper around a \`graphql-js\` validation function.` }, schema, hasSuggestions: hasDidYouMeanSuggestions }, create(context) { if (!ruleFn) { import_utils.logger.warn( `Rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql" version you are using. Skipping\u2026` ); return {}; } return { Document(node) { const schema2 = docs.requiresSchema ? (0, import_utils.requireGraphQLSchemaFromContext)(ruleId, context) : null; const documentNode = getDocumentNode ? getDocumentNode({ ruleId, context, node: node.rawNode() }) : node.rawNode(); validateDocument({ context, schema: schema2, documentNode, rule: ruleFn, hasDidYouMeanSuggestions }); } }; } } }; }; const GRAPHQL_JS_VALIDATIONS = Object.assign( {}, validationToRule( { ruleId: "executable-definitions", ruleName: "ExecutableDefinitions" }, { category: "Operations", description: "A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.", requiresSchema: true } ), validationToRule( { ruleId: "fields-on-correct-type", ruleName: "FieldsOnCorrectType", hasDidYouMeanSuggestions: true }, { category: "Operations", 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`.", requiresSchema: true } ), validationToRule( { ruleId: "fragments-on-composite-type", ruleName: "FragmentsOnCompositeTypes" }, { category: "Operations", 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.", requiresSchema: true } ), validationToRule( { ruleId: "known-argument-names", ruleName: "KnownArgumentNames", hasDidYouMeanSuggestions: true }, { category: ["Schema", "Operations"], description: "A GraphQL field is only valid if all supplied arguments are defined by that field.", requiresSchema: true } ), validationToRule( { ruleId: "known-directives", ruleName: "KnownDirectives", getDocumentNode({ context, node: documentNode }) { const { ignoreClientDirectives = [] } = context.options[0] || {}; if (ignoreClientDirectives.length === 0) { return documentNode; } const filterDirectives = (node) => { var _a; return { ...node, directives: (_a = node.directives) == null ? void 0 : _a.filter( (directive) => !ignoreClientDirectives.includes(directive.name.value) ) }; }; return (0, import_graphql.visit)(documentNode, { Field: filterDirectives, OperationDefinition: filterDirectives }); }, schema: { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, required: ["ignoreClientDirectives"], properties: { ignoreClientDirectives: import_utils.ARRAY_DEFAULT_OPTIONS } } } }, { category: ["Schema", "Operations"], description: "A GraphQL document is only valid if all `@directive`s are known by the schema and legally positioned.", requiresSchema: true, examples: [ { title: "Valid", usage: [{ ignoreClientDirectives: ["client"] }], code: ( /* GraphQL */ ` { product { someClientField @client } } ` ) } ] } ), validationToRule( { ruleId: "known-fragment-names", ruleName: "KnownFragmentNames", getDocumentNode: handleMissingFragments }, { category: "Operations", description: "A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.", requiresSchema: true, requiresSiblings: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` query { user { id ...UserFields # fragment not defined in the document } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` fragment UserFields on User { firstName lastName } query { user { id ...UserFields } } ` ) }, { title: "Correct (`UserFields` fragment located in a separate file)", code: ( /* GraphQL */ ` # user.gql query { user { id ...UserFields } } # user-fields.gql fragment UserFields on User { id } ` ) } ] } ), validationToRule( { ruleId: "known-type-names", ruleName: "KnownTypeNames", hasDidYouMeanSuggestions: true }, { category: ["Schema", "Operations"], description: "A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.", requiresSchema: true } ), validationToRule( { ruleId: "lone-anonymous-operation", ruleName: "LoneAnonymousOperation" }, { category: "Operations", description: "A GraphQL document that contains an anonymous operation (the `query` short-hand) is only valid if it contains only that one operation definition.", requiresSchema: true } ), validationToRule( { ruleId: "lone-schema-definition", ruleName: "LoneSchemaDefinition" }, { category: "Schema", description: "A GraphQL document is only valid if it contains only one schema definition." } ), validationToRule( { ruleId: "no-fragment-cycles", ruleName: "NoFragmentCycles" }, { category: "Operations", description: "A GraphQL fragment is only valid when it does not have cycles in fragments usage.", requiresSchema: true } ), validationToRule( { ruleId: "no-undefined-variables", ruleName: "NoUndefinedVariables", getDocumentNode: handleMissingFragments }, { category: "Operations", description: "A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.", requiresSchema: true, requiresSiblings: true } ), validationToRule( { ruleId: "no-unused-fragments", ruleName: "NoUnusedFragments", getDocumentNode: ({ ruleId, context, node }) => { const siblings = (0, import_utils.requireSiblingsOperations)(ruleId, context); const FilePathToDocumentsMap = [ ...siblings.getOperations(), ...siblings.getFragments() ].reduce((map, { filePath, document }) => { var _a; (_a = map[filePath]) != null ? _a : map[filePath] = []; map[filePath].push(document); return map; }, /* @__PURE__ */ Object.create(null)); const getParentNode = (currentFilePath, node2) => { const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node2); if (fragmentDefs.size === 0) { return node2; } delete FilePathToDocumentsMap[currentFilePath]; for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) { const missingFragments = getMissingFragments({ kind: import_graphql.Kind.DOCUMENT, definitions: documents }); const isCurrentFileImportFragment = missingFragments.some( (fragment) => fragmentDefs.has(fragment) ); if (isCurrentFileImportFragment) { return getParentNode(filePath, { kind: import_graphql.Kind.DOCUMENT, definitions: [...node2.definitions, ...documents] }); } } return node2; }; return getParentNode(context.getFilename(), node); } }, { category: "Operations", description: "A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.", requiresSchema: true, requiresSiblings: true } ), validationToRule( { ruleId: "no-unused-variables", ruleName: "NoUnusedVariables", getDocumentNode: handleMissingFragments }, { category: "Operations", description: "A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.", requiresSchema: true, requiresSiblings: true } ), validationToRule( { ruleId: "overlapping-fields-can-be-merged", ruleName: "OverlappingFieldsCanBeMerged" }, { category: "Operations", 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.", requiresSchema: true } ), validationToRule( { ruleId: "possible-fragment-spread", ruleName: "PossibleFragmentSpreads" }, { category: "Operations", 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.", requiresSchema: true } ), validationToRule( { ruleId: "possible-type-extension", ruleName: "PossibleTypeExtensions", hasDidYouMeanSuggestions: true }, { category: "Schema", description: "A type extension is only valid if the type is defined and has the same kind.", // TODO: add in graphql-eslint v4 recommended: false, requiresSchema: true, isDisabledForAllConfig: true } ), validationToRule( { ruleId: "provided-required-arguments", ruleName: "ProvidedRequiredArguments" }, { category: ["Schema", "Operations"], description: "A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.", requiresSchema: true } ), validationToRule( { ruleId: "scalar-leafs", ruleName: "ScalarLeafs", hasDidYouMeanSuggestions: true }, { category: "Operations", description: "A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.", requiresSchema: true } ), validationToRule( { ruleId: "one-field-subscriptions", ruleName: "SingleFieldSubscriptions" }, { category: "Operations", description: "A GraphQL subscription is valid only if it contains a single root field.", requiresSchema: true } ), validationToRule( { ruleId: "unique-argument-names", ruleName: "UniqueArgumentNames" }, { category: "Operations", description: "A GraphQL field or directive is only valid if all supplied arguments are uniquely named.", requiresSchema: true } ), validationToRule( { ruleId: "unique-directive-names", ruleName: "UniqueDirectiveNames" }, { category: "Schema", description: "A GraphQL document is only valid if all defined directives have unique names." } ), validationToRule( { ruleId: "unique-directive-names-per-location", ruleName: "UniqueDirectivesPerLocation" }, { category: ["Schema", "Operations"], description: "A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.", requiresSchema: true } ), validationToRule( { ruleId: "unique-enum-value-names", ruleName: "UniqueEnumValueNames" }, { category: "Schema", description: "A GraphQL enum type is only valid if all its values are uniquely named.", recommended: false, isDisabledForAllConfig: true } ), validationToRule( { ruleId: "unique-field-definition-names", ruleName: "UniqueFieldDefinitionNames" }, { category: "Schema", description: "A GraphQL complex type is only valid if all its fields are uniquely named." } ), validationToRule( { ruleId: "unique-input-field-names", ruleName: "UniqueInputFieldNames" }, { category: "Operations", description: "A GraphQL input object value is only valid if all supplied fields are uniquely named." } ), validationToRule( { ruleId: "unique-operation-types", ruleName: "UniqueOperationTypes" }, { category: "Schema", description: "A GraphQL document is only valid if it has only one type per operation." } ), validationToRule( { ruleId: "unique-type-names", ruleName: "UniqueTypeNames" }, { category: "Schema", description: "A GraphQL document is only valid if all defined types have unique names." } ), validationToRule( { ruleId: "unique-variable-names", ruleName: "UniqueVariableNames" }, { category: "Operations", description: "A GraphQL operation is only valid if all its variables are uniquely named.", requiresSchema: true } ), validationToRule( { ruleId: "value-literals-of-correct-type", ruleName: "ValuesOfCorrectType", hasDidYouMeanSuggestions: true }, { category: "Operations", description: "A GraphQL document is only valid if all value literals are of the type expected at their position.", requiresSchema: true } ), validationToRule( { ruleId: "variables-are-input-types", ruleName: "VariablesAreInputTypes" }, { category: "Operations", description: "A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).", requiresSchema: true } ), validationToRule( { ruleId: "variables-in-allowed-position", ruleName: "VariablesInAllowedPosition" }, { category: "Operations", description: "Variables passed to field arguments conform to type.", requiresSchema: true } ) ); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GRAPHQL_JS_VALIDATIONS });