@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
1,587 lines (1,571 loc) • 172 kB
JavaScript
// 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"), ModuleCache = class {
map = /* @__PURE__ */ new Map();
set(cacheKey, result) {
}
get(cacheKey, settings = {
lifetime: 10
/* seconds */
}) {
}
};
// 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) => valueFromASTUntyped(...args);
function getBaseType(type) {
return isNonNullType(type) || isListType(type) ? getBaseType(type.ofType) : type;
}
function convertToken(token, type) {
let { 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) {
let source = new Source(code, filePath), lexer = new Lexer(source), tokens = [], token = lexer.advance();
for (; token && token.kind !== TokenKind.EOF; ) {
let result = convertToken(token, token.kind);
tokens.push(result), token = lexer.advance();
}
return tokens;
}
function extractComments(loc) {
if (!loc)
return [];
let comments = [], token = loc.startToken;
for (; token; ) {
if (token.kind === TokenKind.COMMENT) {
let 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) {
let { startToken, endToken, source, start, end } = location, 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
};
return loc.start.column === loc.end.column && (loc.end.column += end - start), loc;
}
// src/estree-converter/converter.ts
function convertToESTree(node, schema16) {
let typeInfo = schema16 && new TypeInfo(schema16), visitor = {
leave(node2, key, parent) {
let leadingComments = "description" in node2 && node2.description ? [
{
type: node2.description.block ? "Block" : "Line",
value: node2.description.value
}
] : [], 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()
} : {}, rawNode = () => parent && key !== void 0 ? parent[key] : node2.kind === Kind.DOCUMENT ? {
...node2,
definitions: node2.definitions.map(
(definition) => definition.rawNode()
)
} : node2, 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 = process.env.VERSION;
// 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) {
let { 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) {
let { 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`
}, 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)
)
}, slash = (path2) => path2.replaceAll("\\", "/"), VIRTUAL_DOCUMENT_REGEX = /[/\\]\d+_document.graphql$/, CWD = process.cwd(), getTypeName = (node) => "type" in node ? getTypeName(node.type) : "name" in node && node.name ? node.name.value : "", 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
], pascalCase = (str) => lowerCase(str).split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""), camelCase = (str) => {
let result = pascalCase(str);
return result.charAt(0).toLowerCase() + result.slice(1);
}, 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 = "") {
let { line, column } = start;
return {
start: {
line,
column
},
end: {
line,
column: column + fieldName.length
}
};
}
var REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 }, ARRAY_DEFAULT_OPTIONS = {
type: "array",
uniqueItems: !0,
minItems: 1,
items: {
type: "string"
}
}, englishJoinWords = (words) => new Intl.ListFormat("en-US", { type: "disjunction" }).format(words);
function truthy(value) {
return !!value;
}
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 "";
}
// src/siblings.ts
var siblingOperationsCache = /* @__PURE__ */ new Map();
function getSiblings(documents) {
if (documents.length === 0) {
let printed = !1, noopWarn = () => (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 = !0), []);
return {
available: !1,
getFragment: noopWarn,
getFragments: noopWarn,
getFragmentByType: noopWarn,
getFragmentsInUse: noopWarn,
getOperation: noopWarn,
getOperations: noopWarn,
getOperationByType: noopWarn
};
}
let value = siblingOperationsCache.get(documents);
if (value)
return value;
let fragmentsCache = null, getFragments = () => {
if (fragmentsCache === null) {
let result = [];
for (let source of documents)
for (let definition of source.document?.definitions || [])
definition.kind === Kind3.FRAGMENT_DEFINITION && result.push({
filePath: source.location,
document: definition
});
fragmentsCache = result;
}
return fragmentsCache;
}, cachedOperations = null, getOperations = () => {
if (cachedOperations === null) {
let result = [];
for (let source of documents)
for (let definition of source.document?.definitions || [])
definition.kind === Kind3.OPERATION_DEFINITION && result.push({
filePath: source.location,
document: definition
});
cachedOperations = result;
}
return cachedOperations;
}, getFragment = (name) => getFragments().filter((f) => f.document.name.value === name), collectFragments = (selectable, recursive, collected = /* @__PURE__ */ new Map()) => (visit2(selectable, {
FragmentSpread(spread) {
let fragmentName = spread.name.value, [fragment] = getFragment(fragmentName);
if (!fragment) {
logger.warn(
`Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`
);
return;
}
collected.has(fragmentName) || (collected.set(fragmentName, fragment.document), recursive && collectFragments(fragment.document, recursive, collected));
}
}), collected), siblingOperations = {
available: !0,
getFragment,
getFragments,
getFragmentByType: (typeName) => getFragments().filter((f) => f.document.typeCondition.name.value === typeName),
getFragmentsInUse: (selectable, recursive = !0) => 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)
};
return siblingOperationsCache.set(documents, siblingOperations), 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 (let 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 {
let { filePath } = options, { document } = parseGraphQLSDL(filePath, code, { noLocation: !1 }), project, schema16, documents;
documents = [
parseGraphQLSDL(
"operation.graphql",
options.graphQLConfig.documents,
{ noLocation: !0 }
)
];
try {
schema16 = buildSchema(options.graphQLConfig.schema);
} catch (error) {
throw error instanceof Error && (error.message = `Error while loading schema: ${error.message}`), error;
}
let 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}`), error instanceof GraphQLError) {
let location = error.locations?.[0];
throw {
index: error.positions?.[0],
...location && {
lineNumber: location.line,
column: location.column - 1
},
message: error.message
};
}
throw error;
}
}
var parser = {
parseForESLint,
meta: {
name: "@graphql-eslint/parser",
version
}
};
// src/rules/alphabetize/index.ts
import {
Kind as Kind4
} from "graphql";
import lowerCase2 from "lodash.lowercase";
var RULE_ID = "alphabetize", fieldsEnum = [
Kind4.OBJECT_TYPE_DEFINITION,
Kind4.INTERFACE_TYPE_DEFINITION,
Kind4.INPUT_OBJECT_TYPE_DEFINITION
], selectionsEnum = [
Kind4.OPERATION_DEFINITION,
Kind4.FRAGMENT_DEFINITION
], argumentsEnum = [
Kind4.FIELD_DEFINITION,
Kind4.FIELD,
Kind4.DIRECTIVE_DEFINITION,
Kind4.DIRECTIVE
], schema = {
type: "array",
minItems: 1,
maxItems: 1,
items: {
type: "object",
additionalProperties: !1,
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(`
`)
}
}
}
}, 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: !0 }],
code: (
/* GraphQL */
`
enum Role {
SUPER_ADMIN
ADMIN # should be before "SUPER_ADMIN"
USER
GOD # should be before "USER"
}
`
)
},
{
title: "Correct",
usage: [{ values: !0 }],
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: !0,
fields: fieldsEnum,
values: !0,
arguments: argumentsEnum,
groups: ["id", "*", "createdAt", "updatedAt"]
}
],
operations: [
{
definitions: !0,
selections: selectionsEnum,
variables: !0,
arguments: [Kind4.FIELD, Kind4.DIRECTIVE],
groups: ["...", "id", "*", "{"]
}
]
}
},
messages: {
[RULE_ID]: "{{ currNode }} should be before {{ prevNode }}"
},
schema
},
create(context) {
let sourceCode = context.getSourceCode();
function isNodeAndCommentOnSameLine(node, comment) {
return node.loc.end.line === comment.loc.start.line;
}
function getBeforeComments(node) {
let commentsBefore = sourceCode.getCommentsBefore(node);
if (commentsBefore.length === 0)
return [];
let tokenBefore = sourceCode.getTokenBefore(node);
if (tokenBefore)
return commentsBefore.filter((comment) => !isNodeAndCommentOnSameLine(tokenBefore, comment));
let filteredComments = [], nodeLine = node.loc.start.line;
for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
let comment = commentsBefore[i];
if (nodeLine - comment.loc.start.line - filteredComments.length > 1)
break;
filteredComments.unshift(comment);
}
return filteredComments;
}
function getRangeWithComments(node) {
node.kind === Kind4.VARIABLE && (node = node.parent);
let [firstBeforeComment] = getBeforeComments(node), [firstAfterComment] = sourceCode.getCommentsAfter(node), from = firstBeforeComment || node, 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) {
let currNode = nodes[i], currName = getName(currNode);
if (!currName)
continue;
let prevNode = nodes[i - 1], prevName = getName(prevNode);
if (prevName) {
let compareResult = prevName.localeCompare(currName), { groups } = opts, shouldSortByGroup = !1;
if (groups?.length) {
if (!groups.includes("*"))
throw new Error("`groups` option should contain `*` string.");
let indexForPrev = getIndex({ node: prevNode, groups }), indexForCurr = getIndex({ node: currNode, groups });
if (shouldSortByGroup = indexForPrev - indexForCurr > 0, indexForPrev < indexForCurr)
continue;
}
if (!shouldSortByGroup && !(compareResult === 1) && (!(compareResult === 0) || !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) {
let prevRange = getRangeWithComments(prevNode), currRange = getRangeWithComments(currNode);
yield fixer.replaceTextRange(
prevRange,
sourceCode.getText({ range: currRange })
), yield fixer.replaceTextRange(
currRange,
sourceCode.getText({ range: prevRange })
);
}
});
}
}
let opts = context.options[0], fields = new Set(opts.fields ?? []), listeners = {}, fieldsSelector = [
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(truthy).flat().join(","), selectionsSelector = opts.selections?.join(","), argumentsSelector = opts.arguments?.join(",");
if (fieldsSelector && (listeners[fieldsSelector] = (node) => {
checkNodes(node.fields);
}), opts.values) {
let enumValuesSelector = [Kind4.ENUM_TYPE_DEFINITION, Kind4.ENUM_TYPE_EXTENSION].join(",");
listeners[enumValuesSelector] = (node) => {
checkNodes(node.values);
};
}
return selectionsSelector && (listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => {
checkNodes(node.selections);
}), opts.variables && (listeners.OperationDefinition = (node) => {
checkNodes(node.variableDefinitions?.map((varDef) => varDef.variable));
}), argumentsSelector && (listeners[argumentsSelector] = (node) => {
checkNodes(node.arguments);
}), opts.definitions && (listeners.Document = (node) => {
checkNodes(node.definitions);
}), listeners;
}
};
function getIndex({
node,
groups
}) {
let index = groups.indexOf(getName(node));
return index === -1 && "selectionSet" in node && node.selectionSet && (index = groups.indexOf("{")), index === -1 && node.kind === Kind4.FRAGMENT_SPREAD && (index = groups.indexOf("...")), index === -1 && (index = groups.indexOf("*")), 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: !1,
minProperties: 1,
properties: {
style: {
enum: ["block", "inline"],
default: "block"
}
}
}
}, rule2 = {
meta: {
type: "suggestion",
hasSuggestions: !0,
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: !0
},
schema: schema2
},
create(context) {
let { style = "block" } = context.options[0] || {}, 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) {
let originalText = context.getSourceCode().getText(node), 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)
try {
let validationErrors = schema16 ? validate(schema16, documentNode, [rule35]) : validateSDL(documentNode, null, [rule35]);
for (let error of validationErrors) {
let { line, column } = error.locations[0], sourceCode = context.getSourceCode(), { tokens } = sourceCode.ast, token = tokens.find(
(token2) => token2.loc.start.line === line && token2.loc.start.column === column - 1
), loc = {
line,
column: column - 1
};
token && (loc = // if cursor on `@` symbol than use next node
token.type === "@" ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc);
let didYouMeanContent = error.message.match(/Did you mean (?<content>.*)\?$/)?.groups.content, matches = didYouMeanContent ? [...didYouMeanContent.matchAll(/"(?<name>[^"]*)"/g)] : [];
context.report({
loc,
message: error.message,
suggest: hasDidYouMeanSuggestions ? matches.map((match) => {
let { 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) => {
let fragmentDefs = /* @__PURE__ */ new Set(), fragmentSpreads = /* @__PURE__ */ new Set();
return visit3(node, {
FragmentDefinition(node2) {
fragmentDefs.add(node2.name.value);
},
FragmentSpread(node2) {
fragmentSpreads.add(node2.name.value);
}
}), { fragmentDefs, fragmentSpreads };
}, getMissingFragments = (node) => {
let { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(node);
return [...fragmentSpreads].filter((name) => !fragmentDefs.has(name));
}, handleMissingFragments = ({ ruleId, context, node }) => {
let missingFragments = getMissingFragments(node);
if (missingFragments.length > 0) {
let siblings = requireGraphQLOperations(ruleId, context), fragmentsToAdd = [];
for (let fragmentName of missingFragments) {
let [foundFragment] = siblings.getFragment(fragmentName).map((source) => source.document);
foundFragment && fragmentsToAdd.push(foundFragment);
}
if (fragmentsToAdd.length > 0)
return handleMissingFragments({
ruleId,
context,
node: {
kind: Kind5.DOCUMENT,
definitions: [...node.definitions, ...fragmentsToAdd]
}
});
}
return node;
}, validationToRule = ({
ruleId,
rule: rule35,
getDocumentNode,
schema: schema16 = [],
hasDidYouMeanSuggestions
}, docs) => ({
[ruleId]: {
meta: {
docs: {
recommended: !0,
...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) {
let schema17 = docs.requiresSchema ? requireGraphQLSchema(ruleId, context) : null, documentNode = getDocumentNode ? getDocumentNode({ ruleId, context, node: node.rawNode() }) : node.rawNode();
validateDocument({
context,
schema: schema17,
documentNode,
rule: rule35,
hasDidYouMeanSuggestions
});
}
};
}
}
}), 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: !0
}
),
validationToRule(
{
ruleId: "fields-on-correct-type",
rule: FieldsOnCorrectTypeRule,
hasDidYouMeanSuggestions: !0
},
{
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: !0
}
),
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: !0
}
),
validationToRule(
{
ruleId: "known-argument-names",
rule: KnownArgumentNamesRule,
hasDidYouMeanSuggestions: !0
},
{
category: ["Schema", "Operations"],
description: "A GraphQL field is only valid if all supplied arguments are defined by that field.",
requiresSchema: !0
}
),
validationToRule(
{
ruleId: "known-directives",
rule: KnownDirectivesRule,
getDocumentNode({ context, node: documentNode }) {
let { ignoreClientDirectives = [] } = context.options[0] || {};
if (ignoreClientDirectives.length === 0)
return documentNode;
let 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: !1,
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: !0,
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: !0,
requiresSiblings: !0,
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: !0
},
{
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: !0
}
),
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: !0
}
),
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: !0
}
),
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: !0,
requiresSiblings: !0
}
),
validationToRule(
{
ruleId: "no-unused-fragments",
rule: NoUnusedFragmentsRule,
getDocumentNode: ({ ruleId, context, node }) => {
let siblings = requireGraphQLOperations(ruleId, context), FilePathToDocumentsMap = [
...siblings.getOperations(),
...siblings.getFragments()
].reduce((map, { filePath, document }) => (map[filePath] ??= [], map[filePath].push(document), map), /* @__PURE__ */ Object.create(null)), getParentNode = (currentFilePath, node2) => {
let { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node2);
if (fragmentDefs.size === 0)
return node2;
delete FilePathToDocumentsMap[currentFilePath];
for (let [filePath, documents] of Object.entries(FilePathToDocumentsMap))
if (getMissingFragments({
kind: Kind5.DOCUMENT,
definitions: documents
}).some(
(fragment) => fragmentDefs.has(fragment)
))
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: !0,
requiresSiblings: !0
}
),
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: !0,
requiresSiblings: !0
}
),
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: !0
}
),
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: !0
}
),
validationToRule(
{
ruleId: "possible-type-extension",
rule: PossibleTypeExtensionsRule,
hasDidYouMeanSuggestions: !0
},
{
category: "Schema",
description: "A type extension is only valid if the type is defined and has the same kind.",
recommended: !0,
requiresSchema: !0
}
),
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: !0
}
),
validationToRule(
{
ruleId: "scalar-leafs",
rule: ScalarLeafsRule,
hasDidYouMeanSuggestions: !0
},
{
category: "Operations",
description: "A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.",
requiresSchema: !0
}
),
validationToRule(
{
ruleId: "one-field-subscriptions",
rule: SingleFieldSubscriptionsRule
},
{
category: "Operations",
description: "A GraphQL subscription is valid only if it contains a single root field.",
requiresSchema: !0
}
),
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: !0
}
),
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: !0
}
),
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: !0
}
),
validationToRule(
{
ruleId: "value-literals-of-correct-type",
rule: ValuesOfCorrectTypeRule,
hasDidYouMeanSuggestions: !0
},
{
category: "Operations",
description: "A GraphQL document is only valid if all value literals are of the type expected at their position.",
requiresSchema: !0
}
),
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: !0
}
),
validationToRule(
{
ruleId: "variables-in-allowed-position",
rule: VariablesInAllowedPositionRule
},
{
category: "Operations",
description: "Variables passed to field arguments conform to type.",
requiresSchema: !0
}
)
);
// src/rules/input-name/index.ts
import {
Kind as Kind6
} from "graphql";
var schema3 = {
type: "array",
maxItems: 1,
items: {
type: "object",
additionalProperties: !1,
properties: {
checkInputType: {
type: "boolean",
default: !1,
description: "Check that the input type name follows the convention \\<mutationName>Input"
},
caseSensitiveInputType: {
type: "boolean",
default: !0,
description: "Allow for case discrepancies in the input type name"
},
checkQueries: {
type: "boolean",
default: !1,
description: "Apply the rule to Queries"
},
checkMutations: {
type: "boolean",
default: !0,
description: "Apply the rule to Mutations"
}
}
}
}, isObjectType = (node) => (
// TODO: remove `as any` when drop support of graphql@15
[Kind6.OBJECT_TYPE_DEFINITION, Kind6.OBJECT_TYPE_EXTENSION].includes(node.type)
), isQueryType = (node) => isObjectType(node) && node.name.value === "Query", isMutationType = (node) => isObjectType(node) && node.name.value === "Mutation", rule3 = {
meta: {
type: "suggestion",
hasSuggestions: !0,
docs: {
description: `Require mutation argument to be always called "input" and input type to be called Mutation name + "Input".
Using 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: !0 }],
code: (
/* GraphQL */
`
type Mutation {
SetMessage(message: InputMessage): String
}
`
)
},
{
title: "Correct (with `checkInputType`)",
usage: [{ checkInputType: !0 }],
code: (
/* GraphQL */
`
type Mutation {
SetMessage(input: SetMessageInput): String
}
`
)
},
{
title: "Correct (without `checkInputType`)",
usage: [{ checkInputType: !1 }],
code: (
/* GraphQL */
`
type Mutation {
SetMessage(input: AnyInputTypeName): String
}
`
)
}
]
},
schema: schema3
},
create(context) {
let options = {
checkInputType: !1,
caseSensitiveInputType: !0,
checkMutations: !0,
...context.options[0]
}, shouldCheckType = (node) => options.checkMutations && isMutationType(node) || options.checkQueries && isQueryType(node) || !1, listeners = {
"FieldDefinition > InputValueDefinition[name.value!=input] > Name"(node) {
let fieldDef = node.parent.parent;
if (shouldCheckType(fieldDef.parent)) {
let 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")
}
]
});
}
}
};
return options.checkInputType && (listeners["FieldDefinition > InputValueDefinition NamedType"] = (node) => {
let inputValueNode = ((item) => {
let currentNode = item;
for (; currentNode.type !== Kind6.INPUT_VALUE_DEFINITION; )
currentNode = currentNode.parent;
return currentNode;
})(node);
if (shouldCheckType(inputValueNode.parent.parent)) {
let mutationName = `${inputValueNode.parent.name.value}Input`, name = node.name.value;
(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)
}
]
});
}
}), listeners;
}
};
// src/rules/lone-executable-definition/index.ts
import { OperationTypeNode as OperationTypeNode2 } from "graphql";
var RULE_ID2 =