Skip to content

Commit 37c1579

Browse files
authored
feat: convert fix to suggestions in no-unreachable-types and no-unused-fields rules (#827)
1 parent cfb48a5 commit 37c1579

14 files changed

+158
-250
lines changed

.changeset/odd-bikes-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
feat: convert fix to suggestions in `no-unreachable-types` and `no-unused-fields` rules

.vscode/settings.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"eslint.enable": true,
2+
"eslint.enable": true,
33
"files.exclude": {
44
"**/.git": true,
55
"**/.DS_Store": true,
@@ -10,5 +10,12 @@
1010
"npm": true,
1111
"**/dist": true
1212
},
13-
"typescript.tsdk": "node_modules/typescript/lib"
14-
}
13+
"typescript.tsdk": "node_modules/typescript/lib",
14+
"eslint.workingDirectories": [
15+
{
16+
"mode": "auto",
17+
"changeProcessCWD": true
18+
}
19+
],
20+
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "graphql"]
21+
}

docs/README.md

Lines changed: 57 additions & 58 deletions
Large diffs are not rendered by default.

docs/rules/no-unreachable-types.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
✅ The `"extends": "plugin:@graphql-eslint/schema-recommended"` property in a configuration file enables this rule.
44

5-
🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
6-
75
- Category: `Schema`
86
- Rule name: `@graphql-eslint/no-unreachable-types`
97
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)

docs/rules/no-unused-fields.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# `no-unused-fields`
22

3-
🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
4-
53
- Category: `Schema`
64
- Rule name: `@graphql-eslint/no-unused-fields`
75
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { pathsToModuleNameMapper } = require('ts-jest/utils');
1+
const { pathsToModuleNameMapper } = require('ts-jest');
22
const { compilerOptions } = require('./tsconfig.json');
33

