@graphql-eslint/eslint-plugin 3.8.0-alpha-269d044.0 → 3.9.1-alpha-ebcc06f.0

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