@graphql-eslint/eslint-plugin 3.7.0 â 3.8.0-alpha-a8fcc7b.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/README.md +1 -1
- package/docs/rules/alphabetize.md +4 -0
- package/docs/rules/require-description.md +18 -1
- package/index.js +110 -43
- package/index.mjs +110 -43
- package/package.json +2 -1
- package/rules/alphabetize.d.ts +1 -1
- package/rules/index.d.ts +2 -21
- package/rules/require-description.d.ts +2 -2
- package/testkit.d.ts +0 -1
package/docs/README.md
CHANGED
@@ -44,7 +44,7 @@ Name &nbs
|
|
44
44
|
[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][]|đŽ
|
45
45
|
[require-deprecation-date](rules/require-deprecation-date.md)|Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.|![all][]|đ
|
46
46
|
[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
|
47
|
+
[require-description](rules/require-description.md)|Enforce descriptions in type definitions and operations.|![recommended][]|đ
|
48
48
|
[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][]|đ
|
49
49
|
[require-id-when-available](rules/require-id-when-available.md)|Enforce selecting specific fields when they are available on the GraphQL type.|![recommended][]|đ
|
50
50
|
[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][]|đŽ
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# `alphabetize`
|
2
2
|
|
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
|
+
|
3
5
|
- Category: `Schema & Operations`
|
4
6
|
- Rule name: `@graphql-eslint/alphabetize`
|
5
7
|
- Requires GraphQL Schema: `false` [âšī¸](../../README.md#extended-linting-rules-with-graphql-schema)
|
@@ -7,6 +9,8 @@
|
|
7
9
|
|
8
10
|
Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.
|
9
11
|
|
12
|
+
> Note: autofix will work only for fields without comments (between or around)
|
13
|
+
|
10
14
|
## Usage Examples
|
11
15
|
|
12
16
|
### Incorrect
|
@@ -7,7 +7,7 @@
|
|
7
7
|
- Requires GraphQL Schema: `false` [âšī¸](../../README.md#extended-linting-rules-with-graphql-schema)
|
8
8
|
- Requires GraphQL Operations: `false` [âšī¸](../../README.md#extended-linting-rules-with-siblings-operations)
|
9
9
|
|
10
|
-
Enforce descriptions in
|
10
|
+
Enforce descriptions in type definitions and operations.
|
11
11
|
|
12
12
|
## Usage Examples
|
13
13
|
|
@@ -37,6 +37,17 @@ type someTypeName {
|
|
37
37
|
}
|
38
38
|
```
|
39
39
|
|
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
|
+
|
40
51
|
## Config Schema
|
41
52
|
|
42
53
|
The schema defines the following properties:
|
@@ -84,6 +95,12 @@ Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October
|
|
84
95
|
|
85
96
|
Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#ObjectTypeDefinition).
|
86
97
|
|
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
|
+
|
87
104
|
### `ScalarTypeDefinition` (boolean)
|
88
105
|
|
89
106
|
Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#ScalarTypeDefinition).
|
package/index.js
CHANGED
@@ -17,6 +17,7 @@ const graphqlConfig = require('graphql-config');
|
|
17
17
|
const codeFileLoader = require('@graphql-tools/code-file-loader');
|
18
18
|
const eslint = require('eslint');
|
19
19
|
const codeFrame = require('@babel/code-frame');
|
20
|
+
const dedent = _interopDefault(require('dedent'));
|
20
21
|
|
21
22
|
const base = {
|
22
23
|
parser: '@graphql-eslint/eslint-plugin',
|
@@ -688,9 +689,13 @@ const argumentsEnum = [
|
|
688
689
|
const rule = {
|
689
690
|
meta: {
|
690
691
|
type: 'suggestion',
|
692
|
+
fixable: 'code',
|
691
693
|
docs: {
|
692
694
|
category: ['Schema', 'Operations'],
|
693
|
-
description:
|
695
|
+
description: [
|
696
|
+
'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
|
697
|
+
'> Note: autofix will work only for fields without comments (between or around)',
|
698
|
+
].join('\n\n'),
|
694
699
|
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
|
695
700
|
examples: [
|
696
701
|
{
|
@@ -848,14 +853,20 @@ const rule = {
|
|
848
853
|
},
|
849
854
|
create(context) {
|
850
855
|
var _a, _b, _c, _d, _e;
|
856
|
+
function isOnSameLineNodeAndComment(beforeNode, afterNode) {
|
857
|
+
return beforeNode.loc.end.line === afterNode.loc.start.line;
|
858
|
+
}
|
851
859
|
function checkNodes(nodes) {
|
852
|
-
|
853
|
-
for (
|
854
|
-
const
|
855
|
-
|
856
|
-
|
860
|
+
// Starts from 1, ignore nodes.length <= 1
|
861
|
+
for (let i = 1; i < nodes.length; i += 1) {
|
862
|
+
const prevNode = nodes[i - 1];
|
863
|
+
const currNode = nodes[i];
|
864
|
+
const prevName = prevNode.name.value;
|
865
|
+
const currName = currNode.name.value;
|
866
|
+
if (prevName.localeCompare(currName) === 1) {
|
867
|
+
const isVariableNode = currNode.kind === graphql.Kind.VARIABLE;
|
857
868
|
context.report({
|
858
|
-
loc: getLocation(
|
869
|
+
loc: getLocation(currNode.loc, currName, { offsetEnd: isVariableNode ? 0 : 1 }),
|
859
870
|
messageId: ALPHABETIZE,
|
860
871
|
data: isVariableNode
|
861
872
|
? {
|
@@ -863,9 +874,28 @@ const rule = {
|
|
863
874
|
prevName: `$${prevName}`,
|
864
875
|
}
|
865
876
|
: { currName, prevName },
|
877
|
+
*fix(fixer) {
|
878
|
+
const prev = prevNode;
|
879
|
+
const curr = currNode;
|
880
|
+
const sourceCode = context.getSourceCode();
|
881
|
+
const beforeComments = sourceCode.getCommentsBefore(prev);
|
882
|
+
if (beforeComments.length > 0) {
|
883
|
+
const tokenBefore = sourceCode.getTokenBefore(prev);
|
884
|
+
const lastBeforeComment = beforeComments.at(-1);
|
885
|
+
if (!tokenBefore || !isOnSameLineNodeAndComment(tokenBefore, lastBeforeComment))
|
886
|
+
return;
|
887
|
+
}
|
888
|
+
const betweenComments = sourceCode.getCommentsBefore(curr);
|
889
|
+
if (betweenComments.length > 0)
|
890
|
+
return;
|
891
|
+
const [firstAfterComment] = sourceCode.getCommentsAfter(curr);
|
892
|
+
if (firstAfterComment && isOnSameLineNodeAndComment(curr, firstAfterComment))
|
893
|
+
return;
|
894
|
+
yield fixer.replaceText(prev, sourceCode.getText(curr));
|
895
|
+
yield fixer.replaceText(curr, sourceCode.getText(prev));
|
896
|
+
},
|
866
897
|
});
|
867
898
|
}
|
868
|
-
prevName = currName;
|
869
899
|
}
|
870
900
|
}
|
871
901
|
const opts = context.options[0];
|
@@ -2625,20 +2655,21 @@ const rule$g = {
|
|
2625
2655
|
},
|
2626
2656
|
};
|
2627
2657
|
|
2628
|
-
const
|
2658
|
+
const RULE_ID$2 = 'require-description';
|
2629
2659
|
const ALLOWED_KINDS$1 = [
|
2630
2660
|
...TYPES_KINDS,
|
2631
2661
|
graphql.Kind.FIELD_DEFINITION,
|
2632
2662
|
graphql.Kind.INPUT_VALUE_DEFINITION,
|
2633
2663
|
graphql.Kind.ENUM_VALUE_DEFINITION,
|
2634
2664
|
graphql.Kind.DIRECTIVE_DEFINITION,
|
2665
|
+
graphql.Kind.OPERATION_DEFINITION,
|
2635
2666
|
];
|
2636
2667
|
const rule$h = {
|
2637
2668
|
meta: {
|
2638
2669
|
docs: {
|
2639
2670
|
category: 'Schema',
|
2640
|
-
description: 'Enforce descriptions in
|
2641
|
-
url:
|
2671
|
+
description: 'Enforce descriptions in type definitions and operations.',
|
2672
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
|
2642
2673
|
examples: [
|
2643
2674
|
{
|
2644
2675
|
title: 'Incorrect',
|
@@ -2662,6 +2693,16 @@ const rule$h = {
|
|
2662
2693
|
"""
|
2663
2694
|
name: String
|
2664
2695
|
}
|
2696
|
+
`,
|
2697
|
+
},
|
2698
|
+
{
|
2699
|
+
title: 'Correct',
|
2700
|
+
usage: [{ OperationDefinition: true }],
|
2701
|
+
code: /* GraphQL */ `
|
2702
|
+
# Create a new user
|
2703
|
+
mutation createUser {
|
2704
|
+
# ...
|
2705
|
+
}
|
2665
2706
|
`,
|
2666
2707
|
},
|
2667
2708
|
],
|
@@ -2675,7 +2716,7 @@ const rule$h = {
|
|
2675
2716
|
},
|
2676
2717
|
type: 'suggestion',
|
2677
2718
|
messages: {
|
2678
|
-
[
|
2719
|
+
[RULE_ID$2]: 'Description is required for nodes of type "{{ nodeType }}"',
|
2679
2720
|
},
|
2680
2721
|
schema: {
|
2681
2722
|
type: 'array',
|
@@ -2690,19 +2731,19 @@ const rule$h = {
|
|
2690
2731
|
type: 'boolean',
|
2691
2732
|
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
|
2692
2733
|
},
|
2693
|
-
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind =>
|
2694
|
-
kind
|
2695
|
-
{
|
2696
|
-
|
2697
|
-
|
2698
|
-
}
|
2699
|
-
|
2734
|
+
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind => {
|
2735
|
+
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
|
2736
|
+
if (kind === graphql.Kind.OPERATION_DEFINITION) {
|
2737
|
+
description += '\n\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
|
2738
|
+
}
|
2739
|
+
return [kind, { type: 'boolean', description }];
|
2740
|
+
})),
|
2700
2741
|
},
|
2701
2742
|
},
|
2702
2743
|
},
|
2703
2744
|
},
|
2704
2745
|
create(context) {
|
2705
|
-
const { types, ...restOptions } = context.options[0];
|
2746
|
+
const { types, ...restOptions } = context.options[0] || {};
|
2706
2747
|
const kinds = new Set(types ? TYPES_KINDS : []);
|
2707
2748
|
for (const [kind, isEnabled] of Object.entries(restOptions)) {
|
2708
2749
|
if (isEnabled) {
|
@@ -2716,11 +2757,26 @@ const rule$h = {
|
|
2716
2757
|
return {
|
2717
2758
|
[selector](node) {
|
2718
2759
|
var _a;
|
2719
|
-
|
2720
|
-
|
2760
|
+
let description = '';
|
2761
|
+
const isOperation = node.kind === graphql.Kind.OPERATION_DEFINITION;
|
2762
|
+
if (isOperation) {
|
2763
|
+
const rawNode = node.rawNode();
|
2764
|
+
const { prev, line } = rawNode.loc.startToken;
|
2765
|
+
if (prev.kind === graphql.TokenKind.COMMENT) {
|
2766
|
+
const value = prev.value.trim();
|
2767
|
+
const linesBefore = line - prev.line;
|
2768
|
+
if (!value.startsWith('eslint') && linesBefore === 1) {
|
2769
|
+
description = value;
|
2770
|
+
}
|
2771
|
+
}
|
2772
|
+
}
|
2773
|
+
else {
|
2774
|
+
description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
|
2775
|
+
}
|
2776
|
+
if (description.length === 0) {
|
2721
2777
|
context.report({
|
2722
|
-
loc: getLocation(node.name.loc, node.name.value),
|
2723
|
-
messageId:
|
2778
|
+
loc: isOperation ? getLocation(node.loc, node.operation) : getLocation(node.name.loc, node.name.value),
|
2779
|
+
messageId: RULE_ID$2,
|
2724
2780
|
data: {
|
2725
2781
|
nodeType: node.kind,
|
2726
2782
|
},
|
@@ -2896,7 +2952,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2896
2952
|
}
|
2897
2953
|
};
|
2898
2954
|
|
2899
|
-
const RULE_ID$
|
2955
|
+
const RULE_ID$3 = 'require-id-when-available';
|
2900
2956
|
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2901
2957
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2902
2958
|
const rule$j = {
|
@@ -2905,7 +2961,7 @@ const rule$j = {
|
|
2905
2961
|
docs: {
|
2906
2962
|
category: 'Operations',
|
2907
2963
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2908
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
2964
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
|
2909
2965
|
requiresSchema: true,
|
2910
2966
|
requiresSiblings: true,
|
2911
2967
|
examples: [
|
@@ -2979,8 +3035,8 @@ const rule$j = {
|
|
2979
3035
|
},
|
2980
3036
|
},
|
2981
3037
|
create(context) {
|
2982
|
-
requireGraphQLSchemaFromContext(RULE_ID$
|
2983
|
-
const siblings = requireSiblingsOperations(RULE_ID$
|
3038
|
+
requireGraphQLSchemaFromContext(RULE_ID$3, context);
|
3039
|
+
const siblings = requireSiblingsOperations(RULE_ID$3, context);
|
2984
3040
|
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2985
3041
|
const idNames = utils.asArray(fieldName);
|
2986
3042
|
const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
|
@@ -3042,13 +3098,13 @@ const rule$j = {
|
|
3042
3098
|
},
|
3043
3099
|
};
|
3044
3100
|
|
3045
|
-
const RULE_ID$
|
3101
|
+
const RULE_ID$4 = 'selection-set-depth';
|
3046
3102
|
const rule$k = {
|
3047
3103
|
meta: {
|
3048
3104
|
docs: {
|
3049
3105
|
category: 'Operations',
|
3050
3106
|
description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
|
3051
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3107
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
|
3052
3108
|
requiresSiblings: true,
|
3053
3109
|
examples: [
|
3054
3110
|
{
|
@@ -3122,10 +3178,10 @@ const rule$k = {
|
|
3122
3178
|
create(context) {
|
3123
3179
|
let siblings = null;
|
3124
3180
|
try {
|
3125
|
-
siblings = requireSiblingsOperations(RULE_ID$
|
3181
|
+
siblings = requireSiblingsOperations(RULE_ID$4, context);
|
3126
3182
|
}
|
3127
3183
|
catch (e) {
|
3128
|
-
logger.warn(`Rule "${RULE_ID$
|
3184
|
+
logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3129
3185
|
}
|
3130
3186
|
const { maxDepth } = context.options[0];
|
3131
3187
|
const ignore = context.options[0].ignore || [];
|
@@ -3150,14 +3206,14 @@ const rule$k = {
|
|
3150
3206
|
});
|
3151
3207
|
}
|
3152
3208
|
catch (e) {
|
3153
|
-
logger.warn(`Rule "${RULE_ID$
|
3209
|
+
logger.warn(`Rule "${RULE_ID$4}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
|
3154
3210
|
}
|
3155
3211
|
},
|
3156
3212
|
};
|
3157
3213
|
},
|
3158
3214
|
};
|
3159
3215
|
|
3160
|
-
const RULE_ID$
|
3216
|
+
const RULE_ID$5 = 'strict-id-in-types';
|
3161
3217
|
const rule$l = {
|
3162
3218
|
meta: {
|
3163
3219
|
type: 'suggestion',
|
@@ -3165,7 +3221,7 @@ const rule$l = {
|
|
3165
3221
|
description: `Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.`,
|
3166
3222
|
category: 'Schema',
|
3167
3223
|
recommended: true,
|
3168
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3224
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
|
3169
3225
|
requiresSchema: true,
|
3170
3226
|
examples: [
|
3171
3227
|
{
|
@@ -3279,7 +3335,7 @@ const rule$l = {
|
|
3279
3335
|
},
|
3280
3336
|
},
|
3281
3337
|
messages: {
|
3282
|
-
[RULE_ID$
|
3338
|
+
[RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
|
3283
3339
|
},
|
3284
3340
|
},
|
3285
3341
|
create(context) {
|
@@ -3289,7 +3345,7 @@ const rule$l = {
|
|
3289
3345
|
exceptions: {},
|
3290
3346
|
...context.options[0],
|
3291
3347
|
};
|
3292
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$
|
3348
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
|
3293
3349
|
const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
|
3294
3350
|
.filter(Boolean)
|
3295
3351
|
.map(type => type.name);
|
@@ -3319,7 +3375,7 @@ const rule$l = {
|
|
3319
3375
|
if (validIds.length !== 1) {
|
3320
3376
|
context.report({
|
3321
3377
|
loc: getLocation(node.name.loc, typeName),
|
3322
|
-
messageId: RULE_ID$
|
3378
|
+
messageId: RULE_ID$5,
|
3323
3379
|
data: {
|
3324
3380
|
typeName,
|
3325
3381
|
acceptedNamesString: options.acceptedIdNames.join(', '),
|
@@ -3949,19 +4005,30 @@ class GraphQLRuleTester extends eslint.RuleTester {
|
|
3949
4005
|
}
|
3950
4006
|
const linter = new eslint.Linter();
|
3951
4007
|
linter.defineRule(name, rule);
|
4008
|
+
const hasOnlyTest = tests.invalid.some(t => t.only);
|
3952
4009
|
for (const testCase of tests.invalid) {
|
4010
|
+
const { only, code, filename } = testCase;
|
4011
|
+
if (hasOnlyTest && !only) {
|
4012
|
+
continue;
|
4013
|
+
}
|
3953
4014
|
const verifyConfig = getVerifyConfig(name, this.config, testCase);
|
3954
4015
|
defineParser(linter, verifyConfig.parser);
|
3955
|
-
const { code, filename } = testCase;
|
3956
4016
|
const messages = linter.verify(code, verifyConfig, { filename });
|
3957
|
-
|
4017
|
+
const messageForSnapshot = [];
|
4018
|
+
for (const [index, message] of messages.entries()) {
|
3958
4019
|
if (message.fatal) {
|
3959
4020
|
throw new Error(message.message);
|
3960
4021
|
}
|
3961
|
-
|
3962
|
-
|
3963
|
-
|
4022
|
+
messageForSnapshot.push(`Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
|
4023
|
+
}
|
4024
|
+
if (rule.meta.fixable) {
|
4025
|
+
const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
|
4026
|
+
if (fixed) {
|
4027
|
+
messageForSnapshot.push('Autofix output', dedent(output));
|
4028
|
+
}
|
3964
4029
|
}
|
4030
|
+
// eslint-disable-next-line no-undef
|
4031
|
+
expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
|
3965
4032
|
}
|
3966
4033
|
}
|
3967
4034
|
}
|
package/index.mjs
CHANGED
@@ -11,6 +11,7 @@ import { loadConfigSync, GraphQLConfig } from 'graphql-config';
|
|
11
11
|
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
|
12
12
|
import { RuleTester, Linter } from 'eslint';
|
13
13
|
import { codeFrameColumns } from '@babel/code-frame';
|
14
|
+
import dedent from 'dedent';
|
14
15
|
|
15
16
|
const base = {
|
16
17
|
parser: '@graphql-eslint/eslint-plugin',
|
@@ -682,9 +683,13 @@ const argumentsEnum = [
|
|
682
683
|
const rule = {
|
683
684
|
meta: {
|
684
685
|
type: 'suggestion',
|
686
|
+
fixable: 'code',
|
685
687
|
docs: {
|
686
688
|
category: ['Schema', 'Operations'],
|
687
|
-
description:
|
689
|
+
description: [
|
690
|
+
'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
|
691
|
+
'> Note: autofix will work only for fields without comments (between or around)',
|
692
|
+
].join('\n\n'),
|
688
693
|
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
|
689
694
|
examples: [
|
690
695
|
{
|
@@ -842,14 +847,20 @@ const rule = {
|
|
842
847
|
},
|
843
848
|
create(context) {
|
844
849
|
var _a, _b, _c, _d, _e;
|
850
|
+
function isOnSameLineNodeAndComment(beforeNode, afterNode) {
|
851
|
+
return beforeNode.loc.end.line === afterNode.loc.start.line;
|
852
|
+
}
|
845
853
|
function checkNodes(nodes) {
|
846
|
-
|
847
|
-
for (
|
848
|
-
const
|
849
|
-
|
850
|
-
|
854
|
+
// Starts from 1, ignore nodes.length <= 1
|
855
|
+
for (let i = 1; i < nodes.length; i += 1) {
|
856
|
+
const prevNode = nodes[i - 1];
|
857
|
+
const currNode = nodes[i];
|
858
|
+
const prevName = prevNode.name.value;
|
859
|
+
const currName = currNode.name.value;
|
860
|
+
if (prevName.localeCompare(currName) === 1) {
|
861
|
+
const isVariableNode = currNode.kind === Kind.VARIABLE;
|
851
862
|
context.report({
|
852
|
-
loc: getLocation(
|
863
|
+
loc: getLocation(currNode.loc, currName, { offsetEnd: isVariableNode ? 0 : 1 }),
|
853
864
|
messageId: ALPHABETIZE,
|
854
865
|
data: isVariableNode
|
855
866
|
? {
|
@@ -857,9 +868,28 @@ const rule = {
|
|
857
868
|
prevName: `$${prevName}`,
|
858
869
|
}
|
859
870
|
: { currName, prevName },
|
871
|
+
*fix(fixer) {
|
872
|
+
const prev = prevNode;
|
873
|
+
const curr = currNode;
|
874
|
+
const sourceCode = context.getSourceCode();
|
875
|
+
const beforeComments = sourceCode.getCommentsBefore(prev);
|
876
|
+
if (beforeComments.length > 0) {
|
877
|
+
const tokenBefore = sourceCode.getTokenBefore(prev);
|
878
|
+
const lastBeforeComment = beforeComments.at(-1);
|
879
|
+
if (!tokenBefore || !isOnSameLineNodeAndComment(tokenBefore, lastBeforeComment))
|
880
|
+
return;
|
881
|
+
}
|
882
|
+
const betweenComments = sourceCode.getCommentsBefore(curr);
|
883
|
+
if (betweenComments.length > 0)
|
884
|
+
return;
|
885
|
+
const [firstAfterComment] = sourceCode.getCommentsAfter(curr);
|
886
|
+
if (firstAfterComment && isOnSameLineNodeAndComment(curr, firstAfterComment))
|
887
|
+
return;
|
888
|
+
yield fixer.replaceText(prev, sourceCode.getText(curr));
|
889
|
+
yield fixer.replaceText(curr, sourceCode.getText(prev));
|
890
|
+
},
|
860
891
|
});
|
861
892
|
}
|
862
|
-
prevName = currName;
|
863
893
|
}
|
864
894
|
}
|
865
895
|
const opts = context.options[0];
|
@@ -2619,20 +2649,21 @@ const rule$g = {
|
|
2619
2649
|
},
|
2620
2650
|
};
|
2621
2651
|
|
2622
|
-
const
|
2652
|
+
const RULE_ID$2 = 'require-description';
|
2623
2653
|
const ALLOWED_KINDS$1 = [
|
2624
2654
|
...TYPES_KINDS,
|
2625
2655
|
Kind.FIELD_DEFINITION,
|
2626
2656
|
Kind.INPUT_VALUE_DEFINITION,
|
2627
2657
|
Kind.ENUM_VALUE_DEFINITION,
|
2628
2658
|
Kind.DIRECTIVE_DEFINITION,
|
2659
|
+
Kind.OPERATION_DEFINITION,
|
2629
2660
|
];
|
2630
2661
|
const rule$h = {
|
2631
2662
|
meta: {
|
2632
2663
|
docs: {
|
2633
2664
|
category: 'Schema',
|
2634
|
-
description: 'Enforce descriptions in
|
2635
|
-
url:
|
2665
|
+
description: 'Enforce descriptions in type definitions and operations.',
|
2666
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
|
2636
2667
|
examples: [
|
2637
2668
|
{
|
2638
2669
|
title: 'Incorrect',
|
@@ -2656,6 +2687,16 @@ const rule$h = {
|
|
2656
2687
|
"""
|
2657
2688
|
name: String
|
2658
2689
|
}
|
2690
|
+
`,
|
2691
|
+
},
|
2692
|
+
{
|
2693
|
+
title: 'Correct',
|
2694
|
+
usage: [{ OperationDefinition: true }],
|
2695
|
+
code: /* GraphQL */ `
|
2696
|
+
# Create a new user
|
2697
|
+
mutation createUser {
|
2698
|
+
# ...
|
2699
|
+
}
|
2659
2700
|
`,
|
2660
2701
|
},
|
2661
2702
|
],
|
@@ -2669,7 +2710,7 @@ const rule$h = {
|
|
2669
2710
|
},
|
2670
2711
|
type: 'suggestion',
|
2671
2712
|
messages: {
|
2672
|
-
[
|
2713
|
+
[RULE_ID$2]: 'Description is required for nodes of type "{{ nodeType }}"',
|
2673
2714
|
},
|
2674
2715
|
schema: {
|
2675
2716
|
type: 'array',
|
@@ -2684,19 +2725,19 @@ const rule$h = {
|
|
2684
2725
|
type: 'boolean',
|
2685
2726
|
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
|
2686
2727
|
},
|
2687
|
-
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind =>
|
2688
|
-
kind
|
2689
|
-
{
|
2690
|
-
|
2691
|
-
|
2692
|
-
}
|
2693
|
-
|
2728
|
+
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind => {
|
2729
|
+
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
|
2730
|
+
if (kind === Kind.OPERATION_DEFINITION) {
|
2731
|
+
description += '\n\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
|
2732
|
+
}
|
2733
|
+
return [kind, { type: 'boolean', description }];
|
2734
|
+
})),
|
2694
2735
|
},
|
2695
2736
|
},
|
2696
2737
|
},
|
2697
2738
|
},
|
2698
2739
|
create(context) {
|
2699
|
-
const { types, ...restOptions } = context.options[0];
|
2740
|
+
const { types, ...restOptions } = context.options[0] || {};
|
2700
2741
|
const kinds = new Set(types ? TYPES_KINDS : []);
|
2701
2742
|
for (const [kind, isEnabled] of Object.entries(restOptions)) {
|
2702
2743
|
if (isEnabled) {
|
@@ -2710,11 +2751,26 @@ const rule$h = {
|
|
2710
2751
|
return {
|
2711
2752
|
[selector](node) {
|
2712
2753
|
var _a;
|
2713
|
-
|
2714
|
-
|
2754
|
+
let description = '';
|
2755
|
+
const isOperation = node.kind === Kind.OPERATION_DEFINITION;
|
2756
|
+
if (isOperation) {
|
2757
|
+
const rawNode = node.rawNode();
|
2758
|
+
const { prev, line } = rawNode.loc.startToken;
|
2759
|
+
if (prev.kind === TokenKind.COMMENT) {
|
2760
|
+
const value = prev.value.trim();
|
2761
|
+
const linesBefore = line - prev.line;
|
2762
|
+
if (!value.startsWith('eslint') && linesBefore === 1) {
|
2763
|
+
description = value;
|
2764
|
+
}
|
2765
|
+
}
|
2766
|
+
}
|
2767
|
+
else {
|
2768
|
+
description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
|
2769
|
+
}
|
2770
|
+
if (description.length === 0) {
|
2715
2771
|
context.report({
|
2716
|
-
loc: getLocation(node.name.loc, node.name.value),
|
2717
|
-
messageId:
|
2772
|
+
loc: isOperation ? getLocation(node.loc, node.operation) : getLocation(node.name.loc, node.name.value),
|
2773
|
+
messageId: RULE_ID$2,
|
2718
2774
|
data: {
|
2719
2775
|
nodeType: node.kind,
|
2720
2776
|
},
|
@@ -2890,7 +2946,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2890
2946
|
}
|
2891
2947
|
};
|
2892
2948
|
|
2893
|
-
const RULE_ID$
|
2949
|
+
const RULE_ID$3 = 'require-id-when-available';
|
2894
2950
|
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2895
2951
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2896
2952
|
const rule$j = {
|
@@ -2899,7 +2955,7 @@ const rule$j = {
|
|
2899
2955
|
docs: {
|
2900
2956
|
category: 'Operations',
|
2901
2957
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2902
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
2958
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
|
2903
2959
|
requiresSchema: true,
|
2904
2960
|
requiresSiblings: true,
|
2905
2961
|
examples: [
|
@@ -2973,8 +3029,8 @@ const rule$j = {
|
|
2973
3029
|
},
|
2974
3030
|
},
|
2975
3031
|
create(context) {
|
2976
|
-
requireGraphQLSchemaFromContext(RULE_ID$
|
2977
|
-
const siblings = requireSiblingsOperations(RULE_ID$
|
3032
|
+
requireGraphQLSchemaFromContext(RULE_ID$3, context);
|
3033
|
+
const siblings = requireSiblingsOperations(RULE_ID$3, context);
|
2978
3034
|
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2979
3035
|
const idNames = asArray(fieldName);
|
2980
3036
|
const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
|
@@ -3036,13 +3092,13 @@ const rule$j = {
|
|
3036
3092
|
},
|
3037
3093
|
};
|
3038
3094
|
|
3039
|
-
const RULE_ID$
|
3095
|
+
const RULE_ID$4 = 'selection-set-depth';
|
3040
3096
|
const rule$k = {
|
3041
3097
|
meta: {
|
3042
3098
|
docs: {
|
3043
3099
|
category: 'Operations',
|
3044
3100
|
description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
|
3045
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3101
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
|
3046
3102
|
requiresSiblings: true,
|
3047
3103
|
examples: [
|
3048
3104
|
{
|
@@ -3116,10 +3172,10 @@ const rule$k = {
|
|
3116
3172
|
create(context) {
|
3117
3173
|
let siblings = null;
|
3118
3174
|
try {
|
3119
|
-
siblings = requireSiblingsOperations(RULE_ID$
|
3175
|
+
siblings = requireSiblingsOperations(RULE_ID$4, context);
|
3120
3176
|
}
|
3121
3177
|
catch (e) {
|
3122
|
-
logger.warn(`Rule "${RULE_ID$
|
3178
|
+
logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3123
3179
|
}
|
3124
3180
|
const { maxDepth } = context.options[0];
|
3125
3181
|
const ignore = context.options[0].ignore || [];
|
@@ -3144,14 +3200,14 @@ const rule$k = {
|
|
3144
3200
|
});
|
3145
3201
|
}
|
3146
3202
|
catch (e) {
|
3147
|
-
logger.warn(`Rule "${RULE_ID$
|
3203
|
+
logger.warn(`Rule "${RULE_ID$4}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
|
3148
3204
|
}
|
3149
3205
|
},
|
3150
3206
|
};
|
3151
3207
|
},
|
3152
3208
|
};
|
3153
3209
|
|
3154
|
-
const RULE_ID$
|
3210
|
+
const RULE_ID$5 = 'strict-id-in-types';
|
3155
3211
|
const rule$l = {
|
3156
3212
|
meta: {
|
3157
3213
|
type: 'suggestion',
|
@@ -3159,7 +3215,7 @@ const rule$l = {
|
|
3159
3215
|
description: `Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.`,
|
3160
3216
|
category: 'Schema',
|
3161
3217
|
recommended: true,
|
3162
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3218
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
|
3163
3219
|
requiresSchema: true,
|
3164
3220
|
examples: [
|
3165
3221
|
{
|
@@ -3273,7 +3329,7 @@ const rule$l = {
|
|
3273
3329
|
},
|
3274
3330
|
},
|
3275
3331
|
messages: {
|
3276
|
-
[RULE_ID$
|
3332
|
+
[RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
|
3277
3333
|
},
|
3278
3334
|
},
|
3279
3335
|
create(context) {
|
@@ -3283,7 +3339,7 @@ const rule$l = {
|
|
3283
3339
|
exceptions: {},
|
3284
3340
|
...context.options[0],
|
3285
3341
|
};
|
3286
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$
|
3342
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
|
3287
3343
|
const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
|
3288
3344
|
.filter(Boolean)
|
3289
3345
|
.map(type => type.name);
|
@@ -3313,7 +3369,7 @@ const rule$l = {
|
|
3313
3369
|
if (validIds.length !== 1) {
|
3314
3370
|
context.report({
|
3315
3371
|
loc: getLocation(node.name.loc, typeName),
|
3316
|
-
messageId: RULE_ID$
|
3372
|
+
messageId: RULE_ID$5,
|
3317
3373
|
data: {
|
3318
3374
|
typeName,
|
3319
3375
|
acceptedNamesString: options.acceptedIdNames.join(', '),
|
@@ -3943,19 +3999,30 @@ class GraphQLRuleTester extends RuleTester {
|
|
3943
3999
|
}
|
3944
4000
|
const linter = new Linter();
|
3945
4001
|
linter.defineRule(name, rule);
|
4002
|
+
const hasOnlyTest = tests.invalid.some(t => t.only);
|
3946
4003
|
for (const testCase of tests.invalid) {
|
4004
|
+
const { only, code, filename } = testCase;
|
4005
|
+
if (hasOnlyTest && !only) {
|
4006
|
+
continue;
|
4007
|
+
}
|
3947
4008
|
const verifyConfig = getVerifyConfig(name, this.config, testCase);
|
3948
4009
|
defineParser(linter, verifyConfig.parser);
|
3949
|
-
const { code, filename } = testCase;
|
3950
4010
|
const messages = linter.verify(code, verifyConfig, { filename });
|
3951
|
-
|
4011
|
+
const messageForSnapshot = [];
|
4012
|
+
for (const [index, message] of messages.entries()) {
|
3952
4013
|
if (message.fatal) {
|
3953
4014
|
throw new Error(message.message);
|
3954
4015
|
}
|
3955
|
-
|
3956
|
-
|
3957
|
-
|
4016
|
+
messageForSnapshot.push(`Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
|
4017
|
+
}
|
4018
|
+
if (rule.meta.fixable) {
|
4019
|
+
const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
|
4020
|
+
if (fixed) {
|
4021
|
+
messageForSnapshot.push('Autofix output', dedent(output));
|
4022
|
+
}
|
3958
4023
|
}
|
4024
|
+
// eslint-disable-next-line no-undef
|
4025
|
+
expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
|
3959
4026
|
}
|
3960
4027
|
}
|
3961
4028
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@graphql-eslint/eslint-plugin",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.8.0-alpha-a8fcc7b.0",
|
4
4
|
"description": "GraphQL plugin for ESLint",
|
5
5
|
"sideEffects": false,
|
6
6
|
"peerDependencies": {
|
@@ -12,6 +12,7 @@
|
|
12
12
|
"@graphql-tools/graphql-tag-pluck": "7.1.5",
|
13
13
|
"@graphql-tools/utils": "8.6.1",
|
14
14
|
"chalk": "4.1.2",
|
15
|
+
"dedent": "0.7.0",
|
15
16
|
"graphql-config": "4.1.0",
|
16
17
|
"graphql-depth-limit": "1.1.0",
|
17
18
|
"lodash.lowercase": "4.3.0"
|
package/rules/alphabetize.d.ts
CHANGED
@@ -4,7 +4,7 @@ declare const valuesEnum: ['EnumTypeDefinition'];
|
|
4
4
|
declare const selectionsEnum: ('OperationDefinition' | 'FragmentDefinition')[];
|
5
5
|
declare const variablesEnum: ['OperationDefinition'];
|
6
6
|
declare const argumentsEnum: ('FieldDefinition' | 'Field' | 'DirectiveDefinition' | 'Directive')[];
|
7
|
-
declare type AlphabetizeConfig = {
|
7
|
+
export declare type AlphabetizeConfig = {
|
8
8
|
fields?: typeof fieldsEnum;
|
9
9
|
values?: typeof valuesEnum;
|
10
10
|
selections?: typeof selectionsEnum;
|
package/rules/index.d.ts
CHANGED
@@ -1,11 +1,5 @@
|
|
1
1
|
export declare const rules: {
|
2
|
-
alphabetize: import("..").GraphQLESLintRule<[
|
3
|
-
fields?: ("ObjectTypeDefinition" | "InterfaceTypeDefinition" | "InputObjectTypeDefinition")[];
|
4
|
-
values?: ["EnumTypeDefinition"];
|
5
|
-
selections?: ("OperationDefinition" | "FragmentDefinition")[];
|
6
|
-
variables?: ["OperationDefinition"];
|
7
|
-
arguments?: ("Field" | "Directive" | "FieldDefinition" | "DirectiveDefinition")[];
|
8
|
-
}], false>;
|
2
|
+
alphabetize: import("..").GraphQLESLintRule<[import("./alphabetize").AlphabetizeConfig], false>;
|
9
3
|
'description-style': import("..").GraphQLESLintRule<[{
|
10
4
|
style: "block" | "inline";
|
11
5
|
}], false>;
|
@@ -33,20 +27,7 @@ export declare const rules: {
|
|
33
27
|
argumentName?: string;
|
34
28
|
}], false>;
|
35
29
|
'require-deprecation-reason': import("..").GraphQLESLintRule<any[], false>;
|
36
|
-
'require-description': import("..").GraphQLESLintRule<[
|
37
|
-
types?: boolean;
|
38
|
-
} & {
|
39
|
-
ScalarTypeDefinition?: boolean;
|
40
|
-
ObjectTypeDefinition?: boolean;
|
41
|
-
FieldDefinition?: boolean;
|
42
|
-
InputValueDefinition?: boolean;
|
43
|
-
InterfaceTypeDefinition?: boolean;
|
44
|
-
UnionTypeDefinition?: boolean;
|
45
|
-
EnumTypeDefinition?: boolean;
|
46
|
-
EnumValueDefinition?: boolean;
|
47
|
-
InputObjectTypeDefinition?: boolean;
|
48
|
-
DirectiveDefinition?: boolean;
|
49
|
-
}], false>;
|
30
|
+
'require-description': import("..").GraphQLESLintRule<[import("./require-description").RequireDescriptionRuleConfig], false>;
|
50
31
|
'require-field-of-type-query-in-mutation-result': import("..").GraphQLESLintRule<any[], false>;
|
51
32
|
'require-id-when-available': import("..").GraphQLESLintRule<[import("./require-id-when-available").RequireIdWhenAvailableRuleConfig], true>;
|
52
33
|
'selection-set-depth': import("..").GraphQLESLintRule<[{
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Kind } from 'graphql';
|
2
2
|
import { GraphQLESLintRule } from '../types';
|
3
|
-
declare const ALLOWED_KINDS: readonly [Kind.OBJECT_TYPE_DEFINITION, Kind.INTERFACE_TYPE_DEFINITION, Kind.ENUM_TYPE_DEFINITION, Kind.SCALAR_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.UNION_TYPE_DEFINITION, Kind.FIELD_DEFINITION, Kind.INPUT_VALUE_DEFINITION, Kind.ENUM_VALUE_DEFINITION, Kind.DIRECTIVE_DEFINITION];
|
3
|
+
declare const ALLOWED_KINDS: readonly [Kind.OBJECT_TYPE_DEFINITION, Kind.INTERFACE_TYPE_DEFINITION, Kind.ENUM_TYPE_DEFINITION, Kind.SCALAR_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.UNION_TYPE_DEFINITION, Kind.FIELD_DEFINITION, Kind.INPUT_VALUE_DEFINITION, Kind.ENUM_VALUE_DEFINITION, Kind.DIRECTIVE_DEFINITION, Kind.OPERATION_DEFINITION];
|
4
4
|
declare type AllowedKind = typeof ALLOWED_KINDS[number];
|
5
|
-
declare type RequireDescriptionRuleConfig = {
|
5
|
+
export declare type RequireDescriptionRuleConfig = {
|
6
6
|
types?: boolean;
|
7
7
|
} & {
|
8
8
|
[key in AllowedKind]?: boolean;
|
package/testkit.d.ts
CHANGED
@@ -6,7 +6,6 @@ export declare type GraphQLESLintRuleListener<WithTypeInfo extends boolean = fal
|
|
6
6
|
[K in keyof ASTKindToNode]?: (node: GraphQLESTreeNode<ASTKindToNode[K], WithTypeInfo>) => void;
|
7
7
|
} & Record<string, any>;
|
8
8
|
export declare type GraphQLValidTestCase<Options> = Omit<RuleTester.ValidTestCase, 'options' | 'parserOptions'> & {
|
9
|
-
name?: string;
|
10
9
|
options?: Options;
|
11
10
|
parserOptions?: ParserOptions;
|
12
11
|
};
|