@graphql-eslint/eslint-plugin 3.8.0 → 3.9.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.
Files changed (80) hide show
  1. package/README.md +5 -5
  2. package/configs/base.json +4 -0
  3. package/configs/operations-all.json +24 -0
  4. package/configs/operations-recommended.json +50 -0
  5. package/configs/schema-all.json +26 -0
  6. package/configs/schema-recommended.json +49 -0
  7. package/docs/README.md +59 -57
  8. package/docs/custom-rules.md +8 -8
  9. package/docs/parser-options.md +4 -4
  10. package/docs/parser.md +2 -2
  11. package/docs/rules/alphabetize.md +11 -5
  12. package/docs/rules/description-style.md +2 -0
  13. package/docs/rules/executable-definitions.md +1 -1
  14. package/docs/rules/fields-on-correct-type.md +1 -1
  15. package/docs/rules/fragments-on-composite-type.md +1 -1
  16. package/docs/rules/input-name.md +2 -0
  17. package/docs/rules/known-argument-names.md +1 -1
  18. package/docs/rules/known-directives.md +29 -2
  19. package/docs/rules/known-fragment-names.md +1 -1
  20. package/docs/rules/known-type-names.md +1 -1
  21. package/docs/rules/lone-anonymous-operation.md +1 -1
  22. package/docs/rules/lone-schema-definition.md +1 -1
  23. package/docs/rules/naming-convention.md +2 -0
  24. package/docs/rules/no-anonymous-operations.md +2 -0
  25. package/docs/rules/no-case-insensitive-enum-values-duplicates.md +2 -0
  26. package/docs/rules/no-deprecated.md +2 -0
  27. package/docs/rules/no-duplicate-fields.md +2 -0
  28. package/docs/rules/no-fragment-cycles.md +1 -1
  29. package/docs/rules/no-hashtag-description.md +2 -0
  30. package/docs/rules/no-root-type.md +2 -0
  31. package/docs/rules/no-scalar-result-type-on-mutation.md +2 -0
  32. package/docs/rules/no-typename-prefix.md +2 -0
  33. package/docs/rules/no-undefined-variables.md +1 -1
  34. package/docs/rules/no-unreachable-types.md +2 -0
  35. package/docs/rules/no-unused-fields.md +2 -0
  36. package/docs/rules/no-unused-fragments.md +1 -1
  37. package/docs/rules/no-unused-variables.md +1 -1
  38. package/docs/rules/one-field-subscriptions.md +1 -1
  39. package/docs/rules/overlapping-fields-can-be-merged.md +1 -1
  40. package/docs/rules/possible-fragment-spread.md +1 -1
  41. package/docs/rules/possible-type-extension.md +1 -1
  42. package/docs/rules/provided-required-arguments.md +1 -1
  43. package/docs/rules/require-deprecation-date.md +2 -0
  44. package/docs/rules/require-id-when-available.md +2 -0
  45. package/docs/rules/scalar-leafs.md +1 -1
  46. package/docs/rules/selection-set-depth.md +2 -0
  47. package/docs/rules/unique-argument-names.md +1 -1
  48. package/docs/rules/unique-directive-names-per-location.md +1 -1
  49. package/docs/rules/unique-directive-names.md +1 -1
  50. package/docs/rules/unique-enum-value-names.md +1 -1
  51. package/docs/rules/unique-field-definition-names.md +1 -1
  52. package/docs/rules/unique-input-field-names.md +1 -1
  53. package/docs/rules/unique-operation-types.md +1 -1
  54. package/docs/rules/unique-type-names.md +1 -1
  55. package/docs/rules/unique-variable-names.md +1 -1
  56. package/docs/rules/value-literals-of-correct-type.md +1 -1
  57. package/docs/rules/variables-are-input-types.md +1 -1
  58. package/docs/rules/variables-in-allowed-position.md +1 -1
  59. package/estree-parser/converter.d.ts +3 -2
  60. package/estree-parser/estree-ast.d.ts +18 -18
  61. package/estree-parser/utils.d.ts +2 -8
  62. package/index.d.ts +6 -2
  63. package/index.js +726 -723
  64. package/index.mjs +726 -717
  65. package/package.json +1 -1
  66. package/rules/alphabetize.d.ts +1 -0
  67. package/rules/graphql-js-validation.d.ts +1 -1
  68. package/rules/index.d.ts +1 -4
  69. package/rules/selection-set-depth.d.ts +1 -1
  70. package/sibling-operations.d.ts +3 -3
  71. package/testkit.d.ts +3 -3
  72. package/types.d.ts +24 -18
  73. package/utils.d.ts +14 -4
  74. package/configs/base.d.ts +0 -5
  75. package/configs/index.d.ts +0 -133
  76. package/configs/operations-all.d.ts +0 -19
  77. package/configs/operations-recommended.d.ts +0 -50
  78. package/configs/schema-all.d.ts +0 -15
  79. package/configs/schema-recommended.d.ts +0 -47
  80. package/graphql-ast.d.ts +0 -6
package/index.js CHANGED
@@ -11,6 +11,7 @@ const path = require('path');
11
11
  const utils = require('@graphql-tools/utils');
12
12
  const lowerCase = _interopDefault(require('lodash.lowercase'));
13
13
  const chalk = _interopDefault(require('chalk'));
14
+ const valueFromASTUntyped = require('graphql/utilities/valueFromASTUntyped');
14
15
  const depthLimit = _interopDefault(require('graphql-depth-limit'));
15
16
  const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
16
17
  const graphqlConfig = require('graphql-config');
@@ -18,163 +19,6 @@ const codeFileLoader = require('@graphql-tools/code-file-loader');
18
19
  const eslint = require('eslint');
19
20
  const codeFrame = require('@babel/code-frame');
20
21
 
21
- const base = {
22
- parser: '@graphql-eslint/eslint-plugin',
23
- plugins: ['@graphql-eslint'],
24
- };
25
-
26
- /*
27
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
28
- */
29
- const schemaRecommendedConfig = {
30
- extends: ['plugin:@graphql-eslint/base'],
31
- rules: {
32
- '@graphql-eslint/description-style': 'error',
33
- '@graphql-eslint/known-argument-names': 'error',
34
- '@graphql-eslint/known-directives': 'error',
35
- '@graphql-eslint/known-type-names': 'error',
36
- '@graphql-eslint/lone-schema-definition': 'error',
37
- '@graphql-eslint/naming-convention': [
38
- 'error',
39
- {
40
- types: 'PascalCase',
41
- FieldDefinition: 'camelCase',
42
- InputValueDefinition: 'camelCase',
43
- Argument: 'camelCase',
44
- DirectiveDefinition: 'camelCase',
45
- EnumValueDefinition: 'UPPER_CASE',
46
- 'FieldDefinition[parent.name.value=Query]': {
47
- forbiddenPrefixes: ['query', 'get'],
48
- forbiddenSuffixes: ['Query'],
49
- },
50
- 'FieldDefinition[parent.name.value=Mutation]': {
51
- forbiddenPrefixes: ['mutation'],
52
- forbiddenSuffixes: ['Mutation'],
53
- },
54
- 'FieldDefinition[parent.name.value=Subscription]': {
55
- forbiddenPrefixes: ['subscription'],
56
- forbiddenSuffixes: ['Subscription'],
57
- },
58
- },
59
- ],
60
- '@graphql-eslint/no-case-insensitive-enum-values-duplicates': 'error',
61
- '@graphql-eslint/no-hashtag-description': 'error',
62
- '@graphql-eslint/no-typename-prefix': 'error',
63
- '@graphql-eslint/no-unreachable-types': 'error',
64
- '@graphql-eslint/provided-required-arguments': 'error',
65
- '@graphql-eslint/require-deprecation-reason': 'error',
66
- '@graphql-eslint/require-description': ['error', { types: true, DirectiveDefinition: true }],
67
- '@graphql-eslint/strict-id-in-types': 'error',
68
- '@graphql-eslint/unique-directive-names': 'error',
69
- '@graphql-eslint/unique-directive-names-per-location': 'error',
70
- '@graphql-eslint/unique-field-definition-names': 'error',
71
- '@graphql-eslint/unique-operation-types': 'error',
72
- '@graphql-eslint/unique-type-names': 'error',
73
- },
74
- };
75
-
76
- /*
77
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
78
- */
79
- const schemaAllConfig = {
80
- extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/schema-recommended'],
81
- rules: {
82
- '@graphql-eslint/alphabetize': [
83
- 'error',
84
- {
85
- fields: ['ObjectTypeDefinition', 'InterfaceTypeDefinition', 'InputObjectTypeDefinition'],
86
- values: ['EnumTypeDefinition'],
87
- arguments: ['FieldDefinition', 'Field', 'DirectiveDefinition', 'Directive'],
88
- },
89
- ],
90
- '@graphql-eslint/input-name': 'error',
91
- '@graphql-eslint/no-scalar-result-type-on-mutation': 'error',
92
- '@graphql-eslint/require-deprecation-date': 'error',
93
- '@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error',
94
- },
95
- };
96
-
97
- /*
98
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
99
- */
100
- const operationsRecommendedConfig = {
101
- extends: ['plugin:@graphql-eslint/base'],
102
- rules: {
103
- '@graphql-eslint/executable-definitions': 'error',
104
- '@graphql-eslint/fields-on-correct-type': 'error',
105
- '@graphql-eslint/fragments-on-composite-type': 'error',
106
- '@graphql-eslint/known-argument-names': 'error',
107
- '@graphql-eslint/known-directives': 'error',
108
- '@graphql-eslint/known-fragment-names': 'error',
109
- '@graphql-eslint/known-type-names': 'error',
110
- '@graphql-eslint/lone-anonymous-operation': 'error',
111
- '@graphql-eslint/naming-convention': [
112
- 'error',
113
- {
114
- VariableDefinition: 'camelCase',
115
- OperationDefinition: {
116
- style: 'PascalCase',
117
- forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
118
- forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
119
- },
120
- FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] },
121
- },
122
- ],
123
- '@graphql-eslint/no-anonymous-operations': 'error',
124
- '@graphql-eslint/no-deprecated': 'error',
125
- '@graphql-eslint/no-duplicate-fields': 'error',
126
- '@graphql-eslint/no-fragment-cycles': 'error',
127
- '@graphql-eslint/no-undefined-variables': 'error',
128
- '@graphql-eslint/no-unused-fragments': 'error',
129
- '@graphql-eslint/no-unused-variables': 'error',
130
- '@graphql-eslint/one-field-subscriptions': 'error',
131
- '@graphql-eslint/overlapping-fields-can-be-merged': 'error',
132
- '@graphql-eslint/possible-fragment-spread': 'error',
133
- '@graphql-eslint/provided-required-arguments': 'error',
134
- '@graphql-eslint/require-id-when-available': 'error',
135
- '@graphql-eslint/scalar-leafs': 'error',
136
- '@graphql-eslint/selection-set-depth': ['error', { maxDepth: 7 }],
137
- '@graphql-eslint/unique-argument-names': 'error',
138
- '@graphql-eslint/unique-directive-names-per-location': 'error',
139
- '@graphql-eslint/unique-input-field-names': 'error',
140
- '@graphql-eslint/unique-variable-names': 'error',
141
- '@graphql-eslint/value-literals-of-correct-type': 'error',
142
- '@graphql-eslint/variables-are-input-types': 'error',
143
- '@graphql-eslint/variables-in-allowed-position': 'error',
144
- },
145
- };
146
-
147
- /*
148
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
149
- */
150
- const operationsAllConfig = {
151
- extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/operations-recommended'],
152
- rules: {
153
- '@graphql-eslint/alphabetize': [
154
- 'error',
155
- {
156
- selections: ['OperationDefinition', 'FragmentDefinition'],
157
- variables: ['OperationDefinition'],
158
- arguments: ['Field', 'Directive'],
159
- },
160
- ],
161
- '@graphql-eslint/match-document-filename': [
162
- 'error',
163
- { query: 'kebab-case', mutation: 'kebab-case', subscription: 'kebab-case', fragment: 'kebab-case' },
164
- ],
165
- '@graphql-eslint/unique-fragment-name': 'error',
166
- '@graphql-eslint/unique-operation-name': 'error',
167
- },
168
- };
169
-
170
- const configs = {
171
- base,
172
- 'schema-recommended': schemaRecommendedConfig,
173
- 'schema-all': schemaAllConfig,
174
- 'operations-recommended': operationsRecommendedConfig,
175
- 'operations-all': operationsAllConfig,
176
- };
177
-
178
22
  function requireSiblingsOperations(ruleName, context) {
179
23
  if (!context.parserServices) {
180
24
  throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
@@ -199,15 +43,6 @@ const logger = {
199
43
  // eslint-disable-next-line no-console
200
44
  warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
201
45
  };
202
- function requireReachableTypesFromContext(ruleName, context) {
203
- const schema = requireGraphQLSchemaFromContext(ruleName, context);
204
- return context.parserServices.reachableTypes(schema);
205
- }
206
- function requireUsedFieldsFromContext(ruleName, context) {
207
- const schema = requireGraphQLSchemaFromContext(ruleName, context);
208
- const siblings = requireSiblingsOperations(ruleName, context);
209
- return context.parserServices.usedFields(schema, siblings);
210
- }
211
46
  const normalizePath = (path) => (path || '').replace(/\\/g, '/');
212
47
  /**
213
48
  * https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
@@ -277,8 +112,8 @@ const convertCase = (style, str) => {
277
112
  return lowerCase(str).replace(/ /g, '-');
278
113
  }
279
114
  };
280
- function getLocation(loc, fieldName = '') {
281
- const { line, column } = loc.start;
115
+ function getLocation(start, fieldName = '') {
116
+ const { line, column } = start;
282
117
  return {
283
118
  start: {
284
119
  line,
@@ -290,6 +125,15 @@ function getLocation(loc, fieldName = '') {
290
125
  },
291
126
  };
292
127
  }
128
+ const REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 };
129
+ const ARRAY_DEFAULT_OPTIONS = {
130
+ type: 'array',
131
+ uniqueItems: true,
132
+ minItems: 1,
133
+ items: {
134
+ type: 'string',
135
+ },
136
+ };
293
137
 
294
138
  function validateDocument(context, schema = null, documentNode, rule) {
295
139
  if (documentNode.definitions.length === 0) {
@@ -301,23 +145,27 @@ function validateDocument(context, schema = null, documentNode, rule) {
301
145
  : validate.validateSDL(documentNode, null, [rule]);
302
146
  for (const error of validationErrors) {
303
147
  const { line, column } = error.locations[0];
304
- const ancestors = context.getAncestors();
305
- const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
148
+ const sourceCode = context.getSourceCode();
149
+ const { tokens } = sourceCode.ast;
150
+ const token = tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
151
+ let loc = {
152
+ line,
153
+ column: column - 1,
154
+ };
155
+ if (token) {
156
+ loc =
157
+ // if cursor on `@` symbol than use next node
158
+ token.type === '@' ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc;
159
+ }
306
160
  context.report({
307
- loc: token
308
- ? token.loc
309
- : {
310
- line,
311
- column: column - 1,
312
- },
161
+ loc,
313
162
  message: error.message,
314
163
  });
315
164
  }
316
165
  }
317
166
  catch (e) {
318
167
  context.report({
319
- // Report on first character
320
- loc: { column: 0, line: 1 },
168
+ loc: REPORT_ON_FIRST_CHARACTER,
321
169
  message: e.message,
322
170
  });
323
171
  }
@@ -374,7 +222,7 @@ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
374
222
  }
375
223
  return node;
376
224
  };
377
- const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
225
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = []) => {
378
226
  let ruleFn = null;
379
227
  try {
380
228
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
@@ -395,8 +243,9 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
395
243
  ...docs,
396
244
  graphQLJSRuleName: ruleName,
397
245
  url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
398
- description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function. [You can find its source code here](https://github.com/graphql/graphql-js/blob/main/src/validation/rules/${ruleName}Rule.ts).`,
246
+ description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
399
247
  },
248
+ schema,
400
249
  },
401
250
  create(context) {
402
251
  if (!ruleFn) {
@@ -434,8 +283,45 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
434
283
  requiresSchema: true,
435
284
  }), validationToRule('known-directives', 'KnownDirectives', {
436
285
  category: ['Schema', 'Operations'],
437
- description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
286
+ description: `A GraphQL document is only valid if all \`@directive\`s are known by the schema and legally positioned.`,
438
287
  requiresSchema: true,
288
+ examples: [
289
+ {
290
+ title: 'Valid',
291
+ usage: [{ ignoreClientDirectives: ['client'] }],
292
+ code: /* GraphQL */ `
293
+ {
294
+ product {
295
+ someClientField @client
296
+ }
297
+ }
298
+ `,
299
+ },
300
+ ],
301
+ }, ({ context, node: documentNode }) => {
302
+ const { ignoreClientDirectives = [] } = context.options[0] || {};
303
+ if (ignoreClientDirectives.length === 0) {
304
+ return documentNode;
305
+ }
306
+ return graphql.visit(documentNode, {
307
+ Field(node) {
308
+ return {
309
+ ...node,
310
+ directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
311
+ };
312
+ },
313
+ });
314
+ }, {
315
+ type: 'array',
316
+ maxItems: 1,
317
+ items: {
318
+ type: 'object',
319
+ additionalProperties: false,
320
+ required: ['ignoreClientDirectives'],
321
+ properties: {
322
+ ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
323
+ },
324
+ },
439
325
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
440
326
  category: 'Operations',
441
327
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
@@ -559,8 +445,10 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
559
445
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
560
446
  category: 'Schema',
561
447
  description: `A type extension is only valid if the type is defined and has the same kind.`,
448
+ // TODO: add in graphql-eslint v4
562
449
  recommended: false,
563
450
  requiresSchema: true,
451
+ isDisabledForAllConfig: true,
564
452
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
565
453
  category: ['Schema', 'Operations'],
566
454
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
@@ -588,6 +476,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
588
476
  category: 'Schema',
589
477
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
590
478
  recommended: false,
479
+ isDisabledForAllConfig: true,
591
480
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
592
481
  category: 'Schema',
593
482
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
@@ -618,7 +507,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
618
507
  requiresSchema: true,
619
508
  }));
