@graphql-eslint/eslint-plugin 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +5 -5
  2. package/configs/base.json +4 -0
  3. package/configs/operations-all.json +24 -0
  4. package/configs/operations-recommended.json +50 -0
  5. package/configs/schema-all.json +26 -0
  6. package/configs/schema-recommended.json +49 -0
  7. package/docs/README.md +59 -57
  8. package/docs/custom-rules.md +8 -8
  9. package/docs/parser-options.md +4 -4
  10. package/docs/parser.md +2 -2
  11. package/docs/rules/alphabetize.md +11 -5
  12. package/docs/rules/description-style.md +2 -0
  13. package/docs/rules/executable-definitions.md +1 -1
  14. package/docs/rules/fields-on-correct-type.md +1 -1
  15. package/docs/rules/fragments-on-composite-type.md +1 -1
  16. package/docs/rules/input-name.md +2 -0
  17. package/docs/rules/known-argument-names.md +1 -1
  18. package/docs/rules/known-directives.md +29 -2
  19. package/docs/rules/known-fragment-names.md +1 -1
  20. package/docs/rules/known-type-names.md +1 -1
  21. package/docs/rules/lone-anonymous-operation.md +1 -1
  22. package/docs/rules/lone-schema-definition.md +1 -1
  23. package/docs/rules/naming-convention.md +2 -0
  24. package/docs/rules/no-anonymous-operations.md +2 -0
  25. package/docs/rules/no-case-insensitive-enum-values-duplicates.md +2 -0
  26. package/docs/rules/no-deprecated.md +2 -0
  27. package/docs/rules/no-duplicate-fields.md +2 -0
  28. package/docs/rules/no-fragment-cycles.md +1 -1
  29. package/docs/rules/no-hashtag-description.md +2 -0
  30. package/docs/rules/no-root-type.md +2 -0
  31. package/docs/rules/no-scalar-result-type-on-mutation.md +2 -0
  32. package/docs/rules/no-typename-prefix.md +2 -0
  33. package/docs/rules/no-undefined-variables.md +1 -1
  34. package/docs/rules/no-unreachable-types.md +2 -0
  35. package/docs/rules/no-unused-fields.md +2 -0
  36. package/docs/rules/no-unused-fragments.md +1 -1
  37. package/docs/rules/no-unused-variables.md +1 -1
  38. package/docs/rules/one-field-subscriptions.md +1 -1
  39. package/docs/rules/overlapping-fields-can-be-merged.md +1 -1
  40. package/docs/rules/possible-fragment-spread.md +1 -1
  41. package/docs/rules/possible-type-extension.md +1 -1
  42. package/docs/rules/provided-required-arguments.md +1 -1
  43. package/docs/rules/require-deprecation-date.md +2 -0
  44. package/docs/rules/require-id-when-available.md +2 -0
  45. package/docs/rules/scalar-leafs.md +1 -1
  46. package/docs/rules/selection-set-depth.md +2 -0
  47. package/docs/rules/unique-argument-names.md +1 -1
  48. package/docs/rules/unique-directive-names-per-location.md +1 -1
  49. package/docs/rules/unique-directive-names.md +1 -1
  50. package/docs/rules/unique-enum-value-names.md +1 -1
  51. package/docs/rules/unique-field-definition-names.md +1 -1
  52. package/docs/rules/unique-input-field-names.md +1 -1
  53. package/docs/rules/unique-operation-types.md +1 -1
  54. package/docs/rules/unique-type-names.md +1 -1
  55. package/docs/rules/unique-variable-names.md +1 -1
  56. package/docs/rules/value-literals-of-correct-type.md +1 -1
  57. package/docs/rules/variables-are-input-types.md +1 -1
  58. package/docs/rules/variables-in-allowed-position.md +1 -1
  59. package/estree-parser/converter.d.ts +3 -2
  60. package/estree-parser/estree-ast.d.ts +18 -18
  61. package/estree-parser/utils.d.ts +2 -8
  62. package/index.d.ts +6 -2
  63. package/index.js +726 -723
  64. package/index.mjs +726 -717
  65. package/package.json +1 -1
  66. package/rules/alphabetize.d.ts +1 -0
  67. package/rules/graphql-js-validation.d.ts +1 -1
  68. package/rules/index.d.ts +1 -4
  69. package/rules/selection-set-depth.d.ts +1 -1
  70. package/sibling-operations.d.ts +3 -3
  71. package/testkit.d.ts +3 -3
  72. package/types.d.ts +24 -18
  73. package/utils.d.ts +14 -4
  74. package/configs/base.d.ts +0 -5
  75. package/configs/index.d.ts +0 -133
  76. package/configs/operations-all.d.ts +0 -19
  77. package/configs/operations-recommended.d.ts +0 -50
  78. package/configs/schema-all.d.ts +0 -15
  79. package/configs/schema-recommended.d.ts +0 -47
  80. package/graphql-ast.d.ts +0 -6
package/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
- import { Kind, validate, TypeInfo, visitWithTypeInfo, visit, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, isInterfaceType, Source, GraphQLError } from 'graphql';
1
+ import { Kind, visit, validate, TypeInfo, visitWithTypeInfo, TokenKind, isScalarType, isInterfaceType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, Source, GraphQLError } from 'graphql';
2
2
  import { validateSDL } from 'graphql/validation/validate';
3
3
  import { statSync, existsSync, readFileSync } from 'fs';
4
4
  import { dirname, extname, basename, relative, resolve } from 'path';
5
5
  import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
6
6
  import lowerCase from 'lodash.lowercase';
7
7
  import chalk from 'chalk';
8
+ import { valueFromASTUntyped } from 'graphql/utilities/valueFromASTUntyped';
8
9
  import depthLimit from 'graphql-depth-limit';
9
10
  import { parseCode } from '@graphql-tools/graphql-tag-pluck';
10
11
  import { loadConfigSync, GraphQLConfig } from 'graphql-config';
@@ -12,163 +13,6 @@ import { CodeFileLoader } from '@graphql-tools/code-file-loader';
12
13
  import { RuleTester, Linter } from 'eslint';
13
14
  import { codeFrameColumns } from '@babel/code-frame';
14
15
 
