UNPKG

@graphql-eslint/eslint-plugin

Version:
347 lines (346 loc) 13.5 kB
import { Kind, } from 'graphql'; import lowerCase from 'lodash.lowercase'; import { ARRAY_DEFAULT_OPTIONS, 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 – `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.", }, }, }, }; export 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://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`, 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]: '`{{ 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 === 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}\`` : 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 !== void 0 ? _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 || _b === void 0 ? void 0 : _b[0]) === 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]) === 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 = [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 _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; }, };