Skip to content

Commit c53cb4e

Browse files
Add ignore config to no-unused-fields rule (#2281)
* Add ignore config to `no-unused-fields` rule * Add changeset * more * more * more * more * more * upd * Update packages/plugin/src/rules/no-unused-fields/index.test.ts * use mdx * rollback * rollback * more * write less --------- Co-authored-by: Dimitri POSTOLOV <[email protected]>
1 parent 1ed70b9 commit c53cb4e

File tree

69 files changed

+333
-110
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+333
-110
lines changed

.changeset/lovely-emus-shake.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': major
3+
---
4+
5+
Add new config option `ignoredFieldSelectors` to `no-unused-fields` rule to ignore all the relay
6+
pagination fields for every connection exposed in schema for example

packages/plugin/src/rules/no-unused-fields/index.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ const ruleTester = new RuleTester<ParserOptionsForTests>({
5050
},
5151
});
5252

53+
const example = rule.meta.docs!.examples!.find(example => example.title.includes('ignoring'));
54+
const [RELAY_SCHEMA, RELAY_QUERY] = example!.code.split('### 2️⃣ YOUR QUERY');
55+
5356
ruleTester.run('no-unused-fields', rule, {
5457
valid: [
5558
{
@@ -95,6 +98,17 @@ ruleTester.run('no-unused-fields', rule, {
9598
},
9699
},
97100
},
101+
{
102+
name: 'should do not report unused fields for Relay',
103+
options: example!.usage,
104+
code: RELAY_SCHEMA,
105+
parserOptions: {
106+
graphQLConfig: {
107+
documents: RELAY_QUERY,
108+
schema: RELAY_SCHEMA,
109+
},
110+
},
111+
},
98112
],
99113
invalid: [
100114
{

packages/plugin/src/rules/no-unused-fields/index.ts

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,109 @@
1-
import { GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
1+
import { FieldDefinitionNode, GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
22
import { GraphQLProjectConfig } from 'graphql-config';
3+
import { FromSchema } from 'json-schema-to-ts';
34
import { ModuleCache } from '../../cache.js';
45
import { SiblingOperations } from '../../siblings.js';
5-
import { GraphQLESLintRule } from '../../types.js';
6+
import { GraphQLESLintRule, GraphQLESTreeNode } from '../../types.js';
67
import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../../utils.js';
78

89
const RULE_ID = 'no-unused-fields';
910

11+
const RELAY_SCHEMA = /* GraphQL */ `
12+
# Root Query Type
13+
type Query {
14+
user: User
15+
}
16+
17+
# User Type
18+
type User {
19+
id: ID!
20+
name: String!
21+
friends(first: Int, after: String): FriendConnection!
22+
}
23+
24+
# FriendConnection Type (Relay Connection)
25+
type FriendConnection {
26+
edges: [FriendEdge]
27+
pageInfo: PageInfo!
28+
}
29+
30+
# FriendEdge Type
31+
type FriendEdge {
32+
cursor: String!
33+
node: Friend!
34+
}
35+
36+
# Friend Type
37+
type Friend {
38+
id: ID!
39+
name: String!
40+
}
41+
42+
# PageInfo Type (Relay Pagination)
43+
type PageInfo {
44+
hasPreviousPage: Boolean!
45+
hasNextPage: Boolean!
46+
startCursor: String
47+
endCursor: String
48+
}
49+
`;
50+
51+
const RELAY_QUERY = /* GraphQL */ `
52+
query {
53+
user {
54+
id
55+
name
56+
friends(first: 10) {
57+
edges {
58+
node {
59+
id
60+
name
61+
}
62+
}
63+
}
64+
}
65+
}
66+
`;
67+
68+
const RELAY_DEFAULT_IGNORED_FIELD_SELECTORS = [
69+
'[parent.name.value=PageInfo][name.value=/(endCursor|startCursor|hasNextPage|hasPreviousPage)/]',
70+
'[parent.name.value=/Edge$/][name.value=cursor]',
71+
'[parent.name.value=/Connection$/][name.value=pageInfo]',
72+
];
73+
74+
const schema = {
75+
type: 'array',
76+
maxItems: 1,
77+
items: {
78+
type: 'object',
79+
additionalProperties: false,
80+
properties: {
81+
ignoredFieldSelectors: {
82+
type: 'array',
83+
uniqueItems: true,
84+
minItems: 1,
85+
description: [
86+
'Fields that will be ignored and are allowed to be unused.',
87+
'',
88+
'E.g. The following selector will ignore all the relay pagination fields for every connection exposed in the schema:',
89+
'```json',
90+
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
91+
'```',
92+
'',
93+
'> These fields are defined by ESLint [`selectors`](https://eslint.org/docs/developer-guide/selectors).',
94+
'> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.',
95+
].join('\n'),
96+
items: {
97+
type: 'string',
98+
pattern: '^\\[(.+)]$',
99+
},
100+
},
101+
},
102+
},
103+
} as const;
104+
105+
export type RuleOptions = FromSchema<typeof schema>;
106+
10107
type UsedFields = Record<string, Set<string>>;
11108

12109
const usedFieldsCache = new ModuleCache<GraphQLProjectConfig['schema'], UsedFields>();
@@ -44,7 +141,7 @@ function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): Us
44141
return usedFields;
45142
}
46143

47-
export const rule: GraphQLESLintRule = {
144+
export const rule: GraphQLESLintRule<RuleOptions> = {
48145
meta: {
49146
messages: {
50147
[RULE_ID]: 'Field "{{fieldName}}" is unused',
@@ -99,23 +196,37 @@ export const rule: GraphQLESLintRule = {
99196
}
100197
`,
101198
},
199+
{
200+
title: 'Correct (ignoring fields)',
201+
usage: [{ ignoredFieldSelectors: RELAY_DEFAULT_IGNORED_FIELD_SELECTORS }],
202+
code: /* GraphQL */ `
203+
### 1️⃣ YOUR SCHEMA
204+
${RELAY_SCHEMA}
205+
206+
### 2️⃣ YOUR QUERY
207+
${RELAY_QUERY}
208+
`,
209+
},
102210
],
103211
},
104212
type: 'suggestion',
105-
schema: [],
213+
schema,
106214
hasSuggestions: true,
107215
},
108216
create(context) {
109217
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
110218
const siblingsOperations = requireSiblingsOperations(RULE_ID, context);
111219
const usedFields = getUsedFields(schema, siblingsOperations);
112-
220+
const { ignoredFieldSelectors } = context.options[0] || {};
221+
const selector = (ignoredFieldSelectors || []).reduce(
222+
(acc, selector) => `${acc}:not(${selector})`,
223+
'FieldDefinition',
224+
);
113225
return {
114-
FieldDefinition(node) {
226+
[selector](node: GraphQLESTreeNode<FieldDefinitionNode>) {
115227
const fieldName = node.name.value;
116228
const parentTypeName = node.parent.name.value;
117229
const isUsed = usedFields[parentTypeName]?.has(fieldName);
118-
119230
if (isUsed) {
120231
return;
121232
}

scripts/generate-docs.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ async function generateDocs(): Promise<void> {
5555
const prettierConfigTs = await prettier.resolveConfig('./_meta.ts');
5656

5757
const result = Object.entries(rules).map(async ([ruleName, rule]) => {
58+
const frontMatterDescription = rule.meta
59+
.docs!.description!.replace(/\n.*/g, '')
60+
.replace(MARKDOWN_LINK_RE, '$1');
5861
const blocks: string[] = [
5962
'---',
60-
`description: ${JSON.stringify(rule.meta.docs!.description!.replace(/\n.*/g, '').replace(MARKDOWN_LINK_RE, '$1'))}`,
63+
`description: ${JSON.stringify(frontMatterDescription)}`,
6164
'---',
6265
`# \`${ruleName}\``,
6366
];
@@ -98,7 +101,7 @@ async function generateDocs(): Promise<void> {
98101
`- Requires GraphQL Schema: \`${requiresSchema}\` [ℹ️](/docs/getting-started#extended-linting-rules-with-graphql-schema)`,
99102
`- Requires GraphQL Operations: \`${requiresSiblings}\` [ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)`,
100103
BR,
101-
docs.description,
104+
docs.description === frontMatterDescription ? '{frontMatter.description}' : docs.description,
102105
);
103106

104107
if (docs.examples?.length > 0) {
@@ -153,7 +156,7 @@ async function generateDocs(): Promise<void> {
153156
);
154157
}
155158
return {
156-
path: resolve(RULES_PATH, `${ruleName}.md`),
159+
path: resolve(RULES_PATH, `${ruleName}.mdx`),
157160
content: blocks.join('\n'),
158161
};
159162
});

website/src/pages/rules/alphabetize.md renamed to website/src/pages/rules/alphabetize.mdx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ fix some of the problems reported by this rule.
1717
- Requires GraphQL Operations: `false`
1818
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1919

20-
Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation
21-
selections and more.
20+
{frontMatter.description}
2221

2322
## Usage Examples
2423

@@ -165,12 +164,12 @@ Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `dir
165164

166165
### `groups` (array)
167166

168-
Custom order group. Example: `['...', 'id', '*', '{']` where:
167+
Order group. Example: `['...', 'id', '*', '{']` where:
169168

170169
- `...` stands for fragment spreads
171170
- `id` stands for field with name `id`
172171
- `*` stands for everything else
173-
- `{` stands for field `selection set`
172+
- `{` stands for fields `selection set`
174173

175174
The object is an array with all elements of the type `string`.
176175

website/src/pages/rules/description-style.md renamed to website/src/pages/rules/description-style.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enables this rule.
1717
- Requires GraphQL Operations: `false`
1818
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1919

20-
Require all comments to follow the same style (either block or inline).
20+
{frontMatter.description}
2121

2222
## Usage Examples
2323

website/src/pages/rules/lone-executable-definition.md renamed to website/src/pages/rules/lone-executable-definition.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ description:
1212
- Requires GraphQL Operations: `false`
1313
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1414

15-
Require queries, mutations, subscriptions or fragments to be located in separate files.
15+
{frontMatter.description}
1616

1717
## Usage Examples
1818

website/src/pages/rules/match-document-filename.md renamed to website/src/pages/rules/match-document-filename.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description: 'This rule allows you to enforce that the file name should match th
1111
- Requires GraphQL Operations: `false`
1212
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1313

14-
This rule allows you to enforce that the file name should match the operation name.
14+
{frontMatter.description}
1515

1616
## Usage Examples
1717

website/src/pages/rules/naming-convention.md renamed to website/src/pages/rules/naming-convention.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ rule.
1818
- Requires GraphQL Operations: `false`
1919
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
2020

21-
Require names to follow specified conventions.
21+
{frontMatter.description}
2222

2323
## Usage Examples
2424

website/src/pages/rules/no-anonymous-operations.md renamed to website/src/pages/rules/no-anonymous-operations.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ enables this rule.
1919
- Requires GraphQL Operations: `false`
2020
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
2121

22-
Require name for your GraphQL operations. This is useful since most GraphQL client libraries are
23-
using the operation name for caching purposes.
22+
{frontMatter.description}
2423

2524
## Usage Examples
2625

website/src/pages/rules/no-deprecated.md renamed to website/src/pages/rules/no-deprecated.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enables this rule.
1717
- Requires GraphQL Operations: `false`
1818
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1919

20-
Enforce that deprecated fields or enum values are not in use by operations.
20+
{frontMatter.description}
2121

2222
## Usage Examples
2323

website/src/pages/rules/no-duplicate-fields.md renamed to website/src/pages/rules/no-duplicate-fields.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ enables this rule.
1919
- Requires GraphQL Operations: `false`
2020
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
2121

22-
Checks for duplicate fields in selection set, variables in operation definition, or in arguments set
23-
of a field.
22+
{frontMatter.description}
2423

2524
## Usage Examples
2625

website/src/pages/rules/no-one-place-fragments.md renamed to website/src/pages/rules/no-one-place-fragments.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description: 'Disallow fragments that are used only in one place.'
1111
- Requires GraphQL Operations: `true`
1212
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1313

14-
Disallow fragments that are used only in one place.
14+
{frontMatter.description}
1515

1616
## Usage Examples
1717

website/src/pages/rules/no-root-type.md renamed to website/src/pages/rules/no-root-type.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: 'Disallow using root types `mutation` and/or `subscription`.'
1414
- Requires GraphQL Operations: `false`
1515
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1616

17-
Disallow using root types `mutation` and/or `subscription`.
17+
{frontMatter.description}
1818

1919
## Usage Examples
2020

website/src/pages/rules/no-scalar-result-type-on-mutation.md renamed to website/src/pages/rules/no-scalar-result-type-on-mutation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: 'Avoid scalar result type on mutation type to make sure to return a
1414
- Requires GraphQL Operations: `false`
1515
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1616

17-
Avoid scalar result type on mutation type to make sure to return a valid state.
17+
{frontMatter.description}
1818

1919
## Usage Examples
2020

website/src/pages/rules/no-typename-prefix.md renamed to website/src/pages/rules/no-typename-prefix.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enables this rule.
1818
- Requires GraphQL Operations: `false`
1919
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
2020

21-
Enforces users to avoid using the type name in a field name while defining your schema.
21+
{frontMatter.description}
2222

2323
## Usage Examples
2424

website/src/pages/rules/no-unreachable-types.md renamed to website/src/pages/rules/no-unreachable-types.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enables this rule.
1717
- Requires GraphQL Operations: `false`
1818
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations)
1919

20-
Requires all types to be reachable at some level by root level fields.
20+
{frontMatter.description}
2121

2222
## Usage Examples
2323

0 commit comments

Comments
 (0)