15
- const base = {
16
- parser: '@graphql-eslint/eslint-plugin',
17
- plugins: ['@graphql-eslint'],
18
- };
19
-
20
- /*
21
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
22
- */
23
- const schemaRecommendedConfig = {
24
- extends: ['plugin:@graphql-eslint/base'],
25
- rules: {
26
- '@graphql-eslint/description-style': 'error',
27
- '@graphql-eslint/known-argument-names': 'error',
28
- '@graphql-eslint/known-directives': 'error',
29
- '@graphql-eslint/known-type-names': 'error',
30
- '@graphql-eslint/lone-schema-definition': 'error',
31
- '@graphql-eslint/naming-convention': [
32
- 'error',
33
- {
34
- types: 'PascalCase',
35
- FieldDefinition: 'camelCase',
36
- InputValueDefinition: 'camelCase',
37
- Argument: 'camelCase',
38
- DirectiveDefinition: 'camelCase',
39
- EnumValueDefinition: 'UPPER_CASE',
40
- 'FieldDefinition[parent.name.value=Query]': {
41
- forbiddenPrefixes: ['query', 'get'],
42
- forbiddenSuffixes: ['Query'],
43
- },
44
- 'FieldDefinition[parent.name.value=Mutation]': {
45
- forbiddenPrefixes: ['mutation'],
46
- forbiddenSuffixes: ['Mutation'],
47
- },
48
- 'FieldDefinition[parent.name.value=Subscription]': {
49
- forbiddenPrefixes: ['subscription'],
50
- forbiddenSuffixes: ['Subscription'],
51
- },
52
- },
53
- ],
54
- '@graphql-eslint/no-case-insensitive-enum-values-duplicates': 'error',
55
- '@graphql-eslint/no-hashtag-description': 'error',
56
- '@graphql-eslint/no-typename-prefix': 'error',
57
- '@graphql-eslint/no-unreachable-types': 'error',
58
- '@graphql-eslint/provided-required-arguments': 'error',
59
- '@graphql-eslint/require-deprecation-reason': 'error',
60
- '@graphql-eslint/require-description': ['error', { types: true, DirectiveDefinition: true }],
61
- '@graphql-eslint/strict-id-in-types': 'error',
62
- '@graphql-eslint/unique-directive-names': 'error',
63
- '@graphql-eslint/unique-directive-names-per-location': 'error',
64
- '@graphql-eslint/unique-field-definition-names': 'error',
65
- '@graphql-eslint/unique-operation-types': 'error',
66
- '@graphql-eslint/unique-type-names': 'error',
67
- },
68
- };
69
-
70
- /*
71
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
72
- */
73
- const schemaAllConfig = {
74
- extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/schema-recommended'],
75
- rules: {
76
- '@graphql-eslint/alphabetize': [
77
- 'error',
78
- {
79
- fields: ['ObjectTypeDefinition', 'InterfaceTypeDefinition', 'InputObjectTypeDefinition'],
80
- values: ['EnumTypeDefinition'],
81
- arguments: ['FieldDefinition', 'Field', 'DirectiveDefinition', 'Directive'],
82
- },
83
- ],
84
- '@graphql-eslint/input-name': 'error',
85
- '@graphql-eslint/no-scalar-result-type-on-mutation': 'error',
86
- '@graphql-eslint/require-deprecation-date': 'error',
87
- '@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error',
88
- },
89
- };
90
-
91
- /*
92
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
93
- */
94
- const operationsRecommendedConfig = {
95
- extends: ['plugin:@graphql-eslint/base'],
96
- rules: {
97
- '@graphql-eslint/executable-definitions': 'error',
98
- '@graphql-eslint/fields-on-correct-type': 'error',
99
- '@graphql-eslint/fragments-on-composite-type': 'error',
100
- '@graphql-eslint/known-argument-names': 'error',
101
- '@graphql-eslint/known-directives': 'error',
102
- '@graphql-eslint/known-fragment-names': 'error',
103
- '@graphql-eslint/known-type-names': 'error',
104
- '@graphql-eslint/lone-anonymous-operation': 'error',
105
- '@graphql-eslint/naming-convention': [
106
- 'error',
107
- {
108
- VariableDefinition: 'camelCase',
109
- OperationDefinition: {
110
- style: 'PascalCase',
111
- forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
112
- forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
113
- },
114
- FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] },
115
- },
116
- ],
117
- '@graphql-eslint/no-anonymous-operations': 'error',
118
- '@graphql-eslint/no-deprecated': 'error',
119
- '@graphql-eslint/no-duplicate-fields': 'error',
120
- '@graphql-eslint/no-fragment-cycles': 'error',
121
- '@graphql-eslint/no-undefined-variables': 'error',
122
- '@graphql-eslint/no-unused-fragments': 'error',
123
- '@graphql-eslint/no-unused-variables': 'error',
124
- '@graphql-eslint/one-field-subscriptions': 'error',
125
- '@graphql-eslint/overlapping-fields-can-be-merged': 'error',
126
- '@graphql-eslint/possible-fragment-spread': 'error',
127
- '@graphql-eslint/provided-required-arguments': 'error',
128
- '@graphql-eslint/require-id-when-available': 'error',
129
- '@graphql-eslint/scalar-leafs': 'error',
130
- '@graphql-eslint/selection-set-depth': ['error', { maxDepth: 7 }],
131
- '@graphql-eslint/unique-argument-names': 'error',
132
- '@graphql-eslint/unique-directive-names-per-location': 'error',
133
- '@graphql-eslint/unique-input-field-names': 'error',
134
- '@graphql-eslint/unique-variable-names': 'error',
135
- '@graphql-eslint/value-literals-of-correct-type': 'error',
136
- '@graphql-eslint/variables-are-input-types': 'error',
137
- '@graphql-eslint/variables-in-allowed-position': 'error',
138
- },
139
- };
140
-
141
- /*
142
- * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
143
- */
144
- const operationsAllConfig = {
145
- extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/operations-recommended'],
146
- rules: {
147
- '@graphql-eslint/alphabetize': [
148
- 'error',
149
- {
150
- selections: ['OperationDefinition', 'FragmentDefinition'],
151
- variables: ['OperationDefinition'],
152
- arguments: ['Field', 'Directive'],
153
- },
154
- ],
155
- '@graphql-eslint/match-document-filename': [
156
- 'error',
157
- { query: 'kebab-case', mutation: 'kebab-case', subscription: 'kebab-case', fragment: 'kebab-case' },
158
- ],
159
- '@graphql-eslint/unique-fragment-name': 'error',
160
- '@graphql-eslint/unique-operation-name': 'error',
161
- },
162
- };
163
-
164
- const configs = {
165
- base,
166
- 'schema-recommended': schemaRecommendedConfig,
167
- 'schema-all': schemaAllConfig,
168
- 'operations-recommended': operationsRecommendedConfig,
169
- 'operations-all': operationsAllConfig,
170
- };
171
-
172
16
  function requireSiblingsOperations(ruleName, context) {
173
17
  if (!context.parserServices) {
174
18
  throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
@@ -193,15 +37,6 @@ const logger = {
193
37
  // eslint-disable-next-line no-console
194
38
  warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
195
39
  };
196
- function requireReachableTypesFromContext(ruleName, context) {
197
- const schema = requireGraphQLSchemaFromContext(ruleName, context);
198
- return context.parserServices.reachableTypes(schema);
199
- }
200
- function requireUsedFieldsFromContext(ruleName, context) {
201
- const schema = requireGraphQLSchemaFromContext(ruleName, context);
202
- const siblings = requireSiblingsOperations(ruleName, context);
203
- return context.parserServices.usedFields(schema, siblings);
204
- }
205
40
  const normalizePath = (path) => (path || '').replace(/\\/g, '/');
206
41
  /**
207
42
  * https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
@@ -271,8 +106,8 @@ const convertCase = (style, str) => {
271
106
  return lowerCase(str).replace(/ /g, '-');
272
107
  }
273
108
  };
274
- function getLocation(loc, fieldName = '') {
275
- const { line, column } = loc.start;
109
+ function getLocation(start, fieldName = '') {
110
+ const { line, column } = start;
276
111
  return {
277
112
  start: {
278
113
  line,
@@ -284,6 +119,15 @@ function getLocation(loc, fieldName = '') {
284
119
  },
285
120
  };
286
121
  }
122
+ const REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 };
123
+ const ARRAY_DEFAULT_OPTIONS = {
124
+ type: 'array',
125
+ uniqueItems: true,
126
+ minItems: 1,
127
+ items: {
128
+ type: 'string',
129
+ },
130
+ };
287
131
 
288
132
  function validateDocument(context, schema = null, documentNode, rule) {
289
133
  if (documentNode.definitions.length === 0) {
@@ -295,23 +139,27 @@ function validateDocument(context, schema = null, documentNode, rule) {
295
139
  : validateSDL(documentNode, null, [rule]);
296
140
  for (const error of validationErrors) {
297
141
  const { line, column } = error.locations[0];
298
- const ancestors = context.getAncestors();
299
- const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
142
+ const sourceCode = context.getSourceCode();
143
+ const { tokens } = sourceCode.ast;
144
+ const token = tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
145
+ let loc = {
146
+ line,
147
+ column: column - 1,
148
+ };
149
+ if (token) {
150
+ loc =
151
+ // if cursor on `@` symbol than use next node
152
+ token.type === '@' ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc;
153
+ }
300
154
  context.report({
301
- loc: token
302
- ? token.loc
303
- : {
304
- line,
305
- column: column - 1,
306
- },
155
+ loc,
307
156
  message: error.message,
308
157
  });
309
158
  }
310
159
  }
311
160
  catch (e) {
312
161
  context.report({
313
- // Report on first character
314
- loc: { column: 0, line: 1 },
162
+ loc: REPORT_ON_FIRST_CHARACTER,
315
163
  message: e.message,
316
164
  });
317
165
  }
@@ -368,7 +216,7 @@ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
368
216
  }
369
217
  return node;
370
218
  };
371
- const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
219
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = []) => {
372
220
  let ruleFn = null;
373
221
  try {
374
222
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
@@ -389,8 +237,9 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
389
237
  ...docs,
390
238
  graphQLJSRuleName: ruleName,
391
239
  url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
392
- 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).`,
240
+ description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
393
241
  },
242
+ schema,
394
243
  },
395
244
  create(context) {
396
245
  if (!ruleFn) {
@@ -428,8 +277,45 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
428
277
  requiresSchema: true,
429
278
  }), validationToRule('known-directives', 'KnownDirectives', {
430
279
  category: ['Schema', 'Operations'],
431
- description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
280
+ description: `A GraphQL document is only valid if all \`@directive\`s are known by the schema and legally positioned.`,
432
281
  requiresSchema: true,
282
+ examples: [
283
+ {
284
+ title: 'Valid',
285
+ usage: [{ ignoreClientDirectives: ['client'] }],
286
+ code: /* GraphQL */ `
287
+ {
288
+ product {
289
+ someClientField @client
290
+ }
291
+ }
292
+ `,
293
+ },
294
+ ],
295
+ }, ({ context, node: documentNode }) => {
296
+ const { ignoreClientDirectives = [] } = context.options[0] || {};
297
+ if (ignoreClientDirectives.length === 0) {
298
+ return documentNode;
299
+ }
300
+ return visit(documentNode, {
301
+ Field(node) {
302
+ return {
303
+ ...node,
304
+ directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
305
+ };
306
+ },
307
+ });
308
+ }, {
309
+ type: 'array',
310
+ maxItems: 1,
311
+ items: {
312
+ type: 'object',
313
+ additionalProperties: false,
314
+ required: ['ignoreClientDirectives'],
315
+ properties: {
316
+ ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
317
+ },
318
+ },
433
319
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
434
320
  category: 'Operations',
435
321
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
@@ -553,8 +439,10 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
553
439
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
554
440
  category: 'Schema',
555
441
  description: `A type extension is only valid if the type is defined and has the same kind.`,
442
+ // TODO: add in graphql-eslint v4
556
443
  recommended: false,
557
444
  requiresSchema: true,
445
+ isDisabledForAllConfig: true,
558
446
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
559
447
  category: ['Schema', 'Operations'],
560
448
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
@@ -582,6 +470,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
582
470
  category: 'Schema',
583
471
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
584
472
  recommended: false,
473
+ isDisabledForAllConfig: true,
585
474
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
586
475
  category: 'Schema',
587
476
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
@@ -612,7 +501,7 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
612
501
  requiresSchema: true,
613
502
  }));
614
503
 
