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