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