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