Skip to content

Commit 9c575c6

Browse files
authored
feat: add definitions option for alphabetize rule (#965)
* feat: add definitions options for `alphabetize` rule * simplify require-id-when-available * improve location report in description-style * change arguments for getLocation * fix runGraphQLTests generic arguments * fix GraphQLESTreeNode expand tooltip * update eslint patch * update eslint in examples to trigger yarn.loc changes * add changeset
1 parent 58b5bfd commit 9c575c6

26 files changed

+978
-328
lines changed

.changeset/beige-bats-happen.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 `definitions` option for `alphabetize` rule

docs/rules/alphabetize.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ The schema defines the following properties:
9797

9898
### `fields` (array)
9999

100-
Fields of `type`, `interface`, and `input`
100+
Fields of `type`, `interface`, and `input`.
101101

102102
The elements of the array can contain the following enum values:
103103

@@ -112,7 +112,7 @@ Additional restrictions:
112112

113113
### `values` (array)
114114

115-
Values of `enum`
115+
Values of `enum`.
116116

117117
The elements of the array can contain the following enum values:
118118

@@ -125,7 +125,7 @@ Additional restrictions:
125125

126126
### `selections` (array)
127127

128-
Selections of operations (`query`, `mutation` and `subscription`) and `fragment`
128+
Selections of `fragment` and operations `query`, `mutation` and `subscription`.
129129

130130
The elements of the array can contain the following enum values:
131131

@@ -139,7 +139,7 @@ Additional restrictions:
139139

140140
### `variables` (array)
141141

142-
Variables of operations (`query`, `mutation` and `subscription`)
142+
Variables of operations `query`, `mutation` and `subscription`.
143143

144144
The elements of the array can contain the following enum values:
145145

@@ -152,7 +152,7 @@ Additional restrictions:
152152

153153
### `arguments` (array)
154154

155-
Arguments of fields and directives
155+
Arguments of fields and directives.
156156

157157
The elements of the array can contain the following enum values:
158158

@@ -166,6 +166,12 @@ Additional restrictions:
166166
* Minimum items: `1`
167167
* Unique items: `true`
168168

169+
### `definitions` (boolean)
170+
171+
Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.
172+
173+
Default: `false`
174+
169175
## Resources
170176

171177
- [Rule source](../../packages/plugin/src/rules/alphabetize.ts)

examples/basic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
},
1414
"devDependencies": {
1515
"@graphql-eslint/eslint-plugin": "3.8.0",
16-
"eslint": "8.7.0"
16+
"eslint": "8.9.0"
1717
}
1818
}

examples/code-file/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
},
1414
"devDependencies": {
1515
"@graphql-eslint/eslint-plugin": "3.8.0",
16-
"eslint": "8.7.0"
16+
"eslint": "8.9.0"
1717
}
1818
}

examples/graphql-config-code-file/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
},
1515
"devDependencies": {
1616
"@graphql-eslint/eslint-plugin": "3.8.0",
17-
"eslint": "8.7.0"
17+
"eslint": "8.9.0"
1818
}
1919
}

examples/graphql-config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
},
1414
"devDependencies": {
1515
"@graphql-eslint/eslint-plugin": "3.8.0",
16-
"eslint": "8.7.0"
16+
"eslint": "8.9.0"
1717
}
1818
}

examples/prettier/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"devDependencies": {
1515
"@graphql-eslint/eslint-plugin": "3.8.0",
16-
"eslint": "8.7.0",
16+
"eslint": "8.9.0",
1717
"eslint-config-prettier": "8.3.0",
1818
"eslint-plugin-prettier": "4.0.0",
1919
"prettier": "2.4.1"

packages/plugin/src/estree-parser/estree-ast.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ type SingleESTreeNode<T extends ASTNode | ValueNode, WithTypeInfo extends boolea
2626
rawNode: () => T;
2727
};
2828

29-
export type GraphQLESTreeNode<T, WithTypeInfo extends boolean = false> = T extends ASTNode | ValueNode
29+
// eslint-disable-next-line @typescript-eslint/ban-types -- only empty object makes the tooltips expand without recursion
30+
export type GraphQLESTreeNode<T, WithTypeInfo extends boolean = false> = {} & (T extends ASTNode | ValueNode
3031
? {
3132
[K in keyof SingleESTreeNode<T, WithTypeInfo>]: SingleESTreeNode<T, WithTypeInfo>[K] extends ReadonlyArray<
3233
infer Nested
@@ -36,4 +37,4 @@ export type GraphQLESTreeNode<T, WithTypeInfo extends boolean = false> = T exten
3637
? GraphQLESTreeNode<SingleESTreeNode<T, WithTypeInfo>[K], WithTypeInfo>
3738
: SingleESTreeNode<T, WithTypeInfo>[K];
3839
}
39-
: T;
40+
: T);

