UNPKG

@graphql-eslint/eslint-plugin

Version:
365 lines (364 loc) • 11.3 kB
import "../chunk-BMTV3EA2.js"; import { Kind } from "graphql"; import lowerCase from "lodash.lowercase"; import { ARRAY_DEFAULT_OPTIONS, displayNodeName, truthy } from "../utils.js"; const RULE_ID = "alphabetize"; const fieldsEnum = [ Kind.OBJECT_TYPE_DEFINITION, Kind.INTERFACE_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_DEFINITION ]; const valuesEnum = [Kind.ENUM_TYPE_DEFINITION]; const selectionsEnum = [ Kind.OPERATION_DEFINITION, Kind.FRAGMENT_DEFINITION ]; const variablesEnum = [Kind.OPERATION_DEFINITION]; const argumentsEnum = [ Kind.FIELD_DEFINITION, Kind.FIELD, Kind.DIRECTIVE_DEFINITION, Kind.DIRECTIVE ]; const schema = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { fields: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: fieldsEnum }, description: "Fields of `type`, `interface`, and `input`." }, values: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: valuesEnum }, description: "Values of `enum`." }, selections: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: selectionsEnum }, description: "Selections of `fragment` and operations `query`, `mutation` and `subscription`." }, variables: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: variablesEnum }, description: "Variables of operations `query`, `mutation` and `subscription`." }, arguments: { ...ARRAY_DEFAULT_OPTIONS, items: { enum: argumentsEnum }, description: "Arguments of fields and directives." }, definitions: { type: "boolean", description: "Definitions \u2013 `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.", default: false }, groups: { ...ARRAY_DEFAULT_OPTIONS, minItems: 2, description: "Custom order group. Example: `['id', '*', 'createdAt', 'updatedAt']` where `*` says for everything else." } } } }; const rule = { meta: { type: "suggestion", fixable: "code", docs: { category: ["Schema", "Operations"], description: "Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.", url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`, examples: [ { title: "Incorrect", usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` type User { password: String firstName: String! # should be before "password" age: Int # should be before "firstName" lastName: String! } ` ) }, { title: "Correct", usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` type User { age: Int firstName: String! lastName: String! password: String } ` ) }, { title: "Incorrect", usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` enum Role { SUPER_ADMIN ADMIN # should be before "SUPER_ADMIN" USER GOD # should be before "USER" } ` ) }, { title: "Correct", usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` enum Role { ADMIN GOD SUPER_ADMIN USER } ` ) }, { title: "Incorrect", usage: [{ selections: [Kind.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { firstName lastName email # should be before "lastName" } } ` ) }, { title: "Correct", usage: [{ selections: [Kind.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { email firstName lastName } } ` ) } ], configOptions: { schema: [ { fields: fieldsEnum, values: valuesEnum, arguments: argumentsEnum // TODO: add in graphql-eslint v4 // definitions: true, // groups: ['id', '*', 'createdAt', 'updatedAt'] } ], operations: [ { selections: selectionsEnum, variables: variablesEnum, arguments: [Kind.FIELD, Kind.DIRECTIVE] } ] } }, messages: { [RULE_ID]: "{{ currNode }} should be before {{ prevNode }}" }, schema }, create(context) { var _a, _b, _c, _d, _e; const sourceCode = context.getSourceCode(); function isNodeAndCommentOnSameLine(node, comment) { return node.loc.end.line === comment.loc.start.line; } function getBeforeComments(node) { const commentsBefore = sourceCode.getCommentsBefore(node); if (commentsBefore.length === 0) { return []; } const tokenBefore = sourceCode.getTokenBefore(node); if (tokenBefore) { return commentsBefore.filter((comment) => !isNodeAndCommentOnSameLine(tokenBefore, comment)); } const filteredComments = []; const nodeLine = node.loc.start.line; for (let i = commentsBefore.length - 1; i >= 0; i -= 1) { const comment = commentsBefore[i]; if (nodeLine - comment.loc.start.line - filteredComments.length > 1) { break; } filteredComments.unshift(comment); } return filteredComments; } function getRangeWithComments(node) { if (node.kind === Kind.VARIABLE) { node = node.parent; } const [firstBeforeComment] = getBeforeComments(node); const [firstAfterComment] = sourceCode.getCommentsAfter(node); const from = firstBeforeComment || node; const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node; return [from.range[0], to.range[1]]; } function checkNodes(nodes = []) { var _a2, _b2, _c2, _d2; for (let i = 1; i < nodes.length; i += 1) { const currNode = nodes[i]; const currName = "alias" in currNode && ((_a2 = currNode.alias) == null ? void 0 : _a2.value) || "name" in currNode && ((_b2 = currNode.name) == null ? void 0 : _b2.value); if (!currName) { continue; } const prevNode = nodes[i - 1]; const prevName = "alias" in prevNode && ((_c2 = prevNode.alias) == null ? void 0 : _c2.value) || "name" in prevNode && ((_d2 = prevNode.name) == null ? void 0 : _d2.value); if (prevName) { const compareResult = prevName.localeCompare(currName); const { groups } = opts; let shouldSortByGroup = false; if (groups == null ? void 0 : groups.length) { if (!groups.includes("*")) { throw new Error("`groups` option should contain `*` string."); } let indexForPrev = groups.indexOf(prevName); if (indexForPrev === -1) indexForPrev = groups.indexOf("*"); let indexForCurr = groups.indexOf(currName); if (indexForCurr === -1) indexForCurr = groups.indexOf("*"); shouldSortByGroup = indexForPrev - indexForCurr > 0; if (indexForPrev < indexForCurr) { continue; } } const shouldSort = compareResult === 1; if (!shouldSortByGroup && !shouldSort) { const isSameName = compareResult === 0; if (!isSameName || !prevNode.kind.endsWith("Extension") || currNode.kind.endsWith("Extension")) { continue; } } } context.report({ // @ts-expect-error can't be undefined node: "alias" in currNode && currNode.alias || currNode.name, messageId: RULE_ID, data: { currNode: displayNodeName(currNode), prevNode: prevName ? displayNodeName(prevNode) : lowerCase(prevNode.kind) }, *fix(fixer) { const prevRange = getRangeWithComments(prevNode); const currRange = getRangeWithComments(currNode); yield fixer.replaceTextRange( prevRange, sourceCode.getText({ range: currRange }) ); yield fixer.replaceTextRange( currRange, sourceCode.getText({ range: prevRange }) ); } }); } } const opts = context.options[0]; const fields = new Set((_a = opts.fields) != null ? _a : []); const listeners = {}; const kinds = [ fields.has(Kind.OBJECT_TYPE_DEFINITION) && [ Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION ], fields.has(Kind.INTERFACE_TYPE_DEFINITION) && [ Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION ], fields.has(Kind.INPUT_OBJECT_TYPE_DEFINITION) && [ Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_EXTENSION ] ].filter(truthy).flat(); const fieldsSelector = kinds.join(","); const hasEnumValues = ((_b = opts.values) == null ? void 0 : _b[0]) === Kind.ENUM_TYPE_DEFINITION; const selectionsSelector = (_c = opts.selections) == null ? void 0 : _c.join(","); const hasVariables = ((_d = opts.variables) == null ? void 0 : _d[0]) === Kind.OPERATION_DEFINITION; const argumentsSelector = (_e = opts.arguments) == null ? void 0 : _e.join(","); if (fieldsSelector) { listeners[fieldsSelector] = (node) => { checkNodes(node.fields); }; } if (hasEnumValues) { const enumValuesSelector = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION].join(","); listeners[enumValuesSelector] = (node) => { checkNodes(node.values); }; } if (selectionsSelector) { listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => { checkNodes(node.selections); }; } if (hasVariables) { listeners.OperationDefinition = (node) => { var _a2; checkNodes((_a2 = node.variableDefinitions) == null ? void 0 : _a2.map((varDef) => varDef.variable)); }; } if (argumentsSelector) { listeners[argumentsSelector] = (node) => { checkNodes(node.arguments); }; } if (opts.definitions) { listeners.Document = (node) => { checkNodes(node.definitions); }; } return listeners; } }; export { rule };