UNPKG

@graphql-eslint/eslint-plugin

Version:
453 lines (442 loc) • 16.1 kB
"use strict";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;