620
509
 
621
- const ALPHABETIZE = 'ALPHABETIZE';
510
+ const RULE_ID = 'alphabetize';
622
511
  const fieldsEnum = [
623
512
  graphql.Kind.OBJECT_TYPE_DEFINITION,
624
513
  graphql.Kind.INTERFACE_TYPE_DEFINITION,
@@ -643,7 +532,7 @@ const rule = {
643
532
  docs: {
644
533
  category: ['Schema', 'Operations'],
645
534
  description: `Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.`,
646
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
535
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
647
536
  examples: [
648
537
  {
649
538
  title: 'Incorrect',
@@ -726,6 +615,8 @@ const rule = {
726
615
  fields: fieldsEnum,
727
616
  values: valuesEnum,
728
617
  arguments: argumentsEnum,
618
+ // TODO: add in graphql-eslint v4
619
+ // definitions: true,
729
620
  },
730
621
  ],
731
622
  operations: [
@@ -738,7 +629,7 @@ const rule = {
738
629
  },
739
630
  },
740
631
  messages: {
741
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
632
+ [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
742
633
  },
743
634
  schema: {
744
635
  type: 'array',
@@ -750,49 +641,44 @@ const rule = {
750
641
  minProperties: 1,
751
642
  properties: {
752
643
  fields: {
753
- type: 'array',
754
- uniqueItems: true,
755
- minItems: 1,
644
+ ...ARRAY_DEFAULT_OPTIONS,
756
645
  items: {
757
646
  enum: fieldsEnum,
758
647
  },
759
- description: 'Fields of `type`, `interface`, and `input`',
648
+ description: 'Fields of `type`, `interface`, and `input`.',
760
649
  },
761
650
  values: {
762
- type: 'array',
763
- uniqueItems: true,
764
- minItems: 1,
651
+ ...ARRAY_DEFAULT_OPTIONS,
765
652
  items: {
766
653
  enum: valuesEnum,
767
654
  },
768
- description: 'Values of `enum`',
655
+ description: 'Values of `enum`.',
769
656
  },
770
657
  selections: {
771
- type: 'array',
772
- uniqueItems: true,
773
- minItems: 1,
658
+ ...ARRAY_DEFAULT_OPTIONS,
774
659
  items: {
775
660
  enum: selectionsEnum,
776
661
  },
777
- description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
662
+ description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
778
663
  },
779
664
  variables: {
780
- type: 'array',
781
- uniqueItems: true,
782
- minItems: 1,
665
+ ...ARRAY_DEFAULT_OPTIONS,
783
666
  items: {
784
667
  enum: variablesEnum,
785
668
  },
786
- description: 'Variables of operations (`query`, `mutation` and `subscription`)',
669
+ description: 'Variables of operations `query`, `mutation` and `subscription`.',
787
670
  },
788
671
  arguments: {
789
- type: 'array',
790
- uniqueItems: true,
791
- minItems: 1,
672
+ ...ARRAY_DEFAULT_OPTIONS,
792
673
  items: {
793
674
  enum: argumentsEnum,
794
675
  },
795
- description: 'Arguments of fields and directives',
676
+ description: 'Arguments of fields and directives.',
677
+ },
678
+ definitions: {
679
+ type: 'boolean',
680
+ description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
681
+ default: false,
796
682
  },
797
683
  },
798
684
  },
@@ -813,9 +699,22 @@ const rule = {
813
699
  if (tokenBefore) {
814
700
  return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
815
701
  }
816
- return commentsBefore;
702
+ const filteredComments = [];
703
+ const nodeLine = node.loc.start.line;
704
+ // Break on comment that not attached to node
705
+ for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
706
+ const comment = commentsBefore[i];
707
+ if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
708
+ break;
709
+ }
710
+ filteredComments.unshift(comment);
711
+ }
712
+ return filteredComments;
817
713
  }
818
714
  function getRangeWithComments(node) {
715
+ if (node.kind === graphql.Kind.VARIABLE) {
716
+ node = node.parent;
717
+ }
819
718
  const [firstBeforeComment] = getBeforeComments(node);
820
719
  const [firstAfterComment] = sourceCode.getCommentsAfter(node);
821
720
  const from = firstBeforeComment || node;
@@ -823,26 +722,35 @@ const rule = {
823
722
  return [from.range[0], to.range[1]];
824
723
  }
825
724
  function checkNodes(nodes) {
725
+ var _a, _b;
826
726
  // Starts from 1, ignore nodes.length <= 1
827
727
  for (let i = 1; i < nodes.length; i += 1) {
828
- const prevNode = nodes[i - 1];
829
728
  const currNode = nodes[i];
830
- const prevName = prevNode.name.value;
831
- const currName = currNode.name.value;
832
- // Compare with lexicographic order
833
- if (prevName.localeCompare(currName) !== 1) {
729
+ const currName = 'name' in currNode && ((_a = currNode.name) === null || _a === void 0 ? void 0 : _a.value);
730
+ if (!currName) {
731
+ // we don't move unnamed current nodes
834
732
  continue;
835
733
  }
836
- const isVariableNode = currNode.kind === graphql.Kind.VARIABLE;
734
+ const prevNode = nodes[i - 1];
735
+ const prevName = 'name' in prevNode && ((_b = prevNode.name) === null || _b === void 0 ? void 0 : _b.value);
736
+ if (prevName) {
737
+ // Compare with lexicographic order
738
+ const compareResult = prevName.localeCompare(currName);
739
+ const shouldSort = compareResult === 1;
740
+ if (!shouldSort) {
741
+ const isSameName = compareResult === 0;
742
+ if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
743
+ continue;
744
+ }
745
+ }
746
+ }
837
747
  context.report({
838
748
  node: currNode.name,
839
- messageId: ALPHABETIZE,
840
- data: isVariableNode
841
- ? {
842
- currName: `$${currName}`,
843
- prevName: `$${prevName}`,
844
- }
845
- : { currName, prevName },
749
+ messageId: RULE_ID,
750
+ data: {
751
+ currName,
752
+ prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
753
+ },
846
754
  *fix(fixer) {
847
755
  const prevRange = getRangeWithComments(prevNode);
848
756
  const currRange = getRangeWithComments(currNode);
@@ -883,10 +791,7 @@ const rule = {
883
791
  }
884
792
  if (selectionsSelector) {
885
793
  listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => {
886
- checkNodes(node.selections
887
- // inline fragment don't have name, so we skip them
888
- .filter(selection => selection.kind !== graphql.Kind.INLINE_FRAGMENT)
889
- .map(selection =>
794
+ checkNodes(node.selections.map(selection =>
890
795
  // sort by alias is field is renamed
891
796
  'alias' in selection && selection.alias ? { name: selection.alias } : selection));
892
797
  };
@@ -901,6 +806,11 @@ const rule = {
901
806
  checkNodes(node.arguments);
902
807
  };
903
808
  }
809
+ if (opts.definitions) {
810
+ listeners.Document = node => {
811
+ checkNodes(node.definitions);
812
+ };
813
+ }
904
814
  return listeners;
905
815
  },
906
816
  };
@@ -908,6 +818,7 @@ const rule = {
908
818
  const rule$1 = {
909
819
  meta: {
910
820
  type: 'suggestion',
821
+ hasSuggestions: true,
911
822
  docs: {
912
823
  examples: [
913
824
  {
@@ -955,8 +866,21 @@ const rule$1 = {
955
866
  return {
956
867
  [`.description[type=StringValue][block!=${isBlock}]`](node) {
957
868
  context.report({
958
- loc: getLocation(node.loc),
959
- message: `Unexpected ${isBlock ? 'inline' : 'block'} description`,
869
+ loc: isBlock ? node.loc : node.loc.start,
870
+ message: `Unexpected ${isBlock ? 'inline' : 'block'} description.`,
871
+ suggest: [
872
+ {
873
+ desc: `Change to ${isBlock ? 'block' : 'inline'} style description`,
874
+ fix(fixer) {
875
+ const sourceCode = context.getSourceCode();
876
+ const originalText = sourceCode.getText(node);
877
+ const newText = isBlock
878
+ ? originalText.replace(/(^")|("$)/g, '"""')
879
+ : originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, ' ');
880
+ return fixer.replaceText(node, newText);
881
+ },
882
+ },
883
+ ],
960
884
  });
961
885
  },
962
886
  };
@@ -969,6 +893,7 @@ const isMutationType = (node) => isObjectType(node) && node.name.value === 'Muta
969
893
  const rule$2 = {
970
894
  meta: {
971
895
  type: 'suggestion',
896
+ hasSuggestions: true,
972
897
  docs: {
973
898
  description: 'Require mutation argument to be always called "input" and input type to be called Mutation name + "Input".\nUsing the same name for all input parameters will make your schemas easier to consume and more predictable. Using the same name as mutation for InputType will make it easier to find mutations that InputType belongs to.',
974
899
  category: 'Schema',
@@ -1042,12 +967,18 @@ const rule$2 = {
1042
967
  };
1043
968
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1044
969
  const listeners = {
1045
- 'FieldDefinition > InputValueDefinition[name.value!=input]'(node) {
1046
- if (shouldCheckType(node.parent.parent)) {
1047
- const name = node.name.value;
970
+ 'FieldDefinition > InputValueDefinition[name.value!=input] > Name'(node) {
971
+ if (shouldCheckType(node.parent.parent.parent)) {
972
+ const inputName = node.value;
1048
973
  context.report({
1049
- node: node.name,
1050
- message: `Input "${name}" should be called "input"`,
974
+ node,
975
+ message: `Input \`${inputName}\` should be called \`input\`.`,
976
+ suggest: [
977
+ {
978
+ desc: 'Rename to `input`',
979
+ fix: fixer => fixer.replaceText(node, 'input'),
980
+ },
981
+ ],
1051
982
  });
1052
983
  }
1053
984
  },
@@ -1069,7 +1000,13 @@ const rule$2 = {
1069
1000
  name.toLowerCase() !== mutationName.toLowerCase()) {
1070
1001
  context.report({
1071
1002
  node: node.name,
1072
- message: `InputType "${name}" name should be "${mutationName}"`,
1003
+ message: `Input type \`${name}\` name should be \`${mutationName}\`.`,
1004
+ suggest: [
1005
+ {
1006
+ desc: `Rename to \`${mutationName}\``,
1007
+ fix: fixer => fixer.replaceText(node, mutationName),
1008
+ },
1009
+ ],
1073
1010
  });
1074
1011
  }
1075
1012
  }
@@ -1082,7 +1019,14 @@ const rule$2 = {
1082
1019
  const MATCH_EXTENSION = 'MATCH_EXTENSION';
1083
1020
  const MATCH_STYLE = 'MATCH_STYLE';
1084
1021
  const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
1085
- const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case', 'matchDocumentStyle'];
1022
+ const CASE_STYLES = [
1023
+ 'camelCase',
1024
+ 'PascalCase',
1025
+ 'snake_case',
1026
+ 'UPPER_CASE',
1027
+ 'kebab-case',
1028
+ 'matchDocumentStyle',
1029
+ ];
1086
1030
  const schemaOption = {
1087
1031
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1088
1032
  };
@@ -1227,8 +1171,7 @@ const rule$3 = {
1227
1171
  var _a;
1228
1172
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1229
1173
  context.report({
1230
- // Report on first character
1231
- loc: { column: 0, line: 1 },
1174
+ loc: REPORT_ON_FIRST_CHARACTER,
1232
1175
  messageId: MATCH_EXTENSION,
1233
1176
  data: {
1234
1177
  fileExtension,
@@ -1267,8 +1210,7 @@ const rule$3 = {
1267
1210
  const filenameWithExtension = filename + expectedExtension;
1268
1211
  if (expectedFilename !== filenameWithExtension) {
1269
1212
  context.report({
1270
- // Report on first character
1271
- loc: { column: 0, line: 1 },
1213
+ loc: REPORT_ON_FIRST_CHARACTER,
1272
1214
  messageId: MATCH_STYLE,
1273
1215
  data: {
1274
1216
  expectedFilename,
@@ -1440,18 +1382,8 @@ const rule$4 = {
1440
1382
  style: { enum: ALLOWED_STYLES },
1441
1383
  prefix: { type: 'string' },
1442
1384
  suffix: { type: 'string' },
1443
- forbiddenPrefixes: {
1444
- type: 'array',
1445
- uniqueItems: true,
1446
- minItems: 1,
1447
- items: { type: 'string' },
1448
- },
1449
- forbiddenSuffixes: {
1450
- type: 'array',
1451
- uniqueItems: true,
1452
- minItems: 1,
1453
- items: { type: 'string' },
1454
- },
1385
+ forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
1386
+ forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
1455
1387
  ignorePattern: {
1456
1388
  type: 'string',
1457
1389
  description: 'Option to skip validation of some words, e.g. acronyms',
@@ -1505,6 +1437,18 @@ const rule$4 = {
1505
1437
  const style = restOptions[kind] || types;
1506
1438
  return typeof style === 'object' ? style : { style };
1507
1439
  }
1440
+ function report(node, message, suggestedName) {
1441
+ context.report({
1442
+ node,
1443
+ message,
1444
+ suggest: [
1445
+ {
1446
+ desc: `Rename to \`${suggestedName}\``,
1447
+ fix: fixer => fixer.replaceText(node, suggestedName),
1448
+ },
1449
+ ],
1450
+ });
1451
+ }
1508
1452
  const checkNode = (selector) => (n) => {
1509
1453
  const { name: node } = n.kind === graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
1510
1454
  if (!node) {
@@ -1519,16 +1463,7 @@ const rule$4 = {
1519
1463
  const [leadingUnderscores] = nodeName.match(/^_*/);
1520
1464
  const [trailingUnderscores] = nodeName.match(/_*$/);
1521
1465
  const suggestedName = leadingUnderscores + renameToName + trailingUnderscores;
1522
- context.report({
1523
- node,
1524
- message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1525
- suggest: [
1526
- {
1527
- desc: `Rename to "${suggestedName}"`,
1528
- fix: fixer => fixer.replaceText(node, suggestedName),
1529
- },
1530
- ],
1531
- });
1466
+ report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedName);
1532
1467
  }
1533
1468
  function getError() {
1534
1469
  const name = nodeName.replace(/(^_+)|(_+$)/g, '');
@@ -1575,18 +1510,8 @@ const rule$4 = {
1575
1510
  }
1576
1511
  };
1577
1512
  const checkUnderscore = (isLeading) => (node) => {
1578
- const name = node.value;
1579
- const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
1580
- context.report({
1581
- node,
1582
- message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
1583
- suggest: [
1584
- {
1585
- desc: `Rename to "${renameToName}"`,
1586
- fix: fixer => fixer.replaceText(node, renameToName),
1587
- },
1588
- ],
1589
- });
1513
+ const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, '');
1514
+ report(node, `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, suggestedName);
1590
1515
  };
1591
1516
  const listeners = {};
1592
1517
  if (!allowLeadingUnderscore) {
@@ -1605,15 +1530,16 @@ const rule$4 = {
1605
1530
  },
1606
1531
  };
1607
1532
 
1608
- const NO_ANONYMOUS_OPERATIONS = 'NO_ANONYMOUS_OPERATIONS';
1533
+ const RULE_ID$1 = 'no-anonymous-operations';
1609
1534
  const rule$5 = {
1610
1535
  meta: {
1611
1536
  type: 'suggestion',
1537
+ hasSuggestions: true,
1612
1538
  docs: {
1613
1539
  category: 'Operations',
1614
1540
  description: 'Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.',
1615
1541
  recommended: true,
1616
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-anonymous-operations.md',
1542
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
1617
1543
  examples: [
1618
1544
  {
1619
1545
  title: 'Incorrect',
@@ -1634,19 +1560,27 @@ const rule$5 = {
1634
1560
  ],
1635
1561
  },
1636
1562
  messages: {
1637
- [NO_ANONYMOUS_OPERATIONS]: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`,
1563
+ [RULE_ID$1]: `Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!`,
1638
1564
  },
1639
1565
  schema: [],
1640
1566
  },
1641
1567
  create(context) {
1642
1568
  return {
1643
1569
  'OperationDefinition[name=undefined]'(node) {
1570
+ const [firstSelection] = node.selectionSet.selections;
1571
+ const suggestedName = firstSelection.type === graphql.Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;
1644
1572
  context.report({
1645
- loc: getLocation(node.loc, node.operation),
1573
+ loc: getLocation(node.loc.start, node.operation),
1574
+ messageId: RULE_ID$1,
1646
1575
  data: {
1647
1576
  operation: node.operation,
1648
1577
  },
1649
- messageId: NO_ANONYMOUS_OPERATIONS,
1578
+ suggest: [
1579
+ {
1580
+ desc: `Rename to \`${suggestedName}\``,
1581
+ fix: fixer => fixer.insertTextAfterRange([node.range[0], node.range[0] + node.operation.length], ` ${suggestedName}`),
1582
+ },
1583
+ ],
1650
1584
  });
1651
1585
  },
1652
1586
  };
@@ -1656,6 +1590,7 @@ const rule$5 = {
1656
1590
  const rule$6 = {
1657
1591
  meta: {
1658
1592
  type: 'suggestion',
1593
+ hasSuggestions: true,
1659
1594
  docs: {
1660
1595
  url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-case-insensitive-enum-values-duplicates.md`,
1661
1596
  category: 'Schema',
@@ -1695,7 +1630,13 @@ const rule$6 = {
1695
1630
  const enumName = duplicate.name.value;
1696
1631
  context.report({
1697
1632
  node: duplicate.name,
1698
- message: `Case-insensitive enum values duplicates are not allowed! Found: "${enumName}"`,
1633
+ message: `Case-insensitive enum values duplicates are not allowed! Found: \`${enumName}\`.`,
1634
+ suggest: [
1635
+ {
1636
+ desc: `Remove \`${enumName}\` enum value`,
1637
+ fix: fixer => fixer.remove(duplicate),
1638
+ },
1639
+ ],
1699
1640
  });
1700
1641
  }
1701
1642
  },
@@ -1703,14 +1644,15 @@ const rule$6 = {
1703
1644
  },
1704
1645
  };
1705
1646
 
1706
- const NO_DEPRECATED = 'NO_DEPRECATED';
1647
+ const RULE_ID$2 = 'no-deprecated';
1707
1648
  const rule$7 = {
1708
1649
  meta: {
1709
1650
  type: 'suggestion',
1651
+ hasSuggestions: true,
1710
1652
  docs: {
1711
1653
  category: 'Operations',
1712
1654
  description: `Enforce that deprecated fields or enum values are not in use by operations.`,
1713
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-deprecated.md`,
1655
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
1714
1656
  requiresSchema: true,
1715
1657
  examples: [
1716
1658
  {
@@ -1777,56 +1719,60 @@ const rule$7 = {
1777
1719
  recommended: true,
1778
1720
  },
1779
1721
  messages: {
1780
- [NO_DEPRECATED]: `This {{ type }} is marked as deprecated in your GraphQL schema {{ reason }}`,
1722
+ [RULE_ID$2]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
1781
1723
  },
1782
1724
  schema: [],
1783
1725
  },
1784
1726
  create(context) {
1727
+ requireGraphQLSchemaFromContext(RULE_ID$2, context);
1728
+ function report(node, reason) {
1729
+ const nodeName = node.type === graphql.Kind.ENUM ? node.value : node.name.value;
1730
+ const nodeType = node.type === graphql.Kind.ENUM ? 'enum value' : 'field';
1731
+ context.report({
1732
+ node,
1733
+ messageId: RULE_ID$2,
1734
+ data: {
1735
+ type: nodeType,
1736
+ reason,
1737
+ },
1738
+ suggest: [
1739
+ {
1740
+ desc: `Remove \`${nodeName}\` ${nodeType}`,
1741
+ fix: fixer => fixer.remove(node),
1742
+ },
1743
+ ],
1744
+ });
1745
+ }
1785
1746
  return {
1786
1747
  EnumValue(node) {
1787
- requireGraphQLSchemaFromContext('no-deprecated', context);
1748
+ var _a;
1788
1749
  const typeInfo = node.typeInfo();
1789
- if (typeInfo && typeInfo.enumValue) {
1790
- if (typeInfo.enumValue.deprecationReason) {
1791
- context.report({
1792
- node,
1793
- messageId: NO_DEPRECATED,
1794
- data: {
1795
- type: 'enum value',
1796
- reason: typeInfo.enumValue.deprecationReason ? `(reason: ${typeInfo.enumValue.deprecationReason})` : '',
1797
- },
1798
- });
1799
- }
1750
+ const reason = (_a = typeInfo.enumValue) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1751
+ if (reason) {
1752
+ report(node, reason);
1800
1753
  }
1801
1754
  },
1802
1755
  Field(node) {
1803
- requireGraphQLSchemaFromContext('no-deprecated', context);
1756
+ var _a;
1804
1757
  const typeInfo = node.typeInfo();
1805
- if (typeInfo && typeInfo.fieldDef) {
1806
- if (typeInfo.fieldDef.deprecationReason) {
1807
- context.report({
1808
- node: node.name,
1809
- messageId: NO_DEPRECATED,
1810
- data: {
1811
- type: 'field',
1812
- reason: typeInfo.fieldDef.deprecationReason ? `(reason: ${typeInfo.fieldDef.deprecationReason})` : '',
1813
- },
1814
- });
1815
- }
1758
+ const reason = (_a = typeInfo.fieldDef) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1759
+ if (reason) {
1760
+ report(node, reason);
1816
1761
  }
1817
1762
  },
1818
1763
  };
1819
1764
  },
1820
1765
  };
1821
1766
 
1822
- const NO_DUPLICATE_FIELDS = 'NO_DUPLICATE_FIELDS';
1767
+ const RULE_ID$3 = 'no-duplicate-fields';
1823
1768
  const rule$8 = {
1824
1769
  meta: {
1825
1770
  type: 'suggestion',
1771
+ hasSuggestions: true,
1826
1772
  docs: {
1827
1773
  description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
1828
1774
  category: 'Operations',
1829
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-duplicate-fields.md',
1775
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
1830
1776
  recommended: true,
1831
1777
  examples: [
1832
1778
  {
@@ -1872,21 +1818,30 @@ const rule$8 = {
1872
1818
  ],
1873
1819
  },
1874
1820
  messages: {
1875
- [NO_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
1821
+ [RULE_ID$3]: '{{ type }} `{{ fieldName }}` defined multiple times.',
1876
1822
  },
1877
1823
  schema: [],
1878
1824
  },
1879
1825
  create(context) {
1880
- function checkNode(usedFields, type, node) {
1826
+ function checkNode(usedFields, node) {
1881
1827
  const fieldName = node.value;
1882
1828
  if (usedFields.has(fieldName)) {
1829
+ const { parent } = node;
1883
1830
  context.report({
1884
1831
  node,
1885
- messageId: NO_DUPLICATE_FIELDS,
1832
+ messageId: RULE_ID$3,
1886
1833
  data: {
1887
- type,
1834
+ type: parent.type,
1888
1835
  fieldName,
1889
1836
  },
1837
+ suggest: [
1838
+ {
1839
+ desc: `Remove \`${fieldName}\` ${parent.type.toLowerCase()}`,
1840
+ fix(fixer) {
1841
+ return fixer.remove(parent.type === graphql.Kind.VARIABLE ? parent.parent : parent);
1842
+ },
1843
+ },
1844
+ ],
1890
1845
  });
1891
1846
  }
1892
1847
  else {
@@ -1897,20 +1852,20 @@ const rule$8 = {
1897
1852
  OperationDefinition(node) {
1898
1853
  const set = new Set();
1899
1854
  for (const varDef of node.variableDefinitions) {
1900
- checkNode(set, 'Operation variable', varDef.variable.name);
1855
+ checkNode(set, varDef.variable.name);
1901
1856
  }
1902
1857
  },
1903
1858
  Field(node) {
1904
1859
  const set = new Set();
1905
1860
  for (const arg of node.arguments) {
1906
- checkNode(set, 'Field argument', arg.name);
1861
+ checkNode(set, arg.name);
1907
1862
  }
1908
1863
  },
1909
1864
  SelectionSet(node) {
1910
1865
  const set = new Set();
1911
1866
  for (const selection of node.selections) {
1912
1867
  if (selection.kind === graphql.Kind.FIELD) {
1913
- checkNode(set, 'Field', selection.alias || selection.name);
1868
+ checkNode(set, selection.alias || selection.name);
1914
1869
  }
1915
1870
  }
1916
1871
  },
@@ -1921,8 +1876,11 @@ const rule$8 = {
1921
1876
  const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
1922
1877
  const rule$9 = {
1923
1878
  meta: {
1879
+ type: 'suggestion',
1880
+ hasSuggestions: true,
1881
+ schema: [],
1924
1882
  messages: {
1925
- [HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
1883
+ [HASHTAG_COMMENT]: 'Using hashtag `#` for adding GraphQL descriptions is not allowed. Prefer using `"""` for multiline, or `"` for a single line description.',
1926
1884
  },
1927
1885
  docs: {
1928
1886
  description: 'Requires to use `"""` or `"` for adding a GraphQL description instead of `#`.\nAllows to use hashtag for comments, as long as it\'s not attached to an AST definition.',
@@ -1965,8 +1923,6 @@ const rule$9 = {
1965
1923
  ],
1966
1924
  recommended: true,
1967
1925
  },
1968
- type: 'suggestion',
1969
- schema: [],
1970
1926
  },
1971
1927
  create(context) {
1972
1928
  const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
@@ -1974,7 +1930,7 @@ const rule$9 = {
1974
1930
  [selector](node) {
1975
1931
  const rawNode = node.rawNode();
1976
1932
  let token = rawNode.loc.startToken;
1977
- while (token !== null) {
1933
+ while (token) {
1978
1934
  const { kind, prev, next, value, line, column } = token;
1979
1935
  if (kind === graphql.TokenKind.COMMENT && prev && next) {
1980
1936
  const isEslintComment = value.trimStart().startsWith('eslint');
@@ -1986,6 +1942,10 @@ const rule$9 = {
1986
1942
  line,
1987
1943
  column: column - 1,
1988
1944
  },
1945
+ suggest: ['"""', '"'].map(descriptionSyntax => ({
1946
+ desc: `Replace with \`${descriptionSyntax}\` description syntax`,
1947
+ fix: fixer => fixer.replaceTextRange([token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join('')),
1948
+ })),
1989
1949
  });
1990
1950
  }
1991
1951
  }
@@ -2000,11 +1960,13 @@ const ROOT_TYPES = ['mutation', 'subscription'];
2000
1960
  const rule$a = {
2001
1961
  meta: {
2002
1962
  type: 'suggestion',
1963
+ hasSuggestions: true,
2003
1964
  docs: {
2004
1965
  category: 'Schema',
2005
1966
  description: 'Disallow using root types `mutation` and/or `subscription`.',
2006
1967
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md',
2007
1968
  requiresSchema: true,
1969
+ isDisabledForAllConfig: true,
2008
1970
  examples: [
2009
1971
  {
2010
1972
  title: 'Incorrect',
@@ -2036,9 +1998,7 @@ const rule$a = {
2036
1998
  required: ['disallow'],
2037
1999
  properties: {
2038
2000
  disallow: {
2039
- type: 'array',
2040
- uniqueItems: true,
2041
- minItems: 1,
2001
+ ...ARRAY_DEFAULT_OPTIONS,
2042
2002
  items: {
2043
2003
  enum: ROOT_TYPES,
2044
2004
  },
@@ -2060,29 +2020,37 @@ const rule$a = {
2060
2020
  return {};
2061
2021
  }
2062
2022
  const selector = [
2063
- `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})`,
2023
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)`,
2064
2024
  '>',
2065
- `${graphql.Kind.NAME}[value=/^(${rootTypeNames.join('|')})$/]`,
2025
+ `Name[value=/^(${rootTypeNames.join('|')})$/]`,
2066
2026
  ].join(' ');
2067
2027
  return {
2068
2028
  [selector](node) {
2069
2029
  const typeName = node.value;
2070
2030
  context.report({
2071
2031
  node,
2072
- message: `Root type "${typeName}" is forbidden`,
2032
+ message: `Root type \`${typeName}\` is forbidden.`,
2033
+ suggest: [
2034
+ {
2035
+ desc: `Remove \`${typeName}\` type`,
2036
+ fix: fixer => fixer.remove(node.parent),
2037
+ },
2038
+ ],
2073
2039
  });
2074
2040
  },
2075
2041
  };
2076
2042
  },
2077
2043
  };
2078
2044
 
2045
+ const RULE_ID$4 = 'no-scalar-result-type-on-mutation';
2079
2046
  const rule$b = {
2080
2047
  meta: {
2081
2048
  type: 'suggestion',
2049
+ hasSuggestions: true,
2082
2050
  docs: {
2083
2051
  category: 'Schema',
2084
2052
  description: 'Avoid scalar result type on mutation type to make sure to return a valid state.',
2085
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-scalar-result-type-on-mutation.md',
2053
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
2086
2054
  requiresSchema: true,
2087
2055
  examples: [
2088
2056
  {
@@ -2106,14 +2074,14 @@ const rule$b = {
2106
2074
  schema: [],
2107
2075
  },
2108
2076
  create(context) {
2109
- const schema = requireGraphQLSchemaFromContext('no-scalar-result-type-on-mutation', context);
2077
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$4, context);
2110
2078
  const mutationType = schema.getMutationType();
2111
2079
  if (!mutationType) {
2112
2080
  return {};
2113
2081
  }
2114
2082
  const selector = [
2115
- `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2116
- `> ${graphql.Kind.FIELD_DEFINITION} > .gqlType ${graphql.Kind.NAME}`,
2083
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
2084
+ '> FieldDefinition > .gqlType Name',
2117
2085
  ].join(' ');
2118
2086
  return {
2119
2087
  [selector](node) {
@@ -2122,7 +2090,13 @@ const rule$b = {
2122
2090
  if (graphql.isScalarType(graphQLType)) {
2123
2091
  context.report({
2124
2092
  node,
2125
- message: `Unexpected scalar result type "${typeName}"`,
2093
+ message: `Unexpected scalar result type \`${typeName}\`.`,
2094
+ suggest: [
2095
+ {
2096
+ desc: `Remove \`${typeName}\``,
2097
+ fix: fixer => fixer.remove(node),
2098
+ },
2099
+ ],
2126
2100
  });
2127
2101
  }
2128
2102
  },
@@ -2134,6 +2108,7 @@ const NO_TYPENAME_PREFIX = 'NO_TYPENAME_PREFIX';
2134
2108
  const rule$c = {
2135
2109
  meta: {
2136
2110
  type: 'suggestion',
2111
+ hasSuggestions: true,
2137
2112
  docs: {
2138
2113
  category: 'Schema',
2139
2114
  description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
@@ -2178,6 +2153,12 @@ const rule$c = {
2178
2153
  },
2179
2154
  messageId: NO_TYPENAME_PREFIX,
2180
2155
  node: field.name,
2156
+ suggest: [
2157
+ {
2158
+ desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
2159
+ fix: fixer => fixer.replaceText(field.name, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
2160
+ },
2161
+ ],
2181
2162
  });
2182
2163
  }
2183
2164
  }
@@ -2186,8 +2167,7 @@ const rule$c = {
2186
2167
  },
2187
2168
  };
2188
2169
 
2189
- const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
2190
- const RULE_ID = 'no-unreachable-types';
2170
+ const RULE_ID$5 = 'no-unreachable-types';
2191
2171
  const KINDS = [
2192
2172
  graphql.Kind.DIRECTIVE_DEFINITION,
2193
2173
  graphql.Kind.OBJECT_TYPE_DEFINITION,
@@ -2203,36 +2183,85 @@ const KINDS = [
2203
2183
  graphql.Kind.ENUM_TYPE_DEFINITION,
2204
2184
  graphql.Kind.ENUM_TYPE_EXTENSION,
2205
2185
  ];
2206
- const rule$d = {
2207
- meta: {
2208
- messages: {
2209
- [UNREACHABLE_TYPE]: 'Type "{{ typeName }}" is unreachable',
2210
- },
2211
- docs: {
2212
- description: `Requires all types to be reachable at some level by root level fields.`,
2213
- category: 'Schema',
2214
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
2215
- requiresSchema: true,
2216
- examples: [
2217
- {
2218
- title: 'Incorrect',
2219
- code: /* GraphQL */ `
2220
- type User {
2221
- id: ID!
2222
- name: String
2223
- }
2224
-
2225
- type Query {
2226
- me: String
2186
+ let reachableTypesCache;
2187
+ function getReachableTypes(schema) {
2188
+ // We don't want cache reachableTypes on test environment
2189
+ // Otherwise reachableTypes will be same for all tests
2190
+ if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
2191
+ return reachableTypesCache;
2192
+ }
2193
+ const reachableTypes = new Set();
2194
+ const collect = (node) => {
2195
+ const typeName = getTypeName(node);
2196
+ if (reachableTypes.has(typeName)) {
2197
+ return;
2198
+ }
2199
+ reachableTypes.add(typeName);
2200
+ const type = schema.getType(typeName) || schema.getDirective(typeName);
2201
+ if (graphql.isInterfaceType(type)) {
2202
+ const { objects, interfaces } = schema.getImplementations(type);
2203
+ for (const { astNode } of [...objects, ...interfaces]) {
2204
+ graphql.visit(astNode, visitor);
2227
2205
  }
2228
- `,
2229
- },
2230
- {
2231
- title: 'Correct',
2232
- code: /* GraphQL */ `
2233
- type User {
2234
- id: ID!
2235
- name: String
2206
+ }
2207
+ else if (type.astNode) {
2208
+ // astNode can be undefined for ID, String, Boolean
2209
+ graphql.visit(type.astNode, visitor);
2210
+ }
2211
+ };
2212
+ const visitor = {
2213
+ InterfaceTypeDefinition: collect,
2214
+ ObjectTypeDefinition: collect,
2215
+ InputValueDefinition: collect,
2216
+ UnionTypeDefinition: collect,
2217
+ FieldDefinition: collect,
2218
+ Directive: collect,
2219
+ NamedType: collect,
2220
+ };
2221
+ for (const type of [
2222
+ schema,
2223
+ schema.getQueryType(),
2224
+ schema.getMutationType(),
2225
+ schema.getSubscriptionType(),
2226
+ ]) {
2227
+ // if schema don't have Query type, schema.astNode will be undefined
2228
+ if (type === null || type === void 0 ? void 0 : type.astNode) {
2229
+ graphql.visit(type.astNode, visitor);
2230
+ }
2231
+ }
2232
+ reachableTypesCache = reachableTypes;
2233
+ return reachableTypesCache;
2234
+ }
2235
+ const rule$d = {
2236
+ meta: {
2237
+ messages: {
2238
+ [RULE_ID$5]: '{{ type }} `{{ typeName }}` is unreachable.',
2239
+ },
2240
+ docs: {
2241
+ description: `Requires all types to be reachable at some level by root level fields.`,
2242
+ category: 'Schema',
2243
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
2244
+ requiresSchema: true,
2245
+ examples: [
2246
+ {
2247
+ title: 'Incorrect',
2248
+ code: /* GraphQL */ `
2249
+ type User {
2250
+ id: ID!
2251
+ name: String
2252
+ }
2253
+
2254
+ type Query {
2255
+ me: String
2256
+ }
2257
+ `,
2258
+ },
2259
+ {
2260
+ title: 'Correct',
2261
+ code: /* GraphQL */ `
2262
+ type User {
2263
+ id: ID!
2264
+ name: String
2236
2265
  }
2237
2266
 
2238
2267
  type Query {
@@ -2248,19 +2277,24 @@ const rule$d = {
2248
2277
  hasSuggestions: true,
2249
2278
  },
2250
2279
  create(context) {
2251
- const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
2280
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
2281
+ const reachableTypes = getReachableTypes(schema);
2252
2282
  const selector = KINDS.join(',');
2253
2283
  return {
2254
2284
  [selector](node) {
2255
2285
  const typeName = node.name.value;
2256
2286
  if (!reachableTypes.has(typeName)) {
2287
+ const type = lowerCase(node.kind.replace(/(Extension|Definition)$/, ''));
2257
2288
  context.report({
2258
2289
  node: node.name,
2259
- messageId: UNREACHABLE_TYPE,
2260
- data: { typeName },
2290
+ messageId: RULE_ID$5,
2291
+ data: {
2292
+ type: type[0].toUpperCase() + type.slice(1),
2293
+ typeName
2294
+ },
2261
2295
  suggest: [
2262
2296
  {
2263
- desc: `Remove ${typeName}`,
2297
+ desc: `Remove \`${typeName}\``,
2264
2298
  fix: fixer => fixer.remove(node),
2265
2299
  },
2266
2300
  ],
@@ -2271,19 +2305,49 @@ const rule$d = {
2271
2305
  },
2272
2306
  };
2273
2307
 
2274
- const UNUSED_FIELD = 'UNUSED_FIELD';
2275
- const RULE_ID$1 = 'no-unused-fields';
2308
+ const RULE_ID$6 = 'no-unused-fields';
2309
+ let usedFieldsCache;
2310
+ function getUsedFields(schema, operations) {
2311
+ // We don't want cache usedFields on test environment
2312
+ // Otherwise usedFields will be same for all tests
2313
+ if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
2314
+ return usedFieldsCache;
2315
+ }
2316
+ const usedFields = Object.create(null);
2317
+ const typeInfo = new graphql.TypeInfo(schema);
2318
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
2319
+ Field(node) {
2320
+ var _a;
2321
+ const fieldDef = typeInfo.getFieldDef();
2322
+ if (!fieldDef) {
2323
+ // skip visiting this node if field is not defined in schema
2324
+ return false;
2325
+ }
2326
+ const parentTypeName = typeInfo.getParentType().name;
2327
+ const fieldName = node.name.value;
2328
+ (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
2329
+ usedFields[parentTypeName].add(fieldName);
2330
+ },
2331
+ });
2332
+ const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
2333
+ for (const { document } of allDocuments) {
2334
+ graphql.visit(document, visitor);
2335
+ }
2336
+ usedFieldsCache = usedFields;
2337
+ return usedFieldsCache;
2338
+ }
2276
2339
  const rule$e = {
2277
2340
  meta: {
2278
2341
  messages: {
2279
- [UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
2342
+ [RULE_ID$6]: `Field "{{fieldName}}" is unused`,
2280
2343
  },
2281
2344
  docs: {
2282
2345
  description: `Requires all fields to be used at some level by siblings operations.`,
2283
2346
  category: 'Schema',
2284
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
2347
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$6}.md`,
2285
2348
  requiresSiblings: true,
2286
2349
  requiresSchema: true,
2350
+ isDisabledForAllConfig: true,
2287
2351
  examples: [
2288
2352
  {
2289
2353
  title: 'Incorrect',
@@ -2333,7 +2397,9 @@ const rule$e = {
2333
2397
  hasSuggestions: true,
2334
2398
  },
2335
2399
  create(context) {
2336
- const usedFields = requireUsedFieldsFromContext(RULE_ID$1, context);
2400
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$6, context);
2401
+ const siblingsOperations = requireSiblingsOperations(RULE_ID$6, context);
2402
+ const usedFields = getUsedFields(schema, siblingsOperations);
2337
2403
  return {
2338
2404
  FieldDefinition(node) {
2339
2405
  var _a;
@@ -2345,11 +2411,11 @@ const rule$e = {
2345
2411
  }
2346
2412
  context.report({
2347
2413
  node: node.name,
2348
- messageId: UNUSED_FIELD,
2414
+ messageId: RULE_ID$6,
2349
2415
  data: { fieldName },
2350
2416
  suggest: [
2351
2417
  {
2352
- desc: `Remove "${fieldName}" field`,
2418
+ desc: `Remove \`${fieldName}\` field`,
2353
2419
  fix(fixer) {
2354
2420
  const sourceCode = context.getSourceCode();
2355
2421
  const tokenBefore = sourceCode.getTokenBefore(node);
@@ -2365,32 +2431,9 @@ const rule$e = {
2365
2431
  },
2366
2432
  };
2367
2433
 
2368
- function keyValMap(list, keyFn, valFn) {
2369
- return list.reduce((map, item) => {
2370
- map[keyFn(item)] = valFn(item);
2371
- return map;
2372
- }, Object.create(null));
2373
- }
2374
- function valueFromNode(valueNode, variables) {
2375
- switch (valueNode.type) {
2376
- case graphql.Kind.NULL:
2377
- return null;
2378
- case graphql.Kind.INT:
2379
- return parseInt(valueNode.value, 10);
2380
- case graphql.Kind.FLOAT:
2381
- return parseFloat(valueNode.value);
2382
- case graphql.Kind.STRING:
2383
- case graphql.Kind.ENUM:
2384
- case graphql.Kind.BOOLEAN:
2385
- return valueNode.value;
2386
- case graphql.Kind.LIST:
2387
- return valueNode.values.map(node => valueFromNode(node, variables));
2388
- case graphql.Kind.OBJECT:
2389
- return keyValMap(valueNode.fields, field => field.name.value, field => valueFromNode(field.value, variables));
2390
- case graphql.Kind.VARIABLE:
2391
- return variables === null || variables === void 0 ? void 0 : variables[valueNode.name.value];
2392
- }
2393
- }
2434
+ const valueFromNode = (...args) => {
2435
+ return valueFromASTUntyped.valueFromASTUntyped(...args);
2436
+ };
2394
2437
  function getBaseType(type) {
2395
2438
  if (graphql.isNonNullType(type) || graphql.isListType(type)) {
2396
2439
  return getBaseType(type.ofType);
@@ -2460,21 +2503,93 @@ function extractCommentsFromAst(loc) {
2460
2503
  }
2461
2504
  return comments;
2462
2505
  }
2463
- function isNodeWithDescription(obj) {
2464
- var _a;
2465
- return (_a = obj) === null || _a === void 0 ? void 0 : _a.description;
2506
+
2507
+ function convertToESTree(node, typeInfo) {
2508
+ const visitor = { leave: convertNode(typeInfo) };
2509
+ return {
2510
+ rootTree: graphql.visit(node, typeInfo ? graphql.visitWithTypeInfo(typeInfo, visitor) : visitor),
2511
+ comments: extractCommentsFromAst(node.loc),
2512
+ };
2513
+ }
2514
+ function hasTypeField(node) {
2515
+ return 'type' in node && Boolean(node.type);
2516
+ }
2517
+ function convertLocation(location) {
2518
+ const { startToken, endToken, source, start, end } = location;
2519
+ /*
2520
+ * ESLint has 0-based column number
2521
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
2522
+ */
2523
+ const loc = {
2524
+ start: {
2525
+ /*
2526
+ * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
2527
+ */
2528
+ line: startToken.line === 0 ? 1 : startToken.line,
2529
+ column: startToken.column === 0 ? 0 : startToken.column - 1,
2530
+ },
2531
+ end: {
2532
+ line: endToken.line,
2533
+ column: endToken.column - 1,
2534
+ },
2535
+ source: source.body,
2536
+ };
2537
+ if (loc.start.column === loc.end.column) {
2538
+ loc.end.column += end - start;
2539
+ }
2540
+ return loc;
2466
2541
  }
2467
- function convertDescription(node) {
2468
- if (isNodeWithDescription(node)) {
2469
- return [
2542
+ const convertNode = (typeInfo) => (node, key, parent) => {
2543
+ const leadingComments = 'description' in node && node.description
2544
+ ? [
2470
2545
  {
2471
2546
  type: node.description.block ? 'Block' : 'Line',
2472
2547
  value: node.description.value,
2473
2548
  },
2474
- ];
2475
- }
2476
- return [];
2477
- }
2549
+ ]
2550
+ : [];
2551
+ const calculatedTypeInfo = typeInfo
2552
+ ? {
2553
+ argument: typeInfo.getArgument(),
2554
+ defaultValue: typeInfo.getDefaultValue(),
2555
+ directive: typeInfo.getDirective(),
2556
+ enumValue: typeInfo.getEnumValue(),
2557
+ fieldDef: typeInfo.getFieldDef(),
2558
+ inputType: typeInfo.getInputType(),
2559
+ parentInputType: typeInfo.getParentInputType(),
2560
+ parentType: typeInfo.getParentType(),
2561
+ gqlType: typeInfo.getType(),
2562
+ }
2563
+ : {};
2564
+ const rawNode = () => {
2565
+ if (parent && key !== undefined) {
2566
+ return parent[key];
2567
+ }
2568
+ return node.kind === graphql.Kind.DOCUMENT
2569
+ ? {
2570
+ kind: node.kind,
2571
+ loc: node.loc,
2572
+ definitions: node.definitions.map(d => d.rawNode()),
2573
+ }
2574
+ : node;
2575
+ };
2576
+ const commonFields = {
2577
+ ...node,
2578
+ type: node.kind,
2579
+ loc: convertLocation(node.loc),
2580
+ range: [node.loc.start, node.loc.end],
2581
+ leadingComments,
2582
+ // Use function to prevent RangeError: Maximum call stack size exceeded
2583
+ typeInfo: () => calculatedTypeInfo,
2584
+ rawNode,
2585
+ };
2586
+ return hasTypeField(node)
2587
+ ? {
2588
+ ...commonFields,
2589
+ gqlType: node.type,
2590
+ }
2591
+ : commonFields;
2592
+ };
2478
2593
 
2479
2594
  // eslint-disable-next-line unicorn/better-regex
2480
2595
  const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
@@ -2485,6 +2600,7 @@ const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
2485
2600
  const rule$f = {
2486
2601
  meta: {
2487
2602
  type: 'suggestion',
2603
+ hasSuggestions: true,
2488
2604
  docs: {
2489
2605
  category: 'Schema',
2490
2606
  description: 'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
@@ -2572,12 +2688,18 @@ const rule$f = {
2572
2688
  }
2573
2689
  const canRemove = Date.now() > deletionDateInMS;
2574
2690
  if (canRemove) {
2691
+ const { parent } = node;
2692
+ const nodeName = parent.name.value;
2575
2693
  context.report({
2576
- node: node.parent.name,
2694
+ node: parent.name,
2577
2695
  messageId: MESSAGE_CAN_BE_REMOVED,
2578
- data: {
2579
- nodeName: node.parent.name.value,
2580
- },
2696
+ data: { nodeName },
2697
+ suggest: [
2698
+ {
2699
+ desc: `Remove \`${nodeName}\``,
2700
+ fix: fixer => fixer.remove(parent),
2701
+ },
2702
+ ],
2581
2703
  });
2582
2704
  }
2583
2705
  },
@@ -2639,7 +2761,7 @@ const rule$g = {
2639
2761
  },
2640
2762
  };
2641
2763
 
2642
- const RULE_ID$2 = 'require-description';
2764
+ const RULE_ID$7 = 'require-description';
2643
2765
  const ALLOWED_KINDS$1 = [
2644
2766
  ...TYPES_KINDS,
2645
2767
  graphql.Kind.DIRECTIVE_DEFINITION,
@@ -2681,7 +2803,7 @@ const rule$h = {
2681
2803
  docs: {
2682
2804
  category: 'Schema',
2683
2805
  description: 'Enforce descriptions in type definitions and operations.',
2684
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
2806
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$7}.md`,
2685
2807
  examples: [
2686
2808
  {
2687
2809
  title: 'Incorrect',
@@ -2728,7 +2850,7 @@ const rule$h = {
2728
2850
  },
2729
2851
  type: 'suggestion',
2730
2852
  messages: {
2731
- [RULE_ID$2]: 'Description is required for `{{ nodeName }}`.',
2853
+ [RULE_ID$7]: 'Description is required for `{{ nodeName }}`.',
2732
2854
  },
2733
2855
  schema: {
2734
2856
  type: 'array',
@@ -2787,8 +2909,8 @@ const rule$h = {
2787
2909
  }
2788
2910
  if (description.length === 0) {
2789
2911
  context.report({
2790
- loc: isOperation ? getLocation(node.loc, node.operation) : node.name.loc,
2791
- messageId: RULE_ID$2,
2912
+ loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
2913
+ messageId: RULE_ID$7,
2792
2914
  data: {
2793
2915
  nodeName: getNodeName(node),
2794
2916
  },
@@ -2870,122 +2992,18 @@ const rule$i = {
2870
2992
  },
2871
2993
  };
2872
2994
 
2873
- function convertToESTree(node, typeInfo) {
2874
- const visitor = { leave: convertNode(typeInfo) };
2875
- return {
2876
- rootTree: graphql.visit(node, typeInfo ? graphql.visitWithTypeInfo(typeInfo, visitor) : visitor),
2877
- comments: extractCommentsFromAst(node.loc),
2878
- };
2879
- }
2880
- function hasTypeField(obj) {
2881
- return obj && !!obj.type;
2882
- }
2883
- function convertLocation(location) {
2884
- const { startToken, endToken, source, start, end } = location;
2885
- /*
2886
- * ESLint has 0-based column number
2887
- * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
2888
- */
2889
- const loc = {
2890
- start: {
2891
- /*
2892
- * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
2893
- */
2894
- line: startToken.line === 0 ? 1 : startToken.line,
2895
- column: startToken.column === 0 ? 0 : startToken.column - 1,
2896
- },
2897
- end: {
2898
- line: endToken.line,
2899
- column: endToken.column - 1,
2900
- },
2901
- source: source.body,
2902
- };
2903
- if (loc.start.column === loc.end.column) {
2904
- loc.end.column += end - start;
2905
- }
2906
- return loc;
2907
- }
2908
- const convertNode = (typeInfo) => (node, key, parent) => {
2909
- const calculatedTypeInfo = typeInfo
2910
- ? {
2911
- argument: typeInfo.getArgument(),
2912
- defaultValue: typeInfo.getDefaultValue(),
2913
- directive: typeInfo.getDirective(),
2914
- enumValue: typeInfo.getEnumValue(),
2915
- fieldDef: typeInfo.getFieldDef(),
2916
- inputType: typeInfo.getInputType(),
2917
- parentInputType: typeInfo.getParentInputType(),
2918
- parentType: typeInfo.getParentType(),
2919
- gqlType: typeInfo.getType(),
2920
- }
2921
- : {};
2922
- const commonFields = {
2923
- typeInfo: () => calculatedTypeInfo,
2924
- leadingComments: convertDescription(node),
2925
- loc: convertLocation(node.loc),
2926
- range: [node.loc.start, node.loc.end],
2927
- };
2928
- if (hasTypeField(node)) {
2929
- const { type: gqlType, loc: gqlLocation, ...rest } = node;
2930
- const typeFieldSafe = {
2931
- ...rest,
2932
- gqlType,
2933
- };
2934
- const estreeNode = {
2935
- ...typeFieldSafe,
2936
- ...commonFields,
2937
- type: node.kind,
2938
- rawNode: () => {
2939
- if (!parent || key === undefined) {
2940
- if (node && node.definitions) {
2941
- return {
2942
- loc: gqlLocation,
2943
- kind: graphql.Kind.DOCUMENT,
2944
- definitions: node.definitions.map(d => d.rawNode()),
2945
- };
2946
- }
2947
- return node;
2948
- }
2949
- return parent[key];
2950
- },
2951
- };
2952
- return estreeNode;
2953
- }
2954
- else {
2955
- const { loc: gqlLocation, ...rest } = node;
2956
- const typeFieldSafe = rest;
2957
- const estreeNode = {
2958
- ...typeFieldSafe,
2959
- ...commonFields,
2960
- type: node.kind,
2961
- rawNode: () => {
2962
- if (!parent || key === undefined) {
2963
- if (node && node.definitions) {
2964
- return {
2965
- loc: gqlLocation,
2966
- kind: graphql.Kind.DOCUMENT,
2967
- definitions: node.definitions.map(d => d.rawNode()),
2968
- };
2969
- }
2970
- return node;
2971
- }
2972
- return parent[key];
2973
- },
2974
- };
2975
- return estreeNode;
2976
- }
2977
- };
2978
-
2979
- const RULE_ID$3 = 'require-id-when-available';
2980
- const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
2995
+ const RULE_ID$8 = 'require-id-when-available';
2981
2996
  const DEFAULT_ID_FIELD_NAME = 'id';
2997
+ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
2982
2998
  const rule$j = {
2983
2999
  meta: {
2984
3000
  type: 'suggestion',
3001
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
3002
+ hasSuggestions: true,
2985
3003
  docs: {
2986
3004
  category: 'Operations',
2987
3005
  description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
2988
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
3006
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$8}.md`,
2989
3007
  requiresSchema: true,
2990
3008
  requiresSiblings: true,
2991
3009
  examples: [
@@ -3028,21 +3046,14 @@ const rule$j = {
3028
3046
  recommended: true,
3029
3047
  },
3030
3048
  messages: {
3031
- [MESSAGE_ID]: [
3032
- `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
3033
- `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
3034
- ].join('\n'),
3049
+ [RULE_ID$8]: `Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.`,
3035
3050
  },
3036
3051
  schema: {
3037
3052
  definitions: {
3038
3053
  asString: {
3039
3054
  type: 'string',
3040
3055
  },
3041
- asArray: {
3042
- type: 'array',
3043
- minItems: 1,
3044
- uniqueItems: true,
3045
- },
3056
+ asArray: ARRAY_DEFAULT_OPTIONS,
3046
3057
  },
3047
3058
  type: 'array',
3048
3059
  maxItems: 1,
@@ -3059,76 +3070,121 @@ const rule$j = {
3059
3070
  },
3060
3071
  },
3061
3072
  create(context) {
3062
- requireGraphQLSchemaFromContext(RULE_ID$3, context);
3063
- const siblings = requireSiblingsOperations(RULE_ID$3, context);
3073
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$8, context);
3074
+ const siblings = requireSiblingsOperations(RULE_ID$8, context);
3064
3075
  const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
3065
3076
  const idNames = utils.asArray(fieldName);
3066
- const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
3067
- // Skip check selections in FragmentDefinition
3068
- const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
3069
- return {
3070
- [selector](node) {
3071
- var _a;
3072
- const typeInfo = node.typeInfo();
3073
- if (!typeInfo.gqlType) {
3074
- return;
3075
- }
3076
- const rawType = getBaseType(typeInfo.gqlType);
3077
- const isObjectType = rawType instanceof graphql.GraphQLObjectType;
3078
- const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
3079
- if (!isObjectType && !isInterfaceType) {
3080
- return;
3077
+ // Check selections only in OperationDefinition,
3078
+ // skip selections of OperationDefinition and InlineFragment
3079
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
3080
+ const typeInfo = new graphql.TypeInfo(schema);
3081
+ function checkFragments(node) {
3082
+ for (const selection of node.selections) {
3083
+ if (selection.kind !== graphql.Kind.FRAGMENT_SPREAD) {
3084
+ continue;
3081
3085
  }
3082
- const fields = rawType.getFields();
3083
- const hasIdFieldInType = idNames.some(name => fields[name]);
3084
- if (!hasIdFieldInType) {
3085
- return;
3086
+ const [foundSpread] = siblings.getFragment(selection.name.value);
3087
+ if (!foundSpread) {
3088
+ continue;
3086
3089
  }
3087
3090
  const checkedFragmentSpreads = new Set();
3088
- for (const selection of node.selections) {
3089
- if (isFound(selection)) {
3090
- return;
3091
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
3092
+ SelectionSet(node, key, parent) {
3093
+ if (parent.kind === graphql.Kind.FRAGMENT_DEFINITION) {
3094
+ checkedFragmentSpreads.add(parent.name.value);
3095
+ }
3096
+ else if (parent.kind !== graphql.Kind.INLINE_FRAGMENT) {
3097
+ checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
3098
+ }
3099
+ },
3100
+ });
3101
+ graphql.visit(foundSpread.document, visitor);
3102
+ }
3103
+ }
3104
+ function checkSelections(node, type,
3105
+ // Fragment can be placed in separate file
3106
+ // Provide actual fragment spread location instead of location in fragment
3107
+ loc,
3108
+ // Can't access to node.parent in GraphQL AST.Node, so pass as argument
3109
+ parent, checkedFragmentSpreads = new Set()) {
3110
+ const rawType = getBaseType(type);
3111
+ const isObjectType = rawType instanceof graphql.GraphQLObjectType;
3112
+ const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
3113
+ if (!isObjectType && !isInterfaceType) {
3114
+ return;
3115
+ }
3116
+ const fields = rawType.getFields();
3117
+ const hasIdFieldInType = idNames.some(name => fields[name]);
3118
+ if (!hasIdFieldInType) {
3119
+ return;
3120
+ }
3121
+ function hasIdField({ selections }) {
3122
+ return selections.some(selection => {
3123
+ if (selection.kind === graphql.Kind.FIELD) {
3124
+ return idNames.includes(selection.name.value);
3091
3125
  }
3092
- if (selection.kind === graphql.Kind.INLINE_FRAGMENT && selection.selectionSet.selections.some(isFound)) {
3093
- return;
3126
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
3127
+ return hasIdField(selection.selectionSet);
3094
3128
  }
3095
3129
  if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
3096
3130
  const [foundSpread] = siblings.getFragment(selection.name.value);
3097
3131
  if (foundSpread) {
3098
- checkedFragmentSpreads.add(foundSpread.document.name.value);
3099
- if (foundSpread.document.selectionSet.selections.some(isFound)) {
3100
- return;
3101
- }
3132
+ const fragmentSpread = foundSpread.document;
3133
+ checkedFragmentSpreads.add(fragmentSpread.name.value);
3134
+ return hasIdField(fragmentSpread.selectionSet);
3102
3135
  }
3103
3136
  }
3104
- }
3105
- const { parent } = node;
3106
- const hasIdFieldInInterfaceSelectionSet = (parent === null || parent === void 0 ? void 0 : parent.kind) === graphql.Kind.INLINE_FRAGMENT &&
3107
- ((_a = parent.parent) === null || _a === void 0 ? void 0 : _a.kind) === graphql.Kind.SELECTION_SET &&
3108
- parent.parent.selections.some(isFound);
3109
- if (hasIdFieldInInterfaceSelectionSet) {
3110
- return;
3111
- }
3112
- context.report({
3113
- loc: getLocation(node.loc),
3114
- messageId: MESSAGE_ID,
3115
- data: {
3116
- checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
3117
- fieldName: idNames.map(name => `"${name}"`).join(' or '),
3118
- },
3137
+ return false;
3119
3138
  });
3139
+ }
3140
+ const hasId = hasIdField(node);
3141
+ checkFragments(node);
3142
+ if (hasId) {
3143
+ return;
3144
+ }
3145
+ const pluralSuffix = idNames.length > 1 ? 's' : '';
3146
+ const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));
3147
+ const addition = checkedFragmentSpreads.size === 0
3148
+ ? ''
3149
+ : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords([...checkedFragmentSpreads].map(name => `\`${name}\``))}`;
3150
+ const problem = {
3151
+ loc,
3152
+ messageId: RULE_ID$8,
3153
+ data: {
3154
+ pluralSuffix,
3155
+ fieldName,
3156
+ addition,
3157
+ },
3158
+ };
3159
+ // Don't provide suggestions for selections in fragments as fragment can be in a separate file
3160
+ if ('type' in node) {
3161
+ problem.suggest = idNames.map(idName => ({
3162
+ desc: `Add \`${idName}\` selection`,
3163
+ fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `),
3164
+ }));
3165
+ }
3166
+ context.report(problem);
3167
+ }
3168
+ return {
3169
+ [selector](node) {
3170
+ const typeInfo = node.typeInfo();
3171
+ if (typeInfo.gqlType) {
3172
+ checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
3173
+ }
3120
3174
  },
3121
3175
  };
3122
3176
  },
3123
3177
  };
3124
3178
 
3125
- const RULE_ID$4 = 'selection-set-depth';
3179
+ const RULE_ID$9 = 'selection-set-depth';
3126
3180
  const rule$k = {
3127
3181
  meta: {
3182
+ type: 'suggestion',
3183
+ hasSuggestions: true,
3128
3184
  docs: {
3129
3185
  category: 'Operations',
3130
3186
  description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
3131
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
3187
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$9}.md`,
3132
3188
  requiresSiblings: true,
3133
3189
  examples: [
3134
3190
  {
@@ -3174,7 +3230,6 @@ const rule$k = {
3174
3230
  recommended: true,
3175
3231
  configOptions: [{ maxDepth: 7 }],
3176
3232
  },
3177
- type: 'suggestion',
3178
3233
  schema: {
3179
3234
  type: 'array',
3180
3235
  minItems: 1,
@@ -3187,14 +3242,7 @@ const rule$k = {
3187
3242
  maxDepth: {
3188
3243
  type: 'number',
3189
3244
  },
3190
- ignore: {
3191
- type: 'array',
3192
- uniqueItems: true,
3193
- minItems: 1,
3194
- items: {
3195
- type: 'string',
3196
- },
3197
- },
3245
+ ignore: ARRAY_DEFAULT_OPTIONS,
3198
3246
  },
3199
3247
  },
3200
3248
  },
@@ -3202,10 +3250,10 @@ const rule$k = {
3202
3250
  create(context) {
3203
3251
  let siblings = null;
3204
3252
  try {
3205
- siblings = requireSiblingsOperations(RULE_ID$4, context);
3253
+ siblings = requireSiblingsOperations(RULE_ID$9, context);
3206
3254
  }
3207
3255
  catch (e) {
3208
- logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3256
+ logger.warn(`Rule "${RULE_ID$9}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3209
3257
  }
3210
3258
  const { maxDepth } = context.options[0];
3211
3259
  const ignore = context.options[0].ignore || [];
@@ -3214,7 +3262,7 @@ const rule$k = {
3214
3262
  'OperationDefinition, FragmentDefinition'(node) {
3215
3263
  try {
3216
3264
  const rawNode = node.rawNode();
3217
- const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode, true) : [];
3265
+ const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
3218
3266
  const document = {
3219
3267
  kind: graphql.Kind.DOCUMENT,
3220
3268
  definitions: [rawNode, ...fragmentsInUse],
@@ -3229,19 +3277,32 @@ const rule$k = {
3229
3277
  column: column - 1,
3230
3278
  },
3231
3279
  message: error.message,
3280
+ suggest: [
3281
+ {
3282
+ desc: 'Remove selections',
3283
+ fix(fixer) {
3284
+ const ancestors = context.getAncestors();
3285
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
3286
+ const sourceCode = context.getSourceCode();
3287
+ const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]);
3288
+ const parentNode = foundNode.parent.parent;
3289
+ return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
3290
+ },
3291
+ },
3292
+ ],
3232
3293
  });
3233
3294
  },
3234
3295
  });
3235
3296
  }
3236
3297
  catch (e) {
3237
- logger.warn(`Rule "${RULE_ID$4}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3298
+ logger.warn(`Rule "${RULE_ID$9}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3238
3299
  }
3239
3300
  },
3240
3301
  };
3241
3302
  },
3242
3303
  };
3243
3304
 
3244
- const RULE_ID$5 = 'strict-id-in-types';
3305
+ const RULE_ID$a = 'strict-id-in-types';
3245
3306
  const rule$l = {
3246
3307
  meta: {
3247
3308
  type: 'suggestion',
@@ -3249,7 +3310,7 @@ const rule$l = {
3249
3310
  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.`,
3250
3311
  category: 'Schema',
3251
3312
  recommended: true,
3252
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
3313
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$a}.md`,
3253
3314
  requiresSchema: true,
3254
3315
  examples: [
3255
3316
  {
@@ -3340,22 +3401,12 @@ const rule$l = {
3340
3401
  type: 'object',
3341
3402
  properties: {
3342
3403
  types: {
3343
- type: 'array',
3344
- uniqueItems: true,
3345
- minItems: 1,
3404
+ ...ARRAY_DEFAULT_OPTIONS,
3346
3405
  description: 'This is used to exclude types with names that match one of the specified values.',
3347
- items: {
3348
- type: 'string',
3349
- },
3350
3406
  },
3351
3407
  suffixes: {
3352
- type: 'array',
3353
- uniqueItems: true,
3354
- minItems: 1,
3408
+ ...ARRAY_DEFAULT_OPTIONS,
3355
3409
  description: 'This is used to exclude types with names with suffixes that match one of the specified values.',
3356
- items: {
3357
- type: 'string',
3358
- },
3359
3410
  },
3360
3411
  },
3361
3412
  },
@@ -3363,7 +3414,7 @@ const rule$l = {
3363
3414
  },
3364
3415
  },
3365
3416
  messages: {
3366
- [RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3417
+ [RULE_ID$a]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3367
3418
  },
3368
3419
  },
3369
3420
  create(context) {
@@ -3373,7 +3424,7 @@ const rule$l = {
3373
3424
  exceptions: {},
3374
3425
  ...context.options[0],
3375
3426
  };
3376
- const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
3427
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$a, context);
3377
3428
  const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
3378
3429
  .filter(Boolean)
3379
3430
  .map(type => type.name);
@@ -3403,7 +3454,7 @@ const rule$l = {
3403
3454
  if (validIds.length !== 1) {
3404
3455
  context.report({
3405
3456
  node: node.name,
3406
- messageId: RULE_ID$5,
3457
+ messageId: RULE_ID$a,
3407
3458
  data: {
3408
3459
  typeName,
3409
3460
  acceptedNamesString: options.acceptedIdNames.join(', '),
@@ -3736,13 +3787,13 @@ function getSiblingOperations(options, gqlConfig) {
3736
3787
  };
3737
3788
  return {
3738
3789
  available: false,
3739
- getFragments: noopWarn,
3740
- getOperations: noopWarn,
3741
3790
  getFragment: noopWarn,
3791
+ getFragments: noopWarn,
3742
3792
  getFragmentByType: noopWarn,
3793
+ getFragmentsInUse: noopWarn,
3743
3794
  getOperation: noopWarn,
3795
+ getOperations: noopWarn,
3744
3796
  getOperationByType: noopWarn,
3745
- getFragmentsInUse: noopWarn,
3746
3797
  };
3747
3798
  }
3748
3799
  // Since the siblings array is cached, we can use it as cache key.
@@ -3755,7 +3806,7 @@ function getSiblingOperations(options, gqlConfig) {
3755
3806
  if (fragmentsCache === null) {
3756
3807
  const result = [];
3757
3808
  for (const source of siblings) {
3758
- for (const definition of source.document.definitions || []) {
3809
+ for (const definition of source.document.definitions) {
3759
3810
  if (definition.kind === graphql.Kind.FRAGMENT_DEFINITION) {
3760
3811
  result.push({
3761
3812
  filePath: source.location,
@@ -3773,7 +3824,7 @@ function getSiblingOperations(options, gqlConfig) {
3773
3824
  if (cachedOperations === null) {
3774
3825
  const result = [];
3775
3826
  for (const source of siblings) {
3776
- for (const definition of source.document.definitions || []) {
3827
+ for (const definition of source.document.definitions) {
3777
3828
  if (definition.kind === graphql.Kind.OPERATION_DEFINITION) {
3778
3829
  result.push({
3779
3830
  filePath: source.location,
@@ -3787,19 +3838,17 @@ function getSiblingOperations(options, gqlConfig) {
3787
3838
  return cachedOperations;
3788
3839
  };
3789
3840
  const getFragment = (name) => getFragments().filter(f => { var _a; return ((_a = f.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; });
3790
- const collectFragments = (selectable, recursive = true, collected = new Map()) => {
3841
+ const collectFragments = (selectable, recursive, collected = new Map()) => {
3791
3842
  graphql.visit(selectable, {
3792
3843
  FragmentSpread(spread) {
3793
- const name = spread.name.value;
3794
- const fragmentInfo = getFragment(name);
3795
- if (fragmentInfo.length === 0) {
3796
- logger.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3844
+ const fragmentName = spread.name.value;
3845
+ const [fragment] = getFragment(fragmentName);
3846
+ if (!fragment) {
3847
+ logger.warn(`Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`);
3797
3848
  return;
3798
3849
  }
3799
- const fragment = fragmentInfo[0];
3800
- const alreadyVisited = collected.has(name);
3801
- if (!alreadyVisited) {
3802
- collected.set(name, fragment.document);
3850
+ if (!collected.has(fragmentName)) {
3851
+ collected.set(fragmentName, fragment.document);
3803
3852
  if (recursive) {
3804
3853
  collectFragments(fragment.document, recursive, collected);
3805
3854
  }
@@ -3810,13 +3859,13 @@ function getSiblingOperations(options, gqlConfig) {
3810
3859
  };
3811
3860
  siblingOperations = {
3812
3861
  available: true,
3813
- getFragments,
3814
- getOperations,
3815
3862
  getFragment,
3863
+ getFragments,
3816
3864
  getFragmentByType: typeName => getFragments().filter(f => { var _a, _b; return ((_b = (_a = f.document.typeCondition) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.value) === typeName; }),
3865
+ getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3817
3866
  getOperation: name => getOperations().filter(o => { var _a; return ((_a = o.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; }),
3867
+ getOperations,
3818
3868
  getOperationByType: type => getOperations().filter(o => o.document.operation === type),
3819
- getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3820
3869
  };
3821
3870
  siblingOperationsCache.set(siblings, siblingOperations);
3822
3871
  }
@@ -3862,86 +3911,6 @@ const addCodeFileLoaderExtension = api => {
3862
3911
  return { name: 'graphql-eslint-loaders' };
3863
3912
  };
3864
3913
 
3865
- let reachableTypesCache;
3866
- function getReachableTypes(schema) {
3867
- // We don't want cache reachableTypes on test environment
3868
- // Otherwise reachableTypes will be same for all tests
3869
- if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
3870
- return reachableTypesCache;
3871
- }
3872
- const reachableTypes = new Set();
3873
- const collect = (node) => {
3874
- const typeName = getTypeName(node);
3875
- if (reachableTypes.has(typeName)) {
3876
- return;
3877
- }
3878
- reachableTypes.add(typeName);
3879
- const type = schema.getType(typeName) || schema.getDirective(typeName);
3880
- if (graphql.isInterfaceType(type)) {
3881
- const { objects, interfaces } = schema.getImplementations(type);
3882
- for (const { astNode } of [...objects, ...interfaces]) {
3883
- graphql.visit(astNode, visitor);
3884
- }
3885
- }
3886
- else if (type.astNode) {
3887
- // astNode can be undefined for ID, String, Boolean
3888
- graphql.visit(type.astNode, visitor);
3889
- }
3890
- };
3891
- const visitor = {
3892
- InterfaceTypeDefinition: collect,
3893
- ObjectTypeDefinition: collect,
3894
- InputValueDefinition: collect,
3895
- UnionTypeDefinition: collect,
3896
- FieldDefinition: collect,
3897
- Directive: collect,
3898
- NamedType: collect,
3899
- };
3900
- for (const type of [
3901
- schema,
3902
- schema.getQueryType(),
3903
- schema.getMutationType(),
3904
- schema.getSubscriptionType(),
3905
- ]) {
3906
- // if schema don't have Query type, schema.astNode will be undefined
3907
- if (type === null || type === void 0 ? void 0 : type.astNode) {
3908
- graphql.visit(type.astNode, visitor);
3909
- }
3910
- }
3911
- reachableTypesCache = reachableTypes;
3912
- return reachableTypesCache;
3913
- }
3914
- let usedFieldsCache;
3915
- function getUsedFields(schema, operations) {
3916
- // We don't want cache usedFields on test environment
3917
- // Otherwise usedFields will be same for all tests
3918
- if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
3919
- return usedFieldsCache;
3920
- }
3921
- const usedFields = Object.create(null);
3922
- const typeInfo = new graphql.TypeInfo(schema);
3923
- const visitor = graphql.visitWithTypeInfo(typeInfo, {
3924
- Field(node) {
3925
- var _a;
3926
- const fieldDef = typeInfo.getFieldDef();
3927
- if (!fieldDef) {
3928
- // skip visiting this node if field is not defined in schema
3929
- return false;
3930
- }
3931
- const parentTypeName = typeInfo.getParentType().name;
3932
- const fieldName = node.name.value;
3933
- (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
3934
- usedFields[parentTypeName].add(fieldName);
3935
- },
3936
- });
3937
- const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
3938
- for (const { document } of allDocuments) {
3939
- graphql.visit(document, visitor);
3940
- }
3941
- usedFieldsCache = usedFields;
3942
- return usedFieldsCache;
3943
- }
3944
-
3945
3914
  function parse(code, options) {
3946
3915
  return parseForESLint(code, options).ast;
3947
3916
  }
@@ -3952,8 +3921,6 @@ function parseForESLint(code, options = {}) {
3952
3921
  hasTypeInfo: schema !== null,
3953
3922
  schema,
3954
3923
  siblingOperations: getSiblingOperations(options, gqlConfig),
3955
- reachableTypes: getReachableTypes,
3956
- usedFields: getUsedFields,
3957
3924
  };
3958
3925
  try {
3959
3926
  const filePath = options.filePath || '';
@@ -3994,6 +3961,7 @@ function parseForESLint(code, options = {}) {
3994
3961
  }
3995
3962
  }
3996
3963
 
3964
+ /* eslint-env jest */
3997
3965
  function indentCode(code, indent = 4) {
3998
3966
  return code.replace(/^/gm, ' '.repeat(indent));
3999
3967
  }
@@ -4003,6 +3971,11 @@ function printCode(code) {
4003
3971
  linesBelow: Number.POSITIVE_INFINITY,
4004
3972
  });
4005
3973
  }
3974
+ // A simple version of `SourceCodeFixer.applyFixes`
3975
+ // https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
3976
+ function applyFix(code, fix) {
3977
+ return [code.slice(0, fix.range[0]), fix.text, code.slice(fix.range[1])].join('');
3978
+ }
4006
3979
  class GraphQLRuleTester extends eslint.RuleTester {
4007
3980
  constructor(parserOptions = {}) {
4008
3981
  const config = {
@@ -4046,32 +4019,63 @@ class GraphQLRuleTester extends eslint.RuleTester {
4046
4019
  linter.defineRule(name, rule);
4047
4020
  const hasOnlyTest = tests.invalid.some(t => t.only);
4048
4021
  for (const testCase of tests.invalid) {
4049
- const { only, code, filename } = testCase;
4022
+ const { only, filename, options } = testCase;
4050
4023
  if (hasOnlyTest && !only) {
4051
4024
  continue;
4052
4025
  }
4026
+ const code = removeTrailingBlankLines(testCase.code);
4053
4027
  const verifyConfig = getVerifyConfig(name, this.config, testCase);
4054
4028
  defineParser(linter, verifyConfig.parser);
4055
4029
  const messages = linter.verify(code, verifyConfig, { filename });
4056
4030
  const messageForSnapshot = [];
4031
+ const hasMultipleMessages = messages.length > 1;
4032
+ if (hasMultipleMessages) {
4033
+ messageForSnapshot.push('Code', indentCode(printCode(code)));
4034
+ }
4035
+ if (options) {
4036
+ const opts = JSON.stringify(options, null, 2).slice(1, -1);
4037
+ messageForSnapshot.push('⚙️ Options', indentCode(removeTrailingBlankLines(opts), 2));
4038
+ }
4057
4039
  for (const [index, message] of messages.entries()) {
4058
4040
  if (message.fatal) {
4059
4041
  throw new Error(message.message);
4060
4042
  }
4061
- messageForSnapshot.push(`❌ Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
4043
+ const codeWithMessage = visualizeEslintMessage(code, message, hasMultipleMessages ? 1 : undefined);
4044
+ messageForSnapshot.push(printWithIndex('❌ Error', index, messages.length), indentCode(codeWithMessage));
4045
+ const { suggestions } = message;
4046
+ // Don't print suggestions in snapshots for too big codes
4047
+ if (suggestions && (code.match(/\n/g) || '').length < 1000) {
4048
+ for (const [i, suggestion] of message.suggestions.entries()) {
4049
+ const output = applyFix(code, suggestion.fix);
4050
+ const title = printWithIndex('💡 Suggestion', i, suggestions.length, suggestion.desc);
4051
+ messageForSnapshot.push(title, indentCode(printCode(output), 2));
4052
+ }
4053
+ }
4062
4054
  }
4063
4055
  if (rule.meta.fixable) {
4064
4056
  const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
4065
4057
  if (fixed) {
4066
- messageForSnapshot.push('🔧 Autofix output', indentCode(printCode(output), 2));
4058
+ messageForSnapshot.push('🔧 Autofix output', indentCode(codeFrame.codeFrameColumns(output, {})));
4067
4059
  }
4068
4060
  }
4069
4061
  expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
4070
4062
  }
4071
4063
  }
4072
4064
  }
4065
+ function removeTrailingBlankLines(text) {
4066
+ return text.replace(/^\s*\n/, '').trimEnd();
4067
+ }
4068
+ function printWithIndex(title, index, total, description) {
4069
+ if (total > 1) {
4070
+ title += ` ${index + 1}/${total}`;
4071
+ }
4072
+ if (description) {
4073
+ title += `: ${description}`;
4074
+ }
4075
+ return title;
4076
+ }
4073
4077
  function getVerifyConfig(ruleId, testerConfig, testCase) {
4074
- const { options, parserOptions, parser = testerConfig.parser } = testCase;
4078
+ const { parser = testerConfig.parser, parserOptions, options } = testCase;
4075
4079
  return {
4076
4080
  ...testerConfig,
4077
4081
  parser,
@@ -4080,7 +4084,7 @@ function getVerifyConfig(ruleId, testerConfig, testCase) {
4080
4084
  ...parserOptions,
4081
4085
  },
4082
4086
  rules: {
4083
- [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
4087
+ [ruleId]: Array.isArray(options) ? ['error', ...options] : 'error',
4084
4088
  },
4085
4089
  };
4086
4090
  }
@@ -4098,7 +4102,7 @@ function defineParser(linter, parser) {
4098
4102
  linter.defineParser(parser, require(parser));
4099
4103
  }
4100
4104
  }
4101
- function visualizeEslintMessage(text, result) {
4105
+ function visualizeEslintMessage(text, result, linesOffset = Number.POSITIVE_INFINITY) {
4102
4106
  const { line, column, endLine, endColumn, message } = result;
4103
4107
  const location = {
4104
4108
  start: {
@@ -4113,23 +4117,22 @@ function visualizeEslintMessage(text, result) {
4113
4117
  };
4114
4118
  }
4115
4119
  return codeFrame.codeFrameColumns(text, location, {
4116
- linesAbove: Number.POSITIVE_INFINITY,
4117
- linesBelow: Number.POSITIVE_INFINITY,
4120
+ linesAbove: linesOffset,
4121
+ linesBelow: linesOffset,
4118
4122
  message,
4119
4123
  });
4120
4124
  }
4121
4125
 
4126
+ const configs = Object.fromEntries(['schema-recommended', 'schema-all', 'operations-recommended', 'operations-all'].map(configName => [
4127
+ configName,
4128
+ { extends: `./configs/${configName}.json` },
4129
+ ]));
4130
+
4122
4131
  exports.GraphQLRuleTester = GraphQLRuleTester;
4123
4132
  exports.configs = configs;
4124
- exports.convertDescription = convertDescription;
4125
- exports.convertToESTree = convertToESTree;
4126
- exports.convertToken = convertToken;
4127
- exports.extractCommentsFromAst = extractCommentsFromAst;
4128
- exports.extractTokens = extractTokens;
4129
- exports.getBaseType = getBaseType;
4130
- exports.isNodeWithDescription = isNodeWithDescription;
4131
4133
  exports.parse = parse;
4132
4134
  exports.parseForESLint = parseForESLint;
4133
4135
  exports.processors = processors;
4136
+ exports.requireGraphQLSchemaFromContext = requireGraphQLSchemaFromContext;
4137
+ exports.requireSiblingsOperations = requireSiblingsOperations;
4134
4138
  exports.rules = rules;
4135
- exports.valueFromNode = valueFromNode;