615
- const ALPHABETIZE = 'ALPHABETIZE';
504
+ const RULE_ID = 'alphabetize';
616
505
  const fieldsEnum = [
617
506
  Kind.OBJECT_TYPE_DEFINITION,
618
507
  Kind.INTERFACE_TYPE_DEFINITION,
@@ -637,7 +526,7 @@ const rule = {
637
526
  docs: {
638
527
  category: ['Schema', 'Operations'],
639
528
  description: `Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.`,
640
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
529
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
641
530
  examples: [
642
531
  {
643
532
  title: 'Incorrect',
@@ -720,6 +609,8 @@ const rule = {
720
609
  fields: fieldsEnum,
721
610
  values: valuesEnum,
722
611
  arguments: argumentsEnum,
612
+ // TODO: add in graphql-eslint v4
613
+ // definitions: true,
723
614
  },
724
615
  ],
725
616
  operations: [
@@ -732,7 +623,7 @@ const rule = {
732
623
  },
733
624
  },
734
625
  messages: {
735
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
626
+ [RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
736
627
  },
737
628
  schema: {
738
629
  type: 'array',
@@ -744,49 +635,44 @@ const rule = {
744
635
  minProperties: 1,
745
636
  properties: {
746
637
  fields: {
747
- type: 'array',
748
- uniqueItems: true,
749
- minItems: 1,
638
+ ...ARRAY_DEFAULT_OPTIONS,
750
639
  items: {
751
640
  enum: fieldsEnum,
752
641
  },
753
- description: 'Fields of `type`, `interface`, and `input`',
642
+ description: 'Fields of `type`, `interface`, and `input`.',
754
643
  },
755
644
  values: {
756
- type: 'array',
757
- uniqueItems: true,
758
- minItems: 1,
645
+ ...ARRAY_DEFAULT_OPTIONS,
759
646
  items: {
760
647
  enum: valuesEnum,
761
648
  },
762
- description: 'Values of `enum`',
649
+ description: 'Values of `enum`.',
763
650
  },
764
651
  selections: {
765
- type: 'array',
766
- uniqueItems: true,
767
- minItems: 1,
652
+ ...ARRAY_DEFAULT_OPTIONS,
768
653
  items: {
769
654
  enum: selectionsEnum,
770
655
  },
771
- description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
656
+ description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
772
657
  },
773
658
  variables: {
774
- type: 'array',
775
- uniqueItems: true,
776
- minItems: 1,
659
+ ...ARRAY_DEFAULT_OPTIONS,
777
660
  items: {
778
661
  enum: variablesEnum,
779
662
  },
780
- description: 'Variables of operations (`query`, `mutation` and `subscription`)',
663
+ description: 'Variables of operations `query`, `mutation` and `subscription`.',
781
664
  },
782
665
  arguments: {
783
- type: 'array',
784
- uniqueItems: true,
785
- minItems: 1,
666
+ ...ARRAY_DEFAULT_OPTIONS,
786
667
  items: {
787
668
  enum: argumentsEnum,
788
669
  },
789
- description: 'Arguments of fields and directives',
670
+ description: 'Arguments of fields and directives.',
671
+ },
672
+ definitions: {
673
+ type: 'boolean',
674
+ description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
675
+ default: false,
790
676
  },
791
677
  },
792
678
  },
@@ -807,9 +693,22 @@ const rule = {
807
693
  if (tokenBefore) {
808
694
  return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
809
695
  }
810
- return commentsBefore;
696
+ const filteredComments = [];
697
+ const nodeLine = node.loc.start.line;
698
+ // Break on comment that not attached to node
699
+ for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
700
+ const comment = commentsBefore[i];
701
+ if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
702
+ break;
703
+ }
704
+ filteredComments.unshift(comment);
705
+ }
706
+ return filteredComments;
811
707
  }
812
708
  function getRangeWithComments(node) {
709
+ if (node.kind === Kind.VARIABLE) {
710
+ node = node.parent;
711
+ }
813
712
  const [firstBeforeComment] = getBeforeComments(node);
814
713
  const [firstAfterComment] = sourceCode.getCommentsAfter(node);
815
714
  const from = firstBeforeComment || node;
@@ -817,26 +716,35 @@ const rule = {
817
716
  return [from.range[0], to.range[1]];
818
717
  }
819
718
  function checkNodes(nodes) {
719
+ var _a, _b;
820
720
  // Starts from 1, ignore nodes.length <= 1
821
721
  for (let i = 1; i < nodes.length; i += 1) {
822
- const prevNode = nodes[i - 1];
823
722
  const currNode = nodes[i];
824
- const prevName = prevNode.name.value;
825
- const currName = currNode.name.value;
826
- // Compare with lexicographic order
827
- if (prevName.localeCompare(currName) !== 1) {
723
+ const currName = 'name' in currNode && ((_a = currNode.name) === null || _a === void 0 ? void 0 : _a.value);
724
+ if (!currName) {
725
+ // we don't move unnamed current nodes
828
726
  continue;
829
727
  }
830
- const isVariableNode = currNode.kind === Kind.VARIABLE;
728
+ const prevNode = nodes[i - 1];
729
+ const prevName = 'name' in prevNode && ((_b = prevNode.name) === null || _b === void 0 ? void 0 : _b.value);
730
+ if (prevName) {
731
+ // Compare with lexicographic order
732
+ const compareResult = prevName.localeCompare(currName);
733
+ const shouldSort = compareResult === 1;
734
+ if (!shouldSort) {
735
+ const isSameName = compareResult === 0;
736
+ if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
737
+ continue;
738
+ }
739
+ }
740
+ }
831
741
  context.report({
832
742
  node: currNode.name,
833
- messageId: ALPHABETIZE,
834
- data: isVariableNode
835
- ? {
836
- currName: `$${currName}`,
837
- prevName: `$${prevName}`,
838
- }
839
- : { currName, prevName },
743
+ messageId: RULE_ID,
744
+ data: {
745
+ currName,
746
+ prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
747
+ },
840
748
  *fix(fixer) {
841
749
  const prevRange = getRangeWithComments(prevNode);
842
750
  const currRange = getRangeWithComments(currNode);
@@ -877,10 +785,7 @@ const rule = {
877
785
  }
878
786
  if (selectionsSelector) {
879
787
  listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => {
880
- checkNodes(node.selections
881
- // inline fragment don't have name, so we skip them
882
- .filter(selection => selection.kind !== Kind.INLINE_FRAGMENT)
883
- .map(selection =>
788
+ checkNodes(node.selections.map(selection =>
884
789
  // sort by alias is field is renamed
885
790
  'alias' in selection && selection.alias ? { name: selection.alias } : selection));
886
791
  };
@@ -895,6 +800,11 @@ const rule = {
895
800
  checkNodes(node.arguments);
896
801
  };
897
802
  }
803
+ if (opts.definitions) {
804
+ listeners.Document = node => {
805
+ checkNodes(node.definitions);
806
+ };
807
+ }
898
808
  return listeners;
899
809
  },
900
810
  };
@@ -902,6 +812,7 @@ const rule = {
902
812
  const rule$1 = {
903
813
  meta: {
904
814
  type: 'suggestion',
815
+ hasSuggestions: true,
905
816
  docs: {
906
817
  examples: [
907
818
  {
@@ -949,8 +860,21 @@ const rule$1 = {
949
860
  return {
950
861
  [`.description[type=StringValue][block!=${isBlock}]`](node) {
951
862
  context.report({
952
- loc: getLocation(node.loc),
953
- message: `Unexpected ${isBlock ? 'inline' : 'block'} description`,
863
+ loc: isBlock ? node.loc : node.loc.start,
864
+ message: `Unexpected ${isBlock ? 'inline' : 'block'} description.`,
865
+ suggest: [
866
+ {
867
+ desc: `Change to ${isBlock ? 'block' : 'inline'} style description`,
868
+ fix(fixer) {
869
+ const sourceCode = context.getSourceCode();
870
+ const originalText = sourceCode.getText(node);
871
+ const newText = isBlock
872
+ ? originalText.replace(/(^")|("$)/g, '"""')
873
+ : originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, ' ');
874
+ return fixer.replaceText(node, newText);
875
+ },
876
+ },
877
+ ],
954
878
  });
955
879
  },
956
880
  };
@@ -963,6 +887,7 @@ const isMutationType = (node) => isObjectType(node) && node.name.value === 'Muta
963
887
  const rule$2 = {
964
888
  meta: {
965
889
  type: 'suggestion',
890
+ hasSuggestions: true,
966
891
  docs: {
967
892
  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.',
968
893
  category: 'Schema',
@@ -1036,12 +961,18 @@ const rule$2 = {
1036
961
  };
1037
962
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1038
963
  const listeners = {
1039
- 'FieldDefinition > InputValueDefinition[name.value!=input]'(node) {
1040
- if (shouldCheckType(node.parent.parent)) {
1041
- const name = node.name.value;
964
+ 'FieldDefinition > InputValueDefinition[name.value!=input] > Name'(node) {
965
+ if (shouldCheckType(node.parent.parent.parent)) {
966
+ const inputName = node.value;
1042
967
  context.report({
1043
- node: node.name,
1044
- message: `Input "${name}" should be called "input"`,
968
+ node,
969
+ message: `Input \`${inputName}\` should be called \`input\`.`,
970
+ suggest: [
971
+ {
972
+ desc: 'Rename to `input`',
973
+ fix: fixer => fixer.replaceText(node, 'input'),
974
+ },
975
+ ],
1045
976
  });
1046
977
  }
1047
978
  },
@@ -1063,7 +994,13 @@ const rule$2 = {
1063
994
  name.toLowerCase() !== mutationName.toLowerCase()) {
1064
995
  context.report({
1065
996
  node: node.name,
1066
- message: `InputType "${name}" name should be "${mutationName}"`,
997
+ message: `Input type \`${name}\` name should be \`${mutationName}\`.`,
998
+ suggest: [
999
+ {
1000
+ desc: `Rename to \`${mutationName}\``,
1001
+ fix: fixer => fixer.replaceText(node, mutationName),
1002
+ },
1003
+ ],
1067
1004
  });
1068
1005
  }
1069
1006
  }
@@ -1076,7 +1013,14 @@ const rule$2 = {
1076
1013
  const MATCH_EXTENSION = 'MATCH_EXTENSION';
1077
1014
  const MATCH_STYLE = 'MATCH_STYLE';
1078
1015
  const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
1079
- const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case', 'matchDocumentStyle'];
1016
+ const CASE_STYLES = [
1017
+ 'camelCase',
1018
+ 'PascalCase',
1019
+ 'snake_case',
1020
+ 'UPPER_CASE',
1021
+ 'kebab-case',
1022
+ 'matchDocumentStyle',
1023
+ ];
1080
1024
  const schemaOption = {
1081
1025
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1082
1026
  };
@@ -1221,8 +1165,7 @@ const rule$3 = {
1221
1165
  var _a;
1222
1166
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1223
1167
  context.report({
1224
- // Report on first character
1225
- loc: { column: 0, line: 1 },
1168
+ loc: REPORT_ON_FIRST_CHARACTER,
1226
1169
  messageId: MATCH_EXTENSION,
1227
1170
  data: {
1228
1171
  fileExtension,
@@ -1261,8 +1204,7 @@ const rule$3 = {
1261
1204
  const filenameWithExtension = filename + expectedExtension;
1262
1205
  if (expectedFilename !== filenameWithExtension) {
1263
1206
  context.report({
1264
- // Report on first character
1265
- loc: { column: 0, line: 1 },
1207
+ loc: REPORT_ON_FIRST_CHARACTER,
1266
1208
  messageId: MATCH_STYLE,
1267
1209
  data: {
1268
1210
  expectedFilename,
@@ -1434,18 +1376,8 @@ const rule$4 = {
1434
1376
  style: { enum: ALLOWED_STYLES },
1435
1377
  prefix: { type: 'string' },
1436
1378
  suffix: { type: 'string' },
1437
- forbiddenPrefixes: {
1438
- type: 'array',
1439
- uniqueItems: true,
1440
- minItems: 1,
1441
- items: { type: 'string' },
1442
- },
1443
- forbiddenSuffixes: {
1444
- type: 'array',
1445
- uniqueItems: true,
1446
- minItems: 1,
1447
- items: { type: 'string' },
1448
- },
1379
+ forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
1380
+ forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
1449
1381
  ignorePattern: {
1450
1382
  type: 'string',
1451
1383
  description: 'Option to skip validation of some words, e.g. acronyms',
@@ -1499,6 +1431,18 @@ const rule$4 = {
1499
1431
  const style = restOptions[kind] || types;
1500
1432
  return typeof style === 'object' ? style : { style };
1501
1433
  }
1434
+ function report(node, message, suggestedName) {
1435
+ context.report({
1436
+ node,
1437
+ message,
1438
+ suggest: [
1439
+ {
1440
+ desc: `Rename to \`${suggestedName}\``,
1441
+ fix: fixer => fixer.replaceText(node, suggestedName),
1442
+ },
1443
+ ],
1444
+ });
1445
+ }
1502
1446
  const checkNode = (selector) => (n) => {
1503
1447
  const { name: node } = n.kind === Kind.VARIABLE_DEFINITION ? n.variable : n;
1504
1448
  if (!node) {
@@ -1513,16 +1457,7 @@ const rule$4 = {
1513
1457
  const [leadingUnderscores] = nodeName.match(/^_*/);
1514
1458
  const [trailingUnderscores] = nodeName.match(/_*$/);
1515
1459
  const suggestedName = leadingUnderscores + renameToName + trailingUnderscores;
1516
- context.report({
1517
- node,
1518
- message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1519
- suggest: [
1520
- {
1521
- desc: `Rename to "${suggestedName}"`,
1522
- fix: fixer => fixer.replaceText(node, suggestedName),
1523
- },
1524
- ],
1525
- });
1460
+ report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedName);
1526
1461
  }
1527
1462
  function getError() {
1528
1463
  const name = nodeName.replace(/(^_+)|(_+$)/g, '');
@@ -1569,18 +1504,8 @@ const rule$4 = {
1569
1504
  }
1570
1505
  };
1571
1506
  const checkUnderscore = (isLeading) => (node) => {
1572
- const name = node.value;
1573
- const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
1574
- context.report({
1575
- node,
1576
- message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
1577
- suggest: [
1578
- {
1579
- desc: `Rename to "${renameToName}"`,
1580
- fix: fixer => fixer.replaceText(node, renameToName),
1581
- },
1582
- ],
1583
- });
1507
+ const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, '');
1508
+ report(node, `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, suggestedName);
1584
1509
  };
1585
1510
  const listeners = {};
1586
1511
  if (!allowLeadingUnderscore) {
@@ -1599,15 +1524,16 @@ const rule$4 = {
1599
1524
  },
1600
1525
  };
1601
1526
 
1602
- const NO_ANONYMOUS_OPERATIONS = 'NO_ANONYMOUS_OPERATIONS';
1527
+ const RULE_ID$1 = 'no-anonymous-operations';
1603
1528
  const rule$5 = {
1604
1529
  meta: {
1605
1530
  type: 'suggestion',
1531
+ hasSuggestions: true,
1606
1532
  docs: {
1607
1533
  category: 'Operations',
1608
1534
  description: 'Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.',
1609
1535
  recommended: true,
1610
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-anonymous-operations.md',
1536
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
1611
1537
  examples: [
1612
1538
  {
1613
1539
  title: 'Incorrect',
@@ -1628,19 +1554,27 @@ const rule$5 = {
1628
1554
  ],
1629
1555
  },
1630
1556
  messages: {
1631
- [NO_ANONYMOUS_OPERATIONS]: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`,
1557
+ [RULE_ID$1]: `Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!`,
1632
1558
  },
1633
1559
  schema: [],
1634
1560
  },
1635
1561
  create(context) {
1636
1562
  return {
1637
1563
  'OperationDefinition[name=undefined]'(node) {
1564
+ const [firstSelection] = node.selectionSet.selections;
1565
+ const suggestedName = firstSelection.type === Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;
1638
1566
  context.report({
1639
- loc: getLocation(node.loc, node.operation),
1567
+ loc: getLocation(node.loc.start, node.operation),
1568
+ messageId: RULE_ID$1,
1640
1569
  data: {
1641
1570
  operation: node.operation,
1642
1571
  },
1643
- messageId: NO_ANONYMOUS_OPERATIONS,
1572
+ suggest: [
1573
+ {
1574
+ desc: `Rename to \`${suggestedName}\``,
1575
+ fix: fixer => fixer.insertTextAfterRange([node.range[0], node.range[0] + node.operation.length], ` ${suggestedName}`),
1576
+ },
1577
+ ],
1644
1578
  });
1645
1579
  },
1646
1580
  };
@@ -1650,6 +1584,7 @@ const rule$5 = {
1650
1584
  const rule$6 = {
1651
1585
  meta: {
1652
1586
  type: 'suggestion',
1587
+ hasSuggestions: true,
1653
1588
  docs: {
1654
1589
  url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-case-insensitive-enum-values-duplicates.md`,
1655
1590
  category: 'Schema',
@@ -1689,7 +1624,13 @@ const rule$6 = {
1689
1624
  const enumName = duplicate.name.value;
1690
1625
  context.report({
1691
1626
  node: duplicate.name,
1692
- message: `Case-insensitive enum values duplicates are not allowed! Found: "${enumName}"`,
1627
+ message: `Case-insensitive enum values duplicates are not allowed! Found: \`${enumName}\`.`,
1628
+ suggest: [
1629
+ {
1630
+ desc: `Remove \`${enumName}\` enum value`,
1631
+ fix: fixer => fixer.remove(duplicate),
1632
+ },
1633
+ ],
1693
1634
  });
1694
1635
  }
1695
1636
  },
@@ -1697,14 +1638,15 @@ const rule$6 = {
1697
1638
  },
1698
1639
  };
1699
1640
 
1700
- const NO_DEPRECATED = 'NO_DEPRECATED';
1641
+ const RULE_ID$2 = 'no-deprecated';
1701
1642
  const rule$7 = {
1702
1643
  meta: {
1703
1644
  type: 'suggestion',
1645
+ hasSuggestions: true,
1704
1646
  docs: {
1705
1647
  category: 'Operations',
1706
1648
  description: `Enforce that deprecated fields or enum values are not in use by operations.`,
1707
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-deprecated.md`,
1649
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
1708
1650
  requiresSchema: true,
1709
1651
  examples: [
1710
1652
  {
@@ -1771,56 +1713,60 @@ const rule$7 = {
1771
1713
  recommended: true,
1772
1714
  },
1773
1715
  messages: {
1774
- [NO_DEPRECATED]: `This {{ type }} is marked as deprecated in your GraphQL schema {{ reason }}`,
1716
+ [RULE_ID$2]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
1775
1717
  },
1776
1718
  schema: [],
1777
1719
  },
1778
1720
  create(context) {
1721
+ requireGraphQLSchemaFromContext(RULE_ID$2, context);
1722
+ function report(node, reason) {
1723
+ const nodeName = node.type === Kind.ENUM ? node.value : node.name.value;
1724
+ const nodeType = node.type === Kind.ENUM ? 'enum value' : 'field';
1725
+ context.report({
1726
+ node,
1727
+ messageId: RULE_ID$2,
1728
+ data: {
1729
+ type: nodeType,
1730
+ reason,
1731
+ },
1732
+ suggest: [
1733
+ {
1734
+ desc: `Remove \`${nodeName}\` ${nodeType}`,
1735
+ fix: fixer => fixer.remove(node),
1736
+ },
1737
+ ],
1738
+ });
1739
+ }
1779
1740
  return {
1780
1741
  EnumValue(node) {
1781
- requireGraphQLSchemaFromContext('no-deprecated', context);
1742
+ var _a;
1782
1743
  const typeInfo = node.typeInfo();
1783
- if (typeInfo && typeInfo.enumValue) {
1784
- if (typeInfo.enumValue.deprecationReason) {
1785
- context.report({
1786
- node,
1787
- messageId: NO_DEPRECATED,
1788
- data: {
1789
- type: 'enum value',
1790
- reason: typeInfo.enumValue.deprecationReason ? `(reason: ${typeInfo.enumValue.deprecationReason})` : '',
1791
- },
1792
- });
1793
- }
1744
+ const reason = (_a = typeInfo.enumValue) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1745
+ if (reason) {
1746
+ report(node, reason);
1794
1747
  }
1795
1748
  },
1796
1749
  Field(node) {
1797
- requireGraphQLSchemaFromContext('no-deprecated', context);
1750
+ var _a;
1798
1751
  const typeInfo = node.typeInfo();
1799
- if (typeInfo && typeInfo.fieldDef) {
1800
- if (typeInfo.fieldDef.deprecationReason) {
1801
- context.report({
1802
- node: node.name,
1803
- messageId: NO_DEPRECATED,
1804
- data: {
1805
- type: 'field',
1806
- reason: typeInfo.fieldDef.deprecationReason ? `(reason: ${typeInfo.fieldDef.deprecationReason})` : '',
1807
- },
1808
- });
1809
- }
1752
+ const reason = (_a = typeInfo.fieldDef) === null || _a === void 0 ? void 0 : _a.deprecationReason;
1753
+ if (reason) {
1754
+ report(node, reason);
1810
1755
  }
1811
1756
  },
1812
1757
  };
1813
1758
  },
1814
1759
  };
1815
1760
 
1816
- const NO_DUPLICATE_FIELDS = 'NO_DUPLICATE_FIELDS';
1761
+ const RULE_ID$3 = 'no-duplicate-fields';
1817
1762
  const rule$8 = {
1818
1763
  meta: {
1819
1764
  type: 'suggestion',
1765
+ hasSuggestions: true,
1820
1766
  docs: {
1821
1767
  description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
1822
1768
  category: 'Operations',
1823
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-duplicate-fields.md',
1769
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
1824
1770
  recommended: true,
1825
1771
  examples: [
1826
1772
  {
@@ -1866,21 +1812,30 @@ const rule$8 = {
1866
1812
  ],
1867
1813
  },
1868
1814
  messages: {
1869
- [NO_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
1815
+ [RULE_ID$3]: '{{ type }} `{{ fieldName }}` defined multiple times.',
1870
1816
  },
1871
1817
  schema: [],
1872
1818
  },
1873
1819
  create(context) {
1874
- function checkNode(usedFields, type, node) {
1820
+ function checkNode(usedFields, node) {
1875
1821
  const fieldName = node.value;
1876
1822
  if (usedFields.has(fieldName)) {
1823
+ const { parent } = node;
1877
1824
  context.report({
1878
1825
  node,
1879
- messageId: NO_DUPLICATE_FIELDS,
1826
+ messageId: RULE_ID$3,
1880
1827
  data: {
1881
- type,
1828
+ type: parent.type,
1882
1829
  fieldName,
1883
1830
  },
1831
+ suggest: [
1832
+ {
1833
+ desc: `Remove \`${fieldName}\` ${parent.type.toLowerCase()}`,
1834
+ fix(fixer) {
1835
+ return fixer.remove(parent.type === Kind.VARIABLE ? parent.parent : parent);
1836
+ },
1837
+ },
1838
+ ],
1884
1839
  });
1885
1840
  }
1886
1841
  else {
@@ -1891,20 +1846,20 @@ const rule$8 = {
1891
1846
  OperationDefinition(node) {
1892
1847
  const set = new Set();
1893
1848
  for (const varDef of node.variableDefinitions) {
1894
- checkNode(set, 'Operation variable', varDef.variable.name);
1849
+ checkNode(set, varDef.variable.name);
1895
1850
  }
1896
1851
  },
1897
1852
  Field(node) {
1898
1853
  const set = new Set();
1899
1854
  for (const arg of node.arguments) {
1900
- checkNode(set, 'Field argument', arg.name);
1855
+ checkNode(set, arg.name);
1901
1856
  }
1902
1857
  },
1903
1858
  SelectionSet(node) {
1904
1859
  const set = new Set();
1905
1860
  for (const selection of node.selections) {
1906
1861
  if (selection.kind === Kind.FIELD) {
1907
- checkNode(set, 'Field', selection.alias || selection.name);
1862
+ checkNode(set, selection.alias || selection.name);
1908
1863
  }
1909
1864
  }
1910
1865
  },
@@ -1915,8 +1870,11 @@ const rule$8 = {
1915
1870
  const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
1916
1871
  const rule$9 = {
1917
1872
  meta: {
1873
+ type: 'suggestion',
1874
+ hasSuggestions: true,
1875
+ schema: [],
1918
1876
  messages: {
1919
- [HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
1877
+ [HASHTAG_COMMENT]: 'Using hashtag `#` for adding GraphQL descriptions is not allowed. Prefer using `"""` for multiline, or `"` for a single line description.',
1920
1878
  },
1921
1879
  docs: {
1922
1880
  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.',
@@ -1959,8 +1917,6 @@ const rule$9 = {
1959
1917
  ],
1960
1918
  recommended: true,
1961
1919
  },
1962
- type: 'suggestion',
1963
- schema: [],
1964
1920
  },
1965
1921
  create(context) {
1966
1922
  const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
@@ -1968,7 +1924,7 @@ const rule$9 = {
1968
1924
  [selector](node) {
1969
1925
  const rawNode = node.rawNode();
1970
1926
  let token = rawNode.loc.startToken;
1971
- while (token !== null) {
1927
+ while (token) {
1972
1928
  const { kind, prev, next, value, line, column } = token;
1973
1929
  if (kind === TokenKind.COMMENT && prev && next) {
1974
1930
  const isEslintComment = value.trimStart().startsWith('eslint');
@@ -1980,6 +1936,10 @@ const rule$9 = {
1980
1936
  line,
1981
1937
  column: column - 1,
1982
1938
  },
1939
+ suggest: ['"""', '"'].map(descriptionSyntax => ({
1940
+ desc: `Replace with \`${descriptionSyntax}\` description syntax`,
1941
+ fix: fixer => fixer.replaceTextRange([token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join('')),
1942
+ })),
1983
1943
  });
1984
1944
  }
1985
1945
  }
@@ -1994,11 +1954,13 @@ const ROOT_TYPES = ['mutation', 'subscription'];
1994
1954
  const rule$a = {
1995
1955
  meta: {
1996
1956
  type: 'suggestion',
1957
+ hasSuggestions: true,
1997
1958
  docs: {
1998
1959
  category: 'Schema',
1999
1960
  description: 'Disallow using root types `mutation` and/or `subscription`.',
2000
1961
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md',
2001
1962
  requiresSchema: true,
1963
+ isDisabledForAllConfig: true,
2002
1964
  examples: [
2003
1965
  {
2004
1966
  title: 'Incorrect',
@@ -2030,9 +1992,7 @@ const rule$a = {
2030
1992
  required: ['disallow'],
2031
1993
  properties: {
2032
1994
  disallow: {
2033
- type: 'array',
2034
- uniqueItems: true,
2035
- minItems: 1,
1995
+ ...ARRAY_DEFAULT_OPTIONS,
2036
1996
  items: {
2037
1997
  enum: ROOT_TYPES,
2038
1998
  },
@@ -2054,29 +2014,37 @@ const rule$a = {
2054
2014
  return {};
2055
2015
  }
2056
2016
  const selector = [
2057
- `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})`,
2017
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)`,
2058
2018
  '>',
2059
- `${Kind.NAME}[value=/^(${rootTypeNames.join('|')})$/]`,
2019
+ `Name[value=/^(${rootTypeNames.join('|')})$/]`,
2060
2020
  ].join(' ');
2061
2021
  return {
2062
2022
  [selector](node) {
2063
2023
  const typeName = node.value;
2064
2024
  context.report({
2065
2025
  node,
2066
- message: `Root type "${typeName}" is forbidden`,
2026
+ message: `Root type \`${typeName}\` is forbidden.`,
2027
+ suggest: [
2028
+ {
2029
+ desc: `Remove \`${typeName}\` type`,
2030
+ fix: fixer => fixer.remove(node.parent),
2031
+ },
2032
+ ],
2067
2033
  });
2068
2034
  },
2069
2035
  };
2070
2036
  },
2071
2037
  };
2072
2038
 
2039
+ const RULE_ID$4 = 'no-scalar-result-type-on-mutation';
2073
2040
  const rule$b = {
2074
2041
  meta: {
2075
2042
  type: 'suggestion',
2043
+ hasSuggestions: true,
2076
2044
  docs: {
2077
2045
  category: 'Schema',
2078
2046
  description: 'Avoid scalar result type on mutation type to make sure to return a valid state.',
2079
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-scalar-result-type-on-mutation.md',
2047
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
2080
2048
  requiresSchema: true,
2081
2049
  examples: [
2082
2050
  {
@@ -2100,14 +2068,14 @@ const rule$b = {
2100
2068
  schema: [],
2101
2069
  },
2102
2070
  create(context) {
2103
- const schema = requireGraphQLSchemaFromContext('no-scalar-result-type-on-mutation', context);
2071
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$4, context);
2104
2072
  const mutationType = schema.getMutationType();
2105
2073
  if (!mutationType) {
2106
2074
  return {};
2107
2075
  }
2108
2076
  const selector = [
2109
- `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2110
- `> ${Kind.FIELD_DEFINITION} > .gqlType ${Kind.NAME}`,
2077
+ `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
2078
+ '> FieldDefinition > .gqlType Name',
2111
2079
  ].join(' ');
2112
2080
  return {
2113
2081
  [selector](node) {
@@ -2116,7 +2084,13 @@ const rule$b = {
2116
2084
  if (isScalarType(graphQLType)) {
2117
2085
  context.report({
2118
2086
  node,
2119
- message: `Unexpected scalar result type "${typeName}"`,
2087
+ message: `Unexpected scalar result type \`${typeName}\`.`,
2088
+ suggest: [
2089
+ {
2090
+ desc: `Remove \`${typeName}\``,
2091
+ fix: fixer => fixer.remove(node),
2092
+ },
2093
+ ],
2120
2094
  });
2121
2095
  }
2122
2096
  },
@@ -2128,6 +2102,7 @@ const NO_TYPENAME_PREFIX = 'NO_TYPENAME_PREFIX';
2128
2102
  const rule$c = {
2129
2103
  meta: {
2130
2104
  type: 'suggestion',
2105
+ hasSuggestions: true,
2131
2106
  docs: {
2132
2107
  category: 'Schema',
2133
2108
  description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
@@ -2172,6 +2147,12 @@ const rule$c = {
2172
2147
  },
2173
2148
  messageId: NO_TYPENAME_PREFIX,
2174
2149
  node: field.name,
2150
+ suggest: [
2151
+ {
2152
+ desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
2153
+ fix: fixer => fixer.replaceText(field.name, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
2154
+ },
2155
+ ],
2175
2156
  });
2176
2157
  }
2177
2158
  }
@@ -2180,8 +2161,7 @@ const rule$c = {
2180
2161
  },
2181
2162
  };
2182
2163
 
2183
- const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
2184
- const RULE_ID = 'no-unreachable-types';
2164
+ const RULE_ID$5 = 'no-unreachable-types';
2185
2165
  const KINDS = [
2186
2166
  Kind.DIRECTIVE_DEFINITION,
2187
2167
  Kind.OBJECT_TYPE_DEFINITION,
@@ -2197,36 +2177,85 @@ const KINDS = [
2197
2177
  Kind.ENUM_TYPE_DEFINITION,
2198
2178
  Kind.ENUM_TYPE_EXTENSION,
2199
2179
  ];
2200
- const rule$d = {
2201
- meta: {
2202
- messages: {
2203
- [UNREACHABLE_TYPE]: 'Type "{{ typeName }}" is unreachable',
2204
- },
2205
- docs: {
2206
- description: `Requires all types to be reachable at some level by root level fields.`,
2207
- category: 'Schema',
2208
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
2209
- requiresSchema: true,
2210
- examples: [
2211
- {
2212
- title: 'Incorrect',
2213
- code: /* GraphQL */ `
2214
- type User {
2215
- id: ID!
2216
- name: String
2217
- }
2218
-
2219
- type Query {
2220
- me: String
2180
+ let reachableTypesCache;
2181
+ function getReachableTypes(schema) {
2182
+ // We don't want cache reachableTypes on test environment
2183
+ // Otherwise reachableTypes will be same for all tests
2184
+ if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
2185
+ return reachableTypesCache;
2186
+ }
2187
+ const reachableTypes = new Set();
2188
+ const collect = (node) => {
2189
+ const typeName = getTypeName(node);
2190
+ if (reachableTypes.has(typeName)) {
2191
+ return;
2192
+ }
2193
+ reachableTypes.add(typeName);
2194
+ const type = schema.getType(typeName) || schema.getDirective(typeName);
2195
+ if (isInterfaceType(type)) {
2196
+ const { objects, interfaces } = schema.getImplementations(type);
2197
+ for (const { astNode } of [...objects, ...interfaces]) {
2198
+ visit(astNode, visitor);
2221
2199
  }
2222
- `,
2223
- },
2224
- {
2225
- title: 'Correct',
2226
- code: /* GraphQL */ `
2227
- type User {
2228
- id: ID!
2229
- name: String
2200
+ }
2201
+ else if (type.astNode) {
2202
+ // astNode can be undefined for ID, String, Boolean
2203
+ visit(type.astNode, visitor);
2204
+ }
2205
+ };
2206
+ const visitor = {
2207
+ InterfaceTypeDefinition: collect,
2208
+ ObjectTypeDefinition: collect,
2209
+ InputValueDefinition: collect,
2210
+ UnionTypeDefinition: collect,
2211
+ FieldDefinition: collect,
2212
+ Directive: collect,
2213
+ NamedType: collect,
2214
+ };
2215
+ for (const type of [
2216
+ schema,
2217
+ schema.getQueryType(),
2218
+ schema.getMutationType(),
2219
+ schema.getSubscriptionType(),
2220
+ ]) {
2221
+ // if schema don't have Query type, schema.astNode will be undefined
2222
+ if (type === null || type === void 0 ? void 0 : type.astNode) {
2223
+ visit(type.astNode, visitor);
2224
+ }
2225
+ }
2226
+ reachableTypesCache = reachableTypes;
2227
+ return reachableTypesCache;
2228
+ }
2229
+ const rule$d = {
2230
+ meta: {
2231
+ messages: {
2232
+ [RULE_ID$5]: '{{ type }} `{{ typeName }}` is unreachable.',
2233
+ },
2234
+ docs: {
2235
+ description: `Requires all types to be reachable at some level by root level fields.`,
2236
+ category: 'Schema',
2237
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
2238
+ requiresSchema: true,
2239
+ examples: [
2240
+ {
2241
+ title: 'Incorrect',
2242
+ code: /* GraphQL */ `
2243
+ type User {
2244
+ id: ID!
2245
+ name: String
2246
+ }
2247
+
2248
+ type Query {
2249
+ me: String
2250
+ }
2251
+ `,
2252
+ },
2253
+ {
2254
+ title: 'Correct',
2255
+ code: /* GraphQL */ `
2256
+ type User {
2257
+ id: ID!
2258
+ name: String
2230
2259
  }
2231
2260
 
2232
2261
  type Query {
@@ -2242,19 +2271,24 @@ const rule$d = {
2242
2271
  hasSuggestions: true,
2243
2272
  },
2244
2273
  create(context) {
2245
- const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
2274
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
2275
+ const reachableTypes = getReachableTypes(schema);
2246
2276
  const selector = KINDS.join(',');
2247
2277
  return {
2248
2278
  [selector](node) {
2249
2279
  const typeName = node.name.value;
2250
2280
  if (!reachableTypes.has(typeName)) {
2281
+ const type = lowerCase(node.kind.replace(/(Extension|Definition)$/, ''));
2251
2282
  context.report({
2252
2283
  node: node.name,
2253
- messageId: UNREACHABLE_TYPE,
2254
- data: { typeName },
2284
+ messageId: RULE_ID$5,
2285
+ data: {
2286
+ type: type[0].toUpperCase() + type.slice(1),
2287
+ typeName
2288
+ },
2255
2289
  suggest: [
2256
2290
  {
2257
- desc: `Remove ${typeName}`,
2291
+ desc: `Remove \`${typeName}\``,
2258
2292
  fix: fixer => fixer.remove(node),
2259
2293
  },
2260
2294
  ],
@@ -2265,19 +2299,49 @@ const rule$d = {
2265
2299
  },
2266
2300
  };
2267
2301
 
2268
- const UNUSED_FIELD = 'UNUSED_FIELD';
2269
- const RULE_ID$1 = 'no-unused-fields';
2302
+ const RULE_ID$6 = 'no-unused-fields';
2303
+ let usedFieldsCache;
2304
+ function getUsedFields(schema, operations) {
2305
+ // We don't want cache usedFields on test environment
2306
+ // Otherwise usedFields will be same for all tests
2307
+ if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
2308
+ return usedFieldsCache;
2309
+ }
2310
+ const usedFields = Object.create(null);
2311
+ const typeInfo = new TypeInfo(schema);
2312
+ const visitor = visitWithTypeInfo(typeInfo, {
2313
+ Field(node) {
2314
+ var _a;
2315
+ const fieldDef = typeInfo.getFieldDef();
2316
+ if (!fieldDef) {
2317
+ // skip visiting this node if field is not defined in schema
2318
+ return false;
2319
+ }
2320
+ const parentTypeName = typeInfo.getParentType().name;
2321
+ const fieldName = node.name.value;
2322
+ (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
2323
+ usedFields[parentTypeName].add(fieldName);
2324
+ },
2325
+ });
2326
+ const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
2327
+ for (const { document } of allDocuments) {
2328
+ visit(document, visitor);
2329
+ }
2330
+ usedFieldsCache = usedFields;
2331
+ return usedFieldsCache;
2332
+ }
2270
2333
  const rule$e = {
2271
2334
  meta: {
2272
2335
  messages: {
2273
- [UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
2336
+ [RULE_ID$6]: `Field "{{fieldName}}" is unused`,
2274
2337
  },
2275
2338
  docs: {
2276
2339
  description: `Requires all fields to be used at some level by siblings operations.`,
2277
2340
  category: 'Schema',
2278
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
2341
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$6}.md`,
2279
2342
  requiresSiblings: true,
2280
2343
  requiresSchema: true,
2344
+ isDisabledForAllConfig: true,
2281
2345
  examples: [
2282
2346
  {
2283
2347
  title: 'Incorrect',
@@ -2327,7 +2391,9 @@ const rule$e = {
2327
2391
  hasSuggestions: true,
2328
2392
  },
2329
2393
  create(context) {
2330
- const usedFields = requireUsedFieldsFromContext(RULE_ID$1, context);
2394
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$6, context);
2395
+ const siblingsOperations = requireSiblingsOperations(RULE_ID$6, context);
2396
+ const usedFields = getUsedFields(schema, siblingsOperations);
2331
2397
  return {
2332
2398
  FieldDefinition(node) {
2333
2399
  var _a;
@@ -2339,11 +2405,11 @@ const rule$e = {
2339
2405
  }
2340
2406
  context.report({
2341
2407
  node: node.name,
2342
- messageId: UNUSED_FIELD,
2408
+ messageId: RULE_ID$6,
2343
2409
  data: { fieldName },
2344
2410
  suggest: [
2345
2411
  {
2346
- desc: `Remove "${fieldName}" field`,
2412
+ desc: `Remove \`${fieldName}\` field`,
2347
2413
  fix(fixer) {
2348
2414
  const sourceCode = context.getSourceCode();
2349
2415
  const tokenBefore = sourceCode.getTokenBefore(node);
@@ -2359,32 +2425,9 @@ const rule$e = {
2359
2425
  },
2360
2426
  };
2361
2427
 
2362
- function keyValMap(list, keyFn, valFn) {
2363
- return list.reduce((map, item) => {
2364
- map[keyFn(item)] = valFn(item);
2365
- return map;
2366
- }, Object.create(null));
2367
- }
2368
- function valueFromNode(valueNode, variables) {
2369
- switch (valueNode.type) {
2370
- case Kind.NULL:
2371
- return null;
2372
- case Kind.INT:
2373
- return parseInt(valueNode.value, 10);
2374
- case Kind.FLOAT:
2375
- return parseFloat(valueNode.value);
2376
- case Kind.STRING:
2377
- case Kind.ENUM:
2378
- case Kind.BOOLEAN:
2379
- return valueNode.value;
2380
- case Kind.LIST:
2381
- return valueNode.values.map(node => valueFromNode(node, variables));
2382
- case Kind.OBJECT:
2383
- return keyValMap(valueNode.fields, field => field.name.value, field => valueFromNode(field.value, variables));
2384
- case Kind.VARIABLE:
2385
- return variables === null || variables === void 0 ? void 0 : variables[valueNode.name.value];
2386
- }
2387
- }
2428
+ const valueFromNode = (...args) => {
2429
+ return valueFromASTUntyped(...args);
2430
+ };
2388
2431
  function getBaseType(type) {
2389
2432
  if (isNonNullType(type) || isListType(type)) {
2390
2433
  return getBaseType(type.ofType);
@@ -2454,21 +2497,93 @@ function extractCommentsFromAst(loc) {
2454
2497
  }
2455
2498
  return comments;
2456
2499
  }
2457
- function isNodeWithDescription(obj) {
2458
- var _a;
2459
- return (_a = obj) === null || _a === void 0 ? void 0 : _a.description;
2500
+
2501
+ function convertToESTree(node, typeInfo) {
2502
+ const visitor = { leave: convertNode(typeInfo) };
2503
+ return {
2504
+ rootTree: visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor),
2505
+ comments: extractCommentsFromAst(node.loc),
2506
+ };
2507
+ }
2508
+ function hasTypeField(node) {
2509
+ return 'type' in node && Boolean(node.type);
2510
+ }
2511
+ function convertLocation(location) {
2512
+ const { startToken, endToken, source, start, end } = location;
2513
+ /*
2514
+ * ESLint has 0-based column number
2515
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
2516
+ */
2517
+ const loc = {
2518
+ start: {
2519
+ /*
2520
+ * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
2521
+ */
2522
+ line: startToken.line === 0 ? 1 : startToken.line,
2523
+ column: startToken.column === 0 ? 0 : startToken.column - 1,
2524
+ },
2525
+ end: {
2526
+ line: endToken.line,
2527
+ column: endToken.column - 1,
2528
+ },
2529
+ source: source.body,
2530
+ };
2531
+ if (loc.start.column === loc.end.column) {
2532
+ loc.end.column += end - start;
2533
+ }
2534
+ return loc;
2460
2535
  }
2461
- function convertDescription(node) {
2462
- if (isNodeWithDescription(node)) {
2463
- return [
2536
+ const convertNode = (typeInfo) => (node, key, parent) => {
2537
+ const leadingComments = 'description' in node && node.description
2538
+ ? [
2464
2539
  {
2465
2540
  type: node.description.block ? 'Block' : 'Line',
2466
2541
  value: node.description.value,
2467
2542
  },
2468
- ];
2469
- }
2470
- return [];
2471
- }
2543
+ ]
2544
+ : [];
2545
+ const calculatedTypeInfo = typeInfo
2546
+ ? {
2547
+ argument: typeInfo.getArgument(),
2548
+ defaultValue: typeInfo.getDefaultValue(),
2549
+ directive: typeInfo.getDirective(),
2550
+ enumValue: typeInfo.getEnumValue(),
2551
+ fieldDef: typeInfo.getFieldDef(),
2552
+ inputType: typeInfo.getInputType(),
2553
+ parentInputType: typeInfo.getParentInputType(),
2554
+ parentType: typeInfo.getParentType(),
2555
+ gqlType: typeInfo.getType(),
2556
+ }
2557
+ : {};
2558
+ const rawNode = () => {
2559
+ if (parent && key !== undefined) {
2560
+ return parent[key];
2561
+ }
2562
+ return node.kind === Kind.DOCUMENT
2563
+ ? {
2564
+ kind: node.kind,
2565
+ loc: node.loc,
2566
+ definitions: node.definitions.map(d => d.rawNode()),
2567
+ }
2568
+ : node;
2569
+ };
2570
+ const commonFields = {
2571
+ ...node,
2572
+ type: node.kind,
2573
+ loc: convertLocation(node.loc),
2574
+ range: [node.loc.start, node.loc.end],
2575
+ leadingComments,
2576
+ // Use function to prevent RangeError: Maximum call stack size exceeded
2577
+ typeInfo: () => calculatedTypeInfo,
2578
+ rawNode,
2579
+ };
2580
+ return hasTypeField(node)
2581
+ ? {
2582
+ ...commonFields,
2583
+ gqlType: node.type,
2584
+ }
2585
+ : commonFields;
2586
+ };
2472
2587
 
2473
2588
  // eslint-disable-next-line unicorn/better-regex
2474
2589
  const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
@@ -2479,6 +2594,7 @@ const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
2479
2594
  const rule$f = {
2480
2595
  meta: {
2481
2596
  type: 'suggestion',
2597
+ hasSuggestions: true,
2482
2598
  docs: {
2483
2599
  category: 'Schema',
2484
2600
  description: 'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
@@ -2566,12 +2682,18 @@ const rule$f = {
2566
2682
  }
2567
2683
  const canRemove = Date.now() > deletionDateInMS;
2568
2684
  if (canRemove) {
2685
+ const { parent } = node;
2686
+ const nodeName = parent.name.value;
2569
2687
  context.report({
2570
- node: node.parent.name,
2688
+ node: parent.name,
2571
2689
  messageId: MESSAGE_CAN_BE_REMOVED,
2572
- data: {
2573
- nodeName: node.parent.name.value,
2574
- },
2690
+ data: { nodeName },
2691
+ suggest: [
2692
+ {
2693
+ desc: `Remove \`${nodeName}\``,
2694
+ fix: fixer => fixer.remove(parent),
2695
+ },
2696
+ ],
2575
2697
  });
2576
2698
  }
2577
2699
  },
@@ -2633,7 +2755,7 @@ const rule$g = {
2633
2755
  },
2634
2756
  };
2635
2757
 
2636
- const RULE_ID$2 = 'require-description';
2758
+ const RULE_ID$7 = 'require-description';
2637
2759
  const ALLOWED_KINDS$1 = [
2638
2760
  ...TYPES_KINDS,
2639
2761
  Kind.DIRECTIVE_DEFINITION,
@@ -2675,7 +2797,7 @@ const rule$h = {
2675
2797
  docs: {
2676
2798
  category: 'Schema',
2677
2799
  description: 'Enforce descriptions in type definitions and operations.',
2678
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
2800
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$7}.md`,
2679
2801
  examples: [
2680
2802
  {
2681
2803
  title: 'Incorrect',
@@ -2722,7 +2844,7 @@ const rule$h = {
2722
2844
  },
2723
2845
  type: 'suggestion',
2724
2846
  messages: {
2725
- [RULE_ID$2]: 'Description is required for `{{ nodeName }}`.',
2847
+ [RULE_ID$7]: 'Description is required for `{{ nodeName }}`.',
2726
2848
  },
2727
2849
  schema: {
2728
2850
  type: 'array',
@@ -2781,8 +2903,8 @@ const rule$h = {
2781
2903
  }
2782
2904
  if (description.length === 0) {
2783
2905
  context.report({
2784
- loc: isOperation ? getLocation(node.loc, node.operation) : node.name.loc,
2785
- messageId: RULE_ID$2,
2906
+ loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
2907
+ messageId: RULE_ID$7,
2786
2908
  data: {
2787
2909
  nodeName: getNodeName(node),
2788
2910
  },
@@ -2864,122 +2986,18 @@ const rule$i = {
2864
2986
  },
2865
2987
  };
2866
2988
 
2867
- function convertToESTree(node, typeInfo) {
2868
- const visitor = { leave: convertNode(typeInfo) };
2869
- return {
2870
- rootTree: visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor),
2871
- comments: extractCommentsFromAst(node.loc),
2872
- };
2873
- }
2874
- function hasTypeField(obj) {
2875
- return obj && !!obj.type;
2876
- }
2877
- function convertLocation(location) {
2878
- const { startToken, endToken, source, start, end } = location;
2879
- /*
2880
- * ESLint has 0-based column number
2881
- * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
2882
- */
2883
- const loc = {
2884
- start: {
2885
- /*
2886
- * Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
2887
- */
2888
- line: startToken.line === 0 ? 1 : startToken.line,
2889
- column: startToken.column === 0 ? 0 : startToken.column - 1,
2890
- },
2891
- end: {
2892
- line: endToken.line,
2893
- column: endToken.column - 1,
2894
- },
2895
- source: source.body,
2896
- };
2897
- if (loc.start.column === loc.end.column) {
2898
- loc.end.column += end - start;
2899
- }
2900
- return loc;
2901
- }
2902
- const convertNode = (typeInfo) => (node, key, parent) => {
2903
- const calculatedTypeInfo = typeInfo
2904
- ? {
2905
- argument: typeInfo.getArgument(),
2906
- defaultValue: typeInfo.getDefaultValue(),
2907
- directive: typeInfo.getDirective(),
2908
- enumValue: typeInfo.getEnumValue(),
2909
- fieldDef: typeInfo.getFieldDef(),
2910
- inputType: typeInfo.getInputType(),
2911
- parentInputType: typeInfo.getParentInputType(),
2912
- parentType: typeInfo.getParentType(),
2913
- gqlType: typeInfo.getType(),
2914
- }
2915
- : {};
2916
- const commonFields = {
2917
- typeInfo: () => calculatedTypeInfo,
2918
- leadingComments: convertDescription(node),
2919
- loc: convertLocation(node.loc),
2920
- range: [node.loc.start, node.loc.end],
2921
- };
2922
- if (hasTypeField(node)) {
2923
- const { type: gqlType, loc: gqlLocation, ...rest } = node;
2924
- const typeFieldSafe = {
2925
- ...rest,
2926
- gqlType,
2927
- };
2928
- const estreeNode = {
2929
- ...typeFieldSafe,
2930
- ...commonFields,
2931
- type: node.kind,
2932
- rawNode: () => {
2933
- if (!parent || key === undefined) {
2934
- if (node && node.definitions) {
2935
- return {
2936
- loc: gqlLocation,
2937
- kind: Kind.DOCUMENT,
2938
- definitions: node.definitions.map(d => d.rawNode()),
2939
- };
2940
- }
2941
- return node;
2942
- }
2943
- return parent[key];
2944
- },
2945
- };
2946
- return estreeNode;
2947
- }
2948
- else {
2949
- const { loc: gqlLocation, ...rest } = node;
2950
- const typeFieldSafe = rest;
2951
- const estreeNode = {
2952
- ...typeFieldSafe,
2953
- ...commonFields,
2954
- type: node.kind,
2955
- rawNode: () => {
2956
- if (!parent || key === undefined) {
2957
- if (node && node.definitions) {
2958
- return {
2959
- loc: gqlLocation,
2960
- kind: Kind.DOCUMENT,
2961
- definitions: node.definitions.map(d => d.rawNode()),
2962
- };
2963
- }
2964
- return node;
2965
- }
2966
- return parent[key];
2967
- },
2968
- };
2969
- return estreeNode;
2970
- }
2971
- };
2972
-
2973
- const RULE_ID$3 = 'require-id-when-available';
2974
- const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
2989
+ const RULE_ID$8 = 'require-id-when-available';
2975
2990
  const DEFAULT_ID_FIELD_NAME = 'id';
2991
+ const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
2976
2992
  const rule$j = {
2977
2993
  meta: {
2978
2994
  type: 'suggestion',
2995
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
2996
+ hasSuggestions: true,
2979
2997
  docs: {
2980
2998
  category: 'Operations',
2981
2999
  description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
2982
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
3000
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$8}.md`,
2983
3001
  requiresSchema: true,
2984
3002
  requiresSiblings: true,
2985
3003
  examples: [
@@ -3022,21 +3040,14 @@ const rule$j = {
3022
3040
  recommended: true,
3023
3041
  },
3024
3042
  messages: {
3025
- [MESSAGE_ID]: [
3026
- `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
3027
- `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
3028
- ].join('\n'),
3043
+ [RULE_ID$8]: `Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.`,
3029
3044
  },
3030
3045
  schema: {
3031
3046
  definitions: {
3032
3047
  asString: {
3033
3048
  type: 'string',
3034
3049
  },
3035
- asArray: {
3036
- type: 'array',
3037
- minItems: 1,
3038
- uniqueItems: true,
3039
- },
3050
+ asArray: ARRAY_DEFAULT_OPTIONS,
3040
3051
  },
3041
3052
  type: 'array',
3042
3053
  maxItems: 1,
@@ -3053,76 +3064,121 @@ const rule$j = {
3053
3064
  },
3054
3065
  },
3055
3066
  create(context) {
3056
- requireGraphQLSchemaFromContext(RULE_ID$3, context);
3057
- const siblings = requireSiblingsOperations(RULE_ID$3, context);
3067
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$8, context);
3068
+ const siblings = requireSiblingsOperations(RULE_ID$8, context);
3058
3069
  const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
3059
3070
  const idNames = asArray(fieldName);
3060
- const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
3061
- // Skip check selections in FragmentDefinition
3062
- const selector = 'OperationDefinition SelectionSet[parent.kind!=OperationDefinition]';
3063
- return {
3064
- [selector](node) {
3065
- var _a;
3066
- const typeInfo = node.typeInfo();
3067
- if (!typeInfo.gqlType) {
3068
- return;
3069
- }
3070
- const rawType = getBaseType(typeInfo.gqlType);
3071
- const isObjectType = rawType instanceof GraphQLObjectType;
3072
- const isInterfaceType = rawType instanceof GraphQLInterfaceType;
3073
- if (!isObjectType && !isInterfaceType) {
3074
- return;
3071
+ // Check selections only in OperationDefinition,
3072
+ // skip selections of OperationDefinition and InlineFragment
3073
+ const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
3074
+ const typeInfo = new TypeInfo(schema);
3075
+ function checkFragments(node) {
3076
+ for (const selection of node.selections) {
3077
+ if (selection.kind !== Kind.FRAGMENT_SPREAD) {
3078
+ continue;
3075
3079
  }
3076
- const fields = rawType.getFields();
3077
- const hasIdFieldInType = idNames.some(name => fields[name]);
3078
- if (!hasIdFieldInType) {
3079
- return;
3080
+ const [foundSpread] = siblings.getFragment(selection.name.value);
3081
+ if (!foundSpread) {
3082
+ continue;
3080
3083
  }
3081
3084
  const checkedFragmentSpreads = new Set();
3082
- for (const selection of node.selections) {
3083
- if (isFound(selection)) {
3084
- return;
3085
+ const visitor = visitWithTypeInfo(typeInfo, {
3086
+ SelectionSet(node, key, parent) {
3087
+ if (parent.kind === Kind.FRAGMENT_DEFINITION) {
3088
+ checkedFragmentSpreads.add(parent.name.value);
3089
+ }
3090
+ else if (parent.kind !== Kind.INLINE_FRAGMENT) {
3091
+ checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
3092
+ }
3093
+ },
3094
+ });
3095
+ visit(foundSpread.document, visitor);
3096
+ }
3097
+ }
3098
+ function checkSelections(node, type,
3099
+ // Fragment can be placed in separate file
3100
+ // Provide actual fragment spread location instead of location in fragment
3101
+ loc,
3102
+ // Can't access to node.parent in GraphQL AST.Node, so pass as argument
3103
+ parent, checkedFragmentSpreads = new Set()) {
3104
+ const rawType = getBaseType(type);
3105
+ const isObjectType = rawType instanceof GraphQLObjectType;
3106
+ const isInterfaceType = rawType instanceof GraphQLInterfaceType;
3107
+ if (!isObjectType && !isInterfaceType) {
3108
+ return;
3109
+ }
3110
+ const fields = rawType.getFields();
3111
+ const hasIdFieldInType = idNames.some(name => fields[name]);
3112
+ if (!hasIdFieldInType) {
3113
+ return;
3114
+ }
3115
+ function hasIdField({ selections }) {
3116
+ return selections.some(selection => {
3117
+ if (selection.kind === Kind.FIELD) {
3118
+ return idNames.includes(selection.name.value);
3085
3119
  }
3086
- if (selection.kind === Kind.INLINE_FRAGMENT && selection.selectionSet.selections.some(isFound)) {
3087
- return;
3120
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
3121
+ return hasIdField(selection.selectionSet);
3088
3122
  }
3089
3123
  if (selection.kind === Kind.FRAGMENT_SPREAD) {
3090
3124
  const [foundSpread] = siblings.getFragment(selection.name.value);
3091
3125
  if (foundSpread) {
3092
- checkedFragmentSpreads.add(foundSpread.document.name.value);
3093
- if (foundSpread.document.selectionSet.selections.some(isFound)) {
3094
- return;
3095
- }
3126
+ const fragmentSpread = foundSpread.document;
3127
+ checkedFragmentSpreads.add(fragmentSpread.name.value);
3128
+ return hasIdField(fragmentSpread.selectionSet);
3096
3129
  }
3097
3130
  }
3098
- }
3099
- const { parent } = node;
3100
- const hasIdFieldInInterfaceSelectionSet = (parent === null || parent === void 0 ? void 0 : parent.kind) === Kind.INLINE_FRAGMENT &&
3101
- ((_a = parent.parent) === null || _a === void 0 ? void 0 : _a.kind) === Kind.SELECTION_SET &&
3102
- parent.parent.selections.some(isFound);
3103
- if (hasIdFieldInInterfaceSelectionSet) {
3104
- return;
3105
- }
3106
- context.report({
3107
- loc: getLocation(node.loc),
3108
- messageId: MESSAGE_ID,
3109
- data: {
3110
- checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
3111
- fieldName: idNames.map(name => `"${name}"`).join(' or '),
3112
- },
3131
+ return false;
3113
3132
  });
3133
+ }
3134
+ const hasId = hasIdField(node);
3135
+ checkFragments(node);
3136
+ if (hasId) {
3137
+ return;
3138
+ }
3139
+ const pluralSuffix = idNames.length > 1 ? 's' : '';
3140
+ const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));
3141
+ const addition = checkedFragmentSpreads.size === 0
3142
+ ? ''
3143
+ : ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords([...checkedFragmentSpreads].map(name => `\`${name}\``))}`;
3144
+ const problem = {
3145
+ loc,
3146
+ messageId: RULE_ID$8,
3147
+ data: {
3148
+ pluralSuffix,
3149
+ fieldName,
3150
+ addition,
3151
+ },
3152
+ };
3153
+ // Don't provide suggestions for selections in fragments as fragment can be in a separate file
3154
+ if ('type' in node) {
3155
+ problem.suggest = idNames.map(idName => ({
3156
+ desc: `Add \`${idName}\` selection`,
3157
+ fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `),
3158
+ }));
3159
+ }
3160
+ context.report(problem);
3161
+ }
3162
+ return {
3163
+ [selector](node) {
3164
+ const typeInfo = node.typeInfo();
3165
+ if (typeInfo.gqlType) {
3166
+ checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
3167
+ }
3114
3168
  },
3115
3169
  };
3116
3170
  },
3117
3171
  };
3118
3172
 
3119
- const RULE_ID$4 = 'selection-set-depth';
3173
+ const RULE_ID$9 = 'selection-set-depth';
3120
3174
  const rule$k = {
3121
3175
  meta: {
3176
+ type: 'suggestion',
3177
+ hasSuggestions: true,
3122
3178
  docs: {
3123
3179
  category: 'Operations',
3124
3180
  description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
3125
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
3181
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$9}.md`,
3126
3182
  requiresSiblings: true,
3127
3183
  examples: [
3128
3184
  {
@@ -3168,7 +3224,6 @@ const rule$k = {
3168
3224
  recommended: true,
3169
3225
  configOptions: [{ maxDepth: 7 }],
3170
3226
  },
3171
- type: 'suggestion',
3172
3227
  schema: {
3173
3228
  type: 'array',
3174
3229
  minItems: 1,
@@ -3181,14 +3236,7 @@ const rule$k = {
3181
3236
  maxDepth: {
3182
3237
  type: 'number',
3183
3238
  },
3184
- ignore: {
3185
- type: 'array',
3186
- uniqueItems: true,
3187
- minItems: 1,
3188
- items: {
3189
- type: 'string',
3190
- },
3191
- },
3239
+ ignore: ARRAY_DEFAULT_OPTIONS,
3192
3240
  },
3193
3241
  },
3194
3242
  },
@@ -3196,10 +3244,10 @@ const rule$k = {
3196
3244
  create(context) {
3197
3245
  let siblings = null;
3198
3246
  try {
3199
- siblings = requireSiblingsOperations(RULE_ID$4, context);
3247
+ siblings = requireSiblingsOperations(RULE_ID$9, context);
3200
3248
  }
3201
3249
  catch (e) {
3202
- logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3250
+ logger.warn(`Rule "${RULE_ID$9}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3203
3251
  }
3204
3252
  const { maxDepth } = context.options[0];
3205
3253
  const ignore = context.options[0].ignore || [];
@@ -3208,7 +3256,7 @@ const rule$k = {
3208
3256
  'OperationDefinition, FragmentDefinition'(node) {
3209
3257
  try {
3210
3258
  const rawNode = node.rawNode();
3211
- const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode, true) : [];
3259
+ const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
3212
3260
  const document = {
3213
3261
  kind: Kind.DOCUMENT,
3214
3262
  definitions: [rawNode, ...fragmentsInUse],
@@ -3223,19 +3271,32 @@ const rule$k = {
3223
3271
  column: column - 1,
3224
3272
  },
3225
3273
  message: error.message,
3274
+ suggest: [
3275
+ {
3276
+ desc: 'Remove selections',
3277
+ fix(fixer) {
3278
+ const ancestors = context.getAncestors();
3279
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
3280
+ const sourceCode = context.getSourceCode();
3281
+ const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]);
3282
+ const parentNode = foundNode.parent.parent;
3283
+ return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
3284
+ },
3285
+ },
3286
+ ],
3226
3287
  });
3227
3288
  },
3228
3289
  });
3229
3290
  }
3230
3291
  catch (e) {
3231
- logger.warn(`Rule "${RULE_ID$4}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3292
+ logger.warn(`Rule "${RULE_ID$9}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3232
3293
  }
3233
3294
  },
3234
3295
  };
3235
3296
  },
3236
3297
  };
3237
3298
 
3238
- const RULE_ID$5 = 'strict-id-in-types';
3299
+ const RULE_ID$a = 'strict-id-in-types';
3239
3300
  const rule$l = {
3240
3301
  meta: {
3241
3302
  type: 'suggestion',
@@ -3243,7 +3304,7 @@ const rule$l = {
3243
3304
  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.`,
3244
3305
  category: 'Schema',
3245
3306
  recommended: true,
3246
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
3307
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$a}.md`,
3247
3308
  requiresSchema: true,
3248
3309
  examples: [
3249
3310
  {
@@ -3334,22 +3395,12 @@ const rule$l = {
3334
3395
  type: 'object',
3335
3396
  properties: {
3336
3397
  types: {
3337
- type: 'array',
3338
- uniqueItems: true,
3339
- minItems: 1,
3398
+ ...ARRAY_DEFAULT_OPTIONS,
3340
3399
  description: 'This is used to exclude types with names that match one of the specified values.',
3341
- items: {
3342
- type: 'string',
3343
- },
3344
3400
  },
3345
3401
  suffixes: {
3346
- type: 'array',
3347
- uniqueItems: true,
3348
- minItems: 1,
3402
+ ...ARRAY_DEFAULT_OPTIONS,
3349
3403
  description: 'This is used to exclude types with names with suffixes that match one of the specified values.',
3350
- items: {
3351
- type: 'string',
3352
- },
3353
3404
  },
3354
3405
  },
3355
3406
  },
@@ -3357,7 +3408,7 @@ const rule$l = {
3357
3408
  },
3358
3409
  },
3359
3410
  messages: {
3360
- [RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3411
+ [RULE_ID$a]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
3361
3412
  },
3362
3413
  },
3363
3414
  create(context) {
@@ -3367,7 +3418,7 @@ const rule$l = {
3367
3418
  exceptions: {},
3368
3419
  ...context.options[0],
3369
3420
  };
3370
- const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
3421
+ const schema = requireGraphQLSchemaFromContext(RULE_ID$a, context);
3371
3422
  const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
3372
3423
  .filter(Boolean)
3373
3424
  .map(type => type.name);
@@ -3397,7 +3448,7 @@ const rule$l = {
3397
3448
  if (validIds.length !== 1) {
3398
3449
  context.report({
3399
3450
  node: node.name,
3400
- messageId: RULE_ID$5,
3451
+ messageId: RULE_ID$a,
3401
3452
  data: {
3402
3453
  typeName,
3403
3454
  acceptedNamesString: options.acceptedIdNames.join(', '),
@@ -3730,13 +3781,13 @@ function getSiblingOperations(options, gqlConfig) {
3730
3781
  };
3731
3782
  return {
3732
3783
  available: false,
3733
- getFragments: noopWarn,
3734
- getOperations: noopWarn,
3735
3784
  getFragment: noopWarn,
3785
+ getFragments: noopWarn,
3736
3786
  getFragmentByType: noopWarn,
3787
+ getFragmentsInUse: noopWarn,
3737
3788
  getOperation: noopWarn,
3789
+ getOperations: noopWarn,
3738
3790
  getOperationByType: noopWarn,
3739
- getFragmentsInUse: noopWarn,
3740
3791
  };
3741
3792
  }
3742
3793
  // Since the siblings array is cached, we can use it as cache key.
@@ -3749,7 +3800,7 @@ function getSiblingOperations(options, gqlConfig) {
3749
3800
  if (fragmentsCache === null) {
3750
3801
  const result = [];
3751
3802
  for (const source of siblings) {
3752
- for (const definition of source.document.definitions || []) {
3803
+ for (const definition of source.document.definitions) {
3753
3804
  if (definition.kind === Kind.FRAGMENT_DEFINITION) {
3754
3805
  result.push({
3755
3806
  filePath: source.location,
@@ -3767,7 +3818,7 @@ function getSiblingOperations(options, gqlConfig) {
3767
3818
  if (cachedOperations === null) {
3768
3819
  const result = [];
3769
3820
  for (const source of siblings) {
3770
- for (const definition of source.document.definitions || []) {
3821
+ for (const definition of source.document.definitions) {
3771
3822
  if (definition.kind === Kind.OPERATION_DEFINITION) {
3772
3823
  result.push({
3773
3824
  filePath: source.location,
@@ -3781,19 +3832,17 @@ function getSiblingOperations(options, gqlConfig) {
3781
3832
  return cachedOperations;
3782
3833
  };
3783
3834
  const getFragment = (name) => getFragments().filter(f => { var _a; return ((_a = f.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; });
3784
- const collectFragments = (selectable, recursive = true, collected = new Map()) => {
3835
+ const collectFragments = (selectable, recursive, collected = new Map()) => {
3785
3836
  visit(selectable, {
3786
3837
  FragmentSpread(spread) {
3787
- const name = spread.name.value;
3788
- const fragmentInfo = getFragment(name);
3789
- if (fragmentInfo.length === 0) {
3790
- logger.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3838
+ const fragmentName = spread.name.value;
3839
+ const [fragment] = getFragment(fragmentName);
3840
+ if (!fragment) {
3841
+ logger.warn(`Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`);
3791
3842
  return;
3792
3843
  }
3793
- const fragment = fragmentInfo[0];
3794
- const alreadyVisited = collected.has(name);
3795
- if (!alreadyVisited) {
3796
- collected.set(name, fragment.document);
3844
+ if (!collected.has(fragmentName)) {
3845
+ collected.set(fragmentName, fragment.document);
3797
3846
  if (recursive) {
3798
3847
  collectFragments(fragment.document, recursive, collected);
3799
3848
  }
@@ -3804,13 +3853,13 @@ function getSiblingOperations(options, gqlConfig) {
3804
3853
  };
3805
3854
  siblingOperations = {
3806
3855
  available: true,
3807
- getFragments,
3808
- getOperations,
3809
3856
  getFragment,
3857
+ getFragments,
3810
3858
  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; }),
3859
+ getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3811
3860
  getOperation: name => getOperations().filter(o => { var _a; return ((_a = o.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; }),
3861
+ getOperations,
3812
3862
  getOperationByType: type => getOperations().filter(o => o.document.operation === type),
3813
- getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
3814
3863
  };
3815
3864
  siblingOperationsCache.set(siblings, siblingOperations);
3816
3865
  }
@@ -3856,86 +3905,6 @@ const addCodeFileLoaderExtension = api => {
3856
3905
  return { name: 'graphql-eslint-loaders' };
3857
3906
  };
3858
3907
 
3859
- let reachableTypesCache;
3860
- function getReachableTypes(schema) {
3861
- // We don't want cache reachableTypes on test environment
3862
- // Otherwise reachableTypes will be same for all tests
3863
- if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
3864
- return reachableTypesCache;
3865
- }
3866
- const reachableTypes = new Set();
3867
- const collect = (node) => {
3868
- const typeName = getTypeName(node);
3869
- if (reachableTypes.has(typeName)) {
3870
- return;
3871
- }
3872
- reachableTypes.add(typeName);
3873
- const type = schema.getType(typeName) || schema.getDirective(typeName);
3874
- if (isInterfaceType(type)) {
3875
- const { objects, interfaces } = schema.getImplementations(type);
3876
- for (const { astNode } of [...objects, ...interfaces]) {
3877
- visit(astNode, visitor);
3878
- }
3879
- }
3880
- else if (type.astNode) {
3881
- // astNode can be undefined for ID, String, Boolean
3882
- visit(type.astNode, visitor);
3883
- }
3884
- };
3885
- const visitor = {
3886
- InterfaceTypeDefinition: collect,
3887
- ObjectTypeDefinition: collect,
3888
- InputValueDefinition: collect,
3889
- UnionTypeDefinition: collect,
3890
- FieldDefinition: collect,
3891
- Directive: collect,
3892
- NamedType: collect,
3893
- };
3894
- for (const type of [
3895
- schema,
3896
- schema.getQueryType(),
3897
- schema.getMutationType(),
3898
- schema.getSubscriptionType(),
3899
- ]) {
3900
- // if schema don't have Query type, schema.astNode will be undefined
3901
- if (type === null || type === void 0 ? void 0 : type.astNode) {
3902
- visit(type.astNode, visitor);
3903
- }
3904
- }
3905
- reachableTypesCache = reachableTypes;
3906
- return reachableTypesCache;
3907
- }
3908
- let usedFieldsCache;
3909
- function getUsedFields(schema, operations) {
3910
- // We don't want cache usedFields on test environment
3911
- // Otherwise usedFields will be same for all tests
3912
- if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
3913
- return usedFieldsCache;
3914
- }
3915
- const usedFields = Object.create(null);
3916
- const typeInfo = new TypeInfo(schema);
3917
- const visitor = visitWithTypeInfo(typeInfo, {
3918
- Field(node) {
3919
- var _a;
3920
- const fieldDef = typeInfo.getFieldDef();
3921
- if (!fieldDef) {
3922
- // skip visiting this node if field is not defined in schema
3923
- return false;
3924
- }
3925
- const parentTypeName = typeInfo.getParentType().name;
3926
- const fieldName = node.name.value;
3927
- (_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
3928
- usedFields[parentTypeName].add(fieldName);
3929
- },
3930
- });
3931
- const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
3932
- for (const { document } of allDocuments) {
3933
- visit(document, visitor);
3934
- }
3935
- usedFieldsCache = usedFields;
3936
- return usedFieldsCache;
3937
- }
3938
-
3939
3908
  function parse(code, options) {
3940
3909
  return parseForESLint(code, options).ast;
3941
3910
  }
@@ -3946,8 +3915,6 @@ function parseForESLint(code, options = {}) {
3946
3915
  hasTypeInfo: schema !== null,
3947
3916
  schema,
3948
3917
  siblingOperations: getSiblingOperations(options, gqlConfig),
3949
- reachableTypes: getReachableTypes,
3950
- usedFields: getUsedFields,
3951
3918
  };
3952
3919
  try {
3953
3920
  const filePath = options.filePath || '';
@@ -3988,6 +3955,7 @@ function parseForESLint(code, options = {}) {
3988
3955
  }
3989
3956
  }
3990
3957
 
3958
+ /* eslint-env jest */
3991
3959
  function indentCode(code, indent = 4) {
3992
3960
  return code.replace(/^/gm, ' '.repeat(indent));
3993
3961
  }
@@ -3997,6 +3965,11 @@ function printCode(code) {
3997
3965
  linesBelow: Number.POSITIVE_INFINITY,
3998
3966
  });
3999
3967
  }
3968
+ // A simple version of `SourceCodeFixer.applyFixes`
3969
+ // https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
3970
+ function applyFix(code, fix) {
3971
+ return [code.slice(0, fix.range[0]), fix.text, code.slice(fix.range[1])].join('');
3972
+ }
4000
3973
  class GraphQLRuleTester extends RuleTester {
4001
3974
  constructor(parserOptions = {}) {
4002
3975
  const config = {
@@ -4040,32 +4013,63 @@ class GraphQLRuleTester extends RuleTester {
4040
4013
  linter.defineRule(name, rule);
4041
4014
  const hasOnlyTest = tests.invalid.some(t => t.only);
4042
4015
  for (const testCase of tests.invalid) {
4043
- const { only, code, filename } = testCase;
4016
+ const { only, filename, options } = testCase;
4044
4017
  if (hasOnlyTest && !only) {
4045
4018
  continue;
4046
4019
  }
4020
+ const code = removeTrailingBlankLines(testCase.code);
4047
4021
  const verifyConfig = getVerifyConfig(name, this.config, testCase);
4048
4022
  defineParser(linter, verifyConfig.parser);
4049
4023
  const messages = linter.verify(code, verifyConfig, { filename });
4050
4024
  const messageForSnapshot = [];
4025
+ const hasMultipleMessages = messages.length > 1;
4026
+ if (hasMultipleMessages) {
4027
+ messageForSnapshot.push('Code', indentCode(printCode(code)));
4028
+ }
4029
+ if (options) {
4030
+ const opts = JSON.stringify(options, null, 2).slice(1, -1);
4031
+ messageForSnapshot.push('⚙️ Options', indentCode(removeTrailingBlankLines(opts), 2));
4032
+ }
4051
4033
  for (const [index, message] of messages.entries()) {
4052
4034
  if (message.fatal) {
4053
4035
  throw new Error(message.message);
4054
4036
  }
4055
- messageForSnapshot.push(`❌ Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
4037
+ const codeWithMessage = visualizeEslintMessage(code, message, hasMultipleMessages ? 1 : undefined);
4038
+ messageForSnapshot.push(printWithIndex('❌ Error', index, messages.length), indentCode(codeWithMessage));
4039
+ const { suggestions } = message;
4040
+ // Don't print suggestions in snapshots for too big codes
4041
+ if (suggestions && (code.match(/\n/g) || '').length < 1000) {
4042
+ for (const [i, suggestion] of message.suggestions.entries()) {
4043
+ const output = applyFix(code, suggestion.fix);
4044
+ const title = printWithIndex('💡 Suggestion', i, suggestions.length, suggestion.desc);
4045
+ messageForSnapshot.push(title, indentCode(printCode(output), 2));
4046
+ }
4047
+ }
4056
4048
  }
4057
4049
  if (rule.meta.fixable) {
4058
4050
  const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
4059
4051
  if (fixed) {
4060
- messageForSnapshot.push('🔧 Autofix output', indentCode(printCode(output), 2));
4052
+ messageForSnapshot.push('🔧 Autofix output', indentCode(codeFrameColumns(output, {})));
4061
4053
  }
4062
4054
  }
4063
4055
  expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
4064
4056
  }
4065
4057
  }
4066
4058
  }
4059
+ function removeTrailingBlankLines(text) {
4060
+ return text.replace(/^\s*\n/, '').trimEnd();
4061
+ }
4062
+ function printWithIndex(title, index, total, description) {
4063
+ if (total > 1) {
4064
+ title += ` ${index + 1}/${total}`;
4065
+ }
4066
+ if (description) {
4067
+ title += `: ${description}`;
4068
+ }
4069
+ return title;
4070
+ }
4067
4071
  function getVerifyConfig(ruleId, testerConfig, testCase) {
4068
- const { options, parserOptions, parser = testerConfig.parser } = testCase;
4072
+ const { parser = testerConfig.parser, parserOptions, options } = testCase;
4069
4073
  return {
4070
4074
  ...testerConfig,
4071
4075
  parser,
@@ -4074,7 +4078,7 @@ function getVerifyConfig(ruleId, testerConfig, testCase) {
4074
4078
  ...parserOptions,
4075
4079
  },
4076
4080
  rules: {
4077
- [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
4081
+ [ruleId]: Array.isArray(options) ? ['error', ...options] : 'error',
4078
4082
  },
4079
4083
  };
4080
4084
  }
@@ -4092,7 +4096,7 @@ function defineParser(linter, parser) {
4092
4096
  linter.defineParser(parser, require(parser));
4093
4097
  }
4094
4098
  }
4095
- function visualizeEslintMessage(text, result) {
4099
+ function visualizeEslintMessage(text, result, linesOffset = Number.POSITIVE_INFINITY) {
4096
4100
  const { line, column, endLine, endColumn, message } = result;
4097
4101
  const location = {
4098
4102
  start: {
@@ -4107,10 +4111,15 @@ function visualizeEslintMessage(text, result) {
4107
4111
  };
4108
4112
  }
4109
4113
  return codeFrameColumns(text, location, {
4110
- linesAbove: Number.POSITIVE_INFINITY,
4111
- linesBelow: Number.POSITIVE_INFINITY,
4114
+ linesAbove: linesOffset,
4115
+ linesBelow: linesOffset,
4112
4116
  message,
4113
4117
  });
4114
4118
  }
4115
4119
 
4116
- export { GraphQLRuleTester, configs, convertDescription, convertToESTree, convertToken, extractCommentsFromAst, extractTokens, getBaseType, isNodeWithDescription, parse, parseForESLint, processors, rules, valueFromNode };
4120
+ const configs = Object.fromEntries(['schema-recommended', 'schema-all', 'operations-recommended', 'operations-all'].map(configName => [
4121
+ configName,
4122
+ { extends: `./configs/${configName}.json` },
4123
+ ]));
4124
+
4125
+ export { GraphQLRuleTester, configs, parse, parseForESLint, processors, requireGraphQLSchemaFromContext, requireSiblingsOperations, rules };