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