Skip to content

Commit bcbda42

Browse files
authored
feat: add suggestions for require-id-when-available, require-deprecation-date, no-deprecated and no-scalar-result-type-on-mutation rules (#970)
1 parent 63c5c78 commit bcbda42

10 files changed

+184
-52
lines changed

.changeset/breezy-poems-begin.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: add suggestions for `require-id-when-available`, `require-deprecation-date`, `no-deprecated` and `no-scalar-result-type-on-mutation` rules

packages/plugin/src/rules/no-deprecated.ts

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import { EnumValueNode, FieldNode, Kind } from 'graphql';
12
import { requireGraphQLSchemaFromContext } from '../utils';
23
import { GraphQLESLintRule } from '../types';
4+
import { GraphQLESTreeNode } from '../estree-parser';
35

4-
const NO_DEPRECATED = 'NO_DEPRECATED';
6+
const RULE_ID = 'no-deprecated';
57

68
const rule: GraphQLESLintRule<[], true> = {
79
meta: {
810
type: 'suggestion',
11+
hasSuggestions: true,
912
docs: {
1013
category: 'Operations',
1114
description: `Enforce that deprecated fields or enum values are not in use by operations.`,
12-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-deprecated.md`,
15+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
1316
requiresSchema: true,
1417
examples: [
1518
{
@@ -76,44 +79,47 @@ const rule: GraphQLESLintRule<[], true> = {
7679
recommended: true,
7780
},
7881
messages: {
79-
[NO_DEPRECATED]: `This {{ type }} is marked as deprecated in your GraphQL schema {{ reason }}`,
82+
[RULE_ID]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
8083
},
8184
schema: [],
8285
},
8386
create(context) {
87+
requireGraphQLSchemaFromContext(RULE_ID, context);
88+
89+
function report(node: GraphQLESTreeNode<EnumValueNode | FieldNode>, reason: string): void {
90+
const nodeName = node.type === Kind.ENUM ? node.value : node.name.value;
91+
const nodeType = node.type === Kind.ENUM ? 'enum value' : 'field';
92+
context.report({
93+
node,
94+
messageId: RULE_ID,
95+
data: {
96+
type: nodeType,
97+
reason,
98+
},
99+
suggest: [
100+
{
101+
desc: `Remove \`${nodeName}\` ${nodeType}`,
102+
fix: fixer => fixer.remove(node as any),
103+
},
104+
],
105+
});
106+
}
107+
84108
return {
85109
EnumValue(node) {
86-
requireGraphQLSchemaFromContext('no-deprecated', context);
87110
const typeInfo = node.typeInfo();
111+
const reason = typeInfo.enumValue?.deprecationReason;
88112

89-
if (typeInfo && typeInfo.enumValue) {
90-
if (typeInfo.enumValue.deprecationReason) {
91-
context.report({
92-
node,
93-
messageId: NO_DEPRECATED,
94-
data: {
95-
type: 'enum value',
96-
reason: typeInfo.enumValue.deprecationReason ? `(reason: ${typeInfo.enumValue.deprecationReason})` : '',
97-
},
98-
});
99-
}
113+
if (reason) {
114+
report(node, reason);
100115
}
101116
},
102117
Field(node) {
103-
requireGraphQLSchemaFromContext('no-deprecated', context);
104118
const typeInfo = node.typeInfo();
119+
const reason = typeInfo.fieldDef?.deprecationReason;
105120

106-
if (typeInfo && typeInfo.fieldDef) {
107-
if (typeInfo.fieldDef.deprecationReason) {
108-
context.report({
109-
node: node.name,
110-
messageId: NO_DEPRECATED,
111-
data: {
112-
type: 'field',
113-
reason: typeInfo.fieldDef.deprecationReason ? `(reason: ${typeInfo.fieldDef.deprecationReason})` : '',
114-
},
115-
});
116-
}
121+
if (reason) {
122+
report(node, reason);
117123
}
118124
},
119125
};

packages/plugin/src/rules/no-scalar-result-type-on-mutation.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import { Kind, isScalarType, NameNode } from 'graphql';
1+
import { isScalarType, NameNode } from 'graphql';
22
import { requireGraphQLSchemaFromContext } from '../utils';
33
import { GraphQLESLintRule } from '../types';
44
import { GraphQLESTreeNode } from '../estree-parser';
55

6+
const RULE_ID = 'no-scalar-result-type-on-mutation';
7+
68
const rule: GraphQLESLintRule = {
79
meta: {
810
type: 'suggestion',
11+
hasSuggestions: true,
912
docs: {
1013
category: 'Schema',
1114
description: 'Avoid scalar result type on mutation type to make sure to return a valid state.',
12-
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-scalar-result-type-on-mutation.md',
15+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
1316
requiresSchema: true,
1417
examples: [
1518
{
@@ -33,14 +36,14 @@ const rule: GraphQLESLintRule = {
3336
schema: [],
3437
},
3538
create(context) {
36-
const schema = requireGraphQLSchemaFromContext('no-scalar-result-type-on-mutation', context);
39+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
3740
const mutationType = schema.getMutationType();
3841
if (!mutationType) {
3942
return {};
4043
}
4144
const selector = [
42-
`:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
43-
`> ${Kind.FIELD_DEFINITION} > .gqlType ${Kind.NAME}`,
45+
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
46+
'> FieldDefinition > .gqlType Name',
4447
].join(' ');
4548

4649
return {
@@ -50,7 +53,13 @@ const rule: GraphQLESLintRule = {
5053
if (isScalarType(graphQLType)) {
5154
context.report({
5255
node,
53-
message: `Unexpected scalar result type "${typeName}"`,
56+
message: `Unexpected scalar result type \`${typeName}\`.`,
57+
suggest: [
58+
{
59+
desc: `Remove \`${typeName}\``,
60+
fix: fixer => fixer.remove(node as any),
61+
},
62+
],
5463
});
5564
}
5665
},

packages/plugin/src/rules/require-deprecation-date.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { DirectiveNode } from 'graphql';
12
import { GraphQLESLintRule } from '../types';
2-
import { valueFromNode } from '../estree-parser/utils';
3+
import { GraphQLESTreeNode, valueFromNode } from '../estree-parser';
34

45
// eslint-disable-next-line unicorn/better-regex
56
const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
@@ -12,6 +13,7 @@ const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
1213
const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
1314
meta: {
1415
type: 'suggestion',
16+
hasSuggestions: true,
1517
docs: {
1618
category: 'Schema',
1719
description:
@@ -67,7 +69,7 @@ const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
6769
},
6870
create(context) {
6971
return {
70-
'Directive[name.value=deprecated]'(node) {
72+
'Directive[name.value=deprecated]'(node: GraphQLESTreeNode<DirectiveNode>) {
7173
const argName = context.options[0]?.argumentName || 'deletionDate';
7274
const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
7375

@@ -78,7 +80,7 @@ const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
7880
});
7981
return;
8082
}
81-
const deletionDate = valueFromNode(deletionDateNode.value);
83+
const deletionDate = valueFromNode(deletionDateNode.value as any);
8284
const isValidDate = DATE_REGEX.test(deletionDate);
8385

8486
if (!isValidDate) {
@@ -104,12 +106,18 @@ const rule: GraphQLESLintRule<[{ argumentName?: string }]> = {
104106
const canRemove = Date.now() > deletionDateInMS;
105107

106108
if (canRemove) {
109+
const { parent } = node as any;
110+
const nodeName = parent.name.value;
107111
context.report({
108-
node: node.parent.name,
112+
node: parent.name,
109113
messageId: MESSAGE_CAN_BE_REMOVED,
110-
data: {
111-
nodeName: node.parent.name.value,
112-
},
114+
data: { nodeName },
115+
suggest: [
116+
{
117+
desc: `Remove \`${nodeName}\``,
118+
fix: fixer => fixer.remove(parent),
119+
},
120+
],
113121
});
114122
}
115123
},

packages/plugin/src/rules/require-id-when-available.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunct
2222
const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
2323
meta: {
2424
type: 'suggestion',
25+
hasSuggestions: true,
2526
docs: {
2627
category: 'Operations',
2728
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
@@ -167,6 +168,10 @@ const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true> = {
167168
fieldName,
168169
addition,
169170
},
171+
suggest: idNames.map(idName => ({
172+
desc: `Add \`${idName}\` selection`,
173+
fix: fixer => fixer.insertTextBefore((node as any).selections[0], `${idName} `),
174+
})),
170175
});
171176
},
172177
};

packages/plugin/tests/__snapshots__/no-deprecated.spec.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@ exports[` 1`] = `
55

66
> 1 | mutation { something(t: OLD) }
77
| ^^^ This enum value is marked as deprecated in your GraphQL schema (reason: No longer supported)
8+
9+
💡 Suggestion: Remove \`OLD\` enum value
10+
11+
1 | mutation { something(t: ) }
812
`;
913

1014
exports[` 2`] = `
1115
❌ Error
1216

1317
> 1 | mutation { something(t: OLD_WITH_REASON) }
1418
| ^^^^^^^^^^^^^^^ This enum value is marked as deprecated in your GraphQL schema (reason: test)
19+
20+
💡 Suggestion: Remove \`OLD_WITH_REASON\` enum value
21+
22+
1 | mutation { something(t: ) }
1523
`;
1624

1725
exports[` 3`] = `
1826
❌ Error
1927

2028
> 1 | query { oldField }
2129
| ^^^^^^^^ This field is marked as deprecated in your GraphQL schema (reason: No longer supported)
30+
31+
💡 Suggestion: Remove \`oldField\` field
32+
33+
1 | query { }
2234
`;
2335

2436
exports[` 4`] = `
2537
❌ Error
2638

2739
> 1 | query { oldFieldWithReason }
2840
| ^^^^^^^^^^^^^^^^^^ This field is marked as deprecated in your GraphQL schema (reason: test)
41+
42+
💡 Suggestion: Remove \`oldFieldWithReason\` field
43+
44+
1 | query { }
2945
`;

packages/plugin/tests/__snapshots__/no-scalar-result-type-on-mutation.spec.md

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ exports[` 1`] = `
55

66
1 | type Mutation {
77
> 2 | createUser(a: ID, b: ID!, c: [ID]!, d: [ID!]!): Boolean
8-
| ^^^^^^^ Unexpected scalar result type "Boolean"
8+
| ^^^^^^^ Unexpected scalar result type \`Boolean\`.
99
3 | }
10+
11+
💡 Suggestion: Remove \`Boolean\`
12+
13+
1 | type Mutation {
14+
2 | createUser(a: ID, b: ID!, c: [ID]!, d: [ID!]!):
15+
3 | }
1016
`;
1117

1218
exports[` 2`] = `
@@ -16,21 +22,39 @@ exports[` 2`] = `
1622
2 |
1723
3 | extend type Mutation {
1824
> 4 | createUser: Boolean!
19-
| ^^^^^^^ Unexpected scalar result type "Boolean"
25+
| ^^^^^^^ Unexpected scalar result type \`Boolean\`.
2026
5 | }
27+
28+
💡 Suggestion: Remove \`Boolean\`
29+
30+
1 | type Mutation
31+
2 |
32+
3 | extend type Mutation {
33+
4 | createUser: !
34+
5 | }
2135
`;
2236

2337
exports[` 3`] = `
2438
❌ Error
2539

2640
1 | type RootMutation {
2741
> 2 | createUser: [Boolean]
28-
| ^^^^^^^ Unexpected scalar result type "Boolean"
42+
| ^^^^^^^ Unexpected scalar result type \`Boolean\`.
2943
3 | }
3044
4 |
3145
5 | schema {
3246
6 | mutation: RootMutation
3347
7 | }
48+
49+
💡 Suggestion: Remove \`Boolean\`
50+
51+
1 | type RootMutation {
52+
2 | createUser: []
53+
3 | }
54+
4 |
55+
5 | schema {
56+
6 | mutation: RootMutation
57+
7 | }
3458
`;
3559

3660
exports[` 4`] = `
@@ -39,12 +63,23 @@ exports[` 4`] = `
3963
1 | type RootMutation
4064
2 | extend type RootMutation {
4165
> 3 | createUser: [Boolean]!
42-
| ^^^^^^^ Unexpected scalar result type "Boolean"
66+
| ^^^^^^^ Unexpected scalar result type \`Boolean\`.
4367
4 | }
4468
5 |
4569
6 | schema {
4670
7 | mutation: RootMutation
4771
8 | }
72+
73+
💡 Suggestion: Remove \`Boolean\`
74+
75+
1 | type RootMutation
76+
2 | extend type RootMutation {
77+
3 | createUser: []!
78+
4 | }
79+
5 |
80+
6 | schema {
81+
7 | mutation: RootMutation
82+
8 | }
4883
`;
4984

5085
exports[` 5`] = `
@@ -60,13 +95,29 @@ Code
6095

6196
2 | createUser: User!
6297
> 3 | updateUser: Int
63-
| ^^^ Unexpected scalar result type "Int"
98+
| ^^^ Unexpected scalar result type \`Int\`.
6499
4 | deleteUser: [Boolean!]!
65100

101+
💡 Suggestion: Remove \`Int\`
102+
103+
1 | type Mutation {
104+
2 | createUser: User!
105+
3 | updateUser:
106+
4 | deleteUser: [Boolean!]!
107+
5 | }
108+
66109
❌ Error 2/2
67110

68111
3 | updateUser: Int
69112
> 4 | deleteUser: [Boolean!]!
70-
| ^^^^^^^ Unexpected scalar result type "Boolean"
113+
| ^^^^^^^ Unexpected scalar result type \`Boolean\`.
71114
5 | }
115+
116+
💡 Suggestion: Remove \`Boolean\`
117+
118+
1 | type Mutation {
119+
2 | createUser: User!
120+
3 | updateUser: Int
121+
4 | deleteUser: [!]!
122+
5 | }
72123
`;

0 commit comments

Comments
 (0)