UNPKG

@graphql-eslint/eslint-plugin

Version:
435 lines (433 loc) • 14.6 kB
"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 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 });