44
module.exports = {

packages/plugin/src/rules/no-unreachable-types.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
1-
import { Kind } from 'graphql';
2-
import { GraphQLESLintRule } from '../types';
1+
import { ASTKindToNode, Kind } from 'graphql';
2+
import { GraphQLESLintRule, ValueOf } from '../types';
33
import { getLocation, requireReachableTypesFromContext } from '../utils';
4+
import { GraphQLESTreeNode } from '../estree-parser';
45

56
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
6-
const RULE_NAME = 'no-unreachable-types';
7+
const RULE_ID = 'no-unreachable-types';
8+
9+
const KINDS = [
10+
Kind.DIRECTIVE_DEFINITION,
11+
Kind.OBJECT_TYPE_DEFINITION,
12+
Kind.OBJECT_TYPE_EXTENSION,
13+
Kind.INTERFACE_TYPE_DEFINITION,
14+
Kind.INTERFACE_TYPE_EXTENSION,
15+
Kind.SCALAR_TYPE_DEFINITION,
16+
Kind.SCALAR_TYPE_EXTENSION,
17+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
18+
Kind.INPUT_OBJECT_TYPE_EXTENSION,
19+
Kind.UNION_TYPE_DEFINITION,
20+
Kind.UNION_TYPE_EXTENSION,
21+
Kind.ENUM_TYPE_DEFINITION,
22+
Kind.ENUM_TYPE_EXTENSION,
23+
] as const;
24+
25+
type AllowedKind = typeof KINDS[number];
26+
type AllowedKindToNode = Pick<ASTKindToNode, AllowedKind>;
727

828
const rule: GraphQLESLintRule = {
929
meta: {
@@ -13,7 +33,7 @@ const rule: GraphQLESLintRule = {
1333
docs: {
1434
description: `Requires all types to be reachable at some level by root level fields.`,
1535
category: 'Schema',
16-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`,
36+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
1737
requiresSchema: true,
1838
examples: [
1939
{
@@ -45,40 +65,32 @@ const rule: GraphQLESLintRule = {
4565
],
4666
recommended: true,
4767
},
48-
fixable: 'code',
4968
type: 'suggestion',
5069
schema: [],
70+
hasSuggestions: true,
5171
},
5272
create(context) {
53-
const reachableTypes = requireReachableTypesFromContext(RULE_NAME, context);
54-
55-
function ensureReachability(node): void {
56-
const typeName = node.name.value;
57-
58-
if (!reachableTypes.has(typeName)) {
59-
context.report({
60-
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
61-
messageId: UNREACHABLE_TYPE,
62-
data: { typeName },
63-
fix: fixer => fixer.remove(node),
64-
});
65-
}
66-
}
73+
const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
74+
const selector = KINDS.join(',');
6775

6876
return {
69-
DirectiveDefinition: ensureReachability,
70-
ObjectTypeDefinition: ensureReachability,
71-
ObjectTypeExtension: ensureReachability,
72-
InterfaceTypeDefinition: ensureReachability,
73-
InterfaceTypeExtension: ensureReachability,
74-
ScalarTypeDefinition: ensureReachability,
75-
ScalarTypeExtension: ensureReachability,
76-
InputObjectTypeDefinition: ensureReachability,
77-
InputObjectTypeExtension: ensureReachability,
78-
UnionTypeDefinition: ensureReachability,
79-
UnionTypeExtension: ensureReachability,
80-
EnumTypeDefinition: ensureReachability,
81-
EnumTypeExtension: ensureReachability,
77+
[selector](node: GraphQLESTreeNode<ValueOf<AllowedKindToNode>>) {
78+
const typeName = node.name.value;
79+
80+
if (!reachableTypes.has(typeName)) {
81+
context.report({
82+
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
83+
messageId: UNREACHABLE_TYPE,
84+
data: { typeName },
85+
suggest: [
86+
{
87+
desc: `Remove ${typeName}`,
88+
fix: fixer => fixer.remove(node as any),
89+
},
90+
],
91+
});
92+
}
93+
},
8294
};
8395
},
8496
};

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

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GraphQLESLintRule } from '../types';
22
import { getLocation, requireUsedFieldsFromContext } from '../utils';
33

44
const UNUSED_FIELD = 'UNUSED_FIELD';
5-
const RULE_NAME = 'no-unused-fields';
5+
const RULE_ID = 'no-unused-fields';
66

77
const rule: GraphQLESLintRule = {
88
meta: {
@@ -12,7 +12,7 @@ const rule: GraphQLESLintRule = {
1212
docs: {
1313
description: `Requires all fields to be used at some level by siblings operations.`,
1414
category: 'Schema',
15-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`,
15+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
1616
requiresSiblings: true,
1717
requiresSchema: true,
1818
examples: [
@@ -59,12 +59,12 @@ const rule: GraphQLESLintRule = {
5959
},
6060
],
6161
},
62-
fixable: 'code',
6362
type: 'suggestion',
6463
schema: [],
64+
hasSuggestions: true,
6565
},
6666
create(context) {
67-
const usedFields = requireUsedFieldsFromContext(RULE_NAME, context);
67+
const usedFields = requireUsedFieldsFromContext(RULE_ID, context);
6868

6969
return {
7070
FieldDefinition(node) {
@@ -80,24 +80,19 @@ const rule: GraphQLESLintRule = {
8080
loc: getLocation(node.loc, fieldName),
8181
messageId: UNUSED_FIELD,
8282
data: { fieldName },
83-
fix(fixer) {
84-
const sourceCode = context.getSourceCode();
85-
const tokenBefore = (sourceCode as any).getTokenBefore(node);
86-
const tokenAfter = (sourceCode as any).getTokenAfter(node);
87-
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
83+
suggest: [
84+
{
85+
desc: `Remove "${fieldName}" field`,
86+
fix(fixer) {
87+
const sourceCode = context.getSourceCode() as any;
88+
const tokenBefore = sourceCode.getTokenBefore(node);
89+
const tokenAfter = sourceCode.getTokenAfter(node);
90+
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
8891

89-
if (isEmptyType) {
90-
// Remove type
91-
const { parent } = node as any;
92-
const parentBeforeToken = sourceCode.getTokenBefore(parent);
93-
return parentBeforeToken
94-
? fixer.removeRange([parentBeforeToken.range[1], parent.range[1]])
95-
: fixer.remove(parent);
96-
}
97-
98-
// Remove whitespace before token
99-
return fixer.removeRange([tokenBefore.range[1], node.range[1]]);
100-
},
92+
return isEmptyType ? fixer.remove((node as any).parent) : fixer.remove(node as any);
93+
},
94+
},
95+
],
10196
});
10297
},
10398
};

packages/plugin/tests/__snapshots__/no-unreachable-types.spec.ts.snap

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ exports[` 4`] = `
116116
28 | type User implements Address {
117117
29 | city: String
118118
30 | }
119-
31 |
120-
32 | type Query
121-
33 |
119+
31 |
122120
`;
123121

124122
exports[` 5`] = `
@@ -153,9 +151,7 @@ exports[` 5`] = `
153151
28 | type User implements Address {
154152
29 | city: String
155153
30 | }
156-
31 |
157-
32 | type Query
158-
33 |
154+
31 |
159155
`;
160156

161157
exports[` 6`] = `
@@ -190,9 +186,7 @@ exports[` 6`] = `
190186
28 | type User implements Address {
191187
29 | city: String
192188
30 | }
193-
31 |
194-
32 | type Query
195-
33 |
189+
31 |
196190
`;
197191

198192
exports[` 7`] = `
@@ -227,9 +221,7 @@ exports[` 7`] = `
227221
28 | type User implements Address {
228222
29 | city: String
229223
30 | }
230-
31 |
231-
32 | type Query
232-
33 |
224+
31 |
233225
`;
234226

235227
exports[` 8`] = `
@@ -264,9 +256,7 @@ exports[` 8`] = `
264256
28 | type User implements Address {
265257
29 | city: String
266258
30 | }
267-
31 |
268-
32 | type Query
269-
33 |
259+
31 |
270260
`;
271261

272262
exports[` 9`] = `
@@ -301,9 +291,7 @@ exports[` 9`] = `
301291
28 | type User implements Address {
302292
29 | city: String
303293
30 | }
304-
31 |
305-
32 | type Query
306-
33 |
294+
31 |
307295
`;
308296

309297
exports[` 10`] = `
@@ -338,9 +326,7 @@ exports[` 10`] = `
338326
| ^^^^ Type "User" is unreachable
339327
29 | city: String
340328
30 | }
341-
31 |
342-
32 | type Query
343-
33 |
329+
31 |
344330
`;
345331

346332
exports[` 11`] = `

packages/plugin/tests/__snapshots__/no-unused-fields.spec.ts.snap

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,23 @@
22

33
exports[` 1`] = `
44
1 |
5-
2 | # normalize graphql
6-
3 | type User {
7-
4 | id: ID!
8-
> 5 | firstName: String
5+
2 | type User {
6+
3 | id: ID!
7+
> 4 | firstName: String
98
| ^^^^^^^^^ Field "firstName" is unused
10-
6 | }
11-
7 |
9+
5 | }
10+
6 |
1211
`;
1312

1413
exports[` 2`] = `
15-
1 |
16-
2 | # normalize graphql
17-
3 | type Query {
18-
4 | user(id: ID!): User
19-
5 | }
20-
6 |
21-
7 | type Mutation {
22-
> 8 | deleteUser(id: ID!): User
23-
| ^^^^^^^^^^ Field "deleteUser" is unused
24-
9 | }
25-
10 |
14+
1 |
15+
2 | type Query {
16+
3 | user(id: ID!): User
17+
4 | }
18+
5 |
19+
6 | type Mutation {
20+
> 7 | deleteUser(id: ID!): User
21+
| ^^^^^^^^^^ Field "deleteUser" is unused
22+
8 | }
23+
9 |
2624
`;

0 commit comments

Comments
 (0)