@graphql-eslint/eslint-plugin 3.8.0-alpha-269d044.0 → 3.9.1-alpha-ebcc06f.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 (81) 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 +730 -703
  64. package/index.mjs +730 -696
  65. package/package.json +3 -1
  66. package/parser.d.ts +0 -2
  67. package/rules/alphabetize.d.ts +1 -0
  68. package/rules/graphql-js-validation.d.ts +1 -1
  69. package/rules/index.d.ts +1 -4
  70. package/rules/selection-set-depth.d.ts +1 -1
  71. package/sibling-operations.d.ts +3 -3
  72. package/testkit.d.ts +3 -3
  73. package/types.d.ts +24 -18
  74. package/utils.d.ts +14 -4
  75. package/configs/base.d.ts +0 -5
  76. package/configs/index.d.ts +0 -133
  77. package/configs/operations-all.d.ts +0 -19
  78. package/configs/operations-recommended.d.ts +0 -50
  79. package/configs/schema-all.d.ts +0 -15
  80. package/configs/schema-recommended.d.ts +0 -47
  81. package/graphql-ast.d.ts +0 -6
package/index.js CHANGED
@@ -11,170 +11,16 @@ 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');
17
+ const debugFactory = _interopDefault(require('debug'));
18
+ const fastGlob = _interopDefault(require('fast-glob'));
16
19
  const graphqlConfig = require('graphql-config');
17
20
  const codeFileLoader = require('@graphql-tools/code-file-loader');
18
21
  const eslint = require('eslint');
19
22
  const codeFrame = require('@babel/code-frame');
20
23
 
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
24
  function requireSiblingsOperations(ruleName, context) {
179
25
  if (!context.parserServices) {
180
26
  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 +45,6 @@ const logger = {
199
45
  // eslint-disable-next-line no-console
200
46
  warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
201
47
  };
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
48
  const normalizePath = (path) => (path || '').replace(/\\/g, '/');
212
49
  /**
213
50
  * https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
@@ -277,8 +114,8 @@ const convertCase = (style, str) => {
277
114
  return lowerCase(str).replace(/ /g, '-');
278
115
  }
279
116
  };
280
- function getLocation(loc, fieldName = '') {
281
- const { line, column } = loc.start;
117
+ function getLocation(start, fieldName = '') {
118
+ const { line, column } = start;
282
119
  return {
283
120
  start: {
284
121
  line,
@@ -290,6 +127,15 @@ function getLocation(loc, fieldName = '') {
290
127
  },
291
128
  };
292
129
  }
130
+ const REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 };
131
+ const ARRAY_DEFAULT_OPTIONS = {
132
+ type: 'array',
133
+ uniqueItems: true,
134
+ minItems: 1,
135
+ items: {
136
+ type: 'string',
137
+ },
138
+ };
293
139
 
294
140
  function validateDocument(context, schema = null, documentNode, rule) {
295
141
  if (documentNode.definitions.length === 0) {
@@ -301,23 +147,27 @@ function validateDocument(context, schema = null, documentNode, rule) {
301
147
  : validate.validateSDL(documentNode, null, [rule]);
302
148
  for (const error of validationErrors) {
303
149
  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);
150
+ const sourceCode = context.getSourceCode();
151
+ const { tokens } = sourceCode.ast;
152
+ const token = tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
153
+ let loc = {
154
+ line,
155
+ column: column - 1,
156
+ };
157
+ if (token) {
158
+ loc =
159
+ // if cursor on `@` symbol than use next node
160
+ token.type === '@' ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc;
161
+ }
306
162
  context.report({
307
- loc: token
308
- ? token.loc
309
- : {
310
- line,
311
- column: column - 1,
312
- },
163
+ loc,
313
164
  message: error.message,
314
165
  });
315
166
  }
316
167
  }
317
168
  catch (e) {
318
169
  context.report({
319
- // Report on first character
320
- loc: { column: 0, line: 1 },
170
+ loc: REPORT_ON_FIRST_CHARACTER,
321
171
  message: e.message,
322
172
  });
323
173
  }
@@ -374,7 +224,7 @@ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
374
224
  }
375
225
  return node;
376
226
  };
377
- const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
227
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = []) => {
378
228
  let ruleFn = null;
379
229
  try {
380
230
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
@@ -395,8 +245,9 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
395
245
  ...docs,
396
246
  graphQLJSRuleName: ruleName,
397
247
  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).`,
248
+ description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
399
249
  },
250
+ schema,
400
251
  },
401
252
  create(context) {
402
253
  if (!ruleFn) {
@@ -434,8 +285,45 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
434
285
  requiresSchema: true,
435
286
  }), validationToRule('known-directives', 'KnownDirectives', {
436
287
  category: ['Schema', 'Operations'],
437
- description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
288
+ description: `A GraphQL document is only valid if all \`@directive\`s are known by the schema and legally positioned.`,
438
289
  requiresSchema: true,
290
+ examples: [
291
+ {
292
+ title: 'Valid',
293
+ usage: [{ ignoreClientDirectives: ['client'] }],
294
+ code: /* GraphQL */ `
295
+ {
296
+ product {
297
+ someClientField @client
298
+ }
299
+ }
300
+ `,
301
+ },
302
+ ],
303
+ }, ({ context, node: documentNode }) => {
304
+ const { ignoreClientDirectives = [] } = context.options[0] || {};
305
+ if (ignoreClientDirectives.length === 0) {
306
+ return documentNode;
307
+ }
308
+ return graphql.visit(documentNode, {
309
+ Field(node) {
310
+ return {
311
+ ...node,
312
+ directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
313
+ };
314
+ },
315
+ });
316
+ }, {
317
+ type: 'array',
318
+ maxItems: 1,
319
+ items: {
320
+ type: 'object',
321
+ additionalProperties: false,
322
+ required: ['ignoreClientDirectives'],
323
+ properties: {
324
+ ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
325
+ },
326
+ },
439
327
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
440
328
  category: 'Operations',
441
329
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
@@ -559,8 +447,10 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
559
447
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
560
448
  category: 'Schema',
561
449
  description: `A type extension is only valid if the type is defined and has the same kind.`,
450
+ // TODO: add in graphql-eslint v4
562
451
  recommended: false,
563
452
  requiresSchema: true,
453
+ isDisabledForAllConfig: true,
564
454
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
565
455
  category: ['Schema', 'Operations'],
566
456
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
@@ -588,6 +478,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
588
478
  category: 'Schema',
589
479
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
590
480
  recommended: false,
481
+ isDisabledForAllConfig: true,
591
482
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
592
483
  category: 'Schema',
593
484
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
@@ -618,7 +509,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
618
509
  requiresSchema: true,
619
510
  }));
620
511
 
