@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
435 lines (433 loc) • 14.6 kB
JavaScript
;
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 naming_convention_exports = {};
__export(naming_convention_exports, {
rule: () => rule
});
module.exports = __toCommonJS(naming_convention_exports);
var import_graphql = require("graphql");
var import_utils = require("../utils.js");
const KindToDisplayName = {
// types
[import_graphql.Kind.OBJECT_TYPE_DEFINITION]: "Type",
[import_graphql.Kind.INTERFACE_TYPE_DEFINITION]: "Interface",
[import_graphql.Kind.ENUM_TYPE_DEFINITION]: "Enumerator",
[import_graphql.Kind.SCALAR_TYPE_DEFINITION]: "Scalar",
[import_graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: "Input type",
[import_graphql.Kind.UNION_TYPE_DEFINITION]: "Union",
// fields
[import_graphql.Kind.FIELD_DEFINITION]: "Field",
[import_graphql.Kind.INPUT_VALUE_DEFINITION]: "Input property",
[import_graphql.Kind.ARGUMENT]: "Argument",
[import_graphql.Kind.DIRECTIVE_DEFINITION]: "Directive",
// rest
[import_graphql.Kind.ENUM_VALUE_DEFINITION]: "Enumeration value",
[import_graphql.Kind.OPERATION_DEFINITION]: "Operation",
[import_graphql.Kind.FRAGMENT_DEFINITION]: "Fragment",
[import_graphql.Kind.VARIABLE_DEFINITION]: "Variable"
};
const 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]*$/
};
const ALLOWED_KINDS = Object.keys(KindToDisplayName).sort();
const ALLOWED_STYLES = Object.keys(StyleToRegex);
const schemaOption = {
oneOf: [{ $ref: "#/definitions/asString" }, { $ref: "#/definitions/asObject" }]
};
const schema = {
definitions: {
asString: {
enum: ALLOWED_STYLES,
description: `One of: ${ALLOWED_STYLES.map((t) => `\`${t}\``).join(", ")}`
},
asObject: {
type: "object",
additionalProperties: false,
properties: {
style: { enum: ALLOWED_STYLES },
prefix: { type: "string" },
suffix: { type: "string" },
forbiddenPrefixes: import_utils.ARRAY_DEFAULT_OPTIONS,
forbiddenSuffixes: import_utils.ARRAY_DEFAULT_OPTIONS,
requiredPrefixes: import_utils.ARRAY_DEFAULT_OPTIONS,
requiredSuffixes: import_utils.ARRAY_DEFAULT_OPTIONS,
ignorePattern: {
type: "string",
description: "Option to skip validation of some words, e.g. acronyms"
}
}
}
},
type: "array",
maxItems: 1,
items: {
type: "object",
additionalProperties: false,
properties: {
types: {
...schemaOption,
description: `Includes:
${import_utils.TYPES_KINDS.map((kind) => `- \`${kind}\``).join("\n")}`
},
...Object.fromEntries(
ALLOWED_KINDS.map((kind) => [
kind,
{
...schemaOption,
description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`
}
])
),
allowLeadingUnderscore: {
type: "boolean",
default: false
},
allowTrailingUnderscore: {
type: "boolean",
default: false
}
},
patternProperties: {
[`^(${ALLOWED_KINDS.join("|")})(.+)?$`]: 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("\n")
}
};
const rule = {
meta: {
type: "suggestion",
docs: {
description: "Require names to follow specified conventions.",
category: ["Schema", "Operations"],
recommended: true,
url: "https://the-guild.dev/graphql/eslint/rules/naming-convention",
examples: [
{
title: "Incorrect",
usage: [{ types: "PascalCase", FieldDefinition: "camelCase" }],
code: (
/* GraphQL */
`
type user {
first_name: String!
}
`
)
},
{
title: "Incorrect",
usage: [{ FragmentDefinition: { style: "PascalCase", 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"]
}
}
],
operations: [
{
VariableDefinition: "camelCase",
OperationDefinition: {
style: "PascalCase",
forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"],
forbiddenSuffixes: ["Query", "Mutation", "Subscription"]
},
FragmentDefinition: {
style: "PascalCase",
forbiddenPrefixes: ["Fragment"],
forbiddenSuffixes: ["Fragment"]
}
}
]
}
},
hasSuggestions: true,
schema
},
create(context) {
const options = context.options[0] || {};
const { 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 === import_graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
if (!node) {
return;
}
const {
prefix,
suffix,
forbiddenPrefixes,
forbiddenSuffixes,
style,
ignorePattern,
requiredPrefixes,
requiredSuffixes
} = normalisePropertyOption(selector);
const nodeType = KindToDisplayName[n.kind] || n.kind;
const nodeName = node.value;
const error = getError();
if (error) {
const { errorMessage, renameToNames } = error;
const [leadingUnderscores] = nodeName.match(/^_*/);
const [trailingUnderscores] = nodeName.match(/_*$/);
const suggestedNames = renameToNames.map(
(renameToName) => leadingUnderscores + renameToName + trailingUnderscores
);
report(node, `${nodeType} "${nodeName}" 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 forbiddenPrefix = forbiddenPrefixes == null ? void 0 : forbiddenPrefixes.find((prefix2) => name.startsWith(prefix2));
if (forbiddenPrefix) {
return {
errorMessage: `not have "${forbiddenPrefix}" prefix`,
renameToNames: [name.replace(new RegExp(`^${forbiddenPrefix}`), "")]
};
}
const forbiddenSuffix = forbiddenSuffixes == null ? void 0 : forbiddenSuffixes.find((suffix2) => name.endsWith(suffix2));
if (forbiddenSuffix) {
return {
errorMessage: `not have "${forbiddenSuffix}" suffix`,
renameToNames: [name.replace(new RegExp(`${forbiddenSuffix}$`), "")]
};
}
if (requiredPrefixes && !requiredPrefixes.some((requiredPrefix) => name.startsWith(requiredPrefix))) {
return {
errorMessage: `have one of the following prefixes: ${(0, import_utils.englishJoinWords)(
requiredPrefixes
)}`,
renameToNames: style ? requiredPrefixes.map((prefix2) => (0, import_utils.convertCase)(style, `${prefix2} ${name}`)) : requiredPrefixes.map((prefix2) => `${prefix2}${name}`)
};
}
if (requiredSuffixes && !requiredSuffixes.some((requiredSuffix) => name.endsWith(requiredSuffix))) {
return {
errorMessage: `have one of the following suffixes: ${(0, import_utils.englishJoinWords)(
requiredSuffixes
)}`,
renameToNames: style ? requiredSuffixes.map((suffix2) => (0, import_utils.convertCase)(style, `${name} ${suffix2}`)) : requiredSuffixes.map((suffix2) => `${name}${suffix2}`)
};
}
if (!style) {
return;
}
const caseRegex = StyleToRegex[style];
if (!caseRegex.test(name)) {
return {
errorMessage: `be in ${style} format`,
renameToNames: [(0, import_utils.convertCase)(style, name)]
};
}
}
};
const checkUnderscore = (isLeading) => (node) => {
const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, "");
report(node, `${isLeading ? "Leading" : "Trailing"} underscores are not allowed`, [
suggestedName
]);
};
const listeners = {};
if (!allowLeadingUnderscore) {
listeners["Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])"] = checkUnderscore(true);
}
if (!allowTrailingUnderscore) {
listeners["Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])"] = checkUnderscore(false);
}
const selectors = new Set(
[types && import_utils.TYPES_KINDS, Object.keys(restOptions)].flat().filter(import_utils.truthy)
);
for (const selector of selectors) {
listeners[selector] = checkNode(selector);
}
return listeners;
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
rule
});