UNPKG

@graphql-eslint/eslint-plugin

Version:
6,273 lines (6,176 loc) • 186 kB
// src/configs/operations-all.ts var operations_all_default = { extends: "./configs/operations-recommended", rules: { "@graphql-eslint/alphabetize": [ "error", { definitions: true, selections: ["OperationDefinition", "FragmentDefinition"], variables: true, arguments: ["Field", "Directive"], groups: ["...", "id", "*", "{"] } ], "@graphql-eslint/lone-executable-definition": "error", "@graphql-eslint/match-document-filename": [ "error", { query: "kebab-case", mutation: "kebab-case", subscription: "kebab-case", fragment: "kebab-case" } ], "@graphql-eslint/no-one-place-fragments": "error", "@graphql-eslint/require-import-fragment": "error" } }; // src/configs/operations-recommended.ts var operations_recommended_default = { parser: "@graphql-eslint/eslint-plugin", plugins: ["@graphql-eslint"], rules: { "@graphql-eslint/executable-definitions": "error", "@graphql-eslint/fields-on-correct-type": "error", "@graphql-eslint/fragments-on-composite-type": "error", "@graphql-eslint/known-argument-names": "error", "@graphql-eslint/known-directives": "error", "@graphql-eslint/known-fragment-names": "error", "@graphql-eslint/known-type-names": "error", "@graphql-eslint/lone-anonymous-operation": "error", "@graphql-eslint/naming-convention": [ "error", { VariableDefinition: "camelCase", OperationDefinition: { style: "PascalCase", forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"], forbiddenSuffixes: ["Query", "Mutation", "Subscription"] }, FragmentDefinition: { style: "PascalCase", forbiddenPrefixes: ["Fragment"], forbiddenSuffixes: ["Fragment"] } } ], "@graphql-eslint/no-anonymous-operations": "error", "@graphql-eslint/no-deprecated": "error", "@graphql-eslint/no-duplicate-fields": "error", "@graphql-eslint/no-fragment-cycles": "error", "@graphql-eslint/no-undefined-variables": "error", "@graphql-eslint/no-unused-fragments": "error", "@graphql-eslint/no-unused-variables": "error", "@graphql-eslint/one-field-subscriptions": "error", "@graphql-eslint/overlapping-fields-can-be-merged": "error", "@graphql-eslint/possible-fragment-spread": "error", "@graphql-eslint/provided-required-arguments": "error", "@graphql-eslint/require-selections": "error", "@graphql-eslint/scalar-leafs": "error", "@graphql-eslint/selection-set-depth": ["error", { maxDepth: 7 }], "@graphql-eslint/unique-argument-names": "error", "@graphql-eslint/unique-directive-names-per-location": "error", "@graphql-eslint/unique-fragment-name": "error", "@graphql-eslint/unique-input-field-names": "error", "@graphql-eslint/unique-operation-name": "error", "@graphql-eslint/unique-variable-names": "error", "@graphql-eslint/value-literals-of-correct-type": "error", "@graphql-eslint/variables-are-input-types": "error", "@graphql-eslint/variables-in-allowed-position": "error" } }; // src/configs/schema-all.ts var schema_all_default = { extends: "./configs/schema-recommended", rules: { "@graphql-eslint/alphabetize": [ "error", { definitions: true, fields: ["ObjectTypeDefinition", "InterfaceTypeDefinition", "InputObjectTypeDefinition"], values: true, arguments: ["FieldDefinition", "Field", "DirectiveDefinition", "Directive"], groups: ["id", "*", "createdAt", "updatedAt"] } ], "@graphql-eslint/input-name": "error", "@graphql-eslint/no-root-type": ["error", { disallow: ["mutation", "subscription"] }], "@graphql-eslint/no-scalar-result-type-on-mutation": "error", "@graphql-eslint/require-deprecation-date": "error", "@graphql-eslint/require-field-of-type-query-in-mutation-result": "error", "@graphql-eslint/require-nullable-fields-with-oneof": "error", "@graphql-eslint/require-nullable-result-in-root": "error", "@graphql-eslint/require-type-pattern-with-oneof": "error" } }; // src/configs/schema-recommended.ts var schema_recommended_default = { parser: "@graphql-eslint/eslint-plugin", plugins: ["@graphql-eslint"], rules: { "@graphql-eslint/description-style": "error", "@graphql-eslint/known-argument-names": "error", "@graphql-eslint/known-directives": "error", "@graphql-eslint/known-type-names": "error", "@graphql-eslint/lone-schema-definition": "error", "@graphql-eslint/naming-convention": [ "error", { types: "PascalCase", FieldDefinition: "camelCase", InputValueDefinition: "camelCase", Argument: "camelCase", DirectiveDefinition: "camelCase", EnumValueDefinition: "UPPER_CASE", "FieldDefinition[parent.name.value=Query]": { forbiddenPrefixes: ["query", "get"], forbiddenSuffixes: ["Query"] }, "FieldDefinition[parent.name.value=Mutation]": { forbiddenPrefixes: ["mutation"], forbiddenSuffixes: ["Mutation"] }, "FieldDefinition[parent.name.value=Subscription]": { forbiddenPrefixes: ["subscription"], forbiddenSuffixes: ["Subscription"] }, "EnumTypeDefinition,EnumTypeExtension": { forbiddenPrefixes: ["Enum"], forbiddenSuffixes: ["Enum"] }, "InterfaceTypeDefinition,InterfaceTypeExtension": { forbiddenPrefixes: ["Interface"], forbiddenSuffixes: ["Interface"] }, "UnionTypeDefinition,UnionTypeExtension": { forbiddenPrefixes: ["Union"], forbiddenSuffixes: ["Union"] }, "ObjectTypeDefinition,ObjectTypeExtension": { forbiddenPrefixes: ["Type"], forbiddenSuffixes: ["Type"] } } ], "@graphql-eslint/no-hashtag-description": "error", "@graphql-eslint/no-typename-prefix": "error", "@graphql-eslint/no-unreachable-types": "error", "@graphql-eslint/possible-type-extension": "error", "@graphql-eslint/provided-required-arguments": "error", "@graphql-eslint/require-deprecation-reason": "error", "@graphql-eslint/require-description": [ "error", { types: true, DirectiveDefinition: true, rootField: true } ], "@graphql-eslint/strict-id-in-types": "error", "@graphql-eslint/unique-directive-names": "error", "@graphql-eslint/unique-directive-names-per-location": "error", "@graphql-eslint/unique-enum-value-names": "error", "@graphql-eslint/unique-field-definition-names": "error", "@graphql-eslint/unique-operation-types": "error", "@graphql-eslint/unique-type-names": "error" } }; // src/configs/schema-relay.ts var schema_relay_default = { parser: "@graphql-eslint/eslint-plugin", plugins: ["@graphql-eslint"], rules: { "@graphql-eslint/relay-arguments": "error", "@graphql-eslint/relay-connection-types": "error", "@graphql-eslint/relay-edge-types": "error", "@graphql-eslint/relay-page-info": "error" } }; // src/configs/index.ts var configs = { "schema-recommended": schema_recommended_default, "schema-all": schema_all_default, "schema-relay": schema_relay_default, "operations-recommended": operations_recommended_default, "operations-all": operations_all_default, "flat/schema-recommended": { rules: schema_recommended_default.rules }, "flat/schema-all": { rules: { ...schema_recommended_default.rules, ...schema_all_default.rules } }, "flat/schema-relay": { rules: schema_relay_default.rules }, "flat/operations-recommended": { rules: operations_recommended_default.rules }, "flat/operations-all": { rules: { ...operations_recommended_default.rules, ...operations_all_default.rules } } }; // src/parser.ts import debugFactory2 from "debug"; import { buildSchema, GraphQLError } from "graphql"; import { parseGraphQLSDL } from "@graphql-tools/utils"; // src/cache.ts import debugFactory from "debug"; var log = debugFactory("graphql-eslint:ModuleCache"); var ModuleCache = class { map = /* @__PURE__ */ new Map(); set(cacheKey, result) { if (true) return; this.map.set(cacheKey, { lastSeen: process.hrtime(), result }); log("setting entry for", cacheKey); } get(cacheKey, settings = { lifetime: 10 /* seconds */ }) { if (true) return; const value = this.map.get(cacheKey); if (!value) { log("cache miss for", cacheKey); return; } const { lastSeen, result } = value; if (process.env.NODE || process.hrtime(lastSeen)[0] < settings.lifetime) { return result; } } }; // src/estree-converter/converter.ts import { Kind, TypeInfo, visit, visitWithTypeInfo } from "graphql"; // src/estree-converter/utils.ts import { isListType, isNonNullType, Lexer, Source, TokenKind } from "graphql"; import { valueFromASTUntyped } from "graphql/utilities/valueFromASTUntyped.js"; var valueFromNode = (...args) => { return valueFromASTUntyped(...args); }; function getBaseType(type) { if (isNonNullType(type) || isListType(type)) { return getBaseType(type.ofType); } return type; } function convertToken(token, type) { const { line, column, end, start, value } = token; return { type, value, /* * ESLint has 0-based column number * https://eslint.org/docs/developer-guide/working-with-rules#contextreport */ loc: { start: { line, column: column - 1 }, end: { line, column: column - 1 + (end - start) } }, range: [start, end] }; } function extractTokens(filePath, code) { const source = new Source(code, filePath); const lexer = new Lexer(source); const tokens = []; let token = lexer.advance(); while (token && token.kind !== TokenKind.EOF) { const result = convertToken(token, token.kind); tokens.push(result); token = lexer.advance(); } return tokens; } function extractComments(loc) { if (!loc) { return []; } const comments = []; let token = loc.startToken; while (token) { if (token.kind === TokenKind.COMMENT) { const comment = convertToken( token, // `eslint-disable` directive works only with `Block` type comment token.value.trimStart().startsWith("eslint") ? "Block" : "Line" ); comments.push(comment); } token = token.next; } return comments; } function convertLocation(location) { const { startToken, endToken, source, start, end } = location; const loc = { start: { /* * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0 */ line: startToken.line === 0 ? 1 : startToken.line, column: startToken.column === 0 ? 0 : startToken.column - 1 }, end: { line: endToken.line, column: endToken.column - 1 }, source: source.body }; if (loc.start.column === loc.end.column) { loc.end.column += end - start; } return loc; } // src/estree-converter/converter.ts function convertToESTree(node, schema16) { const typeInfo = schema16 && new TypeInfo(schema16); const visitor = { leave(node2, key, parent) { const leadingComments = "description" in node2 && node2.description ? [ { type: node2.description.block ? "Block" : "Line", value: node2.description.value } ] : []; const calculatedTypeInfo = typeInfo ? { argument: typeInfo.getArgument(), defaultValue: typeInfo.getDefaultValue(), directive: typeInfo.getDirective(), enumValue: typeInfo.getEnumValue(), fieldDef: typeInfo.getFieldDef(), inputType: typeInfo.getInputType(), parentInputType: typeInfo.getParentInputType(), parentType: typeInfo.getParentType(), gqlType: typeInfo.getType() } : {}; const rawNode = () => { if (parent && key !== void 0) { return parent[key]; } return node2.kind === Kind.DOCUMENT ? { ...node2, definitions: node2.definitions.map( (definition) => definition.rawNode() ) } : node2; }; const commonFields = { ...node2, type: node2.kind, loc: convertLocation(node2.loc), range: [node2.loc.start, node2.loc.end], leadingComments, // Use function to prevent RangeError: Maximum call stack size exceeded typeInfo: () => calculatedTypeInfo, // Don't know if can fix error rawNode }; return "type" in node2 ? { ...commonFields, gqlType: node2.type } : commonFields; } }; return visit( node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor ); } // src/meta.ts var version = "4.4.0-alpha-20250319153556-1fa518ea6a124817b7282bf35a6e680f87ca53d8"; // src/siblings.ts import { Kind as Kind3, visit as visit2 } from "graphql"; // src/utils.ts import { Kind as Kind2 } from "graphql"; import lowerCase from "lodash.lowercase"; function requireGraphQLOperations(ruleId, context) { const { siblingOperations } = context.sourceCode.parserServices; if (!siblingOperations.available) { throw new Error( `Rule \`${ruleId}\` requires graphql-config \`documents\` field to be set and loaded. See https://the-guild.dev/graphql/eslint/docs/usage#providing-operations for more info` ); } return siblingOperations; } function requireGraphQLSchema(ruleId, context) { const { schema: schema16 } = context.sourceCode.parserServices; if (!schema16) { throw new Error( `Rule \`${ruleId}\` requires graphql-config \`schema\` field to be set and loaded. See https://the-guild.dev/graphql/eslint/docs/usage#providing-schema for more info` ); } return schema16; } var chalk = { red: (str) => `\x1B[31m${str}\x1B[39m`, yellow: (str) => `\x1B[33m${str}\x1B[39m` }; var logger = { error: (...args) => ( // eslint-disable-next-line no-console console.error(chalk.red("error"), "[graphql-eslint]", ...args) ), warn: (...args) => ( // eslint-disable-next-line no-console console.warn(chalk.yellow("warning"), "[graphql-eslint]", ...args) ) }; var slash = (path2) => path2.replaceAll("\\", "/"); var VIRTUAL_DOCUMENT_REGEX = /[/\\]\d+_document.graphql$/; var CWD = process.cwd(); var getTypeName = (node) => "type" in node ? getTypeName(node.type) : "name" in node && node.name ? node.name.value : ""; var TYPES_KINDS = [ Kind2.OBJECT_TYPE_DEFINITION, Kind2.INTERFACE_TYPE_DEFINITION, Kind2.ENUM_TYPE_DEFINITION, Kind2.SCALAR_TYPE_DEFINITION, Kind2.INPUT_OBJECT_TYPE_DEFINITION, Kind2.UNION_TYPE_DEFINITION ]; var pascalCase = (str) => lowerCase(str).split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); var camelCase = (str) => { const result = pascalCase(str); return result.charAt(0).toLowerCase() + result.slice(1); }; var convertCase = (style, str) => { switch (style) { case "camelCase": return camelCase(str); case "PascalCase": return pascalCase(str); case "snake_case": return lowerCase(str).replace(/ /g, "_"); case "UPPER_CASE": return lowerCase(str).replace(/ /g, "_").toUpperCase(); case "kebab-case": return lowerCase(str).replace(/ /g, "-"); } }; function getLocation(start, fieldName = "") { const { line, column } = start; return { start: { line, column }, end: { line, column: column + fieldName.length } }; } var REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 }; var ARRAY_DEFAULT_OPTIONS = { type: "array", uniqueItems: true, minItems: 1, items: { type: "string" } }; var englishJoinWords = (words) => new Intl.ListFormat("en-US", { type: "disjunction" }).format(words); var DisplayNodeNameMap = { [Kind2.ARGUMENT]: "argument", [Kind2.BOOLEAN]: "boolean", [Kind2.DIRECTIVE_DEFINITION]: "directive", [Kind2.DIRECTIVE]: "directive", [Kind2.DOCUMENT]: "document", [Kind2.ENUM_TYPE_DEFINITION]: "enum", [Kind2.ENUM_TYPE_EXTENSION]: "enum", [Kind2.ENUM_VALUE_DEFINITION]: "enum value", [Kind2.ENUM]: "enum", [Kind2.FIELD_DEFINITION]: "field", [Kind2.FIELD]: "field", [Kind2.FLOAT]: "float", [Kind2.FRAGMENT_DEFINITION]: "fragment", [Kind2.FRAGMENT_SPREAD]: "fragment spread", [Kind2.INLINE_FRAGMENT]: "inline fragment", [Kind2.INPUT_OBJECT_TYPE_DEFINITION]: "input", [Kind2.INPUT_OBJECT_TYPE_EXTENSION]: "input", [Kind2.INPUT_VALUE_DEFINITION]: "input value", [Kind2.INT]: "int", [Kind2.INTERFACE_TYPE_DEFINITION]: "interface", [Kind2.INTERFACE_TYPE_EXTENSION]: "interface", [Kind2.LIST_TYPE]: "list type", [Kind2.LIST]: "list", [Kind2.NAME]: "name", [Kind2.NAMED_TYPE]: "named type", [Kind2.NON_NULL_TYPE]: "non-null type", [Kind2.NULL]: "null", [Kind2.OBJECT_FIELD]: "object field", [Kind2.OBJECT_TYPE_DEFINITION]: "type", [Kind2.OBJECT_TYPE_EXTENSION]: "type", [Kind2.OBJECT]: "object", [Kind2.OPERATION_DEFINITION]: "operation", [Kind2.OPERATION_TYPE_DEFINITION]: "operation type", [Kind2.SCALAR_TYPE_DEFINITION]: "scalar", [Kind2.SCALAR_TYPE_EXTENSION]: "scalar", [Kind2.SCHEMA_DEFINITION]: "schema", [Kind2.SCHEMA_EXTENSION]: "schema", [Kind2.SELECTION_SET]: "selection set", [Kind2.STRING]: "string", [Kind2.UNION_TYPE_DEFINITION]: "union", [Kind2.UNION_TYPE_EXTENSION]: "union", [Kind2.VARIABLE_DEFINITION]: "variable", [Kind2.VARIABLE]: "variable" }; function displayNodeName(node) { return `${node.kind === Kind2.OPERATION_DEFINITION ? node.operation : DisplayNodeNameMap[node.kind]} "${"alias" in node && node.alias?.value || "name" in node && node.name?.value || node.value}"`; } function getNodeName(node) { switch (node.kind) { case Kind2.OBJECT_TYPE_DEFINITION: case Kind2.OBJECT_TYPE_EXTENSION: case Kind2.INTERFACE_TYPE_DEFINITION: case Kind2.ENUM_TYPE_DEFINITION: case Kind2.SCALAR_TYPE_DEFINITION: case Kind2.INPUT_OBJECT_TYPE_DEFINITION: case Kind2.UNION_TYPE_DEFINITION: case Kind2.DIRECTIVE_DEFINITION: return displayNodeName(node); case Kind2.FIELD_DEFINITION: case Kind2.INPUT_VALUE_DEFINITION: case Kind2.ENUM_VALUE_DEFINITION: return `${displayNodeName(node)} in ${displayNodeName(node.parent)}`; case Kind2.OPERATION_DEFINITION: return node.name ? displayNodeName(node) : node.operation; } return ""; } var eslintSelectorsTip = `> [!TIP] > > These fields are defined by ESLint [\`selectors\`](https://eslint.org/docs/developer-guide/selectors). > Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.`; // src/siblings.ts var siblingOperationsCache = /* @__PURE__ */ new Map(); function getSiblings(documents) { if (documents.length === 0) { let printed = false; const noopWarn = () => { if (!printed) { logger.warn( "getSiblingOperations was called without any operations. Make sure to set graphql-config `documents` field to make this feature available! See https://the-guild.dev/graphql/config/docs/user/documents for more info" ); printed = true; } return []; }; return { available: false, getFragment: noopWarn, getFragments: noopWarn, getFragmentByType: noopWarn, getFragmentsInUse: noopWarn, getOperation: noopWarn, getOperations: noopWarn, getOperationByType: noopWarn }; } const value = siblingOperationsCache.get(documents); if (value) { return value; } let fragmentsCache = null; const getFragments = () => { if (fragmentsCache === null) { const result = []; for (const source of documents) { for (const definition of source.document?.definitions || []) { if (definition.kind === Kind3.FRAGMENT_DEFINITION) { result.push({ filePath: source.location, document: definition }); } } } fragmentsCache = result; } return fragmentsCache; }; let cachedOperations = null; const getOperations = () => { if (cachedOperations === null) { const result = []; for (const source of documents) { for (const definition of source.document?.definitions || []) { if (definition.kind === Kind3.OPERATION_DEFINITION) { result.push({ filePath: source.location, document: definition }); } } } cachedOperations = result; } return cachedOperations; }; const getFragment = (name) => getFragments().filter((f) => f.document.name.value === name); const collectFragments = (selectable, recursive, collected = /* @__PURE__ */ new Map()) => { visit2(selectable, { FragmentSpread(spread) { const fragmentName = spread.name.value; const [fragment] = getFragment(fragmentName); if (!fragment) { logger.warn( `Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"` ); return; } if (!collected.has(fragmentName)) { collected.set(fragmentName, fragment.document); if (recursive) { collectFragments(fragment.document, recursive, collected); } } } }); return collected; }; const siblingOperations = { available: true, getFragment, getFragments, getFragmentByType: (typeName) => getFragments().filter((f) => f.document.typeCondition.name.value === typeName), getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()), getOperation: (name) => getOperations().filter((o) => o.document.name?.value === name), getOperations, getOperationByType: (type) => getOperations().filter((o) => o.document.operation === type) }; siblingOperationsCache.set(documents, siblingOperations); return siblingOperations; } // src/parser.ts var debug = debugFactory2("graphql-eslint:parser"); debug("cwd %o", CWD); var LEGACY_PARSER_OPTIONS_KEYS = [ "schema", "documents", "extensions", "include", "exclude", "projects", "schemaOptions", "graphQLParserOptions", "skipGraphQLConfig", "operations" ]; function parseForESLint(code, options) { for (const key of LEGACY_PARSER_OPTIONS_KEYS) { if (key in options) { throw new Error( `\`parserOptions.${key}\` was removed in graphql-eslint@4. Use physical graphql-config for setting schema and documents or \`parserOptions.graphQLConfig\` for programmatic usage.` ); } } try { const { filePath } = options; const { document } = parseGraphQLSDL(filePath, code, { noLocation: false }); let project; let schema16, documents; if (false) { const gqlConfig = loadGraphQLConfig(options); project = gqlConfig.getProjectForFile(getFirstExistingPath(filePath)); documents = getDocuments(project); } else { documents = [ parseGraphQLSDL( "operation.graphql", options.graphQLConfig.documents, { noLocation: true } ) ]; } try { if (false) { schema16 = getSchema(project); } else { schema16 = buildSchema(options.graphQLConfig.schema); } } catch (error) { if (error instanceof Error) { error.message = `Error while loading schema: ${error.message}`; } throw error; } const rootTree = convertToESTree(document, schema16); return { services: { schema: schema16, siblingOperations: getSiblings(documents) }, ast: { comments: extractComments(document.loc), tokens: extractTokens(filePath, code), loc: rootTree.loc, range: rootTree.range, type: "Program", sourceType: "script", body: [rootTree] } }; } catch (error) { if (error instanceof Error) { error.message = `[graphql-eslint] ${error.message}`; } if (error instanceof GraphQLError) { const location = error.locations?.[0]; const eslintError = { index: error.positions?.[0], ...location && { lineNumber: location.line, column: location.column - 1 }, message: error.message }; throw eslintError; } throw error; } } var parser = { parseForESLint, meta: { name: "@graphql-eslint/parser", version } }; // src/processor.ts var processor = {}; // src/rules/alphabetize/index.ts import { Kind as Kind4 } from "graphql"; import lowerCase2 from "lodash.lowercase"; var RULE_ID = "alphabetize"; var fieldsEnum = [ Kind4.OBJECT_TYPE_DEFINITION, Kind4.INTERFACE_TYPE_DEFINITION, Kind4.INPUT_OBJECT_TYPE_DEFINITION ]; var selectionsEnum = [ Kind4.OPERATION_DEFINITION, Kind4.FRAGMENT_DEFINITION ]; var argumentsEnum = [ Kind4.FIELD_DEFINITION, Kind4.FIELD, Kind4.DIRECTIVE_DEFINITION, Kind4.DIRECTIVE ]; var schema = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { fields: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: fieldsEnum }, description: "Fields of `type`, `interface`, and `input`." }, values: { type: "boolean", description: "Values of `enum`." }, selections: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: selectionsEnum }, description: "Selections of `fragment` and operations `query`, `mutation` and `subscription`." }, variables: { type: "boolean", description: "Variables of operations `query`, `mutation` and `subscription`." }, arguments: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: argumentsEnum }, description: "Arguments of fields and directives." }, definitions: { type: "boolean", description: "Definitions \u2013 `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`." }, groups: { ...ARRAY_DEFAULT_OPTIONS, minItems: 2, description: [ "Order group. Example: `['...', 'id', '*', '{']` where:", "- `...` stands for fragment spreads", "- `id` stands for field with name `id`", "- `*` stands for everything else", "- `{` stands for fields `selection set`" ].join("\n") } } } }; var rule = { meta: { type: "suggestion", fixable: "code", docs: { category: ["Schema", "Operations"], description: "Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`, examples: [ { title: "Incorrect", usage: [{ fields: [Kind4.OBJECT_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` type User { password: String firstName: String! # should be before "password" age: Int # should be before "firstName" lastName: String! } ` ) }, { title: "Correct", usage: [{ fields: [Kind4.OBJECT_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` type User { age: Int firstName: String! lastName: String! password: String } ` ) }, { title: "Incorrect", usage: [{ values: true }], code: ( /* GraphQL */ ` enum Role { SUPER_ADMIN ADMIN # should be before "SUPER_ADMIN" USER GOD # should be before "USER" } ` ) }, { title: "Correct", usage: [{ values: true }], code: ( /* GraphQL */ ` enum Role { ADMIN GOD SUPER_ADMIN USER } ` ) }, { title: "Incorrect", usage: [{ selections: [Kind4.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { firstName lastName email # should be before "lastName" } } ` ) }, { title: "Correct", usage: [{ selections: [Kind4.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { email firstName lastName } } ` ) } ], configOptions: { schema: [ { definitions: true, fields: fieldsEnum, values: true, arguments: argumentsEnum, groups: ["id", "*", "createdAt", "updatedAt"] } ], operations: [ { definitions: true, selections: selectionsEnum, variables: true, arguments: [Kind4.FIELD, Kind4.DIRECTIVE], groups: ["...", "id", "*", "{"] } ] } }, messages: { [RULE_ID]: "{{ currNode }} should be before {{ prevNode }}" }, schema }, create(context) { const sourceCode = context.getSourceCode(); function isNodeAndCommentOnSameLine(node, comment) { return node.loc.end.line === comment.loc.start.line; } function getBeforeComments(node) { const commentsBefore = sourceCode.getCommentsBefore(node); if (commentsBefore.length === 0) { return []; } const tokenBefore = sourceCode.getTokenBefore(node); if (tokenBefore) { return commentsBefore.filter((comment) => !isNodeAndCommentOnSameLine(tokenBefore, comment)); } const filteredComments = []; const nodeLine = node.loc.start.line; for (let i = commentsBefore.length - 1; i >= 0; i -= 1) { const comment = commentsBefore[i]; if (nodeLine - comment.loc.start.line - filteredComments.length > 1) { break; } filteredComments.unshift(comment); } return filteredComments; } function getRangeWithComments(node) { if (node.kind === Kind4.VARIABLE) { node = node.parent; } const [firstBeforeComment] = getBeforeComments(node); const [firstAfterComment] = sourceCode.getCommentsAfter(node); const from = firstBeforeComment || node; const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node; return [from.range[0], to.range[1]]; } function checkNodes(nodes = []) { for (let i = 1; i < nodes.length; i += 1) { const currNode = nodes[i]; const currName = getName(currNode); if (!currName) { continue; } const prevNode = nodes[i - 1]; const prevName = getName(prevNode); if (prevName) { const compareResult = prevName.localeCompare(currName); const { groups } = opts; let shouldSortByGroup = false; if (groups?.length) { if (!groups.includes("*")) { throw new Error("`groups` option should contain `*` string."); } const indexForPrev = getIndex({ node: prevNode, groups }); const indexForCurr = getIndex({ node: currNode, groups }); shouldSortByGroup = indexForPrev - indexForCurr > 0; if (indexForPrev < indexForCurr) { continue; } } const shouldSort = compareResult === 1; if (!shouldSortByGroup && !shouldSort) { const isSameName = compareResult === 0; if (!isSameName || !prevNode.kind.endsWith("Extension") || currNode.kind.endsWith("Extension")) { continue; } } } context.report({ // @ts-expect-error can't be undefined node: "alias" in currNode && currNode.alias || currNode.name, messageId: RULE_ID, data: { currNode: displayNodeName(currNode), prevNode: prevName ? displayNodeName(prevNode) : lowerCase2(prevNode.kind) }, *fix(fixer) { const prevRange = getRangeWithComments(prevNode); const currRange = getRangeWithComments(currNode); yield fixer.replaceTextRange( prevRange, sourceCode.getText({ range: currRange }) ); yield fixer.replaceTextRange( currRange, sourceCode.getText({ range: prevRange }) ); } }); } } const opts = context.options[0]; const fields = new Set(opts.fields ?? []); const listeners = {}; const kinds = [ fields.has(Kind4.OBJECT_TYPE_DEFINITION) && [ Kind4.OBJECT_TYPE_DEFINITION, Kind4.OBJECT_TYPE_EXTENSION ], fields.has(Kind4.INTERFACE_TYPE_DEFINITION) && [ Kind4.INTERFACE_TYPE_DEFINITION, Kind4.INTERFACE_TYPE_EXTENSION ], fields.has(Kind4.INPUT_OBJECT_TYPE_DEFINITION) && [ Kind4.INPUT_OBJECT_TYPE_DEFINITION, Kind4.INPUT_OBJECT_TYPE_EXTENSION ] ].filter((v) => !!v).flat(); const fieldsSelector = kinds.join(","); const selectionsSelector = opts.selections?.join(","); const argumentsSelector = opts.arguments?.join(","); if (fieldsSelector) { listeners[fieldsSelector] = (node) => { checkNodes(node.fields); }; } if (opts.values) { const enumValuesSelector = [Kind4.ENUM_TYPE_DEFINITION, Kind4.ENUM_TYPE_EXTENSION].join(","); listeners[enumValuesSelector] = (node) => { checkNodes(node.values); }; } if (selectionsSelector) { listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => { checkNodes(node.selections); }; } if (opts.variables) { listeners.OperationDefinition = (node) => { checkNodes(node.variableDefinitions?.map((varDef) => varDef.variable)); }; } if (argumentsSelector) { listeners[argumentsSelector] = (node) => { checkNodes(node.arguments); }; } if (opts.definitions) { listeners.Document = (node) => { checkNodes(node.definitions); }; } return listeners; } }; function getIndex({ node, groups }) { let index = groups.indexOf(getName(node)); if (index === -1 && "selectionSet" in node && node.selectionSet) index = groups.indexOf("{"); if (index === -1 && node.kind === Kind4.FRAGMENT_SPREAD) index = groups.indexOf("..."); if (index === -1) index = groups.indexOf("*"); return index; } function getName(node) { return "alias" in node && node.alias?.value || // "name" in node && node.name?.value || ""; } // src/rules/description-style/index.ts var schema2 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { style: { enum: ["block", "inline"], default: "block" } } } }; var rule2 = { meta: { type: "suggestion", hasSuggestions: true, docs: { examples: [ { title: "Incorrect", usage: [{ style: "inline" }], code: ( /* GraphQL */ ` """ Description """ type someTypeName { # ... } ` ) }, { title: "Correct", usage: [{ style: "inline" }], code: ( /* GraphQL */ ` " Description " type someTypeName { # ... } ` ) } ], description: "Require all comments to follow the same style (either block or inline).", category: "Schema", url: "https://the-guild.dev/graphql/eslint/rules/description-style", recommended: true }, schema: schema2 }, create(context) { const { style = "block" } = context.options[0] || {}; const isBlock = style === "block"; return { [`.description[type=StringValue][block!=${isBlock}]`](node) { context.report({ loc: isBlock ? node.loc : node.loc.start, message: `Unexpected ${isBlock ? "inline" : "block"} description for ${getNodeName( node.parent )}`, suggest: [ { desc: `Change to ${isBlock ? "block" : "inline"} style description`, fix(fixer) { const sourceCode = context.getSourceCode(); const originalText = sourceCode.getText(node); const newText = isBlock ? originalText.replace(/(^")|("$)/g, '"""') : originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, " "); return fixer.replaceText(node, newText); } } ] }); } }; } }; // src/rules/graphql-js-validation.ts import { Kind as Kind5, validate, visit as visit3 } from "graphql"; import { ExecutableDefinitionsRule, FieldsOnCorrectTypeRule, FragmentsOnCompositeTypesRule, KnownArgumentNamesRule, KnownDirectivesRule, KnownFragmentNamesRule, KnownTypeNamesRule, LoneAnonymousOperationRule, LoneSchemaDefinitionRule, NoFragmentCyclesRule, NoUndefinedVariablesRule, NoUnusedFragmentsRule, NoUnusedVariablesRule, OverlappingFieldsCanBeMergedRule, PossibleFragmentSpreadsRule, PossibleTypeExtensionsRule, ProvidedRequiredArgumentsRule, ScalarLeafsRule, SingleFieldSubscriptionsRule, UniqueArgumentNamesRule, UniqueDirectiveNamesRule, UniqueDirectivesPerLocationRule, UniqueFieldDefinitionNamesRule, UniqueInputFieldNamesRule, UniqueOperationTypesRule, UniqueTypeNamesRule, UniqueVariableNamesRule, ValuesOfCorrectTypeRule, VariablesAreInputTypesRule, VariablesInAllowedPositionRule } from "graphql/validation/index.js"; import { validateSDL } from "graphql/validation/validate.js"; function validateDocument({ context, schema: schema16 = null, documentNode, rule: rule35, hasDidYouMeanSuggestions }) { if (documentNode.definitions.length === 0) { return; } try { const validationErrors = schema16 ? validate(schema16, documentNode, [rule35]) : validateSDL(documentNode, null, [rule35]); 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 = error.message.match(/Did you mean (?<content>.*)\?$/)?.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: REPORT_ON_FIRST_CHARACTER, message: error.message }); } } var 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); } }; visit3(node, visitor); return { fragmentDefs, fragmentSpreads }; }; var getMissingFragments = (node) => { const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(node); return [...fragmentSpreads].filter((name) => !fragmentDefs.has(name)); }; var handleMissingFragments = ({ ruleId, context, node }) => { const missingFragments = getMissingFragments(node); if (missingFragments.length > 0) { const siblings = requireGraphQLOperations(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: Kind5.DOCUMENT, definitions: [...node.definitions, ...fragmentsToAdd] } }); } } return node; }; var validationToRule = ({ ruleId, rule: rule35, getDocumentNode, schema: schema16 = [], hasDidYouMeanSuggestions }, docs) => { return { [ruleId]: { meta: { docs: { recommended: true, ...docs, graphQLJSRuleName: rule35.name, url: `https://the-guild.dev/graphql/eslint/rules/${ruleId}`, description: `${docs.description} > This rule is a wrapper around a \`graphql-js\` validation function.` }, schema: schema16, hasSuggestions: hasDidYouMeanSuggestions }, create(context) { return { Document(node) { const schema17 = docs.requiresSchema ? requireGraphQLSchema(ruleId, context) : null; const documentNode = getDocumentNode ? getDocumentNode({ ruleId, context, node: node.rawNode() }) : node.rawNode(); validateDocument({ context, schema: schema17, documentNode, rule: rule35, hasDidYouMeanSuggestions }); } }; } } }; }; var GRAPHQL_JS_VALIDATIONS = Object.assign( {}, validationToRule( { ruleId: "executable-definitions", rule: ExecutableDefinitionsRule }, { 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", rule: FieldsOnCorrectTypeRule, 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", rule: FragmentsOnCompositeTypesRule }, { 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", rule: KnownArgumentNamesRule, 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", rule: KnownDirectivesRule, getDocumentNode({ context, node: documentNode }) { const { ignoreClientDirectives = [] } = context.options[0] || {}; if (ignoreClientDirectives.length === 0) { return documentNode; } const filterDirectives = (node) => ({ ...node, directives: node.directives?.filter( (directive) => !ignoreClientDirectives.includes(directive.name.value) ) }); return visit3(documentNode, { Field: filterDirectives, OperationDefinition: filterDirectives }); }, schema: { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, required: ["ignoreClientDirectives"], properties: { ignoreClientDirectives: 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", rule: KnownFragmentNamesRule, 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", rule: KnownTypeNamesRule, 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", rule: LoneAnonymousOperationRule }, { 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", rule: LoneSchemaDefinitionRule }, { category: "Schema", description: "A GraphQL document is only valid if it contains only one schema definition." } ), validationToRule( { ruleId: "no-fragment-cycles", rule: NoFragmentCyclesRule }, { 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", rule: NoUndefinedVariablesRule, 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", rule: NoUnusedFragmentsRule, getDocumentNode: ({ ruleId, context, node }) => { const siblings = requireGraphQLOperations(ruleId, context); const FilePathToDocumentsMap = [ ...siblings.getOperations(), ...siblings.getFragments() ].reduce((map, { filePath, document }) => { 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: Kind5.DOCUMENT, definitions: documents }); const isCurrentFileImportFragment = missingFragments.some( (fragment) => fragmentDefs.has(fragment) ); if (isCurrentFileImportFragment) { return getParentNode(filePath, { kind: Kind5.DOCUMENT, definitions: [...node2.definitions, ...documents] }); } } return node2; }; return getParentNode(context.filename, 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", rule: NoUnusedVariablesRule, 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", rule: OverlappingFieldsCanBeMergedRule }, { 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", rule: PossibleFragmentSpreadsRule }, { 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", rule: PossibleTypeExtensionsRule, hasDidYouMeanSuggestions: true }, { category: "Schema", description: "A type extension is only valid if the type is defined and has the same kind.", recommended: true, requiresSchema: true } ), validationToRule( { ruleId: "provided-required-arguments", rule: ProvidedRequiredArgumentsRule }, { 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", rule: ScalarLeafsRule, 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", rule: SingleFieldSubscriptionsRule }, { category: "Operations", description: "A GraphQL subscription is valid only if it contains a single root field.", requiresSchema: true } ), validationToRule( { ruleId: "unique-argument-names", rule: UniqueArgumentNamesRule }, { 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", rule: UniqueDirectiveNamesRule }, { category: "Schema", description: "A GraphQL document is only valid if all defined directives have unique names." } ), validationToRule( { ruleId: "unique-directive-names-per-location", rule: UniqueDirectivesPerLocationRule }, { 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-field-definition-names", rule: UniqueFieldDefinitionNamesRule }, { category: "Schema", description: "A GraphQL complex type is only valid if all its fields are uniquely named." } ), validationToRule( { ruleId: "unique-input-field-names", rule: UniqueInputFieldNamesRule }, { category: "Operations", description: "A GraphQL input object value is only valid if all supplied fields are uniquely named." } ), validationToRule( { ruleId: "unique-operation-types", rule: UniqueOperationTypesRule }, { category: "Schema", description: "A GraphQL document is only valid if it has only one type per operation." } ), validationToRule( { ruleId: "unique-type-names", rule: UniqueTypeNamesRule }, { category: "Schema", description: "A GraphQL document is only valid if all defined types have unique names." } ), validationToRule( { ruleId: "unique-variable-names", rule: UniqueVariableNamesRule }, { 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", rule: ValuesOfCorrectTypeRule, 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", rule: VariablesAreInputTypesRule }, { 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", rule: VariablesInAllowedPositionRule }, { category: "Operations", description: "Variables passed to field arguments conform to type.", requiresSchema: true } ) ); // src/rules/input-name/index.ts import { Kind as Kind6 } from "graphql"; var schema3 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { checkInputType: { type: "boolean", default: false, description: "Check that the input type name follows the convention \\<mutationName>Input" }, caseSensitiveInputType: { type: "boolean", default: true, description: "Allow for case discrepancies in the input type name" }, checkQueries: { type: "boolean", default: false, description: "Apply the rule to Queries" }, checkMutations: { type: "boolean", default: true, description: "Apply the rule to Mutations" } } } }; var isObjectType = (node) => ( // TODO: remove `as any` when drop support of graphql@15 [Kind6.OBJECT_TYPE_DEFINITION, Kind6.OBJECT_TYPE_EXTENSION].includes(node.type) ); var isQueryType = (node) => isObjectType(node) && node.name.value === "Query"; var isMutationType = (node) => isObjectType(node) && node.name.value === "Mutation"; var rule3 = { meta: { type: "suggestion", hasSuggestions: true, docs: { description: 'Require mutation argument to be always called "input" and input type to be called Mutation name + "Input".\nUsing the same name for all input parameters will make your schemas easier to consume and more predictable. Using the same name as mutation for InputType will make it easier to find mutations that InputType belongs to.', category: "Schema", url: "https://the-guild.dev/graphql/eslint/rules/input-name", examples: [ { title: "Incorrect", usage: [{ checkInputType: true }], code: ( /* GraphQL */ ` type Mutation { SetMessage(message: InputMessage): String } ` ) }, { title: "Correct (with `checkInputType`)", usage: [{ checkInputType: true }], code: ( /* GraphQL */ ` type Mutation { SetMessage(input: SetMessageInput): String } ` ) }, { title: "Correct (without `checkInputType`)", usage: [{ checkInputType: false }], code: ( /* GraphQL */ ` type Mutation { SetMessage(input: AnyInputTypeName): String } ` ) } ] }, schema: schema3 }, create(context) { const options = { checkInputType: false, caseSensitiveInputType: true, checkMutations: true, ...context.options[0] }; const shouldCheckType = (node) => options.checkMutations && isMutationType(node) || options.checkQueries && isQueryType(node) || false; const listeners = { "FieldDefinition > InputValueDefinition[name.value!=input] > Name"(node) { const fieldDef = node.parent.parent; if (shouldCheckType(fieldDef.parent)) { const inputName = node.value; context.report({ node, message: `Input "${inputName}" should be named "input" for "${fieldDef.parent.name.value}.${fieldDef.name.value}"`, suggest: [ { desc: "Rename to `input`", fix: (fixer) => fixer.replaceText(node, "input") } ] }); } } }; if (options.checkInputType) { listeners["FieldDefinition > InputValueDefinition NamedType"] = (node) => { const findInputType = (item) => { let currentNode = item; while (currentNode.type !== Kind6.INPUT_VALUE_DEFINITION) { currentNode = currentNode.parent; } return currentNode; }; const inputValueNode = findInputType(node); if (shouldCheckType(inputValueNode.parent.parent)) { const mutationName = `${inputValueNode.parent.name.value}Input`; const name = node.name.value; if (options.caseSensitiveInputType && node.name.value !== mutationName || name.toLowerCase() !== mutationName.toLowerCase()) { context.report({ node: node.name, message: `Input type \`${name}\` name should be \`${mutationName}\`.`, suggest: [ { desc: `Rename to \`${mutationName}\``, fix: (fixer) => fixer.replaceText(node, mutationName) } ] }); } } }; } return listeners; } }; // src/rules/lone-executable-definition/index.ts import { OperationTypeNode as OperationTypeNode2 } from "graphql"; var RULE_ID2 = "lone-executable-definition"; var definitionTypes = ["fragment", ...Object.values(OperationTypeNode2)]; var schema4 = { type: "array", maxItems: 1, items: { type: "object", minProperties: 1, additionalProperties: false, properties: { ignore: { ...ARRAY_DEFAULT_OPTIONS, maxItems: 3, // ignore all 4 types is redundant items: { enum: definitionTypes }, description: "Allow certain definitions to be placed alongside others." } } } }; var rule4 = { meta: { type: "suggestion", docs: { category: "Operations", description: "Require queries, mutations, subscriptions or fragments to be located in separate files.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID2}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` query Foo { id } fragment Bar on Baz { id } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` query Foo { id } ` ) } ] }, messages: { [RULE_ID2]: "{{name}} should be in a separate file." }, schema: schema4 }, create(context) { const ignore = new Set(context.options[0]?.ignore || []); const definitions = []; return { ":matches(OperationDefinition, FragmentDefinition)"(node) { const type = "operation" in node ? node.operation : "fragment"; if (!ignore.has(type)) { definitions.push({ type, node }); } }, "Document:exit"() { for (const { node, type } of definitions.slice(1)) { let name = pascalCase(type); const definitionName = node.name?.value; if (definitionName) { name += ` "${definitionName}"`; } context.report({ loc: node.name?.loc || getLocation(node.loc.start, type), messageId: RULE_ID2, data: { name } }); } } }; } }; // src/rules/match-document-filename/index.ts import { basename, extname } from "node:path"; import { Kind as Kind7 } from "graphql"; var MATCH_EXTENSION = "MATCH_EXTENSION"; var MATCH_STYLE = "MATCH_STYLE"; var CASE_STYLES = [ "camelCase", "PascalCase", "snake_case", "UPPER_CASE", "kebab-case", "matchDocumentStyle" ]; var schemaOption = { oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asObject" }] }; var caseSchema = { enum: CASE_STYLES, description: `One of: ${CASE_STYLES.map((t) => `\`${t}\``).join(", ")}` }; var schema5 = { definitions: { asString: caseSchema, asObject: { type: "object", additionalProperties: false, minProperties: 1, properties: { style: caseSchema, suffix: { type: "string" }, prefix: { type: "string" } } } }, type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { fileExtension: { enum: [".gql", ".graphql"] }, query: schemaOption, mutation: schemaOption, subscription: schemaOption, fragment: schemaOption } } }; var rule5 = { meta: { type: "suggestion", docs: { category: "Operations", description: "This rule allows you to enforce that the file name should match the operation name.", url: "https://the-guild.dev/graphql/eslint/rules/match-document-filename", examples: [ { title: "Correct", usage: [{ fileExtension: ".gql" }], code: ( /* GraphQL */ ` # user.gql type User { id: ID! } ` ) }, { title: "Correct", usage: [{ query: "snake_case" }], code: ( /* GraphQL */ ` # user_by_id.gql query UserById { userById(id: 5) { id name fullName } } ` ) }, { title: "Correct", usage: [{ fragment: { style: "kebab-case", suffix: ".fragment" } }], code: ( /* GraphQL */ ` # user-fields.fragment.gql fragment user_fields on User { id email } ` ) }, { title: "Correct", usage: [{ mutation: { style: "PascalCase", suffix: "Mutation" } }], code: ( /* GraphQL */ ` # DeleteUserMutation.gql mutation DELETE_USER { deleteUser(id: 5) } ` ) }, { title: "Incorrect", usage: [{ fileExtension: ".graphql" }], code: ( /* GraphQL */ ` # post.gql type Post { id: ID! } ` ) }, { title: "Incorrect", usage: [{ query: "PascalCase" }], code: ( /* GraphQL */ ` # user-by-id.gql query UserById { userById(id: 5) { id name fullName } } ` ) }, { title: "Correct", usage: [{ fragment: { style: "kebab-case", prefix: "mutation." } }], code: ( /* GraphQL */ ` # mutation.add-alert.graphql mutation addAlert { foo } ` ) }, { title: "Correct", usage: [{ fragment: { prefix: "query." } }], code: ( /* GraphQL */ ` # query.me.graphql query me { foo } ` ) } ], configOptions: [ { query: "kebab-case", mutation: "kebab-case", subscription: "kebab-case", fragment: "kebab-case" } ] }, messages: { [MATCH_EXTENSION]: `File extension "{{ fileExtension }}" don't match extension "{{ expectedFileExtension }}"`, [MATCH_STYLE]: 'Unexpected filename "{{ filename }}". Rename it to "{{ expectedFilename }}"' }, schema: schema5 }, create(context) { const options = context.options[0] || { fileExtension: null }; const filePath = context.filename; const isVirtualFile = VIRTUAL_DOCUMENT_REGEX.test(filePath); if (isVirtualFile) { return {}; } const fileExtension = extname(filePath); const filename = basename(filePath, fileExtension); return { Document(documentNode) { if (options.fileExtension && options.fileExtension !== fileExtension) { context.report({ loc: REPORT_ON_FIRST_CHARACTER, messageId: MATCH_EXTENSION, data: { fileExtension, expectedFileExtension: options.fileExtension } }); } const firstOperation = documentNode.definitions.find( (n) => n.kind === Kind7.OPERATION_DEFINITION ); const firstFragment = documentNode.definitions.find( (n) => n.kind === Kind7.FRAGMENT_DEFINITION ); const node = firstOperation || firstFragment; if (!node) { return; } const docName = node.name?.value; if (!docName) { return; } const docType = "operation" in node ? node.operation : "fragment"; let option = options[docType]; if (!option) { return; } if (typeof option === "string") { option = { style: option }; } const expectedExtension = options.fileExtension || fileExtension; let expectedFilename = option.prefix || ""; if (option.style) { expectedFilename += option.style === "matchDocumentStyle" ? docName : convertCase(option.style, docName); } else { expectedFilename += filename; } expectedFilename += (option.suffix || "") + expectedExtension; const filenameWithExtension = filename + expectedExtension; if (expectedFilename !== filenameWithExtension) { context.report({ loc: REPORT_ON_FIRST_CHARACTER, messageId: MATCH_STYLE, data: { expectedFilename, filename: filenameWithExtension } }); } } }; } }; // src/rules/naming-convention/index.ts import { Kind as Kind8 } from "graphql"; var KindToDisplayName = { // types [Kind8.OBJECT_TYPE_DEFINITION]: "Type", [Kind8.INTERFACE_TYPE_DEFINITION]: "Interface", [Kind8.ENUM_TYPE_DEFINITION]: "Enumerator", [Kind8.SCALAR_TYPE_DEFINITION]: "Scalar", [Kind8.INPUT_OBJECT_TYPE_DEFINITION]: "Input type", [Kind8.UNION_TYPE_DEFINITION]: "Union", // fields [Kind8.FIELD_DEFINITION]: "Field", [Kind8.INPUT_VALUE_DEFINITION]: "Input property", [Kind8.ARGUMENT]: "Argument", [Kind8.DIRECTIVE_DEFINITION]: "Directive", // rest [Kind8.ENUM_VALUE_DEFINITION]: "Enumeration value", [Kind8.OPERATION_DEFINITION]: "Operation", [Kind8.FRAGMENT_DEFINITION]: "Fragment", [Kind8.VARIABLE_DEFINITION]: "Variable" }; var StyleToRegex = { camelCase: /^[a-z][\dA-Za-z]*$/, PascalCase: /^[A-Z][\dA-Za-z]*$/, snake_case: /^[a-z][\d_a-z]*[\da-z]*$/, UPPER_CASE: /^[A-Z][\dA-Z_]*[\dA-Z]*$/ }; var ALLOWED_KINDS = Object.keys(KindToDisplayName).sort(); var ALLOWED_STYLES = Object.keys(StyleToRegex); var schemaOption2 = { oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asObject" }] }; var descriptionPrefixesSuffixes = (name, id) => `> [!WARNING] > > This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${id}) instead.`; var caseSchema2 = { enum: ALLOWED_STYLES, description: `One of: ${ALLOWED_STYLES.map((t) => `\`${t}\``).join(", ")}` }; var schema6 = { definitions: { asString: caseSchema2, asObject: { type: "object", additionalProperties: false, properties: { style: caseSchema2, prefix: { type: "string" }, suffix: { type: "string" }, forbiddenPatterns: { ...ARRAY_DEFAULT_OPTIONS, items: { type: "object" }, description: "Should be of instance of `RegEx`" }, requiredPattern: { type: "object", description: "Should be of instance of `RegEx`" }, forbiddenPrefixes: { ...ARRAY_DEFAULT_OPTIONS, description: descriptionPrefixesSuffixes("forbiddenPatterns", "forbiddenpatterns-array") }, forbiddenSuffixes: { ...ARRAY_DEFAULT_OPTIONS, description: descriptionPrefixesSuffixes("forbiddenPatterns", "forbiddenpatterns-array") }, requiredPrefixes: { ...ARRAY_DEFAULT_OPTIONS, description: descriptionPrefixesSuffixes("requiredPattern", "requiredpattern-object") }, requiredSuffixes: { ...ARRAY_DEFAULT_OPTIONS, description: descriptionPrefixesSuffixes("requiredPattern", "requiredpattern-object") }, ignorePattern: { type: "string", description: "Option to skip validation of some words, e.g. acronyms" } } } }, type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { types: { ...schemaOption2, description: `Includes: ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join("\n")}` }, ...Object.fromEntries( ALLOWED_KINDS.map((kind) => [ kind, { ...schemaOption2, description: `> [!NOTE] > > Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).` } ]) ), allowLeadingUnderscore: { type: "boolean", default: false }, allowTrailingUnderscore: { type: "boolean", default: false } }, patternProperties: { [`^(${ALLOWED_KINDS.join("|")})(.+)?$`]: schemaOption2 }, description: [ "> It's possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts with allowed `ASTNode` names which are described below.", ">", "> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.", ">", "> Example: pattern property `FieldDefinition[parent.name.value=Query]` will match only fields for type `Query`." ].join("\n") } }; var rule6 = { meta: { type: "suggestion", docs: { description: "Require names to follow specified conventions.", category: ["Schema", "Operations"], recommended: true, url: "https://the-guild.dev/graphql/eslint/rules/naming-convention", examples: [ { title: "Incorrect", usage: [{ types: "PascalCase", FieldDefinition: "camelCase" }], code: ( /* GraphQL */ ` type user { first_name: String! } ` ) }, { title: "Incorrect", usage: [ { FragmentDefinition: { style: "PascalCase", forbiddenPatterns: [/(^fragment)|(fragment$)/i] } } ], code: ( /* GraphQL */ ` fragment UserFragment on User { # ... } ` ) }, { title: "Incorrect", usage: [{ "FieldDefinition[parent.name.value=Query]": { forbiddenPatterns: [/^get/i] } }], code: ( /* GraphQL */ ` type Query { getUsers: [User!]! } ` ) }, { title: "Correct", usage: [{ types: "PascalCase", FieldDefinition: "camelCase" }], code: ( /* GraphQL */ ` type User { firstName: String } ` ) }, { title: "Correct", usage: [ { FragmentDefinition: { style: "PascalCase", forbiddenPatterns: [/(^fragment)|(fragment$)/i] } } ], code: ( /* GraphQL */ ` fragment UserFields on User { # ... } ` ) }, { title: "Correct", usage: [{ "FieldDefinition[parent.name.value=Query]": { forbiddenPatterns: [/^get/i] } }], code: ( /* GraphQL */ ` type Query { users: [User!]! } ` ) }, { title: "Correct", usage: [{ FieldDefinition: { style: "camelCase", ignorePattern: "^(EAN13|UPC|UK)" } }], code: ( /* GraphQL */ ` type Product { EAN13: String UPC: String UKFlag: String } ` ) }, { title: "Correct", usage: [ { "FieldDefinition[gqlType.name.value=Boolean]": { style: "camelCase", requiredPattern: /^(is|has)/ }, "FieldDefinition[gqlType.gqlType.name.value=Boolean]": { style: "camelCase", requiredPattern: /^(is|has)/ } } ], code: ( /* GraphQL */ ` type Product { isBackordered: Boolean isNew: Boolean! hasDiscount: Boolean! } ` ) }, { title: "Correct", usage: [ { "FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]": { style: "camelCase", requiredPattern: /SensitiveSecret$/ } } ], code: ( /* GraphQL */ ` scalar SensitiveSecret type Account { accountSensitiveSecret: SensitiveSecret! } ` ) }, { title: "Correct (Relay fragment convention `<module_name>_<property_name>`)", usage: [ { FragmentDefinition: { style: "PascalCase", requiredPattern: /_(?<camelCase>.+?)$/ } } ], code: ( /* GraphQL */ ` # schema type User { # ... } # operations fragment UserFields_data on User { # ... } ` ) } ], configOptions: { schema: [ { types: "PascalCase", FieldDefinition: "camelCase", InputValueDefinition: "camelCase", Argument: "camelCase", DirectiveDefinition: "camelCase", EnumValueDefinition: "UPPER_CASE", "FieldDefinition[parent.name.value=Query]": { forbiddenPatterns: [/^(query|get)/i, /query$/i] }, "FieldDefinition[parent.name.value=Mutation]": { forbiddenPatterns: [/(^mutation)|(mutation$)/i] }, "FieldDefinition[parent.name.value=Subscription]": { forbiddenPatterns: [/(^subscription)|(subscription$)/i] }, "EnumTypeDefinition,EnumTypeExtension": { forbiddenPatterns: [/(^enum)|(enum$)/i] }, "InterfaceTypeDefinition,InterfaceTypeExtension": { forbiddenPatterns: [/(^interface)|(interface$)/i] }, "UnionTypeDefinition,UnionTypeExtension": { forbiddenPatterns: [/(^union)|(union$)/i] }, "ObjectTypeDefinition,ObjectTypeExtension": { forbiddenPatterns: [/(^type)|(type$)/i] } } ], operations: [ { VariableDefinition: "camelCase", OperationDefinition: { style: "PascalCase", forbiddenPatterns: [ /^(query|mutation|subscription|get)/i, /(query|mutation|subscription)$/i ] }, FragmentDefinition: { style: "PascalCase", forbiddenPatterns: [/(^fragment)|(fragment$)/i] } } ] } }, hasSuggestions: true, schema: schema6 }, create(context) { const options = context.options[0] || {}; const { allowLeadingUnderscore, allowTrailingUnderscore, types, ...restOptions } = options; const ignoredNodes = /* @__PURE__ */ new Set(); function normalisePropertyOption(kind) { const style = restOptions[kind] || types; return typeof style === "object" ? style : { style }; } function report(node, message, suggestedNames) { context.report({ node, message, suggest: suggestedNames.map((suggestedName) => ({ desc: `Rename to \`${suggestedName}\``, fix: (fixer) => fixer.replaceText(node, suggestedName) })) }); } const checkNode2 = (selector) => (n) => { const { name: node } = n.kind === Kind8.VARIABLE_DEFINITION ? n.variable : n; if (!node) { return; } const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style, ignorePattern, requiredPrefixes, requiredSuffixes, forbiddenPatterns, requiredPattern } = normalisePropertyOption(selector); const nodeName = node.value; const error = getError(); if (error) { const { errorMessage, renameToNames } = error; const [leadingUnderscores] = nodeName.match(/^_*/); const [trailingUnderscores] = nodeName.match(/_*$/); const suggestedNames = renameToNames.map( (renameToName) => leadingUnderscores + renameToName + trailingUnderscores ); const name = displayNodeName(n); report( node, `${name[0].toUpperCase()}${name.slice(1)} should ${errorMessage}`, suggestedNames ); } function getError() { let name = nodeName; if (allowLeadingUnderscore) name = name.replace(/^_+/, ""); if (allowTrailingUnderscore) name = name.replace(/_+$/, ""); if (ignorePattern && new RegExp(ignorePattern, "u").test(name)) { if ("name" in n) { ignoredNodes.add(n.name); } return; } if (prefix && !name.startsWith(prefix)) { return { errorMessage: `have "${prefix}" prefix`, renameToNames: [prefix + name] }; } if (suffix && !name.endsWith(suffix)) { return { errorMessage: `have "${suffix}" suffix`, renameToNames: [name + suffix] }; } if (requiredPattern) { if (requiredPattern.source.includes("(?<")) { try { name = name.replace(requiredPattern, (originalString, ...args) => { const groups = args.at(-1); for (const [styleName, value] of Object.entries(groups)) { if (!(styleName in StyleToRegex)) { throw new Error("Invalid case style in `requiredPatterns` option"); } if (value === convertCase(styleName, value)) { return ""; } throw new Error(`contain the required pattern: ${requiredPattern}`); } return originalString; }); if (name === nodeName) { throw new Error(`contain the required pattern: ${requiredPattern}`); } } catch (error2) { return { errorMessage: error2.message, renameToNames: [] }; } } else if (!requiredPattern.test(name)) { return { errorMessage: `contain the required pattern: ${requiredPattern}`, renameToNames: [] }; } } const forbidden = forbiddenPatterns?.find((pattern) => pattern.test(name)); if (forbidden) { return { errorMessage: `not contain the forbidden pattern "${forbidden}"`, renameToNames: [name.replace(forbidden, "")] }; } const forbiddenPrefix = forbiddenPrefixes?.find((prefix2) => name.startsWith(prefix2)); if (forbiddenPrefix) { return { errorMessage: `not have "${forbiddenPrefix}" prefix`, renameToNames: [name.replace(new RegExp(`^${forbiddenPrefix}`), "")] }; } const forbiddenSuffix = forbiddenSuffixes?.find((suffix2) => name.endsWith(suffix2)); if (forbiddenSuffix) { return { errorMessage: `not have "${forbiddenSuffix}" suffix`, renameToNames: [name.replace(new RegExp(`${forbiddenSuffix}$`), "")] }; } if (requiredPrefixes && !requiredPrefixes.some((requiredPrefix) => name.startsWith(requiredPrefix))) { return { errorMessage: `have one of the following prefixes: ${englishJoinWords( requiredPrefixes )}`, renameToNames: style ? requiredPrefixes.map((prefix2) => convertCase(style, `${prefix2} ${name}`)) : requiredPrefixes.map((prefix2) => `${prefix2}${name}`) }; } if (requiredSuffixes && !requiredSuffixes.some((requiredSuffix) => name.endsWith(requiredSuffix))) { return { errorMessage: `have one of the following suffixes: ${englishJoinWords( requiredSuffixes )}`, renameToNames: style ? requiredSuffixes.map((suffix2) => convertCase(style, `${name} ${suffix2}`)) : requiredSuffixes.map((suffix2) => `${name}${suffix2}`) }; } if (!style) { return; } const caseRegex = StyleToRegex[style]; if (!caseRegex.test(name)) { return { errorMessage: `be in ${style} format`, renameToNames: [convertCase(style, name)] }; } } }; const checkUnderscore = (isLeading) => (node) => { if (ignoredNodes.has(node)) { return; } if (node.parent.kind === "Field" && node.parent.alias !== node) { return; } const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, ""); report(node, `${isLeading ? "Leading" : "Trailing"} underscores are not allowed`, [ suggestedName ]); }; const listeners = {}; if (!allowLeadingUnderscore) { listeners["Name[value=/^_/]"] = checkUnderscore(true); } if (!allowTrailingUnderscore) { listeners["Name[value=/_$/]"] = checkUnderscore(false); } const selectors = new Set( [types && TYPES_KINDS, Object.keys(restOptions)].filter((v) => !!v).flat() ); for (const selector of selectors) { listeners[selector] = checkNode2(selector); } return listeners; } }; // src/rules/no-anonymous-operations/index.ts import { Kind as Kind9 } from "graphql"; var RULE_ID3 = "no-anonymous-operations"; var rule7 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Operations", description: "Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.", recommended: true, url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID3}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` query { # ... } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` query user { # ... } ` ) } ] }, messages: { [RULE_ID3]: "Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!" }, schema: [] }, create(context) { return { "OperationDefinition[name=undefined]"(node) { const [firstSelection] = node.selectionSet.selections; const suggestedName = firstSelection.kind === Kind9.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation; context.report({ loc: getLocation(node.loc.start, node.operation), messageId: RULE_ID3, data: { operation: node.operation }, suggest: [ { desc: `Rename to \`${suggestedName}\``, fix(fixer) { const sourceCode = context.getSourceCode(); const hasQueryKeyword = sourceCode.getText({ range: [node.range[0], node.range[0] + 1] }) !== "{"; return fixer.insertTextAfterRange( [node.range[0], node.range[0] + (hasQueryKeyword ? node.operation.length : 0)], `${hasQueryKeyword ? "" : "query"} ${suggestedName}${hasQueryKeyword ? "" : " "}` ); } } ] }); } }; } }; // src/rules/no-deprecated/index.ts var RULE_ID4 = "no-deprecated"; var rule8 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Operations", description: "Enforce that deprecated fields or enum values are not in use by operations.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID4}`, requiresSchema: true, examples: [ { title: "Incorrect (field)", code: ( /* GraphQL */ ` # In your schema type User { id: ID! name: String! @deprecated(reason: "old field, please use fullName instead") fullName: String! } # Query query user { user { name # This is deprecated, so you'll get an error } } ` ) }, { title: "Incorrect (enum value)", code: ( /* GraphQL */ ` # In your schema type Mutation { changeSomething(type: SomeType): Boolean! } enum SomeType { NEW OLD @deprecated(reason: "old field, please use NEW instead") } # Mutation mutation { changeSomething( type: OLD # This is deprecated, so you'll get an error ) { ... } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # In your schema type User { id: ID! name: String! @deprecated(reason: "old field, please use fullName instead") fullName: String! } # Query query user { user { id fullName } } ` ) } ], recommended: true }, messages: { [RULE_ID4]: "{{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})" }, schema: [] }, create(context) { requireGraphQLSchema(RULE_ID4, context); function report(node, reason) { const nodeType = displayNodeName(node); context.report({ node, messageId: RULE_ID4, data: { type: nodeType[0].toUpperCase() + nodeType.slice(1), reason }, suggest: [ { desc: `Remove ${nodeType}`, fix: (fixer) => fixer.remove(node) } ] }); } return { EnumValue(node) { const typeInfo = node.typeInfo(); const reason = typeInfo.enumValue?.deprecationReason; if (reason) { report(node, reason); } }, Field(node) { const typeInfo = node.typeInfo(); const reason = typeInfo.fieldDef?.deprecationReason; if (reason) { report(node, reason); } }, Argument(node) { const typeInfo = node.typeInfo(); const reason = typeInfo.argument?.deprecationReason; if (reason) { report(node, reason); } }, ObjectValue(node) { const { inputType } = node.typeInfo(); if (!inputType) return; if ("getFields" in inputType) { const fields = inputType.getFields(); for (const field of node.fields) { const fieldName = field.name.value; const reason = fields[fieldName].deprecationReason; if (reason) { report(field, reason); } } } } }; } }; // src/rules/no-duplicate-fields/index.ts import { Kind as Kind10 } from "graphql"; var RULE_ID5 = "no-duplicate-fields"; var rule9 = { meta: { type: "suggestion", hasSuggestions: true, docs: { description: "Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.", category: "Operations", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID5}`, recommended: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` query { user { name email name # duplicate field } } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` query { users( first: 100 skip: 50 after: "cji629tngfgou0b73kt7vi5jo" first: 100 # duplicate argument ) { id } } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` query ( $first: Int! $first: Int! # duplicate variable ) { users(first: $first, skip: 50) { id } } ` ) } ] }, messages: { [RULE_ID5]: "{{ type }} `{{ fieldName }}` defined multiple times." }, schema: [] }, create(context) { function checkNode2(usedFields, node) { const fieldName = node.value; if (usedFields.has(fieldName)) { const { parent } = node; context.report({ node, messageId: RULE_ID5, data: { type: parent.type, fieldName }, suggest: [ { desc: `Remove \`${fieldName}\` ${parent.type.toLowerCase()}`, fix(fixer) { return fixer.remove( parent.type === Kind10.VARIABLE ? parent.parent : parent ); } } ] }); } else { usedFields.add(fieldName); } } return { OperationDefinition(node) { const set = /* @__PURE__ */ new Set(); for (const varDef of node.variableDefinitions || []) { checkNode2(set, varDef.variable.name); } }, Field(node) { const set = /* @__PURE__ */ new Set(); for (const arg of node.arguments || []) { checkNode2(set, arg.name); } }, SelectionSet(node) { const set = /* @__PURE__ */ new Set(); for (const selection of node.selections) { if (selection.kind === Kind10.FIELD) { checkNode2(set, selection.alias || selection.name); } } } }; } }; // src/rules/no-hashtag-description/index.ts import { TokenKind as TokenKind2 } from "graphql"; var RULE_ID6 = "HASHTAG_COMMENT"; var rule10 = { meta: { type: "suggestion", hasSuggestions: true, schema: [], messages: { [RULE_ID6]: 'Unexpected GraphQL descriptions as hashtag `#` for {{ nodeName }}.\nPrefer using `"""` for multiline, or `"` for a single line description.' }, docs: { description: 'Requires to use `"""` or `"` for adding a GraphQL description instead of `#`.\nAllows to use hashtag for comments, as long as it\'s not attached to an AST definition.', category: "Schema", url: "https://the-guild.dev/graphql/eslint/rules/no-hashtag-description", examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` # Represents a user type User { id: ID! name: String } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` " Represents a user " type User { id: ID! name: String } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # This file defines the basic User type. # This comment is valid because it's not attached specifically to an AST object. " Represents a user " type User { id: ID! # This one is also valid, since it comes after the AST object name: String } ` ) } ], recommended: true } }, create(context) { const selector = "Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]"; return { [selector](node) { const rawNode = node.rawNode(); let token = rawNode.loc.startToken; while (token) { const { kind, prev, next, value, line, column } = token; if (kind === TokenKind2.COMMENT && prev && next) { const isEslintComment = value.trimStart().startsWith("eslint"); const linesAfter = next.line - line; if (!isEslintComment && line !== prev.line && next.kind === TokenKind2.NAME && linesAfter < 2) { const sourceCode = context.getSourceCode(); const { tokens } = sourceCode.ast; const t = tokens.find( (token2) => token2.loc.start.line === next.line && token2.loc.start.column === next.column - 1 ); const nextNode = sourceCode.getNodeByRangeIndex(t.range[1] + 1); context.report({ messageId: RULE_ID6, data: { nodeName: getNodeName( "name" in nextNode ? nextNode : nextNode.parent ) }, loc: { line, column: column - 1 }, suggest: ['"""', '"'].map((descriptionSyntax) => ({ desc: `Replace with \`${descriptionSyntax}\` description syntax`, fix: (fixer) => fixer.replaceTextRange( [token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join("") ) })) }); } } if (!next) { break; } token = next; } } }; } }; // src/rules/no-one-place-fragments/index.ts import { relative } from "node:path"; import { visit as visit4 } from "graphql"; var RULE_ID7 = "no-one-place-fragments"; var rule11 = { meta: { type: "suggestion", docs: { category: "Operations", description: "Disallow fragments that are used only in one place.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID7}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` fragment UserFields on User { id } { user { ...UserFields } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` fragment UserFields on User { id } { user { ...UserFields friends { ...UserFields } } } ` ) } ], requiresSiblings: true }, messages: { [RULE_ID7]: 'Fragment `{{fragmentName}}` used only once. Inline him in "{{filePath}}".' }, schema: [] }, create(context) { const operations = requireGraphQLOperations(RULE_ID7, context); const allDocuments = [...operations.getOperations(), ...operations.getFragments()]; const usedFragmentsMap = /* @__PURE__ */ Object.create(null); for (const { document, filePath } of allDocuments) { const relativeFilePath = relative(CWD, filePath); visit4(document, { FragmentSpread({ name }) { const spreadName = name.value; usedFragmentsMap[spreadName] ||= []; usedFragmentsMap[spreadName].push(relativeFilePath); } }); } return { "FragmentDefinition > Name"(node) { const fragmentName = node.value; const fragmentUsage = usedFragmentsMap[fragmentName]; if (fragmentUsage.length === 1) { context.report({ node, messageId: RULE_ID7, data: { fragmentName, filePath: fragmentUsage[0] } }); } } }; } }; // src/rules/no-root-type/index.ts var schema7 = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, required: ["disallow"], properties: { disallow: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: ["mutation", "subscription"] } } } } }; var rule12 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Schema", description: "Disallow using root types `mutation` and/or `subscription`.", url: "https://the-guild.dev/graphql/eslint/rules/no-root-type", requiresSchema: true, examples: [ { title: "Incorrect", usage: [{ disallow: ["mutation", "subscription"] }], code: ( /* GraphQL */ ` type Mutation { createUser(input: CreateUserInput!): User! } ` ) }, { title: "Correct", usage: [{ disallow: ["mutation", "subscription"] }], code: ( /* GraphQL */ ` type Query { users: [User!]! } ` ) } ], configOptions: [{ disallow: ["mutation", "subscription"] }] }, schema: schema7 }, create(context) { const schema16 = requireGraphQLSchema("no-root-type", context); const disallow = new Set(context.options[0].disallow); const rootTypeNames = [ disallow.has("mutation") && schema16.getMutationType(), disallow.has("subscription") && schema16.getSubscriptionType() ].filter((v) => !!v).map((type) => type.name).join("|"); if (!rootTypeNames) { return {}; } const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension) > .name[value=/^(${rootTypeNames})$/]`; return { [selector](node) { const typeName = node.value; context.report({ node, message: `Root type \`${typeName}\` is forbidden.`, suggest: [ { desc: `Remove \`${typeName}\` type`, fix: (fixer) => fixer.remove(node.parent) } ] }); } }; } }; // src/rules/no-scalar-result-type-on-mutation/index.ts import { isScalarType, Kind as Kind11 } from "graphql"; var RULE_ID8 = "no-scalar-result-type-on-mutation"; var rule13 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Schema", description: "Avoid scalar result type on mutation type to make sure to return a valid state.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID8}`, requiresSchema: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type Mutation { createUser: Boolean } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type Mutation { createUser: User! } ` ) } ] }, schema: [] }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID8, context); const mutationType = schema16.getMutationType(); if (!mutationType) { return {}; } const selector = [ `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`, "> FieldDefinition > .gqlType Name" ].join(" "); return { [selector](node) { const typeName = node.value; const graphQLType = schema16.getType(typeName); if (isScalarType(graphQLType)) { let fieldDef = node.parent; while (fieldDef.kind !== Kind11.FIELD_DEFINITION) { fieldDef = fieldDef.parent; } context.report({ node, message: `Unexpected scalar result type \`${typeName}\` for ${getNodeName(fieldDef)}`, suggest: [ { desc: `Remove \`${typeName}\``, fix: (fixer) => fixer.remove(node) } ] }); } } }; } }; // src/rules/no-typename-prefix/index.ts var NO_TYPENAME_PREFIX = "NO_TYPENAME_PREFIX"; var rule14 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Schema", description: "Enforces users to avoid using the type name in a field name while defining your schema.", recommended: true, url: "https://the-guild.dev/graphql/eslint/rules/no-typename-prefix", examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { userId: ID! } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { id: ID! } ` ) } ] }, messages: { [NO_TYPENAME_PREFIX]: 'Field "{{ fieldName }}" starts with the name of the parent type "{{ typeName }}"' }, schema: [] }, create(context) { return { "ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension"(node) { const typeName = node.name.value; const lowerTypeName = typeName.toLowerCase(); for (const field of node.fields || []) { const fieldName = field.name.value; if (fieldName.toLowerCase().startsWith(lowerTypeName)) { context.report({ data: { fieldName, typeName }, messageId: NO_TYPENAME_PREFIX, node: field.name, suggest: [ { desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`, fix: (fixer) => fixer.replaceText( field.name, fieldName.replace(new RegExp(`^${typeName}`, "i"), "") ) } ] }); } } } }; } }; // src/rules/no-unreachable-types/index.ts import { DirectiveLocation, getNamedType, isInterfaceType, Kind as Kind12, visit as visit5 } from "graphql"; import lowerCase3 from "lodash.lowercase"; var RULE_ID9 = "no-unreachable-types"; var KINDS = [ Kind12.DIRECTIVE_DEFINITION, Kind12.OBJECT_TYPE_DEFINITION, Kind12.OBJECT_TYPE_EXTENSION, Kind12.INTERFACE_TYPE_DEFINITION, Kind12.INTERFACE_TYPE_EXTENSION, Kind12.SCALAR_TYPE_DEFINITION, Kind12.SCALAR_TYPE_EXTENSION, Kind12.INPUT_OBJECT_TYPE_DEFINITION, Kind12.INPUT_OBJECT_TYPE_EXTENSION, Kind12.UNION_TYPE_DEFINITION, Kind12.UNION_TYPE_EXTENSION, Kind12.ENUM_TYPE_DEFINITION, Kind12.ENUM_TYPE_EXTENSION ]; var reachableTypesCache = new ModuleCache(); var RequestDirectiveLocations = /* @__PURE__ */ new Set([ DirectiveLocation.QUERY, DirectiveLocation.MUTATION, DirectiveLocation.SUBSCRIPTION, DirectiveLocation.FIELD, DirectiveLocation.FRAGMENT_DEFINITION, DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, DirectiveLocation.VARIABLE_DEFINITION ]); function getReachableTypes(schema16) { const cachedValue = reachableTypesCache.get(schema16); if (cachedValue) { return cachedValue; } const reachableTypes = /* @__PURE__ */ new Set(); const collect = (node) => { const typeName = getTypeName(node); if (reachableTypes.has(typeName)) { return; } reachableTypes.add(typeName); const type = schema16.getType(typeName) || schema16.getDirective(typeName); if (isInterfaceType(type)) { const { objects, interfaces } = schema16.getImplementations(type); for (const { astNode } of [...objects, ...interfaces]) { visit5(astNode, visitor); } } else if (type?.astNode) { visit5(type.astNode, visitor); } }; const visitor = { InterfaceTypeDefinition: collect, ObjectTypeDefinition: collect, InputValueDefinition: collect, UnionTypeDefinition: collect, FieldDefinition: collect, Directive: collect, NamedType: collect }; for (const type of [ schema16, // visiting SchemaDefinition node schema16.getQueryType(), schema16.getMutationType(), schema16.getSubscriptionType() ]) { if (type?.astNode) { visit5(type.astNode, visitor); } } for (const node of schema16.getDirectives()) { if (node.locations.some((location) => RequestDirectiveLocations.has(location))) { reachableTypes.add(node.name); for (const arg of node.args) { reachableTypes.add(getNamedType(arg.type).name); } } } reachableTypesCache.set(schema16, reachableTypes); return reachableTypes; } var rule15 = { meta: { messages: { [RULE_ID9]: "{{ type }} `{{ typeName }}` is unreachable." }, docs: { description: "Requires all types to be reachable at some level by root level fields.", category: "Schema", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID9}`, requiresSchema: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { id: ID! name: String } type Query { me: String } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { id: ID! name: String } type Query { me: User } ` ) } ], recommended: true }, type: "suggestion", schema: [], hasSuggestions: true }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID9, context); const reachableTypes = getReachableTypes(schema16); return { [`:matches(${KINDS}) > .name`](node) { const typeName = node.value; if (!reachableTypes.has(typeName)) { const type = lowerCase3(node.parent.kind.replace(/(Extension|Definition)$/, "")); context.report({ node, messageId: RULE_ID9, data: { type: type[0].toUpperCase() + type.slice(1), typeName }, suggest: [ { desc: `Remove \`${typeName}\``, fix: (fixer) => fixer.remove(node.parent) } ] }); } } }; } }; // src/rules/no-unused-fields/index.ts import { TypeInfo as TypeInfo2, visit as visit6, visitWithTypeInfo as visitWithTypeInfo2 } from "graphql"; var RULE_ID10 = "no-unused-fields"; var RELAY_SCHEMA = ( /* GraphQL */ ` # Root Query Type type Query { user: User } # User Type type User { id: ID! name: String! friends(first: Int, after: String): FriendConnection! } # FriendConnection Type (Relay Connection) type FriendConnection { edges: [FriendEdge] pageInfo: PageInfo! } # FriendEdge Type type FriendEdge { cursor: String! node: Friend! } # Friend Type type Friend { id: ID! name: String! } # PageInfo Type (Relay Pagination) type PageInfo { hasPreviousPage: Boolean! hasNextPage: Boolean! startCursor: String endCursor: String } ` ); var RELAY_QUERY = ( /* GraphQL */ ` query { user { id name friends(first: 10) { edges { node { id name } } } } } ` ); var RELAY_DEFAULT_IGNORED_FIELD_SELECTORS = [ "[parent.name.value=PageInfo][name.value=/(endCursor|startCursor|hasNextPage|hasPreviousPage)/]", "[parent.name.value=/Edge$/][name.value=cursor]", "[parent.name.value=/Connection$/][name.value=pageInfo]" ]; var schema8 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { ignoredFieldSelectors: { type: "array", uniqueItems: true, minItems: 1, description: [ "Fields that will be ignored and are allowed to be unused.", "", "E.g. The following selector will ignore all the relay pagination fields for every connection exposed in the schema:", "```json", JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2), "```", eslintSelectorsTip ].join("\n"), items: { type: "string", pattern: "^\\[(.+)]$" } } } } }; var usedFieldsCache = new ModuleCache(); function getUsedFields(schema16, operations) { const cachedValue = usedFieldsCache.get(schema16); if (cachedValue) { return cachedValue; } const usedFields = /* @__PURE__ */ Object.create(null); const typeInfo = new TypeInfo2(schema16); const visitor = visitWithTypeInfo2(typeInfo, { Field(node) { const fieldDef = typeInfo.getFieldDef(); if (!fieldDef) { return false; } const parentTypeName = typeInfo.getParentType().name; const fieldName = node.name.value; usedFields[parentTypeName] ??= /* @__PURE__ */ new Set(); usedFields[parentTypeName].add(fieldName); } }); const allDocuments = [...operations.getOperations(), ...operations.getFragments()]; for (const { document } of allDocuments) { visit6(document, visitor); } usedFieldsCache.set(schema16, usedFields); return usedFields; } var rule16 = { meta: { messages: { [RULE_ID10]: 'Field "{{fieldName}}" is unused' }, docs: { description: "Requires all fields to be used at some level by siblings operations.", category: "Schema", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID10}`, requiresSiblings: true, requiresSchema: true, // Requires documents to be set isDisabledForAllConfig: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { id: ID! name: String someUnusedField: String } type Query { me: User } query { me { id name } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { id: ID! name: String } type Query { me: User } query { me { id name } } ` ) }, { title: "Correct (ignoring fields)", usage: [{ ignoredFieldSelectors: RELAY_DEFAULT_IGNORED_FIELD_SELECTORS }], code: ( /* GraphQL */ ` ### 1\uFE0F\u20E3 YOUR SCHEMA ${RELAY_SCHEMA} ### 2\uFE0F\u20E3 YOUR QUERY ${RELAY_QUERY} ` ) } ] }, type: "suggestion", schema: schema8, hasSuggestions: true }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID10, context); const siblingsOperations = requireGraphQLOperations(RULE_ID10, context); const usedFields = getUsedFields(schema16, siblingsOperations); const { ignoredFieldSelectors } = context.options[0] || {}; const selector = (ignoredFieldSelectors || []).reduce( (acc, selector2) => `${acc}:not(${selector2})`, "FieldDefinition" ); return { [selector](node) { const fieldName = node.name.value; const parentTypeName = node.parent.name.value; const isUsed = usedFields[parentTypeName]?.has(fieldName); if (isUsed) { return; } context.report({ node: node.name, messageId: RULE_ID10, data: { fieldName }, suggest: [ { desc: `Remove \`${fieldName}\` field`, fix(fixer) { const sourceCode = context.getSourceCode(); const tokenBefore = sourceCode.getTokenBefore(node); const tokenAfter = sourceCode.getTokenAfter(node); const isEmptyType = tokenBefore.type === "{" && tokenAfter.type === "}"; return fixer.remove(isEmptyType ? node.parent : node); } } ] }); } }; } }; // src/rules/relay-arguments/index.ts import { isScalarType as isScalarType2, Kind as Kind13 } from "graphql"; var RULE_ID11 = "relay-arguments"; var MISSING_ARGUMENTS = "MISSING_ARGUMENTS"; var schema9 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { includeBoth: { type: "boolean", default: true, description: "Enforce including both forward and backward pagination arguments" } } } }; var rule17 = { meta: { type: "problem", docs: { category: "Schema", description: [ "Set of rules to follow Relay specification for Arguments.", "", "- A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both", "", "Forward pagination arguments", "", "- `first` takes a non-negative integer", "- `after` takes the Cursor type", "", "Backward pagination arguments", "", "- `last` takes a non-negative integer", "- `before` takes the Cursor type" ].join("\n"), url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID11}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { posts: PostConnection } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { posts(after: String, first: Int, before: String, last: Int): PostConnection } ` ) } ], isDisabledForAllConfig: true }, messages: { [MISSING_ARGUMENTS]: "A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both." }, schema: schema9 }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID11, context); const { includeBoth = true } = context.options[0] || {}; return { "FieldDefinition > .gqlType Name[value=/Connection$/]"(node) { let fieldNode = node.parent; while (fieldNode.kind !== Kind13.FIELD_DEFINITION) { fieldNode = fieldNode.parent; } const args = Object.fromEntries( fieldNode.arguments?.map((argument) => [argument.name.value, argument]) || [] ); const hasForwardPagination = !!(args.first && args.after); const hasBackwardPagination = !!(args.last && args.before); if (!hasForwardPagination && !hasBackwardPagination) { context.report({ node: fieldNode.name, messageId: MISSING_ARGUMENTS }); return; } function checkField(typeName, argumentName) { const argument = args[argumentName]; const hasArgument = !!argument; let type = argument; if (hasArgument && type.gqlType.kind === Kind13.NON_NULL_TYPE) { type = type.gqlType; } const isAllowedNonNullType = hasArgument && type.gqlType.kind === Kind13.NAMED_TYPE && (type.gqlType.name.value === typeName || typeName === "String" && isScalarType2(schema16.getType(type.gqlType.name.value))); if (!isAllowedNonNullType) { const returnType = typeName === "String" ? "String or Scalar" : typeName; context.report({ node: (argument || fieldNode).name, message: hasArgument ? `Argument \`${argumentName}\` must return ${returnType}.` : `Field \`${fieldNode.name.value}\` must contain an argument \`${argumentName}\`, that return ${returnType}.` }); } } if (includeBoth || args.first || args.after) { checkField("Int", "first"); checkField("String", "after"); } if (includeBoth || args.last || args.before) { checkField("Int", "last"); checkField("String", "before"); } } }; } }; // src/rules/relay-connection-types/index.ts import { Kind as Kind14 } from "graphql"; var MUST_BE_OBJECT_TYPE = "MUST_BE_OBJECT_TYPE"; var MUST_CONTAIN_FIELD_EDGES = "MUST_CONTAIN_FIELD_EDGES"; var MUST_CONTAIN_FIELD_PAGE_INFO = "MUST_CONTAIN_FIELD_PAGE_INFO"; var MUST_HAVE_CONNECTION_SUFFIX = "MUST_HAVE_CONNECTION_SUFFIX"; var EDGES_FIELD_MUST_RETURN_LIST_TYPE = "EDGES_FIELD_MUST_RETURN_LIST_TYPE"; var PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE = "PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE"; var NON_OBJECT_TYPES = [ Kind14.SCALAR_TYPE_DEFINITION, Kind14.UNION_TYPE_DEFINITION, Kind14.UNION_TYPE_EXTENSION, Kind14.INPUT_OBJECT_TYPE_DEFINITION, Kind14.INPUT_OBJECT_TYPE_EXTENSION, Kind14.ENUM_TYPE_DEFINITION, Kind14.ENUM_TYPE_EXTENSION, Kind14.INTERFACE_TYPE_DEFINITION, Kind14.INTERFACE_TYPE_EXTENSION ]; var notConnectionTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=/Connection$/] > .name`; var hasEdgesField = (node) => node.fields?.some((field) => field.name.value === "edges"); var hasPageInfoField = (node) => node.fields?.some((field) => field.name.value === "pageInfo"); var rule18 = { meta: { type: "problem", docs: { category: "Schema", description: [ "Set of rules to follow Relay specification for Connection types.", "", '- Any type whose name ends in "Connection" is considered by spec to be a `Connection type`', "- Connection type must be an Object type", "- Connection type must contain a field `edges` that return a list type that wraps an edge type", "- Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type" ].join("\n"), url: "https://the-guild.dev/graphql/eslint/rules/relay-connection-types", isDisabledForAllConfig: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type UserPayload { # should be an Object type with \`Connection\` suffix edges: UserEdge! # should return a list type pageInfo: PageInfo # should return a non-null \`PageInfo\` Object type } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type UserConnection { edges: [UserEdge] pageInfo: PageInfo! } ` ) } ] }, messages: { // Connection types [MUST_BE_OBJECT_TYPE]: "Connection type must be an Object type.", [MUST_HAVE_CONNECTION_SUFFIX]: "Connection type must have `Connection` suffix.", [MUST_CONTAIN_FIELD_EDGES]: "Connection type must contain a field `edges` that return a list type.", [MUST_CONTAIN_FIELD_PAGE_INFO]: "Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type.", [EDGES_FIELD_MUST_RETURN_LIST_TYPE]: "`edges` field must return a list type.", [PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE]: "`pageInfo` field must return a non-null `PageInfo` Object type." }, schema: [] }, create(context) { return { [notConnectionTypesSelector](node) { context.report({ node, messageId: MUST_BE_OBJECT_TYPE }); }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value!=/Connection$/]"(node) { if (hasEdgesField(node) && hasPageInfoField(node)) { context.report({ node: node.name, messageId: MUST_HAVE_CONNECTION_SUFFIX }); } }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/]"(node) { if (!hasEdgesField(node)) { context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_EDGES }); } if (!hasPageInfoField(node)) { context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_PAGE_INFO }); } }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType"(node) { const isListType2 = node.kind === Kind14.LIST_TYPE || node.kind === Kind14.NON_NULL_TYPE && node.gqlType.kind === Kind14.LIST_TYPE; if (!isListType2) { context.report({ node, messageId: EDGES_FIELD_MUST_RETURN_LIST_TYPE }); } }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=pageInfo] > .gqlType"(node) { const isNonNullPageInfoType = node.kind === Kind14.NON_NULL_TYPE && node.gqlType.kind === Kind14.NAMED_TYPE && node.gqlType.name.value === "PageInfo"; if (!isNonNullPageInfoType) { context.report({ node, messageId: PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE }); } } }; } }; // src/rules/relay-edge-types/index.ts import { isObjectType as isObjectType2, isScalarType as isScalarType3, Kind as Kind15, visit as visit7 } from "graphql"; import { getDocumentNodeFromSchema } from "@graphql-tools/utils"; var RULE_ID12 = "relay-edge-types"; var MESSAGE_MUST_BE_OBJECT_TYPE = "MESSAGE_MUST_BE_OBJECT_TYPE"; var MESSAGE_MISSING_EDGE_SUFFIX = "MESSAGE_MISSING_EDGE_SUFFIX"; var MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE = "MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE"; var MESSAGE_SHOULD_IMPLEMENTS_NODE = "MESSAGE_SHOULD_IMPLEMENTS_NODE"; var edgeTypesCache; function getEdgeTypes(schema16) { if (edgeTypesCache) { return edgeTypesCache; } const edgeTypes = /* @__PURE__ */ new Set(); const visitor = { ObjectTypeDefinition(node) { const typeName = node.name.value; const hasConnectionSuffix = typeName.endsWith("Connection"); if (!hasConnectionSuffix) { return; } const edges = node.fields?.find((field) => field.name.value === "edges"); if (edges) { const edgesTypeName = getTypeName(edges); const edgesType = schema16.getType(edgesTypeName); if (isObjectType2(edgesType)) { edgeTypes.add(edgesTypeName); } } } }; const astNode = getDocumentNodeFromSchema(schema16); visit7(astNode, visitor); edgeTypesCache = edgeTypes; return edgeTypesCache; } var schema10 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { withEdgeSuffix: { type: "boolean", default: true, description: 'Edge type name must end in "Edge".' }, shouldImplementNode: { type: "boolean", default: true, description: "Edge type's field `node` must implement `Node` interface." }, listTypeCanWrapOnlyEdgeType: { type: "boolean", default: true, description: "A list type should only wrap an edge type." } } } }; var rule19 = { meta: { type: "problem", docs: { category: "Schema", description: [ "Set of rules to follow Relay specification for Edge types.", "", "- A type that is returned in list form by a connection type's `edges` field is considered by this spec to be an Edge type", "- Edge type must be an Object type", "- Edge type must contain a field `node` that return either Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types. Notably, this field cannot return a list", "- Edge type must contain a field `cursor` that return either String, Scalar, or a non-null wrapper around one of those types", '- Edge type name must end in "Edge" _(optional)_', "- Edge type's field `node` must implement `Node` interface _(optional)_", "- A list type should only wrap an edge type _(optional)_" ].join("\n"), url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID12}`, isDisabledForAllConfig: true, requiresSchema: true, examples: [ { title: "Correct", code: ( /* GraphQL */ ` type UserConnection { edges: [UserEdge] pageInfo: PageInfo! } ` ) } ] }, messages: { [MESSAGE_MUST_BE_OBJECT_TYPE]: "Edge type must be an Object type.", [MESSAGE_MISSING_EDGE_SUFFIX]: 'Edge type must have "Edge" suffix.', [MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE]: "A list type should only wrap an edge type.", [MESSAGE_SHOULD_IMPLEMENTS_NODE]: "Edge type's field `node` must implement `Node` interface." }, schema: schema10 }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID12, context); const edgeTypes = getEdgeTypes(schema16); const options = { withEdgeSuffix: true, shouldImplementNode: true, listTypeCanWrapOnlyEdgeType: true, ...context.options[0] }; const isNamedOrNonNullNamed = (node) => node.kind === Kind15.NAMED_TYPE || node.kind === Kind15.NON_NULL_TYPE && node.gqlType.kind === Kind15.NAMED_TYPE; const checkNodeField = (node) => { const nodeField = node.fields?.find((field) => field.name.value === "node"); const message = "return either a Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types."; if (!nodeField) { context.report({ node: node.name, message: `Edge type must contain a field \`node\` that ${message}` }); } else if (!isNamedOrNonNullNamed(nodeField.gqlType)) { context.report({ node: nodeField.name, message: `Field \`node\` must ${message}` }); } else if (options.shouldImplementNode) { const nodeReturnTypeName = getTypeName(nodeField.gqlType.rawNode()); const type = schema16.getType(nodeReturnTypeName); if (!isObjectType2(type)) { return; } const implementsNode = type.astNode.interfaces?.some((n) => n.name.value === "Node"); if (!implementsNode) { context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE }); } } }; const checkCursorField = (node) => { const cursorField = node.fields?.find((field) => field.name.value === "cursor"); const message = "return either a String, Scalar, or a non-null wrapper wrapper around one of those types."; if (!cursorField) { context.report({ node: node.name, message: `Edge type must contain a field \`cursor\` that ${message}` }); return; } const typeName = getTypeName(cursorField.rawNode()); if (!isNamedOrNonNullNamed(cursorField.gqlType) || typeName !== "String" && !isScalarType3(schema16.getType(typeName))) { context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` }); } }; const listeners = { ":matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name"(node) { const type = schema16.getType(node.value); if (!isObjectType2(type)) { context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE }); } }, ":matches(ObjectTypeDefinition, ObjectTypeExtension)"(node) { const typeName = node.name.value; if (edgeTypes.has(typeName)) { checkNodeField(node); checkCursorField(node); if (options.withEdgeSuffix && !typeName.endsWith("Edge")) { context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX }); } } } }; if (options.listTypeCanWrapOnlyEdgeType) { listeners["FieldDefinition > .gqlType"] = (node) => { if (node.kind === Kind15.LIST_TYPE || node.kind === Kind15.NON_NULL_TYPE && node.gqlType.kind === Kind15.LIST_TYPE) { const typeName = getTypeName(node.rawNode()); if (!edgeTypes.has(typeName)) { context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE }); } } }; } return listeners; } }; // src/rules/relay-page-info/index.ts import { isScalarType as isScalarType4, Kind as Kind16 } from "graphql"; var RULE_ID13 = "relay-page-info"; var MESSAGE_MUST_EXIST = "MESSAGE_MUST_EXIST"; var MESSAGE_MUST_BE_OBJECT_TYPE2 = "MESSAGE_MUST_BE_OBJECT_TYPE"; var notPageInfoTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=PageInfo] > .name`; var hasPageInfoChecked = false; var rule20 = { meta: { type: "problem", docs: { category: "Schema", description: [ "Set of rules to follow Relay specification for `PageInfo` object.", "", "- `PageInfo` must be an Object type", "- `PageInfo` must contain fields `hasPreviousPage` and `hasNextPage`, that return non-null Boolean", "- `PageInfo` must contain fields `startCursor` and `endCursor`, that return either String or Scalar, which can be null if there are no results" ].join("\n"), url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID13}`, examples: [ { title: "Correct", code: ( /* GraphQL */ ` type PageInfo { hasPreviousPage: Boolean! hasNextPage: Boolean! startCursor: String endCursor: String } ` ) } ], isDisabledForAllConfig: true, requiresSchema: true }, messages: { [MESSAGE_MUST_EXIST]: "The server must provide a `PageInfo` object.", [MESSAGE_MUST_BE_OBJECT_TYPE2]: "`PageInfo` must be an Object type." }, schema: [] }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID13, context); if (!hasPageInfoChecked) { const pageInfoType = schema16.getType("PageInfo"); if (!pageInfoType) { context.report({ loc: REPORT_ON_FIRST_CHARACTER, messageId: MESSAGE_MUST_EXIST }); } hasPageInfoChecked = true; } return { [notPageInfoTypesSelector](node) { context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE2 }); }, "ObjectTypeDefinition[name.value=PageInfo]"(node) { const fieldMap = Object.fromEntries( node.fields?.map((field) => [field.name.value, field]) || [] ); const checkField = (fieldName, typeName) => { const field = fieldMap[fieldName]; let isAllowedType = false; if (field) { const type = field.gqlType; if (typeName === "Boolean") { isAllowedType = type.kind === Kind16.NON_NULL_TYPE && type.gqlType.kind === Kind16.NAMED_TYPE && type.gqlType.name.value === "Boolean"; } else if (type.kind === Kind16.NAMED_TYPE) { isAllowedType = type.name.value === "String" || isScalarType4(schema16.getType(type.name.value)); } } if (!isAllowedType) { const returnType = typeName === "Boolean" ? "non-null Boolean" : "either String or Scalar, which can be null if there are no results"; context.report({ node: field ? field.name : node.name, message: field ? `Field \`${fieldName}\` must return ${returnType}.` : `\`PageInfo\` must contain a field \`${fieldName}\`, that return ${returnType}.` }); } }; checkField("hasPreviousPage", "Boolean"); checkField("hasNextPage", "Boolean"); checkField("startCursor", "String"); checkField("endCursor", "String"); } }; } }; // src/rules/require-deprecation-date/index.ts var DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/; var MESSAGE_REQUIRE_DATE = "MESSAGE_REQUIRE_DATE"; var MESSAGE_INVALID_FORMAT = "MESSAGE_INVALID_FORMAT"; var MESSAGE_INVALID_DATE = "MESSAGE_INVALID_DATE"; var MESSAGE_CAN_BE_REMOVED = "MESSAGE_CAN_BE_REMOVED"; var schema11 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { argumentName: { type: "string" } } } }; var rule21 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Schema", description: "Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.", url: "https://the-guild.dev/graphql/eslint/rules/require-deprecation-date", examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { firstname: String @deprecated firstName: String } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` type User { firstname: String @deprecated(reason: "Use 'firstName' instead") firstName: String } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { firstname: String @deprecated(reason: "Use 'firstName' instead", deletionDate: "25/12/2022") firstName: String } ` ) } ] }, messages: { [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date for {{ nodeName }}', [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY" for {{ nodeName }}', [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date for {{ nodeName }}', [MESSAGE_CAN_BE_REMOVED]: "{{ nodeName }} \u0441an be removed" }, schema: schema11 }, create(context) { return { "Directive[name.value=deprecated]"(node) { const argName = context.options[0]?.argumentName || "deletionDate"; const deletionDateNode = node.arguments?.find((arg) => arg.name.value === argName); if (!deletionDateNode) { context.report({ node: node.name, messageId: MESSAGE_REQUIRE_DATE, data: { nodeName: getNodeName(node.parent) } }); return; } const deletionDate = valueFromNode(deletionDateNode.value); const isValidDate = DATE_REGEX.test(deletionDate); if (!isValidDate) { context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT, data: { nodeName: getNodeName(node.parent) } }); return; } let [day, month, year] = deletionDate.split("/"); day = day.padStart(2, "0"); month = month.padStart(2, "0"); const deletionDateInMS = Date.parse(`${year}-${month}-${day}`); if (Number.isNaN(deletionDateInMS)) { context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_DATE, data: { deletionDate, nodeName: getNodeName(node.parent) } }); return; } const canRemove = Date.now() > deletionDateInMS; if (canRemove) { const { parent } = node; const nodeName = parent.name.value; context.report({ node: parent.name, messageId: MESSAGE_CAN_BE_REMOVED, data: { nodeName: getNodeName(parent) }, suggest: [ { desc: `Remove \`${nodeName}\``, fix: (fixer) => fixer.remove(parent) } ] }); } } }; } }; // src/rules/require-deprecation-reason/index.ts var rule22 = { meta: { docs: { description: "Require all deprecation directives to specify a reason.", category: "Schema", url: "https://the-guild.dev/graphql/eslint/rules/require-deprecation-reason", recommended: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type MyType { name: String @deprecated } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` type MyType { name: String @deprecated(reason: "") } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type MyType { name: String @deprecated(reason: "no longer relevant, please use fullName field") } ` ) } ] }, type: "suggestion", schema: [] }, create(context) { return { "Directive[name.value=deprecated]"(node) { const reasonArgument = node.arguments?.find( (arg) => arg.name.value === "reason" ); const value = reasonArgument && String(valueFromNode(reasonArgument.value)).trim(); if (!value) { context.report({ node: node.name, message: `Deprecation reason is required for ${getNodeName(node.parent)}.` }); } } }; } }; // src/rules/require-description/index.ts import { Kind as Kind17, TokenKind as TokenKind3 } from "graphql"; import { getRootTypeNames } from "@graphql-tools/utils"; var RULE_ID14 = "require-description"; var ALLOWED_KINDS2 = [ ...TYPES_KINDS, Kind17.DIRECTIVE_DEFINITION, Kind17.FIELD_DEFINITION, Kind17.INPUT_VALUE_DEFINITION, Kind17.ENUM_VALUE_DEFINITION, Kind17.OPERATION_DEFINITION ]; var entries = /* @__PURE__ */ Object.create(null); for (const kind of [...ALLOWED_KINDS2].sort()) { let description = `> [!NOTE] > > Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`; if (kind === Kind17.OPERATION_DEFINITION) { description += [ "", "", "> [!WARNING]", ">", '> You must use only comment syntax `#` and not description syntax `"""` or `"`.' ].join("\n"); } entries[kind] = { type: "boolean", description }; } var schema12 = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { types: { type: "boolean", enum: [true], description: `Includes: ${TYPES_KINDS.map((kind) => `- \`${kind}\``).join("\n")}` }, rootField: { type: "boolean", enum: [true], description: "Definitions within `Query`, `Mutation`, and `Subscription` root types." }, ignoredSelectors: { ...ARRAY_DEFAULT_OPTIONS, description: ["Ignore specific selectors", eslintSelectorsTip].join("\n") }, ...entries } } }; var rule23 = { meta: { docs: { category: "Schema", description: "Enforce descriptions in type definitions and operations.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID14}`, examples: [ { title: "Incorrect", usage: [{ types: true, FieldDefinition: true }], code: ( /* GraphQL */ ` type someTypeName { name: String } ` ) }, { title: "Correct", usage: [{ types: true, FieldDefinition: true }], code: ( /* GraphQL */ ` """ Some type description """ type someTypeName { """ Name description """ name: String } ` ) }, { title: "Correct", usage: [{ OperationDefinition: true }], code: ( /* GraphQL */ ` # Create a new user mutation createUser { # ... } ` ) }, { title: "Correct", usage: [{ rootField: true }], code: ( /* GraphQL */ ` type Mutation { "Create a new user" createUser: User } type User { name: String } ` ) }, { title: "Correct", usage: [ { ignoredSelectors: [ "[type=ObjectTypeDefinition][name.value=PageInfo]", "[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]" ] } ], code: ( /* GraphQL */ ` type FriendConnection { edges: [FriendEdge] pageInfo: PageInfo! } type FriendEdge { cursor: String! node: Friend! } type PageInfo { hasPreviousPage: Boolean! hasNextPage: Boolean! startCursor: String endCursor: String } ` ) } ], configOptions: [ { types: true, [Kind17.DIRECTIVE_DEFINITION]: true, rootField: true } ], recommended: true }, type: "suggestion", messages: { [RULE_ID14]: "Description is required for {{ nodeName }}" }, schema: schema12 }, create(context) { const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {}; const kinds = new Set(types ? TYPES_KINDS : []); for (const [kind, isEnabled] of Object.entries(restOptions)) { if (isEnabled) { kinds.add(kind); } else { kinds.delete(kind); } } if (rootField) { const schema16 = requireGraphQLSchema(RULE_ID14, context); const rootTypeNames = getRootTypeNames(schema16); kinds.add( `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/^(${[ ...rootTypeNames ].join(",")})$/] > FieldDefinition` ); } let selector = `:matches(${[...kinds]})`; for (const str of ignoredSelectors) { selector += `:not(${str})`; } return { [selector](node) { let description = ""; const isOperation = node.kind === Kind17.OPERATION_DEFINITION; if (isOperation) { const rawNode = node.rawNode(); const { prev, line } = rawNode.loc.startToken; if (prev?.kind === TokenKind3.COMMENT) { const value = prev.value.trim(); const linesBefore = line - prev.line; if (!value.startsWith("eslint") && linesBefore === 1) { description = value; } } } else { description = node.description?.value.trim() || ""; } if (description.length === 0) { context.report({ loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc, messageId: RULE_ID14, data: { nodeName: getNodeName(node) } }); } } }; } }; // src/rules/require-field-of-type-query-in-mutation-result/index.ts import { isObjectType as isObjectType3 } from "graphql"; var RULE_ID15 = "require-field-of-type-query-in-mutation-result"; var rule24 = { meta: { type: "suggestion", docs: { category: "Schema", description: "Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.\n> Currently, no errors are reported for result type `union`, `interface` and `scalar`.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID15}`, requiresSchema: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type User { ... } type Mutation { createUser: User! } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type User { ... } type Query { ... } type CreateUserPayload { user: User! query: Query! } type Mutation { createUser: CreateUserPayload! } ` ) } ] }, schema: [] }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID15, context); const mutationType = schema16.getMutationType(); const queryType = schema16.getQueryType(); if (!mutationType || !queryType) { return {}; } const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}] > FieldDefinition > .gqlType Name`; return { [selector](node) { const typeName = node.value; const graphQLType = schema16.getType(typeName); if (isObjectType3(graphQLType)) { const { fields } = graphQLType.astNode; const hasQueryType = fields?.some((field) => getTypeName(field) === queryType.name); if (!hasQueryType) { context.report({ node, message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}"` }); } } } }; } }; // src/rules/require-import-fragment/index.ts import path from "node:path"; var RULE_ID16 = "require-import-fragment"; var SUGGESTION_ID = "add-import-expression"; var rule25 = { meta: { type: "suggestion", docs: { category: "Operations", description: "Require fragments to be imported via an import expression.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID16}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` query { user { ...UserFields } } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` # import 'post-fields.fragment.graphql' query { user { ...UserFields } } ` ) }, { title: "Incorrect", code: ( /* GraphQL */ ` # import UserFields from 'post-fields.fragment.graphql' query { user { ...UserFields } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # import UserFields from 'user-fields.fragment.graphql' query { user { ...UserFields } } ` ) } ], requiresSiblings: true }, hasSuggestions: true, messages: { [RULE_ID16]: 'Expected "{{fragmentName}}" fragment to be imported.', [SUGGESTION_ID]: 'Add import expression for "{{fragmentName}}".' }, schema: [] }, create(context) { const comments = context.getSourceCode().getAllComments(); const siblings = requireGraphQLOperations(RULE_ID16, context); const filePath = context.filename; return { "FragmentSpread > .name"(node) { const fragmentName = node.value; const fragmentsFromSiblings = siblings.getFragment(fragmentName); for (const comment of comments) { if (comment.type !== "Line") continue; const isPossibleImported = new RegExp( `^\\s*import\\s+(${fragmentName}\\s+from\\s+)?['"]` ).test(comment.value); if (!isPossibleImported) continue; const extractedImportPath = comment.value.match(/(["'])((?:\1|.)*?)\1/)?.[2]; if (!extractedImportPath) continue; const importPath = path.join(filePath, "..", extractedImportPath); const hasInSiblings = fragmentsFromSiblings.some( (source) => source.filePath === importPath ); if (hasInSiblings) return; } const fragmentInSameFile = fragmentsFromSiblings.some( (source) => source.filePath === filePath ); if (fragmentInSameFile) return; const suggestedFilePaths = fragmentsFromSiblings.length ? fragmentsFromSiblings.map( (o) => ( // Use always forward slash for suggested import path slash(path.relative(path.dirname(filePath), o.filePath)) ) ) : ["CHANGE_ME.graphql"]; context.report({ node, messageId: RULE_ID16, data: { fragmentName }, suggest: suggestedFilePaths.map((suggestedPath) => ({ messageId: SUGGESTION_ID, data: { fragmentName }, fix: (fixer) => fixer.insertTextBeforeRange( [0, 0], `# import ${fragmentName} from '${suggestedPath}' ` ) })) }); } }; } }; // src/rules/require-nullable-fields-with-oneof/index.ts import { Kind as Kind18 } from "graphql"; var RULE_ID17 = "require-nullable-fields-with-oneof"; var rule26 = { meta: { type: "suggestion", docs: { category: "Schema", description: "Require `input` or `type` fields to be non-nullable with `@oneOf` directive.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID17}`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` input Input @oneOf { foo: String! b: Int } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` input Input @oneOf { foo: String bar: Int } ` ) } ] }, messages: { [RULE_ID17]: '{{ nodeName }} must be nullable when "@oneOf" is in use' }, schema: [] }, create(context) { return { "Directive[name.value=oneOf]"({ parent }) { const isTypeOrInput = [ Kind18.OBJECT_TYPE_DEFINITION, Kind18.INPUT_OBJECT_TYPE_DEFINITION ].includes(parent.kind); if (!isTypeOrInput) { return; } for (const field of parent.fields || []) { if (field.gqlType.kind === Kind18.NON_NULL_TYPE) { context.report({ node: field.name, messageId: RULE_ID17, data: { nodeName: getNodeName(field) } }); } } } }; } }; // src/rules/require-nullable-result-in-root/index.ts import { Kind as Kind19 } from "graphql"; var RULE_ID18 = "require-nullable-result-in-root"; var rule27 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Schema", description: "Require nullable fields in root types.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID18}`, requiresSchema: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` type Query { user: User! } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` type Query { foo: User baz: [User]! bar: [User!]! } ` ) } ] }, messages: { [RULE_ID18]: "Unexpected non-null result {{ resultType }} in {{ rootType }}" }, schema: [] }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID18, context); const rootTypeNames = new Set( [schema16.getQueryType(), schema16.getMutationType()].filter((v) => !!v).map((type) => type.name) ); const sourceCode = context.getSourceCode(); return { "ObjectTypeDefinition,ObjectTypeExtension"(node) { if (!rootTypeNames.has(node.name.value)) return; for (const field of node.fields || []) { if (field.gqlType.type !== Kind19.NON_NULL_TYPE || field.gqlType.gqlType.type !== Kind19.NAMED_TYPE) continue; const name = field.gqlType.gqlType.name.value; const type = schema16.getType(name); const resultType = type?.astNode ? getNodeName(type.astNode) : type?.name; context.report({ node: field.gqlType, messageId: RULE_ID18, data: { resultType: resultType || "", rootType: getNodeName(node) }, suggest: [ { desc: `Make ${resultType} nullable`, fix(fixer) { const text = sourceCode.getText(field.gqlType); return fixer.replaceText(field.gqlType, text.replace("!", "")); } } ] }); } } }; } }; // src/rules/require-selections/index.ts import { GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, Kind as Kind20, TypeInfo as TypeInfo3, visit as visit8, visitWithTypeInfo as visitWithTypeInfo3 } from "graphql"; import { asArray } from "@graphql-tools/utils"; var RULE_ID19 = "require-selections"; var DEFAULT_ID_FIELD_NAME = "id"; var schema13 = { definitions: { asString: { type: "string" }, asArray: ARRAY_DEFAULT_OPTIONS }, type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { fieldName: { oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asArray" }], default: DEFAULT_ID_FIELD_NAME }, requireAllFields: { type: "boolean", description: "Whether all fields of `fieldName` option must be included." } } } }; var rule28 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Operations", description: "Enforce selecting specific fields when they are available on the GraphQL type.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID19}`, requiresSchema: true, requiresSiblings: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` # In your schema type User { id: ID! name: String! } # Query query { user { name } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # In your schema type User { id: ID! name: String! } # Query query { user { id name } } # Selecting \`id\` with an alias is also valid query { user { id: name } } ` ) } ], recommended: true, whenNotToUseIt: "Relay Compiler automatically adds an `id` field to any type that has an `id` field, even if it hasn't been explicitly requested. Requesting a field that is not used directly in the code can conflict with another Relay rule: `relay/unused-fields`." }, messages: { [RULE_ID19]: "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}." }, schema: schema13 }, create(context) { const schema16 = requireGraphQLSchema(RULE_ID19, context); const siblings = requireGraphQLOperations(RULE_ID19, context); const { fieldName = DEFAULT_ID_FIELD_NAME, requireAllFields } = context.options[0] || {}; const idNames = asArray(fieldName); const selector = "SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]"; const typeInfo = new TypeInfo3(schema16); function checkFragments(node) { for (const selection of node.selections) { if (selection.kind !== Kind20.FRAGMENT_SPREAD) { continue; } const [foundSpread] = siblings.getFragment(selection.name.value); if (!foundSpread) { continue; } const checkedFragmentSpreads = /* @__PURE__ */ new Set(); const visitor = visitWithTypeInfo3(typeInfo, { SelectionSet(node2, key, _parent) { const parent = _parent; if (parent.kind === Kind20.FRAGMENT_DEFINITION) { checkedFragmentSpreads.add(parent.name.value); } else if (parent.kind !== Kind20.INLINE_FRAGMENT) { checkSelections( node2, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads ); } } }); visit8(foundSpread.document, visitor); } } function checkSelections(node, type, loc, parent, checkedFragmentSpreads = /* @__PURE__ */ new Set()) { const rawType = getBaseType(type); if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) { checkFields(rawType); } else if (rawType instanceof GraphQLUnionType) { for (const selection of node.selections) { const types = rawType.getTypes(); if (selection.kind === Kind20.INLINE_FRAGMENT) { const t = types.find((t2) => t2.name === selection.typeCondition.name.value); if (t) { checkFields(t); } } else if (selection.kind === Kind20.FRAGMENT_SPREAD) { const [foundSpread] = siblings.getFragment(selection.name.value); if (!foundSpread) return; const fragmentSpread = foundSpread.document; const t = fragmentSpread.typeCondition.name.value === rawType.name ? rawType : types.find((t2) => t2.name === fragmentSpread.typeCondition.name.value); checkedFragmentSpreads.add(fragmentSpread.name.value); checkSelections(fragmentSpread.selectionSet, t, loc, parent, checkedFragmentSpreads); } } } function checkFields(rawType2) { const fields = rawType2.getFields(); const hasIdFieldInType = idNames.some((name) => fields[name]); if (!hasIdFieldInType) { return; } checkFragments(node); if (requireAllFields) { for (const idName of idNames) { report([idName]); } } else { report(idNames); } } function report(idNames2) { function hasIdField({ selections }) { return selections.some((selection) => { if (selection.kind === Kind20.FIELD) { if (selection.alias && idNames2.includes(selection.alias.value)) { return true; } return idNames2.includes(selection.name.value); } if (selection.kind === Kind20.INLINE_FRAGMENT) { return hasIdField(selection.selectionSet); } if (selection.kind === Kind20.FRAGMENT_SPREAD) { const [foundSpread] = siblings.getFragment(selection.name.value); if (foundSpread) { const fragmentSpread = foundSpread.document; checkedFragmentSpreads.add(fragmentSpread.name.value); return hasIdField(fragmentSpread.selectionSet); } } return false; }); } const hasId = hasIdField(node); if (hasId) { return; } const fieldName2 = englishJoinWords( idNames2.map((name) => `\`${(parent.alias || parent.name).value}.${name}\``) ); const pluralSuffix = idNames2.length > 1 ? "s" : ""; const addition = checkedFragmentSpreads.size === 0 ? "" : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? "s" : ""} ${englishJoinWords([...checkedFragmentSpreads].map((name) => `\`${name}\``))}`; const problem = { loc, messageId: RULE_ID19, data: { pluralSuffix, fieldName: fieldName2, addition } }; if ("type" in node) { problem.suggest = idNames2.map((idName) => ({ desc: `Add \`${idName}\` selection`, fix: (fixer) => { let insertNode = node.selections[0]; insertNode = insertNode.kind === Kind20.INLINE_FRAGMENT ? insertNode.selectionSet.selections[0] : insertNode; return fixer.insertTextBefore(insertNode, `${idName} `); } })); } context.report(problem); } } return { [selector](node) { const typeInfo2 = node.typeInfo(); if (typeInfo2.gqlType) { checkSelections(node, typeInfo2.gqlType, node.loc.start, node.parent); } } }; } }; // src/rules/require-type-pattern-with-oneof/index.ts var RULE_ID20 = "require-type-pattern-with-oneof"; var rule29 = { meta: { type: "suggestion", docs: { category: "Schema", description: "Enforce types with `@oneOf` directive have `error` and `ok` fields.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID20}`, examples: [ { title: "Correct", code: ( /* GraphQL */ ` type Mutation { doSomething: DoSomethingMutationResult! } interface Error { message: String! } type DoSomethingMutationResult @oneOf { ok: DoSomethingSuccess error: Error } type DoSomethingSuccess { # ... } ` ) } ] }, messages: { [RULE_ID20]: '{{ nodeName }} is defined as output with "@oneOf" and must be defined with "{{ fieldName }}" field' }, schema: [] }, create(context) { return { "Directive[name.value=oneOf][parent.kind=ObjectTypeDefinition]"({ parent }) { const requiredFields = ["error", "ok"]; for (const fieldName of requiredFields) { if (!parent.fields?.some((field) => field.name.value === fieldName)) { context.report({ node: parent.name, messageId: RULE_ID20, data: { nodeName: displayNodeName(parent), fieldName } }); } } } }; } }; // src/rules/selection-set-depth/index.ts import { Kind as Kind21 } from "graphql"; import depthLimit from "graphql-depth-limit"; var RULE_ID21 = "selection-set-depth"; var schema14 = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, required: ["maxDepth"], properties: { maxDepth: { type: "number" }, ignore: ARRAY_DEFAULT_OPTIONS } } }; var rule30 = { meta: { type: "suggestion", hasSuggestions: true, docs: { category: "Operations", description: "Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://npmjs.com/package/graphql-depth-limit).", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID21}`, requiresSiblings: true, examples: [ { title: "Incorrect", usage: [{ maxDepth: 1 }], code: ` query deep2 { viewer { # Level 0 albums { # Level 1 title # Level 2 } } } ` }, { title: "Correct", usage: [{ maxDepth: 4 }], code: ` query deep2 { viewer { # Level 0 albums { # Level 1 title # Level 2 } } } ` }, { title: "Correct (ignored field)", usage: [{ maxDepth: 1, ignore: ["albums"] }], code: ` query deep2 { viewer { # Level 0 albums { # Level 1 title # Level 2 } } } ` } ], recommended: true, configOptions: [{ maxDepth: 7 }] }, schema: schema14 }, create(context) { let siblings = null; try { siblings = requireGraphQLOperations(RULE_ID21, context); } catch { logger.warn( `Rule "${RULE_ID21}" works best with siblings operations loaded. See https://the-guild.dev/graphql/eslint/docs/usage#providing-operations for more info` ); } const { maxDepth, ignore = [] } = context.options[0]; const checkFn = depthLimit(maxDepth, { ignore }); return { "OperationDefinition, FragmentDefinition"(node) { try { const rawNode = node.rawNode(); const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : []; const document = { kind: Kind21.DOCUMENT, definitions: [rawNode, ...fragmentsInUse] }; checkFn({ getDocument: () => document, reportError(error) { const { line, column } = error.locations[0]; const ancestors = context.sourceCode.getAncestors(node); const token = ancestors[0].tokens.find( (token2) => token2.loc.start.line === line && token2.loc.start.column === column - 1 ); context.report({ loc: { line, column: column - 1 }, message: error.message, // Don't provide suggestions for fragment that can be in a separate file ...token && { suggest: [ { desc: "Remove selections", fix(fixer) { const sourceCode = context.getSourceCode(); const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]); const parentNode = foundNode.parent.parent; return fixer.remove( foundNode.kind === "Name" ? parentNode.parent : parentNode ); } } ] } }); } }); } catch (e) { logger.warn( `Rule "${RULE_ID21}" check failed due to a missing siblings operations. See https://the-guild.dev/graphql/eslint/docs/usage#providing-operations for more info`, e ); } } }; } }; // src/rules/strict-id-in-types/index.ts import { Kind as Kind22 } from "graphql"; var RULE_ID22 = "strict-id-in-types"; var schema15 = { type: "array", maxItems: 1, items: { type: "object", additionalProperties: false, properties: { acceptedIdNames: { ...ARRAY_DEFAULT_OPTIONS, default: ["id"] }, acceptedIdTypes: { ...ARRAY_DEFAULT_OPTIONS, default: ["ID"] }, exceptions: { type: "object", additionalProperties: false, properties: { types: { ...ARRAY_DEFAULT_OPTIONS, description: "This is used to exclude types with names that match one of the specified values." }, suffixes: { ...ARRAY_DEFAULT_OPTIONS, description: "This is used to exclude types with names with suffixes that match one of the specified values." } } } } } }; var rule31 = { meta: { type: "suggestion", docs: { description: "Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.", category: "Schema", recommended: true, url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID22}`, requiresSchema: true, examples: [ { title: "Incorrect", usage: [ { acceptedIdNames: ["id", "_id"], acceptedIdTypes: ["ID"], exceptions: { suffixes: ["Payload"] } } ], code: ( /* GraphQL */ ` # Incorrect field name type InvalidFieldName { key: ID! } # Incorrect field type type InvalidFieldType { id: String! } # Incorrect exception suffix type InvalidSuffixResult { data: String! } # Too many unique identifiers. Must only contain one. type InvalidFieldName { id: ID! _id: ID! } ` ) }, { title: "Correct", usage: [ { acceptedIdNames: ["id", "_id"], acceptedIdTypes: ["ID"], exceptions: { types: ["Error"], suffixes: ["Payload"] } } ], code: ( /* GraphQL */ ` type User { id: ID! } type Post { _id: ID! } type CreateUserPayload { data: String! } type Error { message: String! } ` ) } ] }, schema: schema15 }, create(context) { const options = { acceptedIdNames: ["id"], acceptedIdTypes: ["ID"], exceptions: {}, ...context.options[0] }; const schema16 = requireGraphQLSchema(RULE_ID22, context); const rootTypeNames = [ schema16.getQueryType(), schema16.getMutationType(), schema16.getSubscriptionType() ].filter((v) => !!v).map((type) => type.name); const selector = `ObjectTypeDefinition[name.value!=/^(${rootTypeNames.join("|")})$/]`; return { [selector](node) { const typeName = node.name.value; const shouldIgnoreNode = options.exceptions.types?.includes(typeName) || options.exceptions.suffixes?.some((suffix) => typeName.endsWith(suffix)); if (shouldIgnoreNode) { return; } const validIds = node.fields?.filter((field) => { const fieldNode = field.rawNode(); const isValidIdName = options.acceptedIdNames.includes(fieldNode.name.value); let isValidIdType = false; if (fieldNode.type.kind === Kind22.NON_NULL_TYPE && fieldNode.type.type.kind === Kind22.NAMED_TYPE) { isValidIdType = options.acceptedIdTypes.includes(fieldNode.type.type.name.value); } return isValidIdName && isValidIdType; }); if (validIds?.length !== 1) { const pluralNamesSuffix = options.acceptedIdNames.length > 1 ? "s" : ""; const pluralTypesSuffix = options.acceptedIdTypes.length > 1 ? "s" : ""; context.report({ node: node.name, message: `${displayNodeName(node)} must have exactly one non-nullable unique identifier. Accepted name${pluralNamesSuffix}: ${englishJoinWords(options.acceptedIdNames)}. Accepted type${pluralTypesSuffix}: ${englishJoinWords(options.acceptedIdTypes)}.` }); } } }; } }; // src/rules/unique-enum-value-names/index.ts import { Kind as Kind23 } from "graphql"; var rule32 = { meta: { type: "suggestion", hasSuggestions: true, docs: { url: "https://the-guild.dev/graphql/eslint/rules/unique-enum-value-names", category: "Schema", recommended: true, description: `A GraphQL enum type is only valid if all its values are uniquely named. > This rule disallows case-insensitive enum values duplicates too.`, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` enum MyEnum { Value VALUE ValuE } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` enum MyEnum { Value1 Value2 Value3 } ` ) } ] }, schema: [] }, create(context) { const selector = [Kind23.ENUM_TYPE_DEFINITION, Kind23.ENUM_TYPE_EXTENSION].join(","); return { [selector](node) { const duplicates = node.values?.filter( (item, index, array) => array.findIndex((v) => v.name.value.toLowerCase() === item.name.value.toLowerCase()) !== index ); for (const duplicate of duplicates || []) { const enumName = duplicate.name.value; context.report({ node: duplicate.name, message: `Unexpected case-insensitive enum values duplicates for ${getNodeName( duplicate )}`, suggest: [ { desc: `Remove \`${enumName}\` enum value`, fix: (fixer) => fixer.remove(duplicate) } ] }); } } }; } }; // src/rules/unique-fragment-name/index.ts import { relative as relative2 } from "node:path"; import { Kind as Kind24 } from "graphql"; var RULE_ID23 = "unique-fragment-name"; var checkNode = (context, node, ruleId) => { const documentName = node.name.value; const siblings = requireGraphQLOperations(ruleId, context); const siblingDocuments = node.kind === Kind24.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName); const filepath = context.filename; const conflictingDocuments = siblingDocuments.filter((f) => { const isSameName = f.document.name?.value === documentName; const isSamePath = slash(f.filePath) === slash(filepath); return isSameName && !isSamePath; }); if (conflictingDocuments.length > 0) { context.report({ messageId: ruleId, data: { documentName, summary: conflictingDocuments.map((f) => ` ${relative2(CWD, f.filePath.replace(VIRTUAL_DOCUMENT_REGEX, ""))}`).join("\n") }, // @ts-expect-error name will exist node: node.name }); } }; var rule33 = { meta: { type: "suggestion", docs: { category: "Operations", description: "Enforce unique fragment names across your project.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID23}`, requiresSiblings: true, recommended: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` # user.fragment.graphql fragment UserFields on User { id name fullName } # user-fields.graphql fragment UserFields on User { id } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # user.fragment.graphql fragment AllUserFields on User { id name fullName } # user-fields.graphql fragment UserFields on User { id } ` ) } ] }, messages: { [RULE_ID23]: 'Fragment named "{{ documentName }}" already defined in:\n{{ summary }}' }, schema: [] }, create(context) { return { FragmentDefinition(node) { checkNode(context, node, RULE_ID23); } }; } }; // src/rules/unique-operation-name/index.ts var RULE_ID24 = "unique-operation-name"; var rule34 = { meta: { type: "suggestion", docs: { category: "Operations", description: "Enforce unique operation names across your project.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID24}`, requiresSiblings: true, recommended: true, examples: [ { title: "Incorrect", code: ( /* GraphQL */ ` # foo.query.graphql query user { user { id } } # bar.query.graphql query user { me { id } } ` ) }, { title: "Correct", code: ( /* GraphQL */ ` # foo.query.graphql query user { user { id } } # bar.query.graphql query me { me { id } } ` ) } ] }, messages: { [RULE_ID24]: 'Operation named "{{ documentName }}" already defined in:\n{{ summary }}' }, schema: [] }, create(context) { return { "OperationDefinition[name!=undefined]"(node) { checkNode(context, node, RULE_ID24); } }; } }; // src/rules/index.ts var rules = { ...GRAPHQL_JS_VALIDATIONS, alphabetize: rule, "description-style": rule2, "input-name": rule3, "lone-executable-definition": rule4, "match-document-filename": rule5, "naming-convention": rule6, "no-anonymous-operations": rule7, "no-deprecated": rule8, "no-duplicate-fields": rule9, "no-hashtag-description": rule10, "no-one-place-fragments": rule11, "no-root-type": rule12, "no-scalar-result-type-on-mutation": rule13, "no-typename-prefix": rule14, "no-unreachable-types": rule15, "no-unused-fields": rule16, "relay-arguments": rule17, "relay-connection-types": rule18, "relay-edge-types": rule19, "relay-page-info": rule20, "require-deprecation-date": rule21, "require-deprecation-reason": rule22, "require-description": rule23, "require-field-of-type-query-in-mutation-result": rule24, "require-import-fragment": rule25, "require-nullable-fields-with-oneof": rule26, "require-nullable-result-in-root": rule27, "require-selections": rule28, "require-type-pattern-with-oneof": rule29, "selection-set-depth": rule30, "strict-id-in-types": rule31, "unique-enum-value-names": rule32, "unique-fragment-name": rule33, "unique-operation-name": rule34 }; // src/index.ts var processors = { graphql: processor }; var src_default = { parser, processor, rules, configs }; export { configs, src_default as default, parseForESLint, parser, processors, requireGraphQLOperations, requireGraphQLSchema, rules };