621
- const ALPHABETIZE = 'ALPHABETIZE';
512
+ const RULE_ID = 'alphabetize';
622
513
  const fieldsEnum = [
623
514
  graphql.Kind.OBJECT_TYPE_DEFINITION,
624
515
  graphql.Kind.INTERFACE_TYPE_DEFINITION,
@@ -643,7 +534,7 @@ const rule = {
643
534
  docs: {
644
535
  category: ['Schema', 'Operations'],
645
536
  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',
537
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
647
538
  examples: [
648
539
  {
649
540
  title: 'Incorrect',
@@ -726,6 +617,8 @@ const rule = {
726
617
  fields: fieldsEnum,
727
618
  values: valuesEnum,
728
619
  arguments: argumentsEnum,
620
+ // TODO: add in graphql-eslint v4
621
+ // definitions: true,
729
622
  },
730
623
  ],
731
624
  operations: [
@@ -738,7 +631,7 @@ const rule = {
738
631
  },
739
632
  },
740
633
  messages: {
741
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
634
+ [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
742
635
  },
743
636
  schema: {
744
637
  type: 'array',
@@ -750,49 +643,44 @@ const rule = {
750
643
  minProperties: 1,
751
644
  properties: {
752
645
  fields: {
753
- type: 'array',
754
- uniqueItems: true,
755
- minItems: 1,
646
+ ...ARRAY_DEFAULT_OPTIONS,
756
647
  items: {
757
648
  enum: fieldsEnum,
758
649
  },
759
- description: 'Fields of `type`, `interface`, and `input`',
650
+ description: 'Fields of `type`, `interface`, and `input`.',
760
651
  },
761
652
  values: {
762
- type: 'array',
763
- uniqueItems: true,
764
- minItems: 1,
653
+ ...ARRAY_DEFAULT_OPTIONS,
765
654
  items: {
766
655
  enum: valuesEnum,
767
656
  },
768
- description: 'Values of `enum`',
657
+ description: 'Values of `enum`.',
769
658
  },
770
659
  selections: {
771
- type: 'array',
772
- uniqueItems: true,
773
- minItems: 1,
660
+ ...ARRAY_DEFAULT_OPTIONS,
774
661
  items: {
775
662
  enum: selectionsEnum,
776
663
  },
777
- description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
664
+ description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
778
665
  },
779
666
  variables: {
780
- type: 'array',
781
- uniqueItems: true,
782
- minItems: 1,
667
+ ...ARRAY_DEFAULT_OPTIONS,
783
668
  items: {
784
669
  enum: variablesEnum,
785
670
  },
786
- description: 'Variables of operations (`query`, `mutation` and `subscription`)',
671
+ description: 'Variables of operations `query`, `mutation` and `subscription`.',
787
672
  },
788
673
  arguments: {
789
- type: 'array',
790
- uniqueItems: true,
791
- minItems: 1,
674
+ ...ARRAY_DEFAULT_OPTIONS,
792
675
  items: {
793
676
  enum: argumentsEnum,
794
677
  },
795
- description: 'Arguments of fields and directives',
678
+ description: 'Arguments of fields and directives.',
679
+ },
680
+ definitions: {
681
+ type: 'boolean',
682
+ description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
683
+ default: false,
796
684
  },
797
685
  },
798
686
  },
@@ -813,9 +701,22 @@ const rule = {
813
701
  if (tokenBefore) {
814
702
  return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
815
703
  }
816
- return commentsBefore;
704
+ const filteredComments = [];
705
+ const nodeLine = node.loc.start.line;
706
+ // Break on comment that not attached to node
707
+ for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
708
+ const comment = commentsBefore[i];
709
+ if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
710
+ break;
711
+ }
712
+ filteredComments.unshift(comment);
713
+ }
714
+ return filteredComments;
817
715
  }
818
716
  function getRangeWithComments(node) {
717
+ if (node.kind === graphql.Kind.VARIABLE) {
718
+ node = node.parent;
719
+ }
819
720
  const [firstBeforeComment] = getBeforeComments(node);
820
721
  const [firstAfterComment] = sourceCode.getCommentsAfter(node);
821
722
  const from = firstBeforeComment || node;
@@ -823,26 +724,35 @@ const rule = {
823
724
  return [from.range[0], to.range[1]];
824
725
  }
825
726
  function checkNodes(nodes) {
727
+ var _a, _b;
826
728
  // Starts from 1, ignore nodes.length <= 1
827
729
  for (let i = 1; i < nodes.length; i += 1) {
828
- const prevNode = nodes[i - 1];
829
730
  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) {
731
+ const currName = 'name' in currNode && ((_a = currNode.name) === null || _a === void 0 ? void 0 : _a.value);
732
+ if (!currName) {
733
+ // we don't move unnamed current nodes
834
734
  continue;
835
735
  }
836
- const isVariableNode = currNode.kind === graphql.Kind.VARIABLE;
736
+ const prevNode = nodes[i - 1];
737
+ const prevName = 'name' in prevNode && ((_b = prevNode.name) === null || _b === void 0 ? void 0 : _b.value);
738
+ if (prevName) {
739
+ // Compare with lexicographic order
740
+ const compareResult = prevName.localeCompare(currName);
741
+ const shouldSort = compareResult === 1;
742
+ if (!shouldSort) {
743
+ const isSameName = compareResult === 0;
744
+ if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
745
+ continue;
746
+ }
747
+ }
748
+ }
837
749
  context.report({
838
750
  node: currNode.name,
839
- messageId: ALPHABETIZE,
840
- data: isVariableNode
841
- ? {
842
- currName: `$${currName}`,
843
- prevName: `$${prevName}`,
844
- }
845
- : { currName, prevName },
751
+ messageId: RULE_ID,
752
+ data: {
753
+ currName,
754
+ prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
755
+ },
846
756
  *fix(fixer) {
847
757
  const prevRange = getRangeWithComments(prevNode);
848
758
  const currRange = getRangeWithComments(currNode);
@@ -883,10 +793,7 @@ const rule = {
883
793
  }
884
794
  if (selectionsSelector) {
885
795
  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 =>
796
+ checkNodes(node.selections.map(selection =>
890
797
  // sort by alias is field is renamed
891
798
  'alias' in selection && selection.alias ? { name: selection.alias } : selection));
892
799
  };
@@ -901,6 +808,11 @@ const rule = {
901
808
  checkNodes(node.arguments);
902
809
  };
903
810
  }
811
+ if (opts.definitions) {
812
+ listeners.Document = node => {
813
+ checkNodes(node.definitions);
814
+ };
815
+ }
904
816
  return listeners;
905
817
  },
906
818
  };
@@ -908,6 +820,7 @@ const rule = {
908
820
  const rule$1 = {
909
821
  meta: {
910
822
  type: 'suggestion',
823
+ hasSuggestions: true,
911
824
  docs: {
912
825
  examples: [
913
826
  {
@@ -955,8 +868,21 @@ const rule$1 = {
955
868
  return {
956
869
  [`.description[type=StringValue][block!=${isBlock}]`](node) {
957
870
  context.report({
958
- loc: getLocation(node.loc),
959
- message: `Unexpected ${isBlock ? 'inline' : 'block'} description`,
871
+ loc: isBlock ? node.loc : node.loc.start,
872
+ message: `Unexpected ${isBlock ? 'inline' : 'block'} description.`,
873
+ suggest: [
874
+ {
875
+ desc: `Change to ${isBlock ? 'block' : 'inline'} style description`,
876
+ fix(fixer) {
877
+ const sourceCode = context.getSourceCode();
878
+ const originalText = sourceCode.getText(node);
879
+ const newText = isBlock
880
+ ? originalText.replace(/(^")|("$)/g, '"""')
881
+ : originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, ' ');
882
+ return fixer.replaceText(node, newText);
883
+ },
884
+ },
885
+ ],
960
886
  });
961
887
  },
962
888
  };
@@ -969,6 +895,7 @@ const isMutationType = (node) => isObjectType(node) && node.name.value === 'Muta
969
895
  const rule$2 = {
970
896
  meta: {
971
897
  type: 'suggestion',
898
+ hasSuggestions: true,
972
899
  docs: {
973
900
  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
901
  category: 'Schema',
@@ -1042,12 +969,18 @@ const rule$2 = {
1042
969
  };
1043
970
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1044
971
  const listeners = {
1045
- 'FieldDefinition > InputValueDefinition[name.value!=input]'(node) {
1046
- if (shouldCheckType(node.parent.parent)) {
1047
- const name = node.name.value;
972
+ 'FieldDefinition > InputValueDefinition[name.value!=input] > Name'(node) {
973
+ if (shouldCheckType(node.parent.parent.parent)) {
974
+ const inputName = node.value;
1048
975
  context.report({
1049
- node: node.name,
1050
- message: `Input "${name}" should be called "input"`,
976
+ node,
977
+ message: `Input \`${inputName}\` should be called \`input\`.`,
978
+ suggest: [
979
+ {
980
+ desc: 'Rename to `input`',
981
+ fix: fixer => fixer.replaceText(node, 'input'),
982
+ },
983
+ ],
1051
984
  });
1052
985
  }
1053
986
  },
@@ -1069,7 +1002,13 @@ const rule$2 = {
1069
1002
  name.toLowerCase() !== mutationName.toLowerCase()) {
1070
1003
  context.report({
1071
1004
  node: node.name,
1072
- message: `InputType "${name}" name should be "${mutationName}"`,
1005
+ message: `Input type \`${name}\` name should be \`${mutationName}\`.`,
1006
+ suggest: [
1007
+ {
1008
+ desc: `Rename to \`${mutationName}\``,
1009
+ fix: fixer => fixer.replaceText(node, mutationName),
1010
+ },
1011
+ ],
1073
1012
  });
1074
1013
  }
1075
1014
  }
@@ -1082,7 +1021,14 @@ const rule$2 = {
1082
1021
  const MATCH_EXTENSION = 'MATCH_EXTENSION';
1083
1022
  const MATCH_STYLE = 'MATCH_STYLE';
1084
1023
  const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
1085
- const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case', 'matchDocumentStyle'];
1024
+ const CASE_STYLES = [
1025
+ 'camelCase',
1026
+ 'PascalCase',
1027
+ 'snake_case',
1028
+ 'UPPER_CASE',
1029
+ 'kebab-case',
1030
+ 'matchDocumentStyle',
1031
+ ];
1086
1032
  const schemaOption = {
1087
1033
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1088
1034
  };
@@ -1227,8 +1173,7 @@ const rule$3 = {
1227
1173
  var _a;
1228
1174
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1229
1175
  context.report({
1230
- // Report on first character
1231
- loc: { column: 0, line: 1 },
1176
+ loc: REPORT_ON_FIRST_CHARACTER,
1232
1177
  messageId: MATCH_EXTENSION,
1233
1178
  data: {
1234
1179
  fileExtension,
@@ -1267,8 +1212,7 @@ const rule$3 = {
1267
1212
  const filenameWithExtension = filename + expectedExtension;
1268
1213
  if (expectedFilename !== filenameWithExtension) {
1269
1214
  context.report({
1270
- // Report on first character
1271
- loc: { column: 0, line: 1 },
1215
+ loc: REPORT_ON_FIRST_CHARACTER,
1272
1216
  messageId: MATCH_STYLE,
1273
1217
  data: {
1274
1218
  expectedFilename,
@@ -1440,18 +1384,8 @@ const rule$4 = {
1440
1384
  style: { enum: ALLOWED_STYLES },
1441
1385
  prefix: { type: 'string' },
1442
1386
  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
- },
1387
+ forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
1388
+ forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
1455
1389
  ignorePattern: {
1456
1390
  type: 'string',
1457
1391
  description: 'Option to skip validation of some words, e.g. acronyms',
@@ -1505,6 +1439,18 @@ const rule$4 = {
1505
1439
  const style = restOptions[kind] || types;
1506
1440
  return typeof style === 'object' ? style : { style };
1507
1441
  }
1442
+ function report(node, message, suggestedName) {
1443
+ context.report({
1444
+ node,
1445
+ message,
1446
+ suggest: [
1447
+ {
1448
+ desc: `Rename to \`${suggestedName}\``,
1449
+ fix: fixer => fixer.replaceText(node, suggestedName),
1450
+ },
1451
+ ],
1452
+ });
1453
+ }
1508
1454
  const checkNode = (selector) => (n) => {
1509
1455
  const { name: node } = n.kind === graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
1510
1456
  if (!node) {
@@ -1519,16 +1465,7 @@ const rule$4 = {
1519
1465
  const [leadingUnderscores] = nodeName.match(/^_*/);
1520
1466
  const [trailingUnderscores] = nodeName.match(/_*$/);
1521
1467
  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
- });
1468
+ report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedName);
1532
1469
  }
1533
1470
  function getError() {
1534
1471
  const name = nodeName.replace(/(^_+)|(_+$)/g, '');
@@ -1575,18 +1512,8 @@ const rule$4 = {
1575
1512
  }
1576
1513
  };
1577
1514
  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
- });
1515
+ const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, '');
1516
+ report(node, `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, suggestedName);
1590
1517
  };
1591
1518
  const listeners = {};
1592
1519
  if (!allowLeadingUnderscore) {
@@ -1605,15 +1532,16 @@ const rule$4 = {
1605
1532
  },
1606
1533
  };
1607
1534
 
1608
- const NO_ANONYMOUS_OPERATIONS = 'NO_ANONYMOUS_OPERATIONS';
1535
+ const RULE_ID$1 = 'no-anonymous-operations';
1609
1536
  const rule$5 = {
1610
1537
  meta: {
1611
1538
  type: 'suggestion',
1539
+ hasSuggestions: true,
1612
1540
  docs: {
1613
1541
  category: 'Operations',
1614
1542
  description: 'Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.',
1615
1543
  recommended: true,
1616
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-anonymous-operations.md',
1544
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
1617
1545
  examples: [
1618
1546
  {
1619
1547
  title: 'Incorrect',
@@ -1634,19 +1562,27 @@ const rule$5 = {
1634
1562
  ],
1635
1563
  },
1636
1564
  messages: {
1637
- [NO_ANONYMOUS_OPERATIONS]: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`,
1565
+ [RULE_ID$1]: `Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!`,
1638
1566
  },
1639
1567
  schema: [],
1640
1568
  },
1641
1569
  create(context) {
1642
1570
  return {
1643
1571
  'OperationDefinition[name=undefined]'(node) {
1572
+ const [firstSelection] = node.selectionSet.selections;
1573
+ const suggestedName = firstSelection.type === graphql.Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;
1644
1574
  context.report({
1645
- loc: getLocation(node.loc, node.operation),
1575
+ loc: getLocation(node.loc.start, node.operation),
1576
+ messageId: RULE_ID$1,
1646
1577
  data: {
1647
1578
  operation: node.operation,
1648
1579
  },
1649
- messageId: NO_ANONYMOUS_OPERATIONS,
1580
+ suggest: [
1581
+ {
1582
+ desc: `Rename to \`${suggestedName}\``,
1583
+ fix: fixer => fixer.insertTextAfterRange([node.range[0], node.range[0] + node.operation.length], ` ${suggestedName}`),
1584
+ },
1585
+ ],
1650
1586
  });
1651
1587
  },
1652
1588
  };
@@ -1656,6 +1592,7 @@ const rule$5 = {
1656
1592
  const rule$6 = {
1657
1593
  meta: {
1658
1594
  type: 'suggestion',
1595
+ hasSuggestions: true,
1659
1596
  docs: {
1660
1597
  url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-case-insensitive-enum-values-duplicates.md`,
1661
1598
  category: 'Schema',
@@ -1695,7 +1632,13 @@ const rule$6 = {
1695
1632
  const enumName = duplicate.name.value;
1696
1633
  context.report({
1697
1634
  node: duplicate.name,
1698
- message: `Case-insensitive enum values duplicates are not allowed! Found: "${enumName}"`,
1635
+ message: `Case-insensitive enum values duplicates are not allowed! Found: \`${enumName}\`.`,
1636
+ suggest: [
1637
+ {
1638
+ desc: `Remove \`${enumName}\` enum value`,
1639
+ fix: fixer => fixer.remove(duplicate),
1640
+ },
1641
+ ],
1699
1642
  });
1700
1643
  }
1701
1644
  },
@@ -1703,14 +1646,15 @@ const rule$6 = {
1703
1646
  },
1704
1647
  };
1705
1648
 
1706
- const NO_DEPRECATED = 'NO_DEPRECATED';
1649
+ const RULE_ID$2 = 'no-deprecated';
1707
1650
  const rule$7 = {
1708
1651
  meta: {
1709
1652
  type: 'suggestion',
1653
+ hasSuggestions: true,
1710
1654
  docs: {
1711
1655
  category: 'Operations',
1712
1656
  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`,
1657
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
1714
1658
  requiresSchema: true,
1715
1659
  examples: [
1716
1660
  {
@@ -1777,56 +1721,60 @@ const rule$7 = {
1777
1721
  recommended: true,
1778
1722
  },
1779
1723
  messages: {
1780
- [NO_DEPRECATED]: `This {{ type }} is marked as deprecated in your GraphQL schema {{ reason }}`,
1724
+ [RULE_ID$2]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
1781
1725
  },
1782
1726
  schema: [],
1783
1727
  },
1784
1728
  create(context) {
1729
+ requireGraphQLSchemaFromContext(RULE_ID$2, context);
1730
+ function report(node, reason) {
1731
+ const nodeName = node.type === graphql.Kind.ENUM ? node.value : node.name.value;
1732
+ const nodeType = node.type === graphql.Kind.ENUM ? 'enum value' : 'field';
1733
+ context.report({
1734
+ node,
1735
+ messageId: RULE_ID$2,
1736
+ data: {
1737
+ type: nodeType,
1738
+ reason,
1739
+ },
1740
+ suggest: [
1741
+ {
1742
+ desc: `Remove \`${nodeName}\` ${nodeType}`,
1743
+ fix: fixer => fixer.remove(node),
1744
+ },
1745
+ ],
1746
+ });
1747
+ }
1785
1748
  return {
1786
1749
  EnumValue(node) {
1787
- requireGraphQLSchemaFromContext('no-deprecated', context);
1750
+ var _a;
1788
1751
  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
- }
1752
+ const reason = (_a = typeInfo.enumValue) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1753
+ if (reason) {
1754
+ report(node, reason);
1800
1755
  }
1801
1756
  },
1802
1757
  Field(node) {
1803
- requireGraphQLSchemaFromContext('no-deprecated', context);
1758
+ var _a;
1804
1759
  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
- }
1760
+ const reason = (_a = typeInfo.fieldDef) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1761
+ if (reason) {
1762
+ report(node, reason);
1816
1763
  }
1817
1764
  },
1818
1765
  };
1819
1766
  },
1820
1767
  };
1821
1768
 
1822
- const NO_DUPLICATE_FIELDS = 'NO_DUPLICATE_FIELDS';
1769
+ const RULE_ID$3 = 'no-duplicate-fields';
1823
1770
  const rule$8 = {
1824
1771
  meta: {
1825
1772
  type: 'suggestion',
1773
+ hasSuggestions: true,
1826
1774
  docs: {
1827
1775
  description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
1828
1776
  category: 'Operations',
1829
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-duplicate-fields.md',
1777
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
1830
1778
  recommended: true,
1831
1779
  examples: [
1832
1780
  {
@@ -1872,21 +1820,30 @@ const rule$8 = {
1872
1820
  ],
1873
1821
  },
1874
1822
  messages: {
1875
- [NO_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
1823
+ [RULE_ID$3]: '{{ type }} `{{ fieldName }}` defined multiple times.',
1876
1824
  },
1877
1825
  schema: [],
1878
1826
  },
1879
1827
  create(context) {
1880
- function checkNode(usedFields, type, node) {
1828
+ function checkNode(usedFields, node) {
1881
1829
  const fieldName = node.value;
1882
1830
  if (usedFields.has(fieldName)) {
1831
+ const { parent } = node;
1883
1832
  context.report({
1884
1833
  node,
1885
- messageId: NO_DUPLICATE_FIELDS,
1834
+ messageId: RULE_ID$3,
1886
1835
  data: {
1887
- type,
1836
+ type: parent.type,
1888
1837
  fieldName,
1889
1838
  },
1839
+ suggest: [
1840
+ {
1841
+ desc: `Remove \`${fieldName}\` ${parent.type.toLowerCase()}`,
1842
+ fix(fixer) {
1843
+ return fixer.remove(parent.type === graphql.Kind.VARIABLE ? parent.parent : parent);
1844
+ },
1845
+ },
1846
+ ],
1890
1847
  });
1891
1848
  }
1892
1849
  else {
@@ -1897,20 +1854,20 @@ const rule$8 = {
1897
1854
  OperationDefinition(node) {
1898
1855
  const set = new Set();
1899
1856
  for (const varDef of node.variableDefinitions) {
1900
- checkNode(set, 'Operation variable', varDef.variable.name);
1857
+ checkNode(set, varDef.variable.name);
1901
1858
  }
1902
1859
  },
1903
1860
  Field(node) {
1904
1861
  const set = new Set();
1905
1862
  for (const arg of node.arguments) {
1906
- checkNode(set, 'Field argument', arg.name);
1863
+ checkNode(set, arg.name);
1907
1864
  }
1908
1865
  },
1909
1866
  SelectionSet(node) {
1910
1867
  const set = new Set();
1911
1868
  for (const selection of node.selections) {
1912
1869
  if (selection.kind === graphql.Kind.FIELD) {
1913
- checkNode(set, 'Field', selection.alias || selection.name);
1870
+ checkNode(set, selection.alias || selection.name);
1914
1871
  }
1915
1872
  }
1916
1873
  },
@@ -1921,8 +1878,11 @@ const rule$8 = {
1921
1878
  const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
1922
1879
  const rule$9 = {
1923
1880
  meta: {
1881
+ type: 'suggestion',
1882
+ hasSuggestions: true,
1883
+ schema: [],
1924
1884
  messages: {
1925
- [HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
1885
+ [HASHTAG_COMMENT]: 'Using hashtag `#` for adding GraphQL descriptions is not allowed. Prefer using `"""` for multiline, or `"` for a single line description.',
1926
1886
  },
1927
1887
  docs: {
1928
1888
  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 +1925,6 @@ const rule$9 = {
1965
1925
  ],
1966
1926
  recommended: true,
1967
1927
  },
1968
- type: 'suggestion',
1969
- schema: [],
1970
1928
  },
1971
1929
  create(context) {
1972
1930
  const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
@@ -1974,7 +1932,7 @@ const rule$9 = {
1974
1932
  [selector](node) {
1975
1933
  const rawNode = node.rawNode();
1976
1934
  let token = rawNode.loc.startToken;
1977
- while (token !== null) {
1935
+ while (token) {
1978
1936
  const { kind, prev, next, value, line, column } = token;
1979
1937
  if (kind === graphql.TokenKind.COMMENT && prev && next) {
1980
1938
  const isEslintComment = value.trimStart().startsWith('eslint');
@@ -1986,6 +1944,10 @@ const rule$9 = {
1986
1944
  line,
1987
1945
  column: column - 1,
1988
1946
  },
1947
+ suggest: ['"""', '"'].map(descriptionSyntax => ({
1948
+ desc: `Replace with \`${descriptionSyntax}\` description syntax`,
1949
+ fix: fixer => fixer.replaceTextRange([token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join('')),
1950
+ })),
1989
1951
  });
1990
1952
  }
1991
1953
  }
@@ -2000,11 +1962,13 @@ const ROOT_TYPES = ['mutation', 'subscription'];
2000
1962
  const rule$a = {
2001
1963
  meta: {
2002
1964
  type: 'suggestion',
1965
+ hasSuggestions: true,
2003
1966
  docs: {
2004
1967
  category: 'Schema',
2005
1968
  description: 'Disallow using root types `mutation` and/or `subscription`.',
2006
1969
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md',
2007
1970
  requiresSchema: true,
1971
+ isDisabledForAllConfig: true,
2008
1972
  examples: [
2009
1973
  {
2010
1974
  title: 'Incorrect',
@@ -2036,9 +2000,7 @@ const rule$a = {
2036
2000
  required: ['disallow'],
2037
2001
  properties: {
2038
2002
  disallow: {
2039
- type: 'array',
2040
- uniqueItems: true,
2041
- minItems: 1,
2003
+ ...ARRAY_DEFAULT_OPTIONS,
2042
2004
  items: {
2043
2005
  enum: ROOT_TYPES,
2044
2006
  },
@@ -2060,29 +2022,37 @@ const rule$a = {
2060
2022
  return {};
2061
2023
  }
2062
2024
  const selector = [
2063
- `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})`,
2025
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)`,
2064
2026
  '>',
2065
- `${graphql.Kind.NAME}[value=/^(${rootTypeNames.join('|')})$/]`,
2027
+ `Name[value=/^(${rootTypeNames.join('|')})$/]`,
2066
2028
  ].join(' ');
2067
2029
  return {
2068
2030
  [selector](node) {
2069
2031
  const typeName = node.value;
2070
2032
  context.report({
2071
2033
  node,
2072
- message: `Root type "${typeName}" is forbidden`,
2034
+ message: `Root type \`${typeName}\` is forbidden.`,
2035
+ suggest: [
2036
+ {
2037
+ desc: `Remove \`${typeName}\` type`,
2038
+ fix: fixer => fixer.remove(node.parent),
2039
+ },
2040
+ ],
2073
2041
  });
2074
2042
  },
2075
2043
  };
2076
2044
  },
2077
2045
  };
2078
2046
 
2047
+ const RULE_ID$4 = 'no-scalar-result-type-on-mutation';
2079
2048
  const rule$b = {
2080
2049
  meta: {
2081
2050
  type: 'suggestion',
2051
+ hasSuggestions: true,
2082
2052
  docs: {
2083
2053
  category: 'Schema',
2084
2054
  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',
2055
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
2086
2056
  requiresSchema: true,
2087
2057
  examples: [
2088
2058
  {
@@ -2106,14 +2076,14 @@ const rule$b = {
2106
2076
  schema: [],
2107
2077
  },
2108
2078
  create(context) {
2109
- const schema = requireGraphQLSchemaFromContext('no-scalar-result-type-on-mutation', context);
2079
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$4, context);
2110
2080
  const mutationType = schema.getMutationType();
2111
2081
  if (!mutationType) {
2112
2082
  return {};
2113
2083
  }
2114
2084
  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}`,
2085
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
2086
+ '> FieldDefinition > .gqlType Name',
2117
2087
  ].join(' ');
2118
2088
  return {
2119
2089
  [selector](node) {
@@ -2122,7 +2092,13 @@ const rule$b = {
2122
2092
  if (graphql.isScalarType(graphQLType)) {
2123
2093
  context.report({
2124
2094
  node,
2125
- message: `Unexpected scalar result type "${typeName}"`,
2095
+ message: `Unexpected scalar result type \`${typeName}\`.`,
2096
+ suggest: [
2097
+ {
2098
+ desc: `Remove \`${typeName}\``,
2099
+ fix: fixer => fixer.remove(node),
2100
+ },
2101
+ ],
2126
2102
  });
2127
2103
  }
2128
2104
  },
@@ -2134,6 +2110,7 @@ const NO_TYPENAME_PREFIX = 'NO_TYPENAME_PREFIX';
2134
2110
  const rule$c = {
2135
2111
  meta: {
2136
2112
  type: 'suggestion',
2113
+ hasSuggestions: true,
2137
2114
  docs: {
2138
2115
  category: 'Schema',
2139
2116
  description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
@@ -2178,6 +2155,12 @@ const rule$c = {
2178
2155
  },
2179
2156
  messageId: NO_TYPENAME_PREFIX,
2180
2157
  node: field.name,
2158
+ suggest: [
2159
+ {
2160
+ desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
2161
+ fix: fixer => fixer.replaceText(field.name, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
2162
+ },
2163
+ ],
2181
2164
  });
2182
2165
  }
2183
2166
  }
@@ -2186,8 +2169,7 @@ const rule$c = {
2186
2169
  },
2187
2170
  };
2188
2171
 
2189
- const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
2190
- const RULE_ID = 'no-unreachable-types';
2172
+ const RULE_ID$5 = 'no-unreachable-types';
2191
2173
  const KINDS = [
2192
2174
  graphql.Kind.DIRECTIVE_DEFINITION,
2193
2175
  graphql.Kind.OBJECT_TYPE_DEFINITION,
@@ -2203,15 +2185,64 @@ const KINDS = [
2203
2185
  graphql.Kind.ENUM_TYPE_DEFINITION,
2204
2186
  graphql.Kind.ENUM_TYPE_EXTENSION,
2205
2187
  ];
2188
+ let reachableTypesCache;
2189
+ function getReachableTypes(schema) {
2190
+ // We don't want cache reachableTypes on test environment
2191
+ // Otherwise reachableTypes will be same for all tests
2192
+ if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
2193
+ return reachableTypesCache;
2194
+ }
2195
+ const reachableTypes = new Set();
2196
+ const collect = (node) => {
2197
+ const typeName = getTypeName(node);
2198
+ if (reachableTypes.has(typeName)) {
2199
+ return;
2200
+ }
2201
+ reachableTypes.add(typeName);
2202
+ const type = schema.getType(typeName) || schema.getDirective(typeName);
2203
+ if (graphql.isInterfaceType(type)) {
2204
+ const { objects, interfaces } = schema.getImplementations(type);
2205
+ for (const { astNode } of [...objects, ...interfaces]) {
2206
+ graphql.visit(astNode, visitor);
2207
+ }
2208
+ }
2209
+ else if (type.astNode) {
2210
+ // astNode can be undefined for ID, String, Boolean
2211
+ graphql.visit(type.astNode, visitor);
2212
+ }
2213
+ };
2214
+ const visitor = {
2215
+ InterfaceTypeDefinition: collect,
2216
+ ObjectTypeDefinition: collect,
2217
+ InputValueDefinition: collect,
2218
+ UnionTypeDefinition: collect,
2219
+ FieldDefinition: collect,
2220
+ Directive: collect,
2221
+ NamedType: collect,
2222
+ };
2223
+ for (const type of [
2224
+ schema,
2225
+ schema.getQueryType(),
2226
+ schema.getMutationType(),
2227
+ schema.getSubscriptionType(),
2228
+ ]) {
2229
+ // if schema don't have Query type, schema.astNode will be undefined
2230
+ if (type === null || type === void 0 ? void 0 : type.astNode) {
2231
+ graphql.visit(type.astNode, visitor);
2232
+ }
2233
+ }
2234
+ reachableTypesCache = reachableTypes;
2235
+ return reachableTypesCache;
2236
+ }
2206
2237
  const rule$d = {
2207
2238
  meta: {
2208
2239
  messages: {
2209
- [UNREACHABLE_TYPE]: 'Type "{{ typeName }}" is unreachable',
2240
+ [RULE_ID$5]: '{{ type }} `{{ typeName }}` is unreachable.',
2210
2241
  },
2211
2242
  docs: {
2212
2243
  description: `Requires all types to be reachable at some level by root level fields.`,
2213
2244
  category: 'Schema',
2214
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
2245
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
2215
2246
  requiresSchema: true,
2216
2247
  examples: [
2217
2248
  {
@@ -2248,19 +2279,24 @@ const rule$d = {
2248
2279
  hasSuggestions: true,
2249
2280
  },
2250
2281
  create(context) {
2251
- const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
2282
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
2283
+ const reachableTypes = getReachableTypes(schema);
2252
2284
  const selector = KINDS.join(',');
2253
2285
  return {
2254
2286
  [selector](node) {
2255
2287
  const typeName = node.name.value;
2256
2288
  if (!reachableTypes.has(typeName)) {
2289
+ const type = lowerCase(node.kind.replace(/(Extension|Definition)$/, ''));
2257
2290
  context.report({
2258
2291
  node: node.name,
2259
- messageId: UNREACHABLE_TYPE,
2260
- data: { typeName },
2292
+ messageId: RULE_ID$5,
2293
+ data: {
2294
+ type: type[0].toUpperCase() + type.slice(1),
2295
+ typeName
2296
+ },
2261
2297
  suggest: [
2262
2298
  {
2263
- desc: `Remove ${typeName}`,
2299
+ desc: `Remove \`${typeName}\``,
2264
2300
  fix: fixer => fixer.remove(node),
2265
2301
  },
2266
2302
  ],
@@ -2271,19 +2307,49 @@ const rule$d = {
2271
2307
  },
2272
2308
  };
2273
2309
 
2274
- const UNUSED_FIELD = 'UNUSED_FIELD';
2275
- const RULE_ID$1 = 'no-unused-fields';
2310
+ const RULE_ID$6 = 'no-unused-fields';
2311
+ let usedFieldsCache;
2312
+ function getUsedFields(schema, operations) {
2313
+ // We don't want cache usedFields on test environment
2314
+ // Otherwise usedFields will be same for all tests
2315
+ if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
2316
+ return usedFieldsCache;
2317
+ }
2318
+ const usedFields = Object.create(null);
2319
+ const typeInfo = new graphql.TypeInfo(schema);
2320
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
2321
+ Field(node) {
2322
+ var _a;
2323
+ const fieldDef = typeInfo.getFieldDef();
2324
+ if (!fieldDef) {
2325
+ // skip visiting this node if field is not defined in schema
2326
+ return false;
2327
+ }
2328
+ const parentTypeName = typeInfo.getParentType().name;
2329
+ const fieldName = node.name.value;
2330
+ (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
2331
+ usedFields[parentTypeName].add(fieldName);
2332
+ },
2333
+ });
2334
+ const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
2335
+ for (const { document } of allDocuments) {
2336
+ graphql.visit(document, visitor);
2337
+ }
2338
+ usedFieldsCache = usedFields;
2339
+ return usedFieldsCache;
2340
+ }
2276
2341
  const rule$e = {
2277
2342
  meta: {
2278
2343
  messages: {
2279
- [UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
2344
+ [RULE_ID$6]: `Field "{{fieldName}}" is unused`,
2280
2345
  },
2281
2346
  docs: {
2282
2347
  description: `Requires all fields to be used at some level by siblings operations.`,
2283
2348
  category: 'Schema',
2284
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
2349
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$6}.md`,
2285
2350
  requiresSiblings: true,
2286
2351
  requiresSchema: true,
2352
+ isDisabledForAllConfig: true,
2287
2353
  examples: [
2288
2354
  {
2289
2355
  title: 'Incorrect',
@@ -2333,7 +2399,9 @@ const rule$e = {
2333
2399
  hasSuggestions: true,
2334
2400
  },
2335
2401
  create(context) {
2336
- const usedFields = requireUsedFieldsFromContext(RULE_ID$1, context);
2402
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$6, context);
2403
+ const siblingsOperations = requireSiblingsOperations(RULE_ID$6, context);
2404
+ const usedFields = getUsedFields(schema, siblingsOperations);
2337
2405
  return {
2338
2406
  FieldDefinition(node) {
2339
2407
  var _a;
@@ -2345,11 +2413,11 @@ const rule$e = {
2345
2413
  }
2346
2414
  context.report({
2347
2415
  node: node.name,
2348
- messageId: UNUSED_FIELD,
2416
+ messageId: RULE_ID$6,
2349
2417
  data: { fieldName },
2350
2418
  suggest: [
2351
2419
  {
2352
- desc: `Remove "${fieldName}" field`,
2420
+ desc: `Remove \`${fieldName}\` field`,
2353
2421
  fix(fixer) {
2354
2422
  const sourceCode = context.getSourceCode();
2355
2423
  const tokenBefore = sourceCode.getTokenBefore(node);
@@ -2365,32 +2433,9 @@ const rule$e = {
2365
2433
  },
2366
2434
  };
2367
2435
 
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
- }
2436
+ const valueFromNode = (...args) => {
2437
+ return valueFromASTUntyped.valueFromASTUntyped(...args);
2438
+ };
2394
2439
  function getBaseType(type) {
2395
2440
  if (graphql.isNonNullType(type) || graphql.isListType(type)) {
2396
2441
  return getBaseType(type.ofType);
@@ -2460,21 +2505,93 @@ function extractCommentsFromAst(loc) {
2460
2505
  }
2461
2506
  return comments;
2462
2507
  }
2463
- function isNodeWithDescription(obj) {
2464
- var _a;
2465
- return (_a = obj) === null || _a === void 0 ? void 0 : _a.description;
2508
+
2509
+ function convertToESTree(node, typeInfo) {
2510
+ const visitor = { leave: convertNode(typeInfo) };
2511
+ return {
2512
+ rootTree: graphql.visit(node, typeInfo ? graphql.visitWithTypeInfo(typeInfo, visitor) : visitor),
2513
+ comments: extractCommentsFromAst(node.loc),
2514
+ };
2515
+ }
2516
+ function hasTypeField(node) {
2517
+ return 'type' in node && Boolean(node.type);
2518
+ }
2519
+ function convertLocation(location) {
2520
+ const { startToken, endToken, source, start, end } = location;
2521
+ /*
2522
+ * ESLint has 0-based column number
2523
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
2524
+ */
2525
+ const loc = {
2526
+ start: {
2527
+ /*
2528
+ * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
2529
+ */
2530
+ line: startToken.line === 0 ? 1 : startToken.line,
2531
+ column: startToken.column === 0 ? 0 : startToken.column - 1,
2532
+ },
2533
+ end: {
2534
+ line: endToken.line,
2535
+ column: endToken.column - 1,
2536
+ },
2537
+ source: source.body,
2538
+ };
2539
+ if (loc.start.column === loc.end.column) {
2540
+ loc.end.column += end - start;
2541
+ }
2542
+ return loc;
2466
2543
  }
2467
- function convertDescription(node) {
2468
- if (isNodeWithDescription(node)) {
2469
- return [
2544
+ const convertNode = (typeInfo) => (node, key, parent) => {
2545
+ const leadingComments = 'description' in node && node.description
2546
+ ? [
2470
2547
  {
2471
2548
  type: node.description.block ? 'Block' : 'Line',
2472
2549
  value: node.description.value,
2473
2550
  },
2474
- ];
2475
- }
2476
- return [];
2477
- }
2551
+ ]
2552
+ : [];
2553
+ const calculatedTypeInfo = typeInfo
2554
+ ? {
2555
+ argument: typeInfo.getArgument(),
2556
+ defaultValue: typeInfo.getDefaultValue(),
2557
+ directive: typeInfo.getDirective(),
2558
+ enumValue: typeInfo.getEnumValue(),
2559
+ fieldDef: typeInfo.getFieldDef(),
2560
+ inputType: typeInfo.getInputType(),
2561
+ parentInputType: typeInfo.getParentInputType(),
2562
+ parentType: typeInfo.getParentType(),
2563
+ gqlType: typeInfo.getType(),
2564
+ }
2565
+ : {};
2566
+ const rawNode = () => {
2567
+ if (parent && key !== undefined) {
2568
+ return parent[key];
2569
+ }
2570
+ return node.kind === graphql.Kind.DOCUMENT
2571
+ ? {
2572
+ kind: node.kind,
2573
+ loc: node.loc,
2574
+ definitions: node.definitions.map(d => d.rawNode()),
2575
+ }
2576
+ : node;
2577
+ };
2578
+ const commonFields = {
2579
+ ...node,
2580
+ type: node.kind,
2581
+ loc: convertLocation(node.loc),
2582
+ range: [node.loc.start, node.loc.end],
2583
+ leadingComments,
2584
+ // Use function to prevent RangeError: Maximum call stack size exceeded
2585
+ typeInfo: () => calculatedTypeInfo,
2586
+ rawNode,
2587
+ };
2588
+ return hasTypeField(node)
2589
+ ? {
2590
+ ...commonFields,
2591
+ gqlType: node.type,
2592
+ }
2593
+ : commonFields;
2594
+ };
2478
2595
 
2479
2596
  // eslint-disable-next-line unicorn/better-regex
2480
2597
  const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
@@ -2485,6 +2602,7 @@ const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
2485
2602
  const rule$f = {
2486
2603
  meta: {
2487
2604
  type: 'suggestion',
2605
+ hasSuggestions: true,
2488
2606
  docs: {
2489
2607
  category: 'Schema',
2490
2608
  description: 'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
@@ -2572,12 +2690,18 @@ const rule$f = {
2572
2690
  }
2573
2691
  const canRemove = Date.now() > deletionDateInMS;
2574
2692
  if (canRemove) {
2693
+ const { parent } = node;
2694
+ const nodeName = parent.name.value;
2575
2695
  context.report({
2576
- node: node.parent.name,
2696
+ node: parent.name,
2577
2697
  messageId: MESSAGE_CAN_BE_REMOVED,
2578
- data: {
2579
- nodeName: node.parent.name.value,
2580
- },
2698
+ data: { nodeName },
2699
+ suggest: [
2700
+ {
2701
+ desc: `Remove \`${nodeName}\``,
2702
+ fix: fixer => fixer.remove(parent),
2703
+ },
2704
+ ],
2581
2705
  });
2582
2706
  }
2583
2707
  },
@@ -2639,7 +2763,7 @@ const rule$g = {
2639
2763
  },
2640
2764
  };
2641
2765
 
2642
- const RULE_ID$2 = 'require-description';
2766
+ const RULE_ID$7 = 'require-description';
2643
2767
  const ALLOWED_KINDS$1 = [
2644
2768
  ...TYPES_KINDS,
2645
2769
  graphql.Kind.DIRECTIVE_DEFINITION,
@@ -2681,7 +2805,7 @@ const rule$h = {
2681
2805
  docs: {
2682
2806
  category: 'Schema',
2683
2807
  description: 'Enforce descriptions in type definitions and operations.',
2684
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
2808
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$7}.md`,
2685
2809
  examples: [
2686
2810
  {
2687
2811
  title: 'Incorrect',
@@ -2728,7 +2852,7 @@ const rule$h = {
2728
2852
  },
2729
2853
  type: 'suggestion',
2730
2854
  messages: {
2731
- [RULE_ID$2]: 'Description is required for `{{ nodeName }}`.',
2855
+ [RULE_ID$7]: 'Description is required for `{{ nodeName }}`.',
2732
2856
  },
2733
2857
  schema: {
2734
2858
  type: 'array',
@@ -2787,8 +2911,8 @@ const rule$h = {
2787
2911
  }
2788
2912
  if (description.length === 0) {
2789
2913
  context.report({
2790
- loc: isOperation ? getLocation(node.loc, node.operation) : node.name.loc,
2791
- messageId: RULE_ID$2,
2914
+ loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
2915
+ messageId: RULE_ID$7,
2792
2916
  data: {
2793
2917
  nodeName: getNodeName(node),
2794
2918
  },
@@ -2870,122 +2994,18 @@ const rule$i = {
2870
2994
  },
2871
2995
  };
2872
2996
 
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';
2997
+ const RULE_ID$8 = 'require-id-when-available';
2981
2998
  const DEFAULT_ID_FIELD_NAME = 'id';
2999
+ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
2982
3000
  const rule$j = {
2983
3001
  meta: {
2984
3002
  type: 'suggestion',
3003
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
3004
+ hasSuggestions: true,
2985
3005
  docs: {
2986
3006
  category: 'Operations',
2987
3007
  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`,
3008
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$8}.md`,
2989
3009
  requiresSchema: true,
2990
3010
  requiresSiblings: true,
2991
3011
  examples: [
@@ -3028,21 +3048,14 @@ const rule$j = {
3028
3048
  recommended: true,
3029
3049
  },
3030
3050
  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'),
3051
+ [RULE_ID$8]: `Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.`,
3035
3052
  },
3036
3053
  schema: {
3037
3054
  definitions: {
3038
3055
  asString: {
3039
3056
  type: 'string',
3040
3057
  },
3041
- asArray: {
3042
- type: 'array',
3043
- minItems: 1,
3044
- uniqueItems: true,
3045
- },
3058
+ asArray: ARRAY_DEFAULT_OPTIONS,
3046
3059
  },
3047
3060
  type: 'array',
3048
3061
  maxItems: 1,
@@ -3059,76 +3072,121 @@ const rule$j = {
3059
3072
  },
3060
3073
  },
3061
3074
  create(context) {
3062
- requireGraphQLSchemaFromContext(RULE_ID$3, context);
3063
- const siblings = requireSiblingsOperations(RULE_ID$3, context);
3075
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$8, context);
3076
+ const siblings = requireSiblingsOperations(RULE_ID$8, context);
3064
3077
  const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
3065
3078
  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;
3079
+ // Check selections only in OperationDefinition,
3080
+ // skip selections of OperationDefinition and InlineFragment
3081
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
3082
+ const typeInfo = new graphql.TypeInfo(schema);
3083
+ function checkFragments(node) {
3084
+ for (const selection of node.selections) {
3085
+ if (selection.kind !== graphql.Kind.FRAGMENT_SPREAD) {
3086
+ continue;
3081
3087
  }
3082
- const fields = rawType.getFields();
3083
- const hasIdFieldInType = idNames.some(name => fields[name]);
3084
- if (!hasIdFieldInType) {
3085
- return;
3088
+ const [foundSpread] = siblings.getFragment(selection.name.value);
3089
+ if (!foundSpread) {
3090
+ continue;
3086
3091
  }
3087
3092
  const checkedFragmentSpreads = new Set();
3088
- for (const selection of node.selections) {
3089
- if (isFound(selection)) {
3090
- return;
3093
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
3094
+ SelectionSet(node, key, parent) {
3095
+ if (parent.kind === graphql.Kind.FRAGMENT_DEFINITION) {
3096
+ checkedFragmentSpreads.add(parent.name.value);
3097
+ }
3098
+ else if (parent.kind !== graphql.Kind.INLINE_FRAGMENT) {
3099
+ checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
3100
+ }
3101
+ },
3102
+ });
3103
+ graphql.visit(foundSpread.document, visitor);
3104
+ }
3105
+ }
3106
+ function checkSelections(node, type,
3107
+ // Fragment can be placed in separate file
3108
+ // Provide actual fragment spread location instead of location in fragment
3109
+ loc,
3110
+ // Can't access to node.parent in GraphQL AST.Node, so pass as argument
3111
+ parent, checkedFragmentSpreads = new Set()) {
3112
+ const rawType = getBaseType(type);
3113
+ const isObjectType = rawType instanceof graphql.GraphQLObjectType;
3114
+ const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
3115
+ if (!isObjectType && !isInterfaceType) {
3116
+ return;
3117
+ }
3118
+ const fields = rawType.getFields();
3119
+ const hasIdFieldInType = idNames.some(name => fields[name]);
3120
+ if (!hasIdFieldInType) {
3121
+ return;
3122
+ }
3123
+ function hasIdField({ selections }) {
3124
+ return selections.some(selection => {
3125
+ if (selection.kind === graphql.Kind.FIELD) {
3126
+ return idNames.includes(selection.name.value);
3091
3127
  }
3092
- if (selection.kind === graphql.Kind.INLINE_FRAGMENT && selection.selectionSet.selections.some(isFound)) {
3093
- return;
3128
+ if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
3129
+ return hasIdField(selection.selectionSet);
3094
3130
  }
3095
3131
  if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
3096
3132
  const [foundSpread] = siblings.getFragment(selection.name.value);
3097
3133
  if (foundSpread) {
3098
- checkedFragmentSpreads.add(foundSpread.document.name.value);
3099
- if (foundSpread.document.selectionSet.selections.some(isFound)) {
3100
- return;
3101
- }
3134
+ const fragmentSpread = foundSpread.document;
3135
+ checkedFragmentSpreads.add(fragmentSpread.name.value);
3136
+ return hasIdField(fragmentSpread.selectionSet);
3102
3137
  }
3103
3138
  }
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
- },
3139
+ return false;
3119
3140
  });
3141
+ }
3142
+ const hasId = hasIdField(node);
3143
+ checkFragments(node);
3144
+ if (hasId) {
3145
+ return;
3146
+ }
3147
+ const pluralSuffix = idNames.length > 1 ? 's' : '';
3148
+ const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));
3149
+ const addition = checkedFragmentSpreads.size === 0
3150
+ ? ''
3151
+ : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords([...checkedFragmentSpreads].map(name => `\`${name}\``))}`;
3152
+ const problem = {
3153
+ loc,
3154
+ messageId: RULE_ID$8,
3155
+ data: {
3156
+ pluralSuffix,
3157
+ fieldName,
3158
+ addition,
3159
+ },
3160
+ };
3161
+ // Don't provide suggestions for selections in fragments as fragment can be in a separate file
3162
+ if ('type' in node) {
3163
+ problem.suggest = idNames.map(idName => ({
3164
+ desc: `Add \`${idName}\` selection`,
3165
+ fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `),
3166
+ }));
3167
+ }
3168
+ context.report(problem);
3169
+ }
3170
+ return {
3171
+ [selector](node) {
3172
+ const typeInfo = node.typeInfo();
3173
+ if (typeInfo.gqlType) {
3174
+ checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
3175
+ }
3120
3176
  },
3121
3177
  };
3122
3178
  },
3123
3179
  };
3124
3180
 
3125
- const RULE_ID$4 = 'selection-set-depth';
3181
+ const RULE_ID$9 = 'selection-set-depth';
3126
3182
  const rule$k = {
3127
3183
  meta: {
3184
+ type: 'suggestion',
3185
+ hasSuggestions: true,
3128
3186
  docs: {
3129
3187
  category: 'Operations',
3130
3188
  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`,
3189
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$9}.md`,
3132
3190
  requiresSiblings: true,
3133
3191
  examples: [
3134
3192
  {
@@ -3174,7 +3232,6 @@ const rule$k = {
3174
3232
  recommended: true,
3175
3233
  configOptions: [{ maxDepth: 7 }],
3176
3234
  },
3177
- type: 'suggestion',
3178
3235
  schema: {
3179
3236
  type: 'array',
3180
3237
  minItems: 1,
@@ -3187,14 +3244,7 @@ const rule$k = {
3187
3244
  maxDepth: {
3188
3245
  type: 'number',
3189
3246
  },
3190
- ignore: {
3191
- type: 'array',
3192
- uniqueItems: true,
3193
- minItems: 1,
3194
- items: {
3195
- type: 'string',
3196
- },
3197
- },
3247
+ ignore: ARRAY_DEFAULT_OPTIONS,
3198
3248
  },
3199
3249
  },
3200
3250
  },
@@ -3202,10 +3252,10 @@ const rule$k = {
3202
3252
  create(context) {
3203
3253
  let siblings = null;
3204
3254
  try {
3205
- siblings = requireSiblingsOperations(RULE_ID$4, context);
3255
+ siblings = requireSiblingsOperations(RULE_ID$9, context);
3206
3256
  }
3207
3257
  catch (e) {
3208
- logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3258
+ logger.warn(`Rule "${RULE_ID$9}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3209
3259
  }
3210
3260
  const { maxDepth } = context.options[0];
3211
3261
  const ignore = context.options[0].ignore || [];
@@ -3214,7 +3264,7 @@ const rule$k = {
3214
3264
  'OperationDefinition, FragmentDefinition'(node) {
3215
3265
  try {
3216
3266
  const rawNode = node.rawNode();
3217
- const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode, true) : [];
3267
+ const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
3218
3268
  const document = {
3219
3269
  kind: graphql.Kind.DOCUMENT,
3220
3270
  definitions: [rawNode, ...fragmentsInUse],
@@ -3229,19 +3279,32 @@ const rule$k = {
3229
3279
  column: column - 1,
3230
3280
  },
3231
3281
  message: error.message,
3282
+ suggest: [
3283
+ {
3284
+ desc: 'Remove selections',
3285
+ fix(fixer) {
3286
+ const ancestors = context.getAncestors();
3287
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
3288
+ const sourceCode = context.getSourceCode();
3289
+ const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]);
3290
+ const parentNode = foundNode.parent.parent;
3291
+ return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
3292
+ },
3293
+ },
3294
+ ],
3232
3295
  });
3233
3296
  },
3234
3297
  });
3235
3298
  }
3236
3299
  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);
3300
+ 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
3301
  }
3239
3302
  },
3240
3303
  };
3241
3304
  },
3242
3305
  };
3243
3306
 
3244
- const RULE_ID$5 = 'strict-id-in-types';
3307
+ const RULE_ID$a = 'strict-id-in-types';
3245
3308
  const rule$l = {
3246
3309
  meta: {
3247
3310
  type: 'suggestion',
@@ -3249,7 +3312,7 @@ const rule$l = {
3249
3312
  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
3313
  category: 'Schema',
3251
3314
  recommended: true,
3252
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
3315
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$a}.md`,
3253
3316
  requiresSchema: true,
3254
3317
  examples: [
3255
3318
  {
@@ -3340,22 +3403,12 @@ const rule$l = {
3340
3403
  type: 'object',
3341
3404
  properties: {
3342
3405
  types: {
3343
- type: 'array',
3344
- uniqueItems: true,
3345
- minItems: 1,
3406
+ ...ARRAY_DEFAULT_OPTIONS,
3346
3407
  description: 'This is used to exclude types with names that match one of the specified values.',
3347
- items: {
3348
- type: 'string',
3349
- },
3350
3408
  },
3351
3409
  suffixes: {
3352
- type: 'array',
3353
- uniqueItems: true,
3354
- minItems: 1,
3410
+ ...ARRAY_DEFAULT_OPTIONS,
3355
3411
  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
3412
  },
3360
3413
  },
3361
3414
  },
@@ -3363,7 +3416,7 @@ const rule$l = {
3363
3416
  },
3364
3417
  },
3365
3418
  messages: {
3366
- [RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3419
+ [RULE_ID$a]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3367
3420
  },
3368
3421
  },
3369
3422
  create(context) {
@@ -3373,7 +3426,7 @@ const rule$l = {
3373
3426
  exceptions: {},
3374
3427
  ...context.options[0],
3375
3428
  };
3376
- const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
3429
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$a, context);
3377
3430
  const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
3378
3431
  .filter(Boolean)
3379
3432
  .map(type => type.name);
@@ -3403,7 +3456,7 @@ const rule$l = {
3403
3456
  if (validIds.length !== 1) {
3404
3457
  context.report({
3405
3458
  node: node.name,
3406
- messageId: RULE_ID$5,
3459
+ messageId: RULE_ID$a,
3407
3460
  data: {
3408
3461
  typeName,
3409
3462
  acceptedNamesString: options.acceptedIdNames.join(', '),
@@ -3662,6 +3715,7 @@ const processor = createGraphqlProcessor();
3662
3715
  const processors = EXTRACTABLE_FILES_EXTENSIONS.reduce((prev, ext) => ({ ...prev, [ext]: processor }), {});
3663
3716
 
3664
3717
  const schemaCache = new Map();
3718
+ const debug = debugFactory('graphql-eslint:schema');
3665
3719
  function getSchema(options = {}, gqlConfig) {
3666
3720
  const realFilepath = options.filePath ? getOnDiskFilepath(options.filePath) : null;
3667
3721
  const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
@@ -3674,10 +3728,18 @@ function getSchema(options = {}, gqlConfig) {
3674
3728
  }
3675
3729
  let schema;
3676
3730
  try {
3731
+ debug('Loading schema from %o', projectForFile.schema);
3677
3732
  schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
3678
3733
  cache: loaderCache,
3679
3734
  ...options.schemaOptions,
3680
3735
  });
3736
+ if (debug.enabled) {
3737
+ debug('Schema loaded: %o', schema instanceof graphql.GraphQLSchema);
3738
+ const schemaPaths = fastGlob.sync(projectForFile.schema, {
3739
+ absolute: true,
3740
+ });
3741
+ debug('Schema pointers %O', schemaPaths);
3742
+ }
3681
3743
  }
3682
3744
  catch (e) {
3683
3745
  schema = null;
@@ -3687,6 +3749,7 @@ function getSchema(options = {}, gqlConfig) {
3687
3749
  return schema;
3688
3750
  }
3689
3751
 
3752
+ const debug$1 = debugFactory('graphql-eslint:operations');
3690
3753
  const handleVirtualPath = (documents) => {
3691
3754
  const filepathMap = Object.create(null);
3692
3755
  return documents.map(source => {
@@ -3714,10 +3777,18 @@ const getSiblings = (filePath, gqlConfig) => {
3714
3777
  }
3715
3778
  let siblings = operationsCache.get(documentsKey);
3716
3779
  if (!siblings) {
3780
+ debug$1('Loading operations from %o', projectForFile.documents);
3717
3781
  const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
3718
3782
  skipGraphQLImport: true,
3719
3783
  cache: loaderCache,
3720
3784
  });
3785
+ if (debug$1.enabled) {
3786
+ debug$1('Loaded %d operations', documents.length);
3787
+ const operationsPaths = fastGlob.sync(projectForFile.documents, {
3788
+ absolute: true
3789
+ });
3790
+ debug$1('Operations pointers %O', operationsPaths);
3791
+ }
3721
3792
  siblings = handleVirtualPath(documents);
3722
3793
  operationsCache.set(documentsKey, siblings);
3723
3794
  }
@@ -3736,13 +3807,13 @@ function getSiblingOperations(options, gqlConfig) {
3736
3807
  };
3737
3808
  return {
3738
3809
  available: false,
3739
- getFragments: noopWarn,
3740
- getOperations: noopWarn,
3741
3810
  getFragment: noopWarn,
3811
+ getFragments: noopWarn,
3742
3812
  getFragmentByType: noopWarn,
3813
+ getFragmentsInUse: noopWarn,
3743
3814
  getOperation: noopWarn,
3815
+ getOperations: noopWarn,
3744
3816
  getOperationByType: noopWarn,
3745
- getFragmentsInUse: noopWarn,
3746
3817
  };
3747
3818
  }
3748
3819
  // Since the siblings array is cached, we can use it as cache key.
@@ -3755,7 +3826,7 @@ function getSiblingOperations(options, gqlConfig) {
3755
3826
  if (fragmentsCache === null) {
3756
3827
  const result = [];
3757
3828
  for (const source of siblings) {
3758
- for (const definition of source.document.definitions || []) {
3829
+ for (const definition of source.document.definitions) {
3759
3830
  if (definition.kind === graphql.Kind.FRAGMENT_DEFINITION) {
3760
3831
  result.push({
3761
3832
  filePath: source.location,
@@ -3773,7 +3844,7 @@ function getSiblingOperations(options, gqlConfig) {
3773
3844
  if (cachedOperations === null) {
3774
3845
  const result = [];
3775
3846
  for (const source of siblings) {
3776
- for (const definition of source.document.definitions || []) {
3847
+ for (const definition of source.document.definitions) {
3777
3848
  if (definition.kind === graphql.Kind.OPERATION_DEFINITION) {
3778
3849
  result.push({
3779
3850
  filePath: source.location,
@@ -3787,19 +3858,17 @@ function getSiblingOperations(options, gqlConfig) {
3787
3858
  return cachedOperations;
3788
3859
  };
3789
3860
  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()) => {
3861
+ const collectFragments = (selectable, recursive, collected = new Map()) => {
3791
3862
  graphql.visit(selectable, {
3792
3863
  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"`);
3864
+ const fragmentName = spread.name.value;
3865
+ const [fragment] = getFragment(fragmentName);
3866
+ if (!fragment) {
3867
+ logger.warn(`Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`);
3797
3868
  return;
3798
3869
  }
3799
- const fragment = fragmentInfo[0];
3800
- const alreadyVisited = collected.has(name);
3801
- if (!alreadyVisited) {
3802
- collected.set(name, fragment.document);
3870
+ if (!collected.has(fragmentName)) {
3871
+ collected.set(fragmentName, fragment.document);
3803
3872
  if (recursive) {
3804
3873
  collectFragments(fragment.document, recursive, collected);
3805
3874
  }
@@ -3810,19 +3879,20 @@ function getSiblingOperations(options, gqlConfig) {
3810
3879
  };
3811
3880
  siblingOperations = {
3812
3881
  available: true,
3813
- getFragments,
3814
- getOperations,
3815
3882
  getFragment,
3883
+ getFragments,
3816
3884
  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; }),
3885
+ getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3817
3886
  getOperation: name => getOperations().filter(o => { var _a; return ((_a = o.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; }),
3887
+ getOperations,
3818
3888
  getOperationByType: type => getOperations().filter(o => o.document.operation === type),
3819
- getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3820
3889
  };
3821
3890
  siblingOperationsCache.set(siblings, siblingOperations);
3822
3891
  }
3823
3892
  return siblingOperations;
3824
3893
  }
3825
3894
 
3895
+ const debug$2 = debugFactory('graphql-eslint:graphql-config');
3826
3896
  let graphQLConfig;
3827
3897
  function loadGraphQLConfig(options) {
3828
3898
  // We don't want cache config on test environment
@@ -3839,6 +3909,10 @@ function loadGraphQLConfig(options) {
3839
3909
  throwOnMissing: false,
3840
3910
  extensions: [addCodeFileLoaderExtension],
3841
3911
  });
3912
+ debug$2('options.skipGraphQLConfig: %o', options.skipGraphQLConfig);
3913
+ if (onDiskConfig) {
3914
+ debug$2('Graphql-config path %o', onDiskConfig.filepath);
3915
+ }
3842
3916
  const configOptions = options.projects
3843
3917
  ? { projects: options.projects }
3844
3918
  : {
@@ -3862,87 +3936,8 @@ const addCodeFileLoaderExtension = api => {
3862
3936
  return { name: 'graphql-eslint-loaders' };
3863
3937
  };
3864
3938
 
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 {
3887
- graphql.visit(type.astNode, visitor);
3888
- }
3889
- };
3890
- const visitor = {
3891
- InterfaceTypeDefinition: collect,
3892
- ObjectTypeDefinition: collect,
3893
- InputValueDefinition: collect,
3894
- UnionTypeDefinition: collect,
3895
- FieldDefinition: collect,
3896
- Directive: collect,
3897
- NamedType: collect,
3898
- };
3899
- for (const type of [
3900
- schema,
3901
- schema.getQueryType(),
3902
- schema.getMutationType(),
3903
- schema.getSubscriptionType(),
3904
- ]) {
3905
- if (type) {
3906
- graphql.visit(type.astNode, visitor);
3907
- }
3908
- }
3909
- reachableTypesCache = reachableTypes;
3910
- return reachableTypesCache;
3911
- }
3912
- let usedFieldsCache;
3913
- function getUsedFields(schema, operations) {
3914
- // We don't want cache usedFields on test environment
3915
- // Otherwise usedFields will be same for all tests
3916
- if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
3917
- return usedFieldsCache;
3918
- }
3919
- const usedFields = Object.create(null);
3920
- const typeInfo = new graphql.TypeInfo(schema);
3921
- const visitor = graphql.visitWithTypeInfo(typeInfo, {
3922
- Field(node) {
3923
- var _a;
3924
- const fieldDef = typeInfo.getFieldDef();
3925
- if (!fieldDef) {
3926
- // skip visiting this node if field is not defined in schema
3927
- return false;
3928
- }
3929
- const parentTypeName = typeInfo.getParentType().name;
3930
- const fieldName = node.name.value;
3931
- (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
3932
- usedFields[parentTypeName].add(fieldName);
3933
- },
3934
- });
3935
- const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
3936
- for (const { document } of allDocuments) {
3937
- graphql.visit(document, visitor);
3938
- }
3939
- usedFieldsCache = usedFields;
3940
- return usedFieldsCache;
3941
- }
3942
-
3943
- function parse(code, options) {
3944
- return parseForESLint(code, options).ast;
3945
- }
3939
+ const debug$3 = debugFactory('graphql-eslint:parser');
3940
+ debug$3('cwd %o', process.cwd());
3946
3941
  function parseForESLint(code, options = {}) {
3947
3942
  const gqlConfig = loadGraphQLConfig(options);
3948
3943
  const schema = getSchema(options, gqlConfig);
@@ -3950,8 +3945,6 @@ function parseForESLint(code, options = {}) {
3950
3945
  hasTypeInfo: schema !== null,
3951
3946
  schema,
3952
3947
  siblingOperations: getSiblingOperations(options, gqlConfig),
3953
- reachableTypes: getReachableTypes,
3954
- usedFields: getUsedFields,
3955
3948
  };
3956
3949
  try {
3957
3950
  const filePath = options.filePath || '';
@@ -3963,15 +3956,14 @@ function parseForESLint(code, options = {}) {
3963
3956
  const tokens = extractTokens(new graphql.Source(code, filePath));
3964
3957
  return {
3965
3958
  services: parserServices,
3966
- parserServices,
3967
3959
  ast: {
3968
- type: 'Program',
3969
- body: [rootTree],
3970
- sourceType: 'script',
3971
3960
  comments,
3961
+ tokens,
3972
3962
  loc: rootTree.loc,
3973
3963
  range: rootTree.range,
3974
- tokens,
3964
+ type: 'Program',
3965
+ sourceType: 'script',
3966
+ body: [rootTree],
3975
3967
  },
3976
3968
  };
3977
3969
  }
@@ -3992,6 +3984,7 @@ function parseForESLint(code, options = {}) {
3992
3984
  }
3993
3985
  }
3994
3986
 
3987
+ /* eslint-env jest */
3995
3988
  function indentCode(code, indent = 4) {
3996
3989
  return code.replace(/^/gm, ' '.repeat(indent));
3997
3990
  }
@@ -4001,6 +3994,11 @@ function printCode(code) {
4001
3994
  linesBelow: Number.POSITIVE_INFINITY,
4002
3995
  });
4003
3996
  }
3997
+ // A simple version of `SourceCodeFixer.applyFixes`
3998
+ // https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
3999
+ function applyFix(code, fix) {
4000
+ return [code.slice(0, fix.range[0]), fix.text, code.slice(fix.range[1])].join('');
4001
+ }
4004
4002
  class GraphQLRuleTester extends eslint.RuleTester {
4005
4003
  constructor(parserOptions = {}) {
4006
4004
  const config = {
@@ -4044,32 +4042,63 @@ class GraphQLRuleTester extends eslint.RuleTester {
4044
4042
  linter.defineRule(name, rule);
4045
4043
  const hasOnlyTest = tests.invalid.some(t => t.only);
4046
4044
  for (const testCase of tests.invalid) {
4047
- const { only, code, filename } = testCase;
4045
+ const { only, filename, options } = testCase;
4048
4046
  if (hasOnlyTest && !only) {
4049
4047
  continue;
4050
4048
  }
4049
+ const code = removeTrailingBlankLines(testCase.code);
4051
4050
  const verifyConfig = getVerifyConfig(name, this.config, testCase);
4052
4051
  defineParser(linter, verifyConfig.parser);
4053
4052
  const messages = linter.verify(code, verifyConfig, { filename });
4054
4053
  const messageForSnapshot = [];
4054
+ const hasMultipleMessages = messages.length > 1;
4055
+ if (hasMultipleMessages) {
4056
+ messageForSnapshot.push('Code', indentCode(printCode(code)));
4057
+ }
4058
+ if (options) {
4059
+ const opts = JSON.stringify(options, null, 2).slice(1, -1);
4060
+ messageForSnapshot.push('⚙️ Options', indentCode(removeTrailingBlankLines(opts), 2));
4061
+ }
4055
4062
  for (const [index, message] of messages.entries()) {
4056
4063
  if (message.fatal) {
4057
4064
  throw new Error(message.message);
4058
4065
  }
4059
- messageForSnapshot.push(`❌ Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
4066
+ const codeWithMessage = visualizeEslintMessage(code, message, hasMultipleMessages ? 1 : undefined);
4067
+ messageForSnapshot.push(printWithIndex('❌ Error', index, messages.length), indentCode(codeWithMessage));
4068
+ const { suggestions } = message;
4069
+ // Don't print suggestions in snapshots for too big codes
4070
+ if (suggestions && (code.match(/\n/g) || '').length < 1000) {
4071
+ for (const [i, suggestion] of message.suggestions.entries()) {
4072
+ const output = applyFix(code, suggestion.fix);
4073
+ const title = printWithIndex('💡 Suggestion', i, suggestions.length, suggestion.desc);
4074
+ messageForSnapshot.push(title, indentCode(printCode(output), 2));
4075
+ }
4076
+ }
4060
4077
  }
4061
4078
  if (rule.meta.fixable) {
4062
4079
  const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
4063
4080
  if (fixed) {
4064
- messageForSnapshot.push('🔧 Autofix output', indentCode(printCode(output), 2));
4081
+ messageForSnapshot.push('🔧 Autofix output', indentCode(codeFrame.codeFrameColumns(output, {})));
4065
4082
  }
4066
4083
  }
4067
4084
  expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
4068
4085
  }
4069
4086
  }
4070
4087
  }
4088
+ function removeTrailingBlankLines(text) {
4089
+ return text.replace(/^\s*\n/, '').trimEnd();
4090
+ }
4091
+ function printWithIndex(title, index, total, description) {
4092
+ if (total > 1) {
4093
+ title += ` ${index + 1}/${total}`;
4094
+ }
4095
+ if (description) {
4096
+ title += `: ${description}`;
4097
+ }
4098
+ return title;
4099
+ }
4071
4100
  function getVerifyConfig(ruleId, testerConfig, testCase) {
4072
- const { options, parserOptions, parser = testerConfig.parser } = testCase;
4101
+ const { parser = testerConfig.parser, parserOptions, options } = testCase;
4073
4102
  return {
4074
4103
  ...testerConfig,
4075
4104
  parser,
@@ -4078,7 +4107,7 @@ function getVerifyConfig(ruleId, testerConfig, testCase) {
4078
4107
  ...parserOptions,
4079
4108
  },
4080
4109
  rules: {
4081
- [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
4110
+ [ruleId]: Array.isArray(options) ? ['error', ...options] : 'error',
4082
4111
  },
4083
4112
  };
4084
4113
  }
@@ -4096,7 +4125,7 @@ function defineParser(linter, parser) {
4096
4125
  linter.defineParser(parser, require(parser));
4097
4126
  }
4098
4127
  }
4099
- function visualizeEslintMessage(text, result) {
4128
+ function visualizeEslintMessage(text, result, linesOffset = Number.POSITIVE_INFINITY) {
4100
4129
  const { line, column, endLine, endColumn, message } = result;
4101
4130
  const location = {
4102
4131
  start: {
@@ -4111,23 +4140,21 @@ function visualizeEslintMessage(text, result) {
4111
4140
  };
4112
4141
  }
4113
4142
  return codeFrame.codeFrameColumns(text, location, {
4114
- linesAbove: Number.POSITIVE_INFINITY,
4115
- linesBelow: Number.POSITIVE_INFINITY,
4143
+ linesAbove: linesOffset,
4144
+ linesBelow: linesOffset,
4116
4145
  message,
4117
4146
  });
4118
4147
  }
4119
4148
 
4149
+ const configs = Object.fromEntries(['schema-recommended', 'schema-all', 'operations-recommended', 'operations-all'].map(configName => [
4150
+ configName,
4151
+ { extends: `./configs/${configName}.json` },
4152
+ ]));
4153
+
4120
4154
  exports.GraphQLRuleTester = GraphQLRuleTester;
4121
4155
  exports.configs = configs;
4122
- exports.convertDescription = convertDescription;
4123
- exports.convertToESTree = convertToESTree;
4124
- exports.convertToken = convertToken;
4125
- exports.extractCommentsFromAst = extractCommentsFromAst;
4126
- exports.extractTokens = extractTokens;
4127
- exports.getBaseType = getBaseType;
4128
- exports.isNodeWithDescription = isNodeWithDescription;
4129
- exports.parse = parse;
4130
4156
  exports.parseForESLint = parseForESLint;
4131
4157
  exports.processors = processors;
4158
+ exports.requireGraphQLSchemaFromContext = requireGraphQLSchemaFromContext;
4159
+ exports.requireSiblingsOperations = requireSiblingsOperations;
4132
4160
  exports.rules = rules;
4133
- exports.valueFromNode = valueFromNode;