Skip to content

Commit 0da135f

Browse files
authored
feat: add new option OperationDefinition in require-description rule (#912)
* feat: add new option `OperationDefinition` in `require-description` rule * docs: note about comment syntax instead description syntax * fix non-breaking space
1 parent 6e7ae77 commit 0da135f

File tree

7 files changed

+195
-26
lines changed

7 files changed

+195
-26
lines changed

.changeset/dull-geckos-tickle.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 new option `OperationDefinition` in `require-description` rule

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Name            &nbs
4444
[provided-required-arguments](rules/provided-required-arguments.md)|A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.|![recommended][]|🔮
4545
[require-deprecation-date](rules/require-deprecation-date.md)|Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.|![all][]|🚀
4646
[require-deprecation-reason](rules/require-deprecation-reason.md)|Require all deprecation directives to specify a reason.|![recommended][]|🚀
47-
[require-description](rules/require-description.md)|Enforce descriptions in your type definitions.|![recommended][]|🚀
47+
[require-description](rules/require-description.md)|Enforce descriptions in type definitions and operations.|![recommended][]|🚀
4848
[require-field-of-type-query-in-mutation-result](rules/require-field-of-type-query-in-mutation-result.md)|Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.|![all][]|🚀
4949
[require-id-when-available](rules/require-id-when-available.md)|Enforce selecting specific fields when they are available on the GraphQL type.|![recommended][]|🚀
5050
[scalar-leafs](rules/scalar-leafs.md)|A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.|![recommended][]|🔮

docs/rules/require-description.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
88
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
99

10-
Enforce descriptions in your type definitions.
10+
Enforce descriptions in type definitions and operations.
1111

1212
## Usage Examples
1313

@@ -37,6 +37,17 @@ type someTypeName {
3737
}
3838
```
3939

40+
### Correct
41+
42+
```graphql
43+
# eslint @graphql-eslint/require-description: ['error', { OperationDefinition: true }]
44+
45+
# Create a new user
46+
mutation createUser {
47+
# ...
48+
}
49+
```
50+
4051
## Config Schema
4152

4253
The schema defines the following properties:
@@ -84,6 +95,12 @@ Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October
8495

8596
Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#ObjectTypeDefinition).
8697

98+
### `OperationDefinition` (boolean)
99+
100+
Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#OperationDefinition).
101+
102+
> You must use only comment syntax `#` and not description syntax `"""` or `"`.
103+
87104
### `ScalarTypeDefinition` (boolean)
88105

89106
Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#ScalarTypeDefinition).

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

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
import { ASTKindToNode, Kind } from 'graphql';
1+
import { ASTKindToNode, Kind, TokenKind } from 'graphql';
22
import { GraphQLESLintRule, ValueOf } from '../types';
3-
import { TYPES_KINDS, getLocation } from '../utils';
3+
import { getLocation, TYPES_KINDS } from '../utils';
44
import { GraphQLESTreeNode } from '../estree-parser/estree-ast';
55

6-
const REQUIRE_DESCRIPTION_ERROR = 'REQUIRE_DESCRIPTION_ERROR';
6+
const RULE_ID = 'require-description';
77

88
const ALLOWED_KINDS = [
99
...TYPES_KINDS,
1010
Kind.FIELD_DEFINITION,
1111
Kind.INPUT_VALUE_DEFINITION,
1212
Kind.ENUM_VALUE_DEFINITION,
1313
Kind.DIRECTIVE_DEFINITION,
14+
Kind.OPERATION_DEFINITION,
1415
] as const;
1516

1617
type AllowedKind = typeof ALLOWED_KINDS[number];
1718
type AllowedKindToNode = Pick<ASTKindToNode, AllowedKind>;
1819

19-
type RequireDescriptionRuleConfig = {
20+
export type RequireDescriptionRuleConfig = {
2021
types?: boolean;
2122
} & {
2223
[key in AllowedKind]?: boolean;
@@ -26,8 +27,8 @@ const rule: GraphQLESLintRule<[RequireDescriptionRuleConfig]> = {
2627
meta: {
2728
docs: {
2829
category: 'Schema',
29-
description: 'Enforce descriptions in your type definitions.',
30-
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-description.md',
30+
description: 'Enforce descriptions in type definitions and operations.',
31+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
3132
examples: [
3233
{
3334
title: 'Incorrect',
@@ -53,6 +54,16 @@ const rule: GraphQLESLintRule<[RequireDescriptionRuleConfig]> = {
5354
}
5455
`,
5556
},
57+
{
58+
title: 'Correct',
59+
usage: [{ OperationDefinition: true }],
60+
code: /* GraphQL */ `
61+
# Create a new user
62+
mutation createUser {
63+
# ...
64+
}
65+
`,
66+
},
5667
],
5768
configOptions: [
5869
{
@@ -64,7 +75,7 @@ const rule: GraphQLESLintRule<[RequireDescriptionRuleConfig]> = {
6475
},
6576
type: 'suggestion',
6677
messages: {
67-
[REQUIRE_DESCRIPTION_ERROR]: 'Description is required for nodes of type "{{ nodeType }}"',
78+
[RULE_ID]: 'Description is required for nodes of type "{{ nodeType }}"',
6879
},
6980
schema: {
7081
type: 'array',
@@ -80,20 +91,20 @@ const rule: GraphQLESLintRule<[RequireDescriptionRuleConfig]> = {
8091
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
8192
},
8293
...Object.fromEntries(
83-
[...ALLOWED_KINDS].sort().map(kind => [
84-
kind,
85-
{
86-
type: 'boolean',
87-
description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
88-
},
89-
])
94+
[...ALLOWED_KINDS].sort().map(kind => {
95+
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
96+
if (kind === Kind.OPERATION_DEFINITION) {
97+
description += '\n\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
98+
}
99+
return [kind, { type: 'boolean', description }];
100+
})
90101
),
91102
},
92103
},
93104
},
94105
},
95106
create(context) {
96-
const { types, ...restOptions } = context.options[0];
107+
const { types, ...restOptions } = context.options[0] || {};
97108

98109
const kinds: Set<string> = new Set(types ? TYPES_KINDS : []);
99110
for (const [kind, isEnabled] of Object.entries(restOptions)) {
@@ -108,11 +119,26 @@ const rule: GraphQLESLintRule<[RequireDescriptionRuleConfig]> = {
108119

109120
return {
110121
[selector](node: GraphQLESTreeNode<ValueOf<AllowedKindToNode>>) {
111-
const description = node.description?.value || '';
112-
if (description.trim().length === 0) {
122+
let description = '';
123+
const isOperation = node.kind === Kind.OPERATION_DEFINITION;
124+
if (isOperation) {
125+
const rawNode = node.rawNode();
126+
const { prev, line } = rawNode.loc.startToken;
127+
if (prev.kind === TokenKind.COMMENT) {
128+
const value = prev.value.trim();
129+
const linesBefore = line - prev.line;
130+
if (!value.startsWith('eslint') && linesBefore === 1) {
131+
description = value;
132+
}
133+
}
134+
} else {
135+
description = node.description?.value.trim() || '';
136+
}
137+
138+
if (description.length === 0) {
113139
context.report({
114-
loc: getLocation(node.name.loc, node.name.value),
115-
messageId: REQUIRE_DESCRIPTION_ERROR,
140+
loc: isOperation ? getLocation(node.loc, node.operation) : getLocation(node.name.loc, node.name.value),
141+
messageId: RULE_ID,
116142
data: {
117143
nodeType: node.kind,
118144
},

packages/plugin/tests/__snapshots__/require-description.spec.ts.snap

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,50 @@ exports[` 12`] = `
157157
5 | }
158158
6 |
159159
`;
160+
161+
exports[` 13`] = `
162+
1 |
163+
2 | # linesBefore !== 1
164+
3 |
165+
> 4 | query {
166+
| ^^^^^ Description is required for nodes of type "OperationDefinition"
167+
5 | foo
168+
6 | }
169+
7 |
170+
`;
171+
172+
exports[` 14`] = `
173+
> 1 | mutation { test }
174+
| ^^^^^^^^ Description is required for nodes of type "OperationDefinition"
175+
`;
176+
177+
exports[` 15`] = `
178+
> 1 | subscription { test }
179+
| ^^^^^^^^^^^^ Description is required for nodes of type "OperationDefinition"
180+
`;
181+
182+
exports[` 16`] = `
183+
1 |
184+
2 | # eslint-disable-next-line semi
185+
> 3 | query {
186+
| ^^^^^ Description is required for nodes of type "OperationDefinition"
187+
4 | foo
188+
5 | }
189+
6 |
190+
`;
191+
192+
exports[` 17`] = `
193+
1 |
194+
2 | # BAD
195+
3 | fragment UserFields on User {
196+
4 | id
197+
5 | }
198+
6 |
199+
> 7 | query {
200+
| ^^^^^ Description is required for nodes of type "OperationDefinition"
201+
8 | user {
202+
9 | ...UserFields
203+
10 | }
204+
11 | }
205+
12 |
206+
`;

packages/plugin/tests/require-description.spec.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { GraphQLRuleTester } from '../src';
2-
import rule from '../src/rules/require-description';
2+
import rule, { RequireDescriptionRuleConfig } from '../src/rules/require-description';
33

44
const ruleTester = new GraphQLRuleTester();
55

6-
ruleTester.runGraphQLTests('require-description', rule, {
6+
const ERROR = { message: 'Description is required for nodes of type "OperationDefinition"' };
7+
const OPERATION = { OperationDefinition: true };
8+
9+
ruleTester.runGraphQLTests<[RequireDescriptionRuleConfig]>('require-description', rule, {
710
valid: [
811
{
912
code: /* GraphQL */ `
@@ -54,6 +57,24 @@ ruleTester.runGraphQLTests('require-description', rule, {
5457
`,
5558
options: [{ types: true, FieldDefinition: true }],
5659
},
60+
{
61+
code: /* GraphQL */ `
62+
# OK
63+
query {
64+
test
65+
}
66+
`,
67+
options: [OPERATION],
68+
},
69+
{
70+
code: /* GraphQL */ `
71+
# ignore fragments
72+
fragment UserFields on User {
73+
id
74+
}
75+
`,
76+
options: [OPERATION],
77+
},
5778
],
5879
invalid: [
5980
{
@@ -103,5 +124,57 @@ ruleTester.runGraphQLTests('require-description', rule, {
103124
],
104125
errors: 2,
105126
},
127+
{
128+
name: 'should report because of linesBefore !== 1',
129+
code: /* GraphQL */ `
130+
# linesBefore !== 1
131+
132+
query {
133+
foo
134+
}
135+
`,
136+
options: [OPERATION],
137+
errors: [ERROR],
138+
},
139+
{
140+
name: 'should validate mutation',
141+
code: 'mutation { test }',
142+
options: [OPERATION],
143+
errors: [ERROR],
144+
},
145+
{
146+
name: 'should validate subscription',
147+
code: 'subscription { test }',
148+
options: [OPERATION],
149+
errors: [ERROR],
150+
},
151+
{
152+
name: 'should report because skips previous comment that starts with `eslint`',
153+
code: /* GraphQL */ `
154+
# eslint-disable-next-line semi
155+
query {
156+
foo
157+
}
158+
`,
159+
options: [OPERATION],
160+
errors: [ERROR],
161+
},
162+
{
163+
name: 'should ignore comments before fragment definition',
164+
code: /* GraphQL */ `
165+
# BAD
166+
fragment UserFields on User {
167+
id
168+
}
169+
170+
query {
171+
user {
172+
...UserFields
173+
}
174+
}
175+
`,
176+
options: [OPERATION],
177+
errors: [ERROR],
178+
},
106179
],
107180
});

scripts/generate-docs.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { rules } from '../packages/plugin/src';
77
import { DISABLED_RULES_FOR_ALL_CONFIG } from './constants';
88

99
const BR = '';
10+
const NBSP = '&nbsp;';
1011
const DOCS_PATH = resolve(process.cwd(), 'docs');
1112

1213
enum Icon {
@@ -158,10 +159,10 @@ function generateDocs(): void {
158159
'<!-- 🚨 IMPORTANT! Do not manually modify this table. Run: `yarn generate:docs` -->',
159160
printMarkdownTable(
160161
[
161-
`Name${'&nbsp;'.repeat(20)}`,
162+
`Name${NBSP.repeat(20)}`,
162163
'Description',
163-
{ name: `${'&nbsp;'.repeat(4)}Config${'&nbsp;'.repeat(4)}`, align: 'center' },
164-
{ name: `${Icon.GRAPHQL_ESLINT}&nbsp;/&nbsp;${Icon.GRAPHQL_JS}`, align: 'center' },
164+
{ name: `${NBSP.repeat(4)}Config${NBSP.repeat(4)}`, align: 'center' },
165+
{ name: `${Icon.GRAPHQL_ESLINT}${NBSP}/${NBSP}${Icon.GRAPHQL_JS}`, align: 'center' },
165166
],
166167
sortedRules
167168
),

0 commit comments

Comments
 (0)