packages/plugin/src/rules/alphabetize.ts

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,23 @@ import {
77
InputObjectTypeDefinitionNode,
88
InputObjectTypeExtensionNode,
99
FieldDefinitionNode,
10-
InputValueDefinitionNode,
1110
EnumTypeDefinitionNode,
1211
EnumTypeExtensionNode,
13-
EnumValueDefinitionNode,
1412
DirectiveDefinitionNode,
15-
ArgumentNode,
16-
VariableDefinitionNode,
1713
OperationDefinitionNode,
1814
FieldNode,
1915
DirectiveNode,
2016
SelectionSetNode,
21-
FragmentSpreadNode,
17+
ASTNode,
2218
} from 'graphql';
2319
import type { SourceLocation, Comment } from 'estree';
2420
import type { AST } from 'eslint';
21+
import lowerCase from 'lodash.lowercase';
2522
import { GraphQLESLintRule } from '../types';
2623
import { GraphQLESTreeNode } from '../estree-parser';
2724
import { GraphQLESLintRuleListener } from '../testkit';
2825

29-
const ALPHABETIZE = 'ALPHABETIZE';
26+
const RULE_ID = 'alphabetize';
3027

3128
const fieldsEnum: ('ObjectTypeDefinition' | 'InterfaceTypeDefinition' | 'InputObjectTypeDefinition')[] = [
3229
Kind.OBJECT_TYPE_DEFINITION,
@@ -52,6 +49,7 @@ export type AlphabetizeConfig = {
5249
selections?: typeof selectionsEnum;
5350
variables?: typeof variablesEnum;
5451
arguments?: typeof argumentsEnum;
52+
definitions?: boolean;
5553
};
5654

5755
const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
@@ -61,7 +59,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
6159
docs: {
6260
category: ['Schema', 'Operations'],
6361
description: `Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.`,
64-
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
62+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
6563
examples: [
6664
{
6765
title: 'Incorrect',
@@ -144,6 +142,8 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
144142
fields: fieldsEnum,
145143
values: valuesEnum,
146144
arguments: argumentsEnum,
145+
// TODO: add in graphql-eslint v4
146+
// definitions: true,
147147
},
148148
],
149149
operations: [
@@ -156,7 +156,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
156156
},
157157
},
158158
messages: {
159-
[ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
159+
[RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
160160
},
161161
schema: {
162162
type: 'array',
@@ -174,7 +174,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
174174
items: {
175175
enum: fieldsEnum,
176176
},
177-
description: 'Fields of `type`, `interface`, and `input`',
177+
description: 'Fields of `type`, `interface`, and `input`.',
178178
},
179179
values: {
180180
type: 'array',
@@ -183,7 +183,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
183183
items: {
184184
enum: valuesEnum,
185185
},
186-
description: 'Values of `enum`',
186+
description: 'Values of `enum`.',
187187
},
188188
selections: {
189189
type: 'array',
@@ -192,7 +192,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
192192
items: {
193193
enum: selectionsEnum,
194194
},
195-
description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
195+
description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
196196
},
197197
variables: {
198198
type: 'array',
@@ -201,7 +201,7 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
201201
items: {
202202
enum: variablesEnum,
203203
},
204-
description: 'Variables of operations (`query`, `mutation` and `subscription`)',
204+
description: 'Variables of operations `query`, `mutation` and `subscription`.',
205205
},
206206
arguments: {
207207
type: 'array',
@@ -210,7 +210,12 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
210210
items: {
211211
enum: argumentsEnum,
212212
},
213-
description: 'Arguments of fields and directives',
213+
description: 'Arguments of fields and directives.',
214+
},
215+
definitions: {
216+
type: 'boolean',
217+
description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
218+
default: false,
214219
},
215220
},
216221
},
@@ -232,7 +237,17 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
232237
if (tokenBefore) {
233238
return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
234239
}
235-
return commentsBefore;
240+
const filteredComments = [];
241+
const nodeLine = node.loc.start.line;
242+
// Break on comment that not attached to node
243+
for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
244+
const comment = commentsBefore[i];
245+
if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
246+
break;
247+
}
248+
filteredComments.unshift(comment);
249+
}
250+
return filteredComments;
236251
}
237252

