Skip to content

Commit 63c5c78

Browse files
authored
feat: add suggestions for description-style, input-name and no-anonymous-operations rules (#969)
1 parent db22ba5 commit 63c5c78

12 files changed

+286
-63
lines changed

.changeset/shiny-kids-hear.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 `description-style`, `input-name` and `no-anonymous-operations` rules

packages/plugin/src/rules/description-style.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type DescriptionStyleRuleConfig = { style: 'inline' | 'block' };
77
const rule: GraphQLESLintRule<[DescriptionStyleRuleConfig]> = {
88
meta: {
99
type: 'suggestion',
10+
hasSuggestions: true,
1011
docs: {
1112
examples: [
1213
{
@@ -55,7 +56,20 @@ const rule: GraphQLESLintRule<[DescriptionStyleRuleConfig]> = {
5556
[`.description[type=StringValue][block!=${isBlock}]`](node: GraphQLESTreeNode<StringValueNode>) {
5657
context.report({
5758
loc: isBlock ? node.loc : node.loc.start,
58-
message: `Unexpected ${isBlock ? 'inline' : 'block'} description`,
59+
message: `Unexpected ${isBlock ? 'inline' : 'block'} description.`,
60+
suggest: [
61+
{
62+
desc: `Change to ${isBlock ? 'block' : 'inline'} style description`,
63+
fix(fixer) {
64+
const sourceCode = context.getSourceCode();
65+
const originalText = sourceCode.getText(node as any);
66+
const newText = isBlock
67+
? originalText.replace(/(^")|("$)/g, '"""')
68+
: originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, ' ');
69+
return fixer.replaceText(node as any, newText);
70+
},
71+
},
72+
],
5973
});
6074
},
6175
};

packages/plugin/src/rules/input-name.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
Kind,
3-
NamedTypeNode,
4-
ObjectTypeExtensionNode,
5-
ObjectTypeDefinitionNode,
6-
InputValueDefinitionNode,
7-
} from 'graphql';
1+
import { Kind, NamedTypeNode, ObjectTypeExtensionNode, ObjectTypeDefinitionNode, NameNode } from 'graphql';
82
import { GraphQLESLintRule } from '../types';
93
import { GraphQLESTreeNode } from '../estree-parser';
104
import { GraphQLESLintRuleListener } from '../testkit';
@@ -26,6 +20,7 @@ const isMutationType = (node: ObjectTypeNode): boolean => isObjectType(node) &&
2620
const rule: GraphQLESLintRule<[InputNameRuleConfig]> = {
2721
meta: {
2822
type: 'suggestion',
23+
hasSuggestions: true,
2924
docs: {
3025
description:
3126
'Require mutation argument to be always called "input" and input type to be called Mutation name + "Input".\nUsing the same name for all input parameters will make your schemas easier to consume and more predictable. Using the same name as mutation for InputType will make it easier to find mutations that InputType belongs to.',
@@ -103,12 +98,18 @@ const rule: GraphQLESLintRule<[InputNameRuleConfig]> = {
10398
(options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
10499

105100
const listeners: GraphQLESLintRuleListener = {
106-
'FieldDefinition > InputValueDefinition[name.value!=input]'(node: GraphQLESTreeNode<InputValueDefinitionNode>) {
107-
if (shouldCheckType((node as any).parent.parent)) {
108-
const name = node.name.value;
101+
'FieldDefinition > InputValueDefinition[name.value!=input] > Name'(node: GraphQLESTreeNode<NameNode>) {
102+
if (shouldCheckType((node as any).parent.parent.parent)) {
103+
const inputName = node.value;
109104
context.report({
110-
node: node.name,
111-
message: `Input "${name}" should be called "input"`,
105+
node,
106+
message: `Input \`${inputName}\` should be called \`input\`.`,
107+
suggest: [
108+
{
109+
desc: 'Rename to `input`',
110+
fix: fixer => fixer.replaceText(node as any, 'input'),
111+
},
112+
],
112113
});
113114
}
114115
},
@@ -134,7 +135,13 @@ const rule: GraphQLESLintRule<[InputNameRuleConfig]> = {
134135
) {
135136
context.report({
136137
node: node.name,
137-
message: `InputType "${name}" name should be "${mutationName}"`,
138+
message: `Input type \`${name}\` name should be \`${mutationName}\`.`,
139+
suggest: [
140+
{
141+
desc: `Rename to \`${mutationName}\``,
142+
fix: fixer => fixer.replaceText(node as any, mutationName),
143+
},
144+
],
138145
});
139146
}
140147
}

packages/plugin/src/rules/no-anonymous-operations.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { OperationDefinitionNode } from 'graphql';
1+
import { Kind, OperationDefinitionNode } from 'graphql';
22
import { GraphQLESLintRule } from '../types';
33
import { getLocation } from '../utils';
44
import { GraphQLESTreeNode } from '../estree-parser';
@@ -8,6 +8,7 @@ const RULE_ID = 'no-anonymous-operations';
88
const rule: GraphQLESLintRule = {
99
meta: {
1010
type: 'suggestion',
11+
hasSuggestions: true,
1112
docs: {
1213
category: 'Operations',
1314
description:
@@ -34,19 +35,30 @@ const rule: GraphQLESLintRule = {
3435
],
3536
},
3637
messages: {
37-
[RULE_ID]: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`,
38+
[RULE_ID]: `Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!`,
3839
},
3940
schema: [],
4041
},
4142
create(context) {
4243
return {
4344
'OperationDefinition[name=undefined]'(node: GraphQLESTreeNode<OperationDefinitionNode>) {
45+
const [firstSelection] = node.selectionSet.selections;
46+
const suggestedName =
47+
firstSelection.type === Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;
48+
4449
context.report({
4550
loc: getLocation(node.loc.start, node.operation),
4651
messageId: RULE_ID,
4752
data: {
4853
operation: node.operation,
4954
},
55+
suggest: [
56+
{
57+
desc: `Rename to \`${suggestedName}\``,
58+
fix: fixer =>
59+
fixer.insertTextAfterRange([node.range[0], node.range[0] + node.operation.length], ` ${suggestedName}`),
60+
},
61+
],
5062
});
5163
},
5264
};

packages/plugin/tests/__snapshots__/description-style.spec.md

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,67 @@ Code
2828

2929
1 | enum EnumUserLanguagesSkill {
3030
> 2 | """
31-
| ^ Unexpected block description
31+
| ^ Unexpected block description.
3232
3 | basic
3333

34+
💡 Suggestion: Change to inline style description
35+
36+
1 | enum EnumUserLanguagesSkill {
37+
2 | " basic "
38+
3 | basic
39+
4 | """
40+
5 | fluent
41+
6 | """
42+
7 | fluent
43+
8 | """
44+
9 | native
45+
10 | """
46+
11 | native
47+
12 | }
48+
3449
❌ Error 2/3
3550

3651
5 | basic
3752
> 6 | """
38-
| ^ Unexpected block description
53+
| ^ Unexpected block description.
3954
7 | fluent
4055

56+
💡 Suggestion: Change to inline style description
57+
58+
1 | enum EnumUserLanguagesSkill {
59+
2 | """
60+
3 | basic
61+
4 | """
62+
5 | basic
63+
6 | " fluent "
64+
7 | fluent
65+
8 | """
66+
9 | native
67+
10 | """
68+
11 | native
69+
12 | }
70+
4171
❌ Error 3/3
4272

4373
9 | fluent
4474
> 10 | """
45-
| ^ Unexpected block description
75+
| ^ Unexpected block description.
4676
11 | native
77+
78+
💡 Suggestion: Change to inline style description
79+
80+
1 | enum EnumUserLanguagesSkill {
81+
2 | """
82+
3 | basic
83+
4 | """
84+
5 | basic
85+
6 | """
86+
7 | fluent
87+
8 | """
88+
9 | fluent
89+
10 | " native "
90+
11 | native
91+
12 | }
4792
`;
4893

4994
exports[` 2`] = `
@@ -67,20 +112,53 @@ Code
67112
❌ Error 1/3
68113

69114
> 1 | " Test "
70-
| ^^^^^^^^ Unexpected inline description
115+
| ^^^^^^^^ Unexpected inline description.
71116
2 | type CreateOneUserPayload {
72117

118+
💡 Suggestion: Change to block style description
119+
120+
1 | """ Test """
121+
2 | type CreateOneUserPayload {
122+
3 | "Created document ID"
123+
4 | recordId: MongoID
124+
5 |
125+
6 | "Created document"
126+
7 | record: User
127+
8 | }
128+
73129
❌ Error 2/3
74130

75131
2 | type CreateOneUserPayload {
76132
> 3 | "Created document ID"
77-
| ^^^^^^^^^^^^^^^^^^^^^ Unexpected inline description
133+
| ^^^^^^^^^^^^^^^^^^^^^ Unexpected inline description.
78134
4 | recordId: MongoID
79135

136+
💡 Suggestion: Change to block style description
137+
138+
1 | " Test "
139+
2 | type CreateOneUserPayload {
140+
3 | """Created document ID"""
141+
4 | recordId: MongoID
142+
5 |
143+
6 | "Created document"
144+
7 | record: User
145+
8 | }
146+
80147
❌ Error 3/3
81148

82149
5 |
83150
> 6 | "Created document"
84-
| ^^^^^^^^^^^^^^^^^^ Unexpected inline description
151+
| ^^^^^^^^^^^^^^^^^^ Unexpected inline description.
85152
7 | record: User
153+
154+
💡 Suggestion: Change to block style description
155+
156+
1 | " Test "
157+
2 | type CreateOneUserPayload {
158+
3 | "Created document ID"
159+
4 | recordId: MongoID
160+
5 |
161+
6 | """Created document"""
162+
7 | record: User
163+
8 | }
86164
`;

packages/plugin/tests/__snapshots__/eslint-directives.spec.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,24 @@ Code
1818

1919
1 | # eslint-disable-next-line non-existing-rule
2020
> 2 | query {
21-
| ^^^^^ Anonymous GraphQL operations are forbidden. Please make sure to name your query!
21+
| ^^^^^ Anonymous GraphQL operations are forbidden. Make sure to name your query!
2222
3 | a
23+
24+
💡 Suggestion: Rename to \`a\`
25+
26+
1 | # eslint-disable-next-line non-existing-rule
27+
2 | query a {
28+
3 | a
29+
4 | }
2330
`;
2431

2532
exports[` 2`] = `
2633
❌ Error
2734

2835
> 1 | query { a }
29-
| ^^^^^ Anonymous GraphQL operations are forbidden. Please make sure to name your query!
36+
| ^^^^^ Anonymous GraphQL operations are forbidden. Make sure to name your query!
37+
38+
💡 Suggestion: Rename to \`a\`
39+
40+
1 | query a { a }
3041
`;

packages/plugin/tests/__snapshots__/examples.spec.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Array [
2626
filePath: examples/basic/query.graphql,
2727
messages: Array [
2828
Object {
29-
message: Anonymous GraphQL operations are forbidden. Please make sure to name your query!,
29+
message: Anonymous GraphQL operations are forbidden. Make sure to name your query!,
3030
ruleId: @graphql-eslint/no-anonymous-operations,
3131
},
3232
],
@@ -57,7 +57,7 @@ Array [
5757
filePath: examples/code-file/query.js,
5858
messages: Array [
5959
Object {
60-
message: Anonymous GraphQL operations are forbidden. Please make sure to name your query!,
60+
message: Anonymous GraphQL operations are forbidden. Make sure to name your query!,
6161
ruleId: @graphql-eslint/no-anonymous-operations,
6262
},
6363
Object {
@@ -182,7 +182,7 @@ Array [
182182
filePath: examples/graphql-config/operations/query.graphql,
183183
messages: Array [
184184
Object {
185-
message: Anonymous GraphQL operations are forbidden. Please make sure to name your query!,
185+
message: Anonymous GraphQL operations are forbidden. Make sure to name your query!,
186186
ruleId: @graphql-eslint/no-anonymous-operations,
187187
},
188188
],
@@ -205,7 +205,7 @@ Array [
205205
filePath: examples/graphql-config-code-file/query.js,
206206
messages: Array [
207207
Object {
208-
message: Anonymous GraphQL operations are forbidden. Please make sure to name your query!,
208+
message: Anonymous GraphQL operations are forbidden. Make sure to name your query!,
209209
ruleId: @graphql-eslint/no-anonymous-operations,
210210
},
211211
Object {

0 commit comments

Comments
 (0)