@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
453 lines (442 loc) • 16.1 kB
JavaScript
;Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _graphql = require('graphql');
var _utilsjs = require('../../utils.js');
const KindToDisplayName = {
// types
[_graphql.Kind.OBJECT_TYPE_DEFINITION]: "Type",
[_graphql.Kind.INTERFACE_TYPE_DEFINITION]: "Interface",
[_graphql.Kind.ENUM_TYPE_DEFINITION]: "Enumerator",
[_graphql.Kind.SCALAR_TYPE_DEFINITION]: "Scalar",
[_graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: "Input type",
[_graphql.Kind.UNION_TYPE_DEFINITION]: "Union",
// fields
[_graphql.Kind.FIELD_DEFINITION]: "Field",
[_graphql.Kind.INPUT_VALUE_DEFINITION]: "Input property",
[_graphql.Kind.ARGUMENT]: "Argument",
[_graphql.Kind.DIRECTIVE_DEFINITION]: "Directive",
// rest
[_graphql.Kind.ENUM_VALUE_DEFINITION]: "Enumeration value",
[_graphql.Kind.OPERATION_DEFINITION]: "Operation",
[_graphql.Kind.FRAGMENT_DEFINITION]: "Fragment",
[_graphql.Kind.VARIABLE_DEFINITION]: "Variable"
}, StyleToRegex = {
camelCase: /^[a-z][\dA-Za-z]*$/,
PascalCase: /^[A-Z][\dA-Za-z]*$/,
snake_case: /^[a-z][\d_a-z]*[\da-z]*$/,
UPPER_CASE: /^[A-Z][\dA-Z_]*[\dA-Z]*$/
}, ALLOWED_KINDS = Object.keys(KindToDisplayName).sort(), ALLOWED_STYLES = Object.keys(StyleToRegex), schemaOption = {
oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asObject" }]
}, descriptionPrefixesSuffixes = (name) => `> [!WARNING]
>
> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`, schema = {
definitions: {
asString: {
enum: ALLOWED_STYLES,
description: `One of: ${ALLOWED_STYLES.map((t) => `\`${t}\``).join(", ")}`
},
asObject: {
type: "object",
additionalProperties: !1,
properties: {
style: { enum: ALLOWED_STYLES },
prefix: { type: "string" },
suffix: { type: "string" },
forbiddenPatterns: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
items: {
type: "object"
},
description: "Should be of instance of `RegEx`"
},
requiredPatterns: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
items: {
type: "object"
},
description: "Should be of instance of `RegEx`"
},
forbiddenPrefixes: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes("forbiddenPatterns")
},
forbiddenSuffixes: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes("forbiddenPatterns")
},
requiredPrefixes: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes("requiredPatterns")
},
requiredSuffixes: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes("requiredPatterns")
},
ignorePattern: {
type: "string",
description: "Option to skip validation of some words, e.g. acronyms"
}
}
}
},
type: "array",
maxItems: 1,
items: {
type: "object",
additionalProperties: !1,
properties: {
types: {
...schemaOption,
description: `Includes:
${_utilsjs.TYPES_KINDS.map((kind) => `- \`${kind}\``).join(`
`)}`
},
...Object.fromEntries(
ALLOWED_KINDS.map((kind) => [
kind,
{
...schemaOption,
description: `> [!NOTE]
>
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`
}
])
),
allowLeadingUnderscore: {
type: "boolean",
default: !1
},
allowTrailingUnderscore: {
type: "boolean",
default: !1
}
},
patternProperties: {
[`^(${ALLOWED_KINDS.join("|")})(.+)?$`]: schemaOption
},
description: [
"> It's possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts with allowed `ASTNode` names which are described below.",
">",
"> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.",
">",
"> Example: pattern property `FieldDefinition[parent.name.value=Query]` will match only fields for type `Query`."
].join(`
`)
}
}, rule = exports.rule = {
meta: {
type: "suggestion",
docs: {
description: "Require names to follow specified conventions.",
category: ["Schema", "Operations"],
recommended: !0,
url: "https://the-guild.dev/graphql/eslint/rules/naming-convention",
examples: [
{
title: "Incorrect",
usage: [{ types: "PascalCase", FieldDefinition: "camelCase" }],
code: (
/* GraphQL */
`
type user {
first_name: String!
}
`
)
},
{
title: "Incorrect",
usage: [{ FragmentDefinition: { style: "PascalCase", forbiddenSuffixes: ["Fragment"] } }],
code: (
/* GraphQL */
`
fragment UserFragment on User {
# ...
}
`
)
},
{
title: "Incorrect",
usage: [{ "FieldDefinition[parent.name.value=Query]": { forbiddenPrefixes: ["get"] } }],
code: (
/* GraphQL */
`
type Query {
getUsers: [User!]!
}
`
)
},
{
title: "Correct",
usage: [{ types: "PascalCase", FieldDefinition: "camelCase" }],
code: (
/* GraphQL */
`
type User {
firstName: String
}
`
)
},
{
title: "Correct",
usage: [{ FragmentDefinition: { style: "PascalCase", forbiddenSuffixes: ["Fragment"] } }],
code: (
/* GraphQL */
`
fragment UserFields on User {
# ...
}
`
)
},
{
title: "Correct",
usage: [{ "FieldDefinition[parent.name.value=Query]": { forbiddenPrefixes: ["get"] } }],
code: (
/* GraphQL */
`
type Query {
users: [User!]!
}
`
)
},
{
title: "Correct",
usage: [{ FieldDefinition: { style: "camelCase", ignorePattern: "^(EAN13|UPC|UK)" } }],
code: (
/* GraphQL */
`
type Product {
EAN13: String
UPC: String
UKFlag: String
}
`
)
},
{
title: "Correct",
usage: [
{
"FieldDefinition[gqlType.name.value=Boolean]": {
style: "camelCase",
requiredPrefixes: ["is", "has"]
},
"FieldDefinition[gqlType.gqlType.name.value=Boolean]": {
style: "camelCase",
requiredPrefixes: ["is", "has"]
}
}
],
code: (
/* GraphQL */
`
type Product {
isBackordered: Boolean
isNew: Boolean!
hasDiscount: Boolean!
}
`
)
},
{
title: "Correct",
usage: [
{
"FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]": {
style: "camelCase",
requiredSuffixes: ["SensitiveSecret"]
}
}
],
code: (
/* GraphQL */
`
scalar SensitiveSecret
type Account {
accountSensitiveSecret: SensitiveSecret!
}
`
)
}
],
configOptions: {
schema: [
{
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"]
}
}
],
operations: [
{
VariableDefinition: "camelCase",
OperationDefinition: {
style: "PascalCase",
forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"],
forbiddenSuffixes: ["Query", "Mutation", "Subscription"]
},
FragmentDefinition: {
style: "PascalCase",
forbiddenPrefixes: ["Fragment"],
forbiddenSuffixes: ["Fragment"]
}
}
]
}
},
hasSuggestions: !0,
schema
},
create(context) {
const options = context.options[0] || {}, { allowLeadingUnderscore, allowTrailingUnderscore, types, ...restOptions } = options;
function normalisePropertyOption(kind) {
const style = restOptions[kind] || types;
return typeof style == "object" ? style : { style };
}
function report(node, message, suggestedNames) {
context.report({
node,
message,
suggest: suggestedNames.map((suggestedName) => ({
desc: `Rename to \`${suggestedName}\``,
fix: (fixer) => fixer.replaceText(node, suggestedName)
}))
});
}
const checkNode = (selector) => (n) => {
const { name: node } = n.kind === _graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
if (!node)
return;
const {
prefix,
suffix,
forbiddenPrefixes,
forbiddenSuffixes,
style,
ignorePattern,
requiredPrefixes,
requiredSuffixes,
forbiddenPatterns,
requiredPatterns
} = normalisePropertyOption(selector), nodeName = node.value, error = getError();
if (error) {
const { errorMessage, renameToNames } = error, [leadingUnderscores] = nodeName.match(/^_*/), [trailingUnderscores] = nodeName.match(/_*$/), suggestedNames = renameToNames.map(
(renameToName) => leadingUnderscores + renameToName + trailingUnderscores
), name = _utilsjs.displayNodeName.call(void 0, n);
report(
node,
`${name[0].toUpperCase()}${name.slice(1)} should ${errorMessage}`,
suggestedNames
);
}
function getError() {
const name = nodeName.replace(/(^_+)|(_+$)/g, "");
if (ignorePattern && new RegExp(ignorePattern, "u").test(name))
return;
if (prefix && !name.startsWith(prefix))
return {
errorMessage: `have "${prefix}" prefix`,
renameToNames: [prefix + name]
};
if (suffix && !name.endsWith(suffix))
return {
errorMessage: `have "${suffix}" suffix`,
renameToNames: [name + suffix]
};
const forbidden = _optionalChain([forbiddenPatterns, 'optionalAccess', _ => _.find, 'call', _2 => _2((pattern) => pattern.test(name))]);
if (forbidden)
return {
errorMessage: `not contain the forbidden pattern "${forbidden}"`,
renameToNames: [name.replace(forbidden, "")]
};
if (requiredPatterns && !requiredPatterns.some((pattern) => pattern.test(name)))
return {
errorMessage: `contain the required pattern: ${_utilsjs.englishJoinWords.call(void 0, requiredPatterns.map((re) => re.source))}`,
renameToNames: []
};
const forbiddenPrefix = _optionalChain([forbiddenPrefixes, 'optionalAccess', _3 => _3.find, 'call', _4 => _4((prefix2) => name.startsWith(prefix2))]);
if (forbiddenPrefix)
return {
errorMessage: `not have "${forbiddenPrefix}" prefix`,
renameToNames: [name.replace(new RegExp(`^${forbiddenPrefix}`), "")]
};
const forbiddenSuffix = _optionalChain([forbiddenSuffixes, 'optionalAccess', _5 => _5.find, 'call', _6 => _6((suffix2) => name.endsWith(suffix2))]);
if (forbiddenSuffix)
return {
errorMessage: `not have "${forbiddenSuffix}" suffix`,
renameToNames: [name.replace(new RegExp(`${forbiddenSuffix}$`), "")]
};
if (requiredPrefixes && !requiredPrefixes.some((requiredPrefix) => name.startsWith(requiredPrefix)))
return {
errorMessage: `have one of the following prefixes: ${_utilsjs.englishJoinWords.call(void 0,
requiredPrefixes
)}`,
renameToNames: style ? requiredPrefixes.map((prefix2) => _utilsjs.convertCase.call(void 0, style, `${prefix2} ${name}`)) : requiredPrefixes.map((prefix2) => `${prefix2}${name}`)
};
if (requiredSuffixes && !requiredSuffixes.some((requiredSuffix) => name.endsWith(requiredSuffix)))
return {
errorMessage: `have one of the following suffixes: ${_utilsjs.englishJoinWords.call(void 0,
requiredSuffixes
)}`,
renameToNames: style ? requiredSuffixes.map((suffix2) => _utilsjs.convertCase.call(void 0, style, `${name} ${suffix2}`)) : requiredSuffixes.map((suffix2) => `${name}${suffix2}`)
};
if (!style)
return;
if (!StyleToRegex[style].test(name))
return {
errorMessage: `be in ${style} format`,
renameToNames: [_utilsjs.convertCase.call(void 0, style, name)]
};
}
}, checkUnderscore = (isLeading) => (node) => {
if (node.parent.kind === "Field" && node.parent.alias !== node)
return;
const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, "");
report(node, `${isLeading ? "Leading" : "Trailing"} underscores are not allowed`, [
suggestedName
]);
}, listeners = {};
allowLeadingUnderscore || (listeners["Name[value=/^_/]"] = checkUnderscore(!0)), allowTrailingUnderscore || (listeners["Name[value=/_$/]"] = checkUnderscore(!1));
const selectors = new Set(
[types && _utilsjs.TYPES_KINDS, Object.keys(restOptions)].flat().filter(_utilsjs.truthy)
);
for (const selector of selectors)
listeners[selector] = checkNode(selector);
return listeners;
}
};
exports.rule = rule;