UNPKG

@graphql-eslint/eslint-plugin

Version:
367 lines (363 loc) • 12.5 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } 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 _lodashlowercase = require('lodash.lowercase'); var _lodashlowercase2 = _interopRequireDefault(_lodashlowercase); var _utilsjs = require('../../utils.js'); const RULE_ID = "alphabetize"; const fieldsEnum = [ _graphql.Kind.OBJECT_TYPE_DEFINITION, _graphql.Kind.INTERFACE_TYPE_DEFINITION, _graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION ]; const selectionsEnum = [ _graphql.Kind.OPERATION_DEFINITION, _graphql.Kind.FRAGMENT_DEFINITION ]; const argumentsEnum = [ _graphql.Kind.FIELD_DEFINITION, _graphql.Kind.FIELD, _graphql.Kind.DIRECTIVE_DEFINITION, _graphql.Kind.DIRECTIVE ]; const schema = { type: "array", minItems: 1, maxItems: 1, items: { type: "object", additionalProperties: false, minProperties: 1, properties: { fields: { ..._utilsjs.ARRAY_DEFAULT_OPTIONS, items: { enum: fieldsEnum }, description: "Fields of `type`, `interface`, and `input`." }, values: { type: "boolean", description: "Values of `enum`." }, selections: { ..._utilsjs.ARRAY_DEFAULT_OPTIONS, items: { enum: selectionsEnum }, description: "Selections of `fragment` and operations `query`, `mutation` and `subscription`." }, variables: { type: "boolean", description: "Variables of operations `query`, `mutation` and `subscription`." }, arguments: { ..._utilsjs.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`." }, groups: { ..._utilsjs.ARRAY_DEFAULT_OPTIONS, minItems: 2, description: [ "Order group. Example: `['...', 'id', '*', '{']` where:", "- `...` stands for fragment spreads", "- `id` stands for field with name `id`", "- `*` stands for everything else", "- `{` stands for fields `selection set`" ].join("\n") } } } }; 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: [_graphql.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.Kind.OBJECT_TYPE_DEFINITION] }], code: ( /* GraphQL */ ` type User { age: Int firstName: String! lastName: String! password: String } ` ) }, { title: "Incorrect", usage: [{ values: true }], code: ( /* GraphQL */ ` enum Role { SUPER_ADMIN ADMIN # should be before "SUPER_ADMIN" USER GOD # should be before "USER" } ` ) }, { title: "Correct", usage: [{ values: true }], code: ( /* GraphQL */ ` enum Role { ADMIN GOD SUPER_ADMIN USER } ` ) }, { title: "Incorrect", usage: [{ selections: [_graphql.Kind.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { firstName lastName email # should be before "lastName" } } ` ) }, { title: "Correct", usage: [{ selections: [_graphql.Kind.OPERATION_DEFINITION] }], code: ( /* GraphQL */ ` query { me { email firstName lastName } } ` ) } ], configOptions: { schema: [ { definitions: true, fields: fieldsEnum, values: true, arguments: argumentsEnum, groups: ["id", "*", "createdAt", "updatedAt"] } ], operations: [ { definitions: true, selections: selectionsEnum, variables: true, arguments: [_graphql.Kind.FIELD, _graphql.Kind.DIRECTIVE], groups: ["...", "id", "*", "{"] } ] } }, messages: { [RULE_ID]: "{{ currNode }} should be before {{ prevNode }}" }, schema }, create(context) { 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 === _graphql.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 = []) { for (let i = 1; i < nodes.length; i += 1) { const currNode = nodes[i]; const currName = getName(currNode); if (!currName) { continue; } const prevNode = nodes[i - 1]; const prevName = getName(prevNode); if (prevName) { const compareResult = prevName.localeCompare(currName); const { groups } = opts; let shouldSortByGroup = false; if (_optionalChain([groups, 'optionalAccess', _ => _.length])) { if (!groups.includes("*")) { throw new Error("`groups` option should contain `*` string."); } const indexForPrev = getIndex({ node: prevNode, groups }); const indexForCurr = getIndex({ node: currNode, groups }); 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: _utilsjs.displayNodeName.call(void 0, currNode), prevNode: prevName ? _utilsjs.displayNodeName.call(void 0, prevNode) : _lodashlowercase2.default.call(void 0, 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(_nullishCoalesce(opts.fields, () => ( []))); const listeners = {}; const kinds = [ fields.has(_graphql.Kind.OBJECT_TYPE_DEFINITION) && [ _graphql.Kind.OBJECT_TYPE_DEFINITION, _graphql.Kind.OBJECT_TYPE_EXTENSION ], fields.has(_graphql.Kind.INTERFACE_TYPE_DEFINITION) && [ _graphql.Kind.INTERFACE_TYPE_DEFINITION, _graphql.Kind.INTERFACE_TYPE_EXTENSION ], fields.has(_graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION) && [ _graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION, _graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION ] ].filter((v) => !!v).flat(); const fieldsSelector = kinds.join(","); const selectionsSelector = _optionalChain([opts, 'access', _2 => _2.selections, 'optionalAccess', _3 => _3.join, 'call', _4 => _4(",")]); const argumentsSelector = _optionalChain([opts, 'access', _5 => _5.arguments, 'optionalAccess', _6 => _6.join, 'call', _7 => _7(",")]); if (fieldsSelector) { listeners[fieldsSelector] = (node) => { checkNodes(node.fields); }; } if (opts.values) { const enumValuesSelector = [_graphql.Kind.ENUM_TYPE_DEFINITION, _graphql.Kind.ENUM_TYPE_EXTENSION].join(","); listeners[enumValuesSelector] = (node) => { checkNodes(node.values); }; } if (selectionsSelector) { listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => { checkNodes(node.selections); }; } if (opts.variables) { listeners.OperationDefinition = (node) => { checkNodes(_optionalChain([node, 'access', _8 => _8.variableDefinitions, 'optionalAccess', _9 => _9.map, 'call', _10 => _10((varDef) => varDef.variable)])); }; } if (argumentsSelector) { listeners[argumentsSelector] = (node) => { checkNodes(node.arguments); }; } if (opts.definitions) { listeners.Document = (node) => { checkNodes(node.definitions); }; } return listeners; } }; function getIndex({ node, groups }) { let index = groups.indexOf(getName(node)); if (index === -1 && "selectionSet" in node && node.selectionSet) index = groups.indexOf("{"); if (index === -1 && node.kind === _graphql.Kind.FRAGMENT_SPREAD) index = groups.indexOf("..."); if (index === -1) index = groups.indexOf("*"); return index; } function getName(node) { return "alias" in node && _optionalChain([node, 'access', _11 => _11.alias, 'optionalAccess', _12 => _12.value]) || // "name" in node && _optionalChain([node, 'access', _13 => _13.name, 'optionalAccess', _14 => _14.value]) || ""; } exports.rule = rule;