238253
function getRangeWithComments(node): AST.Range {
@@ -243,37 +258,37 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
243258
return [from.range[0], to.range[1]];
244259
}
245260

246-
function checkNodes(
247-
nodes: GraphQLESTreeNode<
248-
| FieldDefinitionNode
249-
| InputValueDefinitionNode
250-
| EnumValueDefinitionNode
251-
| FieldNode
252-
| FragmentSpreadNode
253-
| ArgumentNode
254-
| VariableDefinitionNode['variable']
255-
>[]
256-
) {
261+
function checkNodes(nodes: GraphQLESTreeNode<ASTNode>[]) {
257262
// Starts from 1, ignore nodes.length <= 1
258263
for (let i = 1; i < nodes.length; i += 1) {
259-
const prevNode = nodes[i - 1];
260264
const currNode = nodes[i];
261-
const prevName = prevNode.name.value;
262-
const currName = currNode.name.value;
263-
// Compare with lexicographic order
264-
if (prevName.localeCompare(currName) !== 1) {
265+
const currName = 'name' in currNode && currNode.name?.value;
266+
if (!currName) {
267+
// we don't move unnamed current nodes
265268
continue;
266269
}
267-
const isVariableNode = currNode.kind === Kind.VARIABLE;
270+
271+
const prevNode = nodes[i - 1];
272+
const prevName = 'name' in prevNode && prevNode.name?.value;
273+
if (prevName) {
274+
// Compare with lexicographic order
275+
const compareResult = prevName.localeCompare(currName);
276+
const shouldSort = compareResult === 1;
277+
if (!shouldSort) {
278+
const isSameName = compareResult === 0;
279+
if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
280+
continue;
281+
}
282+
}
283+
}
284+
268285
context.report({
269286
node: currNode.name,
270-
messageId: ALPHABETIZE,
271-
data: isVariableNode
272-
? {
273-
currName: `$${currName}`,
274-
prevName: `$${prevName}`,
275-
}
276-
: { currName, prevName },
287+
messageId: RULE_ID,
288+
data: {
289+
currName,
290+
prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
291+
},
277292
*fix(fixer) {
278293
const prevRange = getRangeWithComments(prevNode);
279294
const currRange = getRangeWithComments(currNode);
@@ -331,13 +346,10 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
331346
if (selectionsSelector) {
332347
listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node: GraphQLESTreeNode<SelectionSetNode>) => {
333348
checkNodes(
334-
node.selections
335-
// inline fragment don't have name, so we skip them
336-
.filter(selection => selection.kind !== Kind.INLINE_FRAGMENT)
337-
.map(selection =>
338-
// sort by alias is field is renamed
339-
'alias' in selection && selection.alias ? ({ name: selection.alias } as any) : selection
340-
)
349+
node.selections.map(selection =>
350+
// sort by alias is field is renamed
351+
'alias' in selection && selection.alias ? ({ name: selection.alias } as any) : selection
352+
)
341353
);
342354
};
343355
}
@@ -356,6 +368,12 @@ const rule: GraphQLESLintRule<[AlphabetizeConfig]> = {
356368
};
357369
}
358370

371+
if (opts.definitions) {
372+
listeners.Document = node => {
373+
checkNodes(node.definitions);
374+
};
375+
}
376+
359377
return listeners;
360378
},
361379
};

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { StringValueNode } from 'graphql';
22
import { GraphQLESLintRule } from '../types';
3-
import { getLocation } from '../utils';
43
import { GraphQLESTreeNode } from '../estree-parser';
54

65
type DescriptionStyleRuleConfig = { style: 'inline' | 'block' };
@@ -55,7 +54,7 @@ const rule: GraphQLESLintRule<[DescriptionStyleRuleConfig]> = {
5554
return {
5655
[`.description[type=StringValue][block!=${isBlock}]`](node: GraphQLESTreeNode<StringValueNode>) {
5756
context.report({
58-
loc: getLocation(node.loc),
57+
loc: isBlock ? node.loc : node.loc.start,
5958
message: `Unexpected ${isBlock ? 'inline' : 'block'} description`,
6059
});
6160
},

0 commit comments

Comments
 (0)