@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 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 your type definitions.|![recommended][]|🚀
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 your type definitions.
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: 'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
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
- let prevName = null;
853
- for (const node of nodes) {
854
- const currName = node.name.value;
855
- if (prevName && prevName > currName) {
856
- const isVariableNode = node.kind === graphql.Kind.VARIABLE;
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(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
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 REQUIRE_DESCRIPTION_ERROR = 'REQUIRE_DESCRIPTION_ERROR';
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 your type definitions.',
2641
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-description.md',
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
- [REQUIRE_DESCRIPTION_ERROR]: 'Description is required for nodes of type "{{ nodeType }}"',
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
- type: 'boolean',
2697
- description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
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
- const description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value) || '';
2720
- if (description.trim().length === 0) {
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: REQUIRE_DESCRIPTION_ERROR,
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$2 = 'require-id-when-available';
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$2}.md`,
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$2, context);
2983
- const siblings = requireSiblingsOperations(RULE_ID$2, context);
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$3 = 'selection-set-depth';
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$3}.md`,
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$3, context);
3181
+ siblings = requireSiblingsOperations(RULE_ID$4, context);
3126
3182
  }
3127
3183
  catch (e) {
3128
- logger.warn(`Rule "${RULE_ID$3}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
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$3}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
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$4 = 'strict-id-in-types';
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$4}.md`,
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$4]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
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$4, context);
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$4,
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
- for (const message of messages) {
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
- const messageForSnapshot = visualizeEslintMessage(code, message);
3962
- // eslint-disable-next-line no-undef
3963
- expect(messageForSnapshot).toMatchSnapshot();
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: 'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
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
- let prevName = null;
847
- for (const node of nodes) {
848
- const currName = node.name.value;
849
- if (prevName && prevName > currName) {
850
- const isVariableNode = node.kind === Kind.VARIABLE;
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(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
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 REQUIRE_DESCRIPTION_ERROR = 'REQUIRE_DESCRIPTION_ERROR';
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 your type definitions.',
2635
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/require-description.md',
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
- [REQUIRE_DESCRIPTION_ERROR]: 'Description is required for nodes of type "{{ nodeType }}"',
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
- type: 'boolean',
2691
- description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
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
- const description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value) || '';
2714
- if (description.trim().length === 0) {
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: REQUIRE_DESCRIPTION_ERROR,
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$2 = 'require-id-when-available';
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$2}.md`,
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$2, context);
2977
- const siblings = requireSiblingsOperations(RULE_ID$2, context);
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$3 = 'selection-set-depth';
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$3}.md`,
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$3, context);
3175
+ siblings = requireSiblingsOperations(RULE_ID$4, context);
3120
3176
  }
3121
3177
  catch (e) {
3122
- logger.warn(`Rule "${RULE_ID$3}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
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$3}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
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$4 = 'strict-id-in-types';
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$4}.md`,
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$4]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
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$4, context);
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$4,
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
- for (const message of messages) {
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
- const messageForSnapshot = visualizeEslintMessage(code, message);
3956
- // eslint-disable-next-line no-undef
3957
- expect(messageForSnapshot).toMatchSnapshot();
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.7.0",
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"
@@ -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
  };