UNPKG

@graphql-eslint/eslint-plugin

Version:
1,648 lines (1,627 loc) • 187 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"; // 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 ("schemaSdl" in options) { schema16 = buildSchema(options.schemaSdl); } else { 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