UNPKG

@graphql-eslint/eslint-plugin

Version:
351 lines (350 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); const lodash_lowercase_1 = tslib_1.__importDefault(require("lodash.lowercase")); const utils_js_1 = require("../utils.js"); const RULE_ID = 'alphabetize'; const fieldsEnum = [ graphql_1.Kind.OBJECT_TYPE_DEFINITION, graphql_1.Kind.INTERFACE_TYPE_DEFINITION, graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION, ]; const valuesEnum = [graphql_1.Kind.ENUM_TYPE_DEFINITION]; const selectionsEnum = [ graphql_1.Kind.OPERATION_DEFINITION, graphql_1.Kind.FRAGMENT_DEFINITION, ]; const variablesEnum = [graphql_1.Kind.OPERATION_DEFINITION]; const argumentsEnum = [ graphql_1.Kind.FIELD_DEFINITION, graphql_1.Kind.FIELD, graphql_1.Kind.DIRECTIVE_DEFINITION, graphql_1.Kind.DIRECTIVE, ]; const schema = { type: 'array', minItems: 1, maxItems: 1, items: { type: 'object', additionalProperties: false, minProperties: 1, properties: { fields: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, items: { enum: fieldsEnum, }, description: 'Fields of `type`, `interface`, and `input`.', }, values: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, items: { enum: valuesEnum, }, description: 'Values of `enum`.', }, selections: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, items: { enum: selectionsEnum, }, description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.', }, variables: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, items: { enum: variablesEnum, }, description: 'Variables of operations `query`, `mutation` and `subscription`.', }, arguments: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, items: { enum: argumentsEnum, }, description: 'Arguments of fields and directives.', }, definitions: { type: 'boolean', description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.', default: false, }, groups: { ...utils_js_1.ARRAY_DEFAULT_OPTIONS, minItems: 2, description: "Custom order group. Example: `['id', '*', 'createdAt', 'updatedAt']` where `*` says for everything else.", }, }, }, }; exports.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: [graphql_1.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: [graphql_1.Kind.OBJECT_TYPE_DEFINITION] }], code: /* GraphQL */ ` type User { age: Int firstName: String! lastName: String! password: String } `, }, { title: 'Incorrect', usage: [{ values: [graphql_1.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: [graphql_1.Kind.ENUM_TYPE_DEFINITION] }], code: /* GraphQL */ ` enum Role { ADMIN GOD SUPER_ADMIN USER } `, }, { title: 'Incorrect', usage: [{ selections: [graphql_1.Kind.OPERATION_DEFINITION] }], code: /* GraphQL */ ` query { me { firstName lastName email # should be before "lastName" } } `, }, { title: 'Correct', usage: [{ selections: [graphql_1.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: [graphql_1.Kind.FIELD, graphql_1.Kind.DIRECTIVE], }, ], }, }, messages: { [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.', }, 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; // Break on comment that not attached to node 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 === graphql_1.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 _a, _b, _c, _d; // Starts from 1, ignore nodes.length <= 1 for (let i = 1; i < nodes.length; i += 1) { const currNode = nodes[i]; const currName = ('alias' in currNode && ((_a = currNode.alias) === null || _a === void 0 ? void 0 : _a.value)) || ('name' in currNode && ((_b = currNode.name) === null || _b === void 0 ? void 0 : _b.value)); if (!currName) { // we don't move unnamed current nodes continue; } const prevNode = nodes[i - 1]; const prevName = ('alias' in prevNode && ((_c = prevNode.alias) === null || _c === void 0 ? void 0 : _c.value)) || ('name' in prevNode && ((_d = prevNode.name) === null || _d === void 0 ? void 0 : _d.value)); if (prevName) { // Compare with lexicographic order const compareResult = prevName.localeCompare(currName); const { groups } = opts; let shouldSortByGroup = false; if (groups === null || groups === void 0 ? 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: { currName, prevName: prevName ? `\`${prevName}\`` : (0, lodash_lowercase_1.default)(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 !== void 0 ? _a : []); const listeners = {}; const kinds = [ fields.has(graphql_1.Kind.OBJECT_TYPE_DEFINITION) && [ graphql_1.Kind.OBJECT_TYPE_DEFINITION, graphql_1.Kind.OBJECT_TYPE_EXTENSION, ], fields.has(graphql_1.Kind.INTERFACE_TYPE_DEFINITION) && [ graphql_1.Kind.INTERFACE_TYPE_DEFINITION, graphql_1.Kind.INTERFACE_TYPE_EXTENSION, ], fields.has(graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION) && [ graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION, graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION, ], ] .filter(utils_js_1.truthy) .flat(); const fieldsSelector = kinds.join(','); const hasEnumValues = ((_b = opts.values) === null || _b === void 0 ? void 0 : _b[0]) === graphql_1.Kind.ENUM_TYPE_DEFINITION; const selectionsSelector = (_c = opts.selections) === null || _c === void 0 ? void 0 : _c.join(','); const hasVariables = ((_d = opts.variables) === null || _d === void 0 ? void 0 : _d[0]) === graphql_1.Kind.OPERATION_DEFINITION; const argumentsSelector = (_e = opts.arguments) === null || _e === void 0 ? void 0 : _e.join(','); if (fieldsSelector) { listeners[fieldsSelector] = (node) => { checkNodes(node.fields); }; } if (hasEnumValues) { const enumValuesSelector = [graphql_1.Kind.ENUM_TYPE_DEFINITION, graphql_1.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 _a; checkNodes((_a = node.variableDefinitions) === null || _a === void 0 ? void 0 : _a.map(varDef => varDef.variable)); }; } if (argumentsSelector) { listeners[argumentsSelector] = (node) => { checkNodes(node.arguments); }; } if (opts.definitions) { listeners.Document = node => { checkNodes(node.definitions); }; } return listeners; }, };