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