diff --git a/.changeset/lovely-emus-shake.md b/.changeset/lovely-emus-shake.md new file mode 100644 index 00000000000..b8d53598bcc --- /dev/null +++ b/.changeset/lovely-emus-shake.md @@ -0,0 +1,5 @@ +--- +"@graphql-eslint/eslint-plugin": major +--- + +Add ignore config to `no-unused-fields` rule diff --git a/packages/plugin/__tests__/no-unused-fields.spec.ts b/packages/plugin/__tests__/no-unused-fields.spec.ts index d2f64bd242b..416af3ad5eb 100644 --- a/packages/plugin/__tests__/no-unused-fields.spec.ts +++ b/packages/plugin/__tests__/no-unused-fields.spec.ts @@ -94,6 +94,27 @@ ruleTester.run('no-unused-fields', rule, { }, }, }, + { + name: 'should allow fields if they are ignored', + options: [{ ignoredFieldSelectors: ['FieldDefinition[name.value=firstName]'] }], + code: /* GraphQL */ ` + type User { + id: ID! + firstName: String + } + `, + parserOptions: { + graphQLConfig: { + documents: /* GraphQL */ ` + { + user(id: 1) { + id + } + } + `, + }, + }, + }, ], invalid: [ { diff --git a/packages/plugin/src/rules/no-unused-fields.ts b/packages/plugin/src/rules/no-unused-fields.ts index 60bd7c3af90..e14f4b655b2 100644 --- a/packages/plugin/src/rules/no-unused-fields.ts +++ b/packages/plugin/src/rules/no-unused-fields.ts @@ -1,10 +1,39 @@ -import { GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql'; +import { FieldDefinitionNode, GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql'; +import { FromSchema } from 'json-schema-to-ts'; +import { GraphQLESTreeNode } from '../estree-converter/types.js'; import { SiblingOperations } from '../siblings.js'; -import { GraphQLESLintRule } from '../types.js'; +import { GraphQLESLintRule, GraphQLESLintRuleListener } from '../types.js'; import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils.js'; const RULE_ID = 'no-unused-fields'; +const schema = { + type: 'array', + maxItems: 1, + items: { + type: 'object', + additionalProperties: false, + properties: { + ignoredFieldSelectors: { + type: 'array', + uniqueItems: true, + minItems: 1, + description: [ + 'Fields that will be ignored and are allowed to be unused.', + '', + '> These fields are defined by ESLint [`selectors`](https://eslint.org/docs/developer-guide/selectors). Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.', + ].join('\n'), + items: { + type: 'string', + pattern: '^FieldDefinition(.+)$', + }, + }, + }, + }, +} as const; + +export type RuleOptions = FromSchema; + type UsedFields = Record>; let usedFieldsCache: UsedFields; @@ -41,7 +70,7 @@ function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): Us return usedFieldsCache; } -export const rule: GraphQLESLintRule = { +export const rule: GraphQLESLintRule = { meta: { messages: { [RULE_ID]: 'Field "{{fieldName}}" is unused', @@ -96,45 +125,74 @@ export const rule: GraphQLESLintRule = { } `, }, + { + title: 'Correct (ignoring fields)', + usage: [{ ignoredFieldSelectors: ['FieldDefinition[name.value=lastName]'] }], + code: /* GraphQL */ ` + type User { + id: ID! + firstName: String + lastName: String + } + + type Query { + me: User + } + + query { + me { + id + firstName + } + } + `, + }, ], }, type: 'suggestion', - schema: [], + schema, hasSuggestions: true, }, create(context) { const schema = requireGraphQLSchemaFromContext(RULE_ID, context); const siblingsOperations = requireSiblingsOperations(RULE_ID, context); const usedFields = getUsedFields(schema, siblingsOperations); + const { ignoredFieldSelectors } = context.options[0] || {}; + const selector = (ignoredFieldSelectors || []).reduce( + (acc, selector) => `${acc}:not(${selector})`, + 'FieldDefinition', + ); - return { - FieldDefinition(node) { - const fieldName = node.name.value; - const parentTypeName = node.parent.name.value; - const isUsed = usedFields[parentTypeName]?.has(fieldName); - - if (isUsed) { - return; - } - - context.report({ - node: node.name, - messageId: RULE_ID, - data: { fieldName }, - suggest: [ - { - desc: `Remove \`${fieldName}\` field`, - fix(fixer) { - const sourceCode = context.getSourceCode() as any; - const tokenBefore = sourceCode.getTokenBefore(node); - const tokenAfter = sourceCode.getTokenAfter(node); - const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}'; - return fixer.remove((isEmptyType ? node.parent : node) as any); - }, + const listener: GraphQLESLintRuleListener = (node: GraphQLESTreeNode) => { + const fieldName = node.name.value; + const parentTypeName = node.parent.name.value; + const isUsed = usedFields[parentTypeName]?.has(fieldName); + + if (isUsed) { + return; + } + + context.report({ + node: node.name, + messageId: RULE_ID, + data: { fieldName }, + suggest: [ + { + desc: `Remove \`${fieldName}\` field`, + fix(fixer) { + const sourceCode = context.getSourceCode() as any; + const tokenBefore = sourceCode.getTokenBefore(node); + const tokenAfter = sourceCode.getTokenAfter(node); + const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}'; + return fixer.remove((isEmptyType ? node.parent : node) as any); }, - ], - }); - }, + }, + ], + }); + }; + + return { + [selector]: listener, }; }, }; diff --git a/website/src/pages/rules/no-unused-fields.md b/website/src/pages/rules/no-unused-fields.md index 9d9ac1df456..02c216addc4 100644 --- a/website/src/pages/rules/no-unused-fields.md +++ b/website/src/pages/rules/no-unused-fields.md @@ -59,6 +59,49 @@ query { } ``` +### Correct (ignoring fields) + +```graphql +# eslint @graphql-eslint/no-unused-fields: ['error', { ignoredFieldSelectors: ['FieldDefinition[name.value=lastName]'] }] + +type User { + id: ID! + firstName: String + lastName: String +} + +type Query { + me: User +} + +query { + me { + id + firstName + } +} +``` + +## Config Schema + +The schema defines the following properties: + +### `ignoredFieldSelectors` (array) + +Fields that will be ignored and are allowed to be unused. + +> These fields are defined by ESLint +> [`selectors`](https://eslint.org/docs/developer-guide/selectors) . Paste or drop code into the +> editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your +> selector. + +The object is an array with all elements of the type `string`. + +Additional restrictions: + +- Minimum items: `1` +- Unique items: `true` + ## Resources - [Rule source](https://github.com/B2o5T/graphql-eslint/tree/master/packages/plugin/src/rules/no-unused-fields.ts)