@graphql-eslint/eslint-plugin 3.13.2-alpha-20221109140613-1815aa1 → 3.14.0-alpha-20221220004017-f1f0904
Sign up to get free protection for your applications and to get access to all the features.
- package/documents.d.ts +3 -3
- package/estree-converter/types.d.ts +8 -8
- package/estree-converter/utils.d.ts +1 -1
- package/index.js +215 -91
- package/index.mjs +215 -91
- package/package.json +2 -2
- package/processor.d.ts +1 -1
- package/rules/alphabetize.d.ts +1 -1
- package/rules/description-style.d.ts +1 -1
- package/rules/graphql-js-validation.d.ts +1 -1
- package/rules/input-name.d.ts +1 -1
- package/rules/match-document-filename.d.ts +3 -3
- package/rules/naming-convention.d.ts +5 -5
- package/rules/no-root-type.d.ts +1 -1
- package/rules/relay-arguments.d.ts +1 -1
- package/rules/relay-edge-types.d.ts +1 -1
- package/rules/require-description.d.ts +2 -2
- package/rules/require-id-when-available.d.ts +1 -1
- package/rules/selection-set-depth.d.ts +1 -1
- package/rules/strict-id-in-types.d.ts +1 -1
- package/testkit.d.ts +3 -3
- package/types.d.ts +15 -15
- package/utils.d.ts +1 -1
package/documents.d.ts
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
import { FragmentDefinitionNode, OperationDefinitionNode, SelectionSetNode, OperationTypeNode } from 'graphql';
|
2
2
|
import { GraphQLProjectConfig } from 'graphql-config';
|
3
|
-
export
|
3
|
+
export type FragmentSource = {
|
4
4
|
filePath: string;
|
5
5
|
document: FragmentDefinitionNode;
|
6
6
|
};
|
7
|
-
export
|
7
|
+
export type OperationSource = {
|
8
8
|
filePath: string;
|
9
9
|
document: OperationDefinitionNode;
|
10
10
|
};
|
11
|
-
export
|
11
|
+
export type SiblingOperations = {
|
12
12
|
available: boolean;
|
13
13
|
getFragment(fragmentName: string): FragmentSource[];
|
14
14
|
getFragments(): FragmentSource[];
|
@@ -1,15 +1,15 @@
|
|
1
1
|
import type { ASTNode, TypeInfo, TypeNode, DocumentNode, ExecutableDefinitionNode, NameNode, TypeDefinitionNode, FieldDefinitionNode, ObjectTypeExtensionNode, ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, SelectionSetNode, SelectionNode, DefinitionNode, TypeExtensionNode, DirectiveDefinitionNode, VariableNode, FieldNode, FragmentSpreadNode, EnumValueDefinitionNode, ArgumentNode, NamedTypeNode, EnumTypeDefinitionNode, EnumTypeExtensionNode, InputValueDefinitionNode, InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode, InlineFragmentNode, VariableDefinitionNode, ListTypeNode, NonNullTypeNode, OperationTypeDefinitionNode } from 'graphql';
|
2
2
|
import type { SourceLocation, Comment } from 'estree';
|
3
3
|
import type { AST } from 'eslint';
|
4
|
-
|
4
|
+
type SafeGraphQLType<T extends ASTNode> = T extends {
|
5
5
|
type: TypeNode;
|
6
6
|
} ? Omit<T, 'loc' | 'type'> & {
|
7
7
|
gqlType: T['type'];
|
8
8
|
} : Omit<T, 'loc'>;
|
9
|
-
|
9
|
+
type Writeable<T> = {
|
10
10
|
-readonly [K in keyof T]: T[K];
|
11
11
|
};
|
12
|
-
export
|
12
|
+
export type TypeInformation = {
|
13
13
|
argument: ReturnType<TypeInfo['getArgument']>;
|
14
14
|
defaultValue: ReturnType<TypeInfo['getDefaultValue']>;
|
15
15
|
directive: ReturnType<TypeInfo['getDirective']>;
|
@@ -20,10 +20,10 @@ export declare type TypeInformation = {
|
|
20
20
|
parentType: ReturnType<TypeInfo['getParentType']>;
|
21
21
|
gqlType: ReturnType<TypeInfo['getType']>;
|
22
22
|
};
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
type NodeWithName = TypeDefinitionNode | TypeExtensionNode | ExecutableDefinitionNode | DirectiveDefinitionNode | FieldDefinitionNode | EnumValueDefinitionNode | FieldNode | FragmentSpreadNode | VariableNode | ArgumentNode | NamedTypeNode;
|
24
|
+
type NodeWithType = FieldDefinitionNode | InputValueDefinitionNode | OperationTypeDefinitionNode | NonNullTypeNode | ListTypeNode | VariableDefinitionNode;
|
25
|
+
type ParentNode<T> = T extends DocumentNode ? AST.Program : T extends DefinitionNode ? DocumentNode : T extends EnumValueDefinitionNode ? EnumTypeDefinitionNode | EnumTypeExtensionNode : T extends InputValueDefinitionNode ? InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode | FieldDefinitionNode | DirectiveDefinitionNode : T extends FieldDefinitionNode ? ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode : T extends SelectionSetNode ? ExecutableDefinitionNode | FieldNode | InlineFragmentNode : T extends SelectionNode ? SelectionSetNode : T extends TypeNode ? NodeWithType : T extends NameNode ? NodeWithName : unknown;
|
26
|
+
type Node<T extends ASTNode, WithTypeInfo extends boolean> = Writeable<SafeGraphQLType<T>> & {
|
27
27
|
type: T['kind'];
|
28
28
|
loc: SourceLocation;
|
29
29
|
range: AST.Range;
|
@@ -32,7 +32,7 @@ declare type Node<T extends ASTNode, WithTypeInfo extends boolean> = Writeable<S
|
|
32
32
|
rawNode: () => T;
|
33
33
|
parent: ParentNode<T>;
|
34
34
|
};
|
35
|
-
export
|
35
|
+
export type GraphQLESTreeNode<T, W extends boolean = false> = T extends ASTNode ? {
|
36
36
|
[K in keyof Node<T, W>]: Node<T, W>[K] extends ReadonlyArray<infer ArrayItem> ? GraphQLESTreeNode<ArrayItem, W>[] : GraphQLESTreeNode<Node<T, W>[K], W>;
|
37
37
|
} : T extends AST.Program ? T & {
|
38
38
|
parent: null;
|
@@ -3,7 +3,7 @@ import type { Comment, SourceLocation } from 'estree';
|
|
3
3
|
import type { AST } from 'eslint';
|
4
4
|
export declare const valueFromNode: (valueNode: import("graphql").ValueNode, variables?: import("graphql/jsutils/ObjMap").ObjMap<unknown>) => any;
|
5
5
|
export declare function getBaseType(type: GraphQLOutputType): GraphQLNamedType;
|
6
|
-
|
6
|
+
type TokenKindValue = '<SOF>' | '!' | '$' | '&' | '(' | ')' | '...' | ':' | '=' | '@' | '[' | ']' | '{' | '|' | '}' | 'Name' | 'Int' | 'Float' | 'String' | 'BlockString' | 'Comment';
|
7
7
|
export declare function convertToken<T extends 'Line' | 'Block' | TokenKindValue>(token: Token, type: T): Omit<AST.Token, 'type'> & {
|
8
8
|
type: T;
|
9
9
|
};
|
package/index.js
CHANGED
@@ -221,7 +221,8 @@ const ARRAY_DEFAULT_OPTIONS = {
|
|
221
221
|
};
|
222
222
|
const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
|
223
223
|
|
224
|
-
function validateDocument(context, schema = null, documentNode, rule) {
|
224
|
+
function validateDocument({ context, schema = null, documentNode, rule, hasDidYouMeanSuggestions, }) {
|
225
|
+
var _a;
|
225
226
|
if (documentNode.definitions.length === 0) {
|
226
227
|
return;
|
227
228
|
}
|
@@ -245,9 +246,20 @@ function validateDocument(context, schema = null, documentNode, rule) {
|
|
245
246
|
? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc
|
246
247
|
: token.loc;
|
247
248
|
}
|
249
|
+
const didYouMeanContent = (_a = error.message.match(/Did you mean (?<content>.*)\?$/)) === null || _a === void 0 ? void 0 : _a.groups.content;
|
250
|
+
const matches = didYouMeanContent ? [...didYouMeanContent.matchAll(/"(?<name>[^"]*)"/g)] : [];
|
248
251
|
context.report({
|
249
252
|
loc,
|
250
253
|
message: error.message,
|
254
|
+
suggest: hasDidYouMeanSuggestions
|
255
|
+
? matches.map(match => {
|
256
|
+
const { name } = match.groups;
|
257
|
+
return {
|
258
|
+
desc: `Rename to \`${name}\``,
|
259
|
+
fix: fixer => fixer.replaceText(token, name),
|
260
|
+
};
|
261
|
+
})
|
262
|
+
: [],
|
251
263
|
});
|
252
264
|
}
|
253
265
|
}
|
@@ -301,7 +313,7 @@ const handleMissingFragments = ({ ruleId, context, node }) => {
|
|
301
313
|
}
|
302
314
|
return node;
|
303
315
|
};
|
304
|
-
const validationToRule = (ruleId, ruleName,
|
316
|
+
const validationToRule = ({ ruleId, ruleName, getDocumentNode, schema = [], hasDidYouMeanSuggestions, }, docs) => {
|
305
317
|
let ruleFn = null;
|
306
318
|
try {
|
307
319
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
@@ -325,6 +337,7 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = [])
|
|
325
337
|
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
|
326
338
|
},
|
327
339
|
schema,
|
340
|
+
hasSuggestions: hasDidYouMeanSuggestions,
|
328
341
|
},
|
329
342
|
create(context) {
|
330
343
|
if (!ruleFn) {
|
@@ -339,30 +352,79 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = [])
|
|
339
352
|
const documentNode = getDocumentNode
|
340
353
|
? getDocumentNode({ ruleId, context, node: node.rawNode() })
|
341
354
|
: node.rawNode();
|
342
|
-
validateDocument(
|
355
|
+
validateDocument({
|
356
|
+
context,
|
357
|
+
schema,
|
358
|
+
documentNode,
|
359
|
+
rule: ruleFn,
|
360
|
+
hasDidYouMeanSuggestions,
|
361
|
+
});
|
343
362
|
},
|
344
363
|
};
|
345
364
|
},
|
346
365
|
},
|
347
366
|
};
|
348
367
|
};
|
349
|
-
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule(
|
368
|
+
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule({
|
369
|
+
ruleId: 'executable-definitions',
|
370
|
+
ruleName: 'ExecutableDefinitions',
|
371
|
+
}, {
|
350
372
|
category: 'Operations',
|
351
373
|
description: 'A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.',
|
352
374
|
requiresSchema: true,
|
353
|
-
}), validationToRule(
|
375
|
+
}), validationToRule({
|
376
|
+
ruleId: 'fields-on-correct-type',
|
377
|
+
ruleName: 'FieldsOnCorrectType',
|
378
|
+
hasDidYouMeanSuggestions: true,
|
379
|
+
}, {
|
354
380
|
category: 'Operations',
|
355
381
|
description: 'A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as `__typename`.',
|
356
382
|
requiresSchema: true,
|
357
|
-
}), validationToRule(
|
383
|
+
}), validationToRule({
|
384
|
+
ruleId: 'fragments-on-composite-type',
|
385
|
+
ruleName: 'FragmentsOnCompositeTypes',
|
386
|
+
}, {
|
358
387
|
category: 'Operations',
|
359
388
|
description: 'Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.',
|
360
389
|
requiresSchema: true,
|
361
|
-
}), validationToRule(
|
390
|
+
}), validationToRule({
|
391
|
+
ruleId: 'known-argument-names',
|
392
|
+
ruleName: 'KnownArgumentNames',
|
393
|
+
hasDidYouMeanSuggestions: true,
|
394
|
+
}, {
|
362
395
|
category: ['Schema', 'Operations'],
|
363
396
|
description: 'A GraphQL field is only valid if all supplied arguments are defined by that field.',
|
364
397
|
requiresSchema: true,
|
365
|
-
}), validationToRule(
|
398
|
+
}), validationToRule({
|
399
|
+
ruleId: 'known-directives',
|
400
|
+
ruleName: 'KnownDirectives',
|
401
|
+
getDocumentNode({ context, node: documentNode }) {
|
402
|
+
const { ignoreClientDirectives = [] } = context.options[0] || {};
|
403
|
+
if (ignoreClientDirectives.length === 0) {
|
404
|
+
return documentNode;
|
405
|
+
}
|
406
|
+
const filterDirectives = (node) => ({
|
407
|
+
...node,
|
408
|
+
directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
|
409
|
+
});
|
410
|
+
return graphql.visit(documentNode, {
|
411
|
+
Field: filterDirectives,
|
412
|
+
OperationDefinition: filterDirectives,
|
413
|
+
});
|
414
|
+
},
|
415
|
+
schema: {
|
416
|
+
type: 'array',
|
417
|
+
maxItems: 1,
|
418
|
+
items: {
|
419
|
+
type: 'object',
|
420
|
+
additionalProperties: false,
|
421
|
+
required: ['ignoreClientDirectives'],
|
422
|
+
properties: {
|
423
|
+
ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
|
424
|
+
},
|
425
|
+
},
|
426
|
+
},
|
427
|
+
}, {
|
366
428
|
category: ['Schema', 'Operations'],
|
367
429
|
description: 'A GraphQL document is only valid if all `@directive`s are known by the schema and legally positioned.',
|
368
430
|
requiresSchema: true,
|
@@ -379,31 +441,11 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
379
441
|
`,
|
380
442
|
},
|
381
443
|
],
|
382
|
-
}, ({
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
}
|
387
|
-
const filterDirectives = (node) => ({
|
388
|
-
...node,
|
389
|
-
directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
|
390
|
-
});
|
391
|
-
return graphql.visit(documentNode, {
|
392
|
-
Field: filterDirectives,
|
393
|
-
OperationDefinition: filterDirectives,
|
394
|
-
});
|
444
|
+
}), validationToRule({
|
445
|
+
ruleId: 'known-fragment-names',
|
446
|
+
ruleName: 'KnownFragmentNames',
|
447
|
+
getDocumentNode: handleMissingFragments,
|
395
448
|
}, {
|
396
|
-
type: 'array',
|
397
|
-
maxItems: 1,
|
398
|
-
items: {
|
399
|
-
type: 'object',
|
400
|
-
additionalProperties: false,
|
401
|
-
required: ['ignoreClientDirectives'],
|
402
|
-
properties: {
|
403
|
-
ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
|
404
|
-
},
|
405
|
-
},
|
406
|
-
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
407
449
|
category: 'Operations',
|
408
450
|
description: 'A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.',
|
409
451
|
requiresSchema: true,
|
@@ -454,138 +496,220 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
454
496
|
`,
|
455
497
|
},
|
456
498
|
],
|
457
|
-
}
|
499
|
+
}), validationToRule({
|
500
|
+
ruleId: 'known-type-names',
|
501
|
+
ruleName: 'KnownTypeNames',
|
502
|
+
hasDidYouMeanSuggestions: true,
|
503
|
+
}, {
|
458
504
|
category: ['Schema', 'Operations'],
|
459
505
|
description: 'A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.',
|
460
506
|
requiresSchema: true,
|
461
|
-
}), validationToRule(
|
507
|
+
}), validationToRule({
|
508
|
+
ruleId: 'lone-anonymous-operation',
|
509
|
+
ruleName: 'LoneAnonymousOperation',
|
510
|
+
}, {
|
462
511
|
category: 'Operations',
|
463
512
|
description: 'A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.',
|
464
513
|
requiresSchema: true,
|
465
|
-
}), validationToRule(
|
514
|
+
}), validationToRule({
|
515
|
+
ruleId: 'lone-schema-definition',
|
516
|
+
ruleName: 'LoneSchemaDefinition',
|
517
|
+
}, {
|
466
518
|
category: 'Schema',
|
467
519
|
description: 'A GraphQL document is only valid if it contains only one schema definition.',
|
468
|
-
}), validationToRule(
|
520
|
+
}), validationToRule({
|
521
|
+
ruleId: 'no-fragment-cycles',
|
522
|
+
ruleName: 'NoFragmentCycles',
|
523
|
+
}, {
|
469
524
|
category: 'Operations',
|
470
525
|
description: 'A GraphQL fragment is only valid when it does not have cycles in fragments usage.',
|
471
526
|
requiresSchema: true,
|
472
|
-
}), validationToRule(
|
527
|
+
}), validationToRule({
|
528
|
+
ruleId: 'no-undefined-variables',
|
529
|
+
ruleName: 'NoUndefinedVariables',
|
530
|
+
getDocumentNode: handleMissingFragments,
|
531
|
+
}, {
|
473
532
|
category: 'Operations',
|
474
533
|
description: 'A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.',
|
475
534
|
requiresSchema: true,
|
476
535
|
requiresSiblings: true,
|
477
|
-
}
|
536
|
+
}), validationToRule({
|
537
|
+
ruleId: 'no-unused-fragments',
|
538
|
+
ruleName: 'NoUnusedFragments',
|
539
|
+
getDocumentNode: ({ ruleId, context, node }) => {
|
540
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
541
|
+
const FilePathToDocumentsMap = [
|
542
|
+
...siblings.getOperations(),
|
543
|
+
...siblings.getFragments(),
|
544
|
+
].reduce((map, { filePath, document }) => {
|
545
|
+
var _a;
|
546
|
+
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
547
|
+
map[filePath].push(document);
|
548
|
+
return map;
|
549
|
+
}, Object.create(null));
|
550
|
+
const getParentNode = (currentFilePath, node) => {
|
551
|
+
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
|
552
|
+
if (fragmentDefs.size === 0) {
|
553
|
+
return node;
|
554
|
+
}
|
555
|
+
// skip iteration over documents for current filepath
|
556
|
+
delete FilePathToDocumentsMap[currentFilePath];
|
557
|
+
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
558
|
+
const missingFragments = getMissingFragments({
|
559
|
+
kind: graphql.Kind.DOCUMENT,
|
560
|
+
definitions: documents,
|
561
|
+
});
|
562
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
563
|
+
if (isCurrentFileImportFragment) {
|
564
|
+
return getParentNode(filePath, {
|
565
|
+
kind: graphql.Kind.DOCUMENT,
|
566
|
+
definitions: [...node.definitions, ...documents],
|
567
|
+
});
|
568
|
+
}
|
569
|
+
}
|
570
|
+
return node;
|
571
|
+
};
|
572
|
+
return getParentNode(context.getFilename(), node);
|
573
|
+
},
|
574
|
+
}, {
|
478
575
|
category: 'Operations',
|
479
576
|
description: 'A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.',
|
480
577
|
requiresSchema: true,
|
481
578
|
requiresSiblings: true,
|
482
|
-
}, ({
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
].reduce((map, { filePath, document }) => {
|
488
|
-
var _a;
|
489
|
-
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
490
|
-
map[filePath].push(document);
|
491
|
-
return map;
|
492
|
-
}, Object.create(null));
|
493
|
-
const getParentNode = (currentFilePath, node) => {
|
494
|
-
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
|
495
|
-
if (fragmentDefs.size === 0) {
|
496
|
-
return node;
|
497
|
-
}
|
498
|
-
// skip iteration over documents for current filepath
|
499
|
-
delete FilePathToDocumentsMap[currentFilePath];
|
500
|
-
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
501
|
-
const missingFragments = getMissingFragments({
|
502
|
-
kind: graphql.Kind.DOCUMENT,
|
503
|
-
definitions: documents,
|
504
|
-
});
|
505
|
-
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
506
|
-
if (isCurrentFileImportFragment) {
|
507
|
-
return getParentNode(filePath, {
|
508
|
-
kind: graphql.Kind.DOCUMENT,
|
509
|
-
definitions: [...node.definitions, ...documents],
|
510
|
-
});
|
511
|
-
}
|
512
|
-
}
|
513
|
-
return node;
|
514
|
-
};
|
515
|
-
return getParentNode(context.getFilename(), node);
|
516
|
-
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
579
|
+
}), validationToRule({
|
580
|
+
ruleId: 'no-unused-variables',
|
581
|
+
ruleName: 'NoUnusedVariables',
|
582
|
+
getDocumentNode: handleMissingFragments,
|
583
|
+
}, {
|
517
584
|
category: 'Operations',
|
518
585
|
description: 'A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.',
|
519
586
|
requiresSchema: true,
|
520
587
|
requiresSiblings: true,
|
521
|
-
}
|
588
|
+
}), validationToRule({
|
589
|
+
ruleId: 'overlapping-fields-can-be-merged',
|
590
|
+
ruleName: 'OverlappingFieldsCanBeMerged',
|
591
|
+
}, {
|
522
592
|
category: 'Operations',
|
523
593
|
description: 'A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.',
|
524
594
|
requiresSchema: true,
|
525
|
-
}), validationToRule(
|
595
|
+
}), validationToRule({
|
596
|
+
ruleId: 'possible-fragment-spread',
|
597
|
+
ruleName: 'PossibleFragmentSpreads',
|
598
|
+
}, {
|
526
599
|
category: 'Operations',
|
527
600
|
description: 'A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.',
|
528
601
|
requiresSchema: true,
|
529
|
-
}), validationToRule(
|
602
|
+
}), validationToRule({
|
603
|
+
ruleId: 'possible-type-extension',
|
604
|
+
ruleName: 'PossibleTypeExtensions',
|
605
|
+
hasDidYouMeanSuggestions: true,
|
606
|
+
}, {
|
530
607
|
category: 'Schema',
|
531
608
|
description: 'A type extension is only valid if the type is defined and has the same kind.',
|
532
609
|
// TODO: add in graphql-eslint v4
|
533
610
|
recommended: false,
|
534
611
|
requiresSchema: true,
|
535
612
|
isDisabledForAllConfig: true,
|
536
|
-
}), validationToRule(
|
613
|
+
}), validationToRule({
|
614
|
+
ruleId: 'provided-required-arguments',
|
615
|
+
ruleName: 'ProvidedRequiredArguments',
|
616
|
+
}, {
|
537
617
|
category: ['Schema', 'Operations'],
|
538
618
|
description: 'A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.',
|
539
619
|
requiresSchema: true,
|
540
|
-
}), validationToRule(
|
620
|
+
}), validationToRule({
|
621
|
+
ruleId: 'scalar-leafs',
|
622
|
+
ruleName: 'ScalarLeafs',
|
623
|
+
hasDidYouMeanSuggestions: true,
|
624
|
+
}, {
|
541
625
|
category: 'Operations',
|
542
626
|
description: 'A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.',
|
543
627
|
requiresSchema: true,
|
544
|
-
}), validationToRule(
|
628
|
+
}), validationToRule({
|
629
|
+
ruleId: 'one-field-subscriptions',
|
630
|
+
ruleName: 'SingleFieldSubscriptions',
|
631
|
+
}, {
|
545
632
|
category: 'Operations',
|
546
633
|
description: 'A GraphQL subscription is valid only if it contains a single root field.',
|
547
634
|
requiresSchema: true,
|
548
|
-
}), validationToRule(
|
635
|
+
}), validationToRule({
|
636
|
+
ruleId: 'unique-argument-names',
|
637
|
+
ruleName: 'UniqueArgumentNames',
|
638
|
+
}, {
|
549
639
|
category: 'Operations',
|
550
640
|
description: 'A GraphQL field or directive is only valid if all supplied arguments are uniquely named.',
|
551
641
|
requiresSchema: true,
|
552
|
-
}), validationToRule(
|
642
|
+
}), validationToRule({
|
643
|
+
ruleId: 'unique-directive-names',
|
644
|
+
ruleName: 'UniqueDirectiveNames',
|
645
|
+
}, {
|
553
646
|
category: 'Schema',
|
554
647
|
description: 'A GraphQL document is only valid if all defined directives have unique names.',
|
555
|
-
}), validationToRule(
|
648
|
+
}), validationToRule({
|
649
|
+
ruleId: 'unique-directive-names-per-location',
|
650
|
+
ruleName: 'UniqueDirectivesPerLocation',
|
651
|
+
}, {
|
556
652
|
category: ['Schema', 'Operations'],
|
557
653
|
description: 'A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.',
|
558
654
|
requiresSchema: true,
|
559
|
-
}), validationToRule(
|
655
|
+
}), validationToRule({
|
656
|
+
ruleId: 'unique-enum-value-names',
|
657
|
+
ruleName: 'UniqueEnumValueNames',
|
658
|
+
}, {
|
560
659
|
category: 'Schema',
|
561
660
|
description: 'A GraphQL enum type is only valid if all its values are uniquely named.',
|
562
661
|
recommended: false,
|
563
662
|
isDisabledForAllConfig: true,
|
564
|
-
}), validationToRule(
|
663
|
+
}), validationToRule({
|
664
|
+
ruleId: 'unique-field-definition-names',
|
665
|
+
ruleName: 'UniqueFieldDefinitionNames',
|
666
|
+
}, {
|
565
667
|
category: 'Schema',
|
566
668
|
description: 'A GraphQL complex type is only valid if all its fields are uniquely named.',
|
567
|
-
}), validationToRule(
|
669
|
+
}), validationToRule({
|
670
|
+
ruleId: 'unique-input-field-names',
|
671
|
+
ruleName: 'UniqueInputFieldNames',
|
672
|
+
}, {
|
568
673
|
category: 'Operations',
|
569
674
|
description: 'A GraphQL input object value is only valid if all supplied fields are uniquely named.',
|
570
|
-
}), validationToRule(
|
675
|
+
}), validationToRule({
|
676
|
+
ruleId: 'unique-operation-types',
|
677
|
+
ruleName: 'UniqueOperationTypes',
|
678
|
+
}, {
|
571
679
|
category: 'Schema',
|
572
680
|
description: 'A GraphQL document is only valid if it has only one type per operation.',
|
573
|
-
}), validationToRule(
|
681
|
+
}), validationToRule({
|
682
|
+
ruleId: 'unique-type-names',
|
683
|
+
ruleName: 'UniqueTypeNames',
|
684
|
+
}, {
|
574
685
|
category: 'Schema',
|
575
686
|
description: 'A GraphQL document is only valid if all defined types have unique names.',
|
576
|
-
}), validationToRule(
|
687
|
+
}), validationToRule({
|
688
|
+
ruleId: 'unique-variable-names',
|
689
|
+
ruleName: 'UniqueVariableNames',
|
690
|
+
}, {
|
577
691
|
category: 'Operations',
|
578
692
|
description: 'A GraphQL operation is only valid if all its variables are uniquely named.',
|
579
693
|
requiresSchema: true,
|
580
|
-
}), validationToRule(
|
694
|
+
}), validationToRule({
|
695
|
+
ruleId: 'value-literals-of-correct-type',
|
696
|
+
ruleName: 'ValuesOfCorrectType',
|
697
|
+
hasDidYouMeanSuggestions: true,
|
698
|
+
}, {
|
581
699
|
category: 'Operations',
|
582
700
|
description: 'A GraphQL document is only valid if all value literals are of the type expected at their position.',
|
583
701
|
requiresSchema: true,
|
584
|
-
}), validationToRule(
|
702
|
+
}), validationToRule({
|
703
|
+
ruleId: 'variables-are-input-types',
|
704
|
+
ruleName: 'VariablesAreInputTypes',
|
705
|
+
}, {
|
585
706
|
category: 'Operations',
|
586
707
|
description: 'A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).',
|
587
708
|
requiresSchema: true,
|
588
|
-
}), validationToRule(
|
709
|
+
}), validationToRule({
|
710
|
+
ruleId: 'variables-in-allowed-position',
|
711
|
+
ruleName: 'VariablesInAllowedPosition',
|
712
|
+
}, {
|
589
713
|
category: 'Operations',
|
590
714
|
description: 'Variables passed to field arguments conform to type.',
|
591
715
|
requiresSchema: true,
|
package/index.mjs
CHANGED
@@ -215,7 +215,8 @@ const ARRAY_DEFAULT_OPTIONS = {
|
|
215
215
|
};
|
216
216
|
const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
|
217
217
|
|
218
|
-
function validateDocument(context, schema = null, documentNode, rule) {
|
218
|
+
function validateDocument({ context, schema = null, documentNode, rule, hasDidYouMeanSuggestions, }) {
|
219
|
+
var _a;
|
219
220
|
if (documentNode.definitions.length === 0) {
|
220
221
|
return;
|
221
222
|
}
|
@@ -239,9 +240,20 @@ function validateDocument(context, schema = null, documentNode, rule) {
|
|
239
240
|
? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc
|
240
241
|
: token.loc;
|
241
242
|
}
|
243
|
+
const didYouMeanContent = (_a = error.message.match(/Did you mean (?<content>.*)\?$/)) === null || _a === void 0 ? void 0 : _a.groups.content;
|
244
|
+
const matches = didYouMeanContent ? [...didYouMeanContent.matchAll(/"(?<name>[^"]*)"/g)] : [];
|
242
245
|
context.report({
|
243
246
|
loc,
|
244
247
|
message: error.message,
|
248
|
+
suggest: hasDidYouMeanSuggestions
|
249
|
+
? matches.map(match => {
|
250
|
+
const { name } = match.groups;
|
251
|
+
return {
|
252
|
+
desc: `Rename to \`${name}\``,
|
253
|
+
fix: fixer => fixer.replaceText(token, name),
|
254
|
+
};
|
255
|
+
})
|
256
|
+
: [],
|
245
257
|
});
|
246
258
|
}
|
247
259
|
}
|
@@ -295,7 +307,7 @@ const handleMissingFragments = ({ ruleId, context, node }) => {
|
|
295
307
|
}
|
296
308
|
return node;
|
297
309
|
};
|
298
|
-
const validationToRule = (ruleId, ruleName,
|
310
|
+
const validationToRule = ({ ruleId, ruleName, getDocumentNode, schema = [], hasDidYouMeanSuggestions, }, docs) => {
|
299
311
|
let ruleFn = null;
|
300
312
|
try {
|
301
313
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
@@ -319,6 +331,7 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = [])
|
|
319
331
|
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
|
320
332
|
},
|
321
333
|
schema,
|
334
|
+
hasSuggestions: hasDidYouMeanSuggestions,
|
322
335
|
},
|
323
336
|
create(context) {
|
324
337
|
if (!ruleFn) {
|
@@ -333,30 +346,79 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = [])
|
|
333
346
|
const documentNode = getDocumentNode
|
334
347
|
? getDocumentNode({ ruleId, context, node: node.rawNode() })
|
335
348
|
: node.rawNode();
|
336
|
-
validateDocument(
|
349
|
+
validateDocument({
|
350
|
+
context,
|
351
|
+
schema,
|
352
|
+
documentNode,
|
353
|
+
rule: ruleFn,
|
354
|
+
hasDidYouMeanSuggestions,
|
355
|
+
});
|
337
356
|
},
|
338
357
|
};
|
339
358
|
},
|
340
359
|
},
|
341
360
|
};
|
342
361
|
};
|
343
|
-
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule(
|
362
|
+
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule({
|
363
|
+
ruleId: 'executable-definitions',
|
364
|
+
ruleName: 'ExecutableDefinitions',
|
365
|
+
}, {
|
344
366
|
category: 'Operations',
|
345
367
|
description: 'A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.',
|
346
368
|
requiresSchema: true,
|
347
|
-
}), validationToRule(
|
369
|
+
}), validationToRule({
|
370
|
+
ruleId: 'fields-on-correct-type',
|
371
|
+
ruleName: 'FieldsOnCorrectType',
|
372
|
+
hasDidYouMeanSuggestions: true,
|
373
|
+
}, {
|
348
374
|
category: 'Operations',
|
349
375
|
description: 'A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as `__typename`.',
|
350
376
|
requiresSchema: true,
|
351
|
-
}), validationToRule(
|
377
|
+
}), validationToRule({
|
378
|
+
ruleId: 'fragments-on-composite-type',
|
379
|
+
ruleName: 'FragmentsOnCompositeTypes',
|
380
|
+
}, {
|
352
381
|
category: 'Operations',
|
353
382
|
description: 'Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.',
|
354
383
|
requiresSchema: true,
|
355
|
-
}), validationToRule(
|
384
|
+
}), validationToRule({
|
385
|
+
ruleId: 'known-argument-names',
|
386
|
+
ruleName: 'KnownArgumentNames',
|
387
|
+
hasDidYouMeanSuggestions: true,
|
388
|
+
}, {
|
356
389
|
category: ['Schema', 'Operations'],
|
357
390
|
description: 'A GraphQL field is only valid if all supplied arguments are defined by that field.',
|
358
391
|
requiresSchema: true,
|
359
|
-
}), validationToRule(
|
392
|
+
}), validationToRule({
|
393
|
+
ruleId: 'known-directives',
|
394
|
+
ruleName: 'KnownDirectives',
|
395
|
+
getDocumentNode({ context, node: documentNode }) {
|
396
|
+
const { ignoreClientDirectives = [] } = context.options[0] || {};
|
397
|
+
if (ignoreClientDirectives.length === 0) {
|
398
|
+
return documentNode;
|
399
|
+
}
|
400
|
+
const filterDirectives = (node) => ({
|
401
|
+
...node,
|
402
|
+
directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
|
403
|
+
});
|
404
|
+
return visit(documentNode, {
|
405
|
+
Field: filterDirectives,
|
406
|
+
OperationDefinition: filterDirectives,
|
407
|
+
});
|
408
|
+
},
|
409
|
+
schema: {
|
410
|
+
type: 'array',
|
411
|
+
maxItems: 1,
|
412
|
+
items: {
|
413
|
+
type: 'object',
|
414
|
+
additionalProperties: false,
|
415
|
+
required: ['ignoreClientDirectives'],
|
416
|
+
properties: {
|
417
|
+
ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
|
418
|
+
},
|
419
|
+
},
|
420
|
+
},
|
421
|
+
}, {
|
360
422
|
category: ['Schema', 'Operations'],
|
361
423
|
description: 'A GraphQL document is only valid if all `@directive`s are known by the schema and legally positioned.',
|
362
424
|
requiresSchema: true,
|
@@ -373,31 +435,11 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
373
435
|
`,
|
374
436
|
},
|
375
437
|
],
|
376
|
-
}, ({
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
}
|
381
|
-
const filterDirectives = (node) => ({
|
382
|
-
...node,
|
383
|
-
directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
|
384
|
-
});
|
385
|
-
return visit(documentNode, {
|
386
|
-
Field: filterDirectives,
|
387
|
-
OperationDefinition: filterDirectives,
|
388
|
-
});
|
438
|
+
}), validationToRule({
|
439
|
+
ruleId: 'known-fragment-names',
|
440
|
+
ruleName: 'KnownFragmentNames',
|
441
|
+
getDocumentNode: handleMissingFragments,
|
389
442
|
}, {
|
390
|
-
type: 'array',
|
391
|
-
maxItems: 1,
|
392
|
-
items: {
|
393
|
-
type: 'object',
|
394
|
-
additionalProperties: false,
|
395
|
-
required: ['ignoreClientDirectives'],
|
396
|
-
properties: {
|
397
|
-
ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
|
398
|
-
},
|
399
|
-
},
|
400
|
-
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
401
443
|
category: 'Operations',
|
402
444
|
description: 'A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.',
|
403
445
|
requiresSchema: true,
|
@@ -448,138 +490,220 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
448
490
|
`,
|
449
491
|
},
|
450
492
|
],
|
451
|
-
}
|
493
|
+
}), validationToRule({
|
494
|
+
ruleId: 'known-type-names',
|
495
|
+
ruleName: 'KnownTypeNames',
|
496
|
+
hasDidYouMeanSuggestions: true,
|
497
|
+
}, {
|
452
498
|
category: ['Schema', 'Operations'],
|
453
499
|
description: 'A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.',
|
454
500
|
requiresSchema: true,
|
455
|
-
}), validationToRule(
|
501
|
+
}), validationToRule({
|
502
|
+
ruleId: 'lone-anonymous-operation',
|
503
|
+
ruleName: 'LoneAnonymousOperation',
|
504
|
+
}, {
|
456
505
|
category: 'Operations',
|
457
506
|
description: 'A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.',
|
458
507
|
requiresSchema: true,
|
459
|
-
}), validationToRule(
|
508
|
+
}), validationToRule({
|
509
|
+
ruleId: 'lone-schema-definition',
|
510
|
+
ruleName: 'LoneSchemaDefinition',
|
511
|
+
}, {
|
460
512
|
category: 'Schema',
|
461
513
|
description: 'A GraphQL document is only valid if it contains only one schema definition.',
|
462
|
-
}), validationToRule(
|
514
|
+
}), validationToRule({
|
515
|
+
ruleId: 'no-fragment-cycles',
|
516
|
+
ruleName: 'NoFragmentCycles',
|
517
|
+
}, {
|
463
518
|
category: 'Operations',
|
464
519
|
description: 'A GraphQL fragment is only valid when it does not have cycles in fragments usage.',
|
465
520
|
requiresSchema: true,
|
466
|
-
}), validationToRule(
|
521
|
+
}), validationToRule({
|
522
|
+
ruleId: 'no-undefined-variables',
|
523
|
+
ruleName: 'NoUndefinedVariables',
|
524
|
+
getDocumentNode: handleMissingFragments,
|
525
|
+
}, {
|
467
526
|
category: 'Operations',
|
468
527
|
description: 'A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.',
|
469
528
|
requiresSchema: true,
|
470
529
|
requiresSiblings: true,
|
471
|
-
}
|
530
|
+
}), validationToRule({
|
531
|
+
ruleId: 'no-unused-fragments',
|
532
|
+
ruleName: 'NoUnusedFragments',
|
533
|
+
getDocumentNode: ({ ruleId, context, node }) => {
|
534
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
535
|
+
const FilePathToDocumentsMap = [
|
536
|
+
...siblings.getOperations(),
|
537
|
+
...siblings.getFragments(),
|
538
|
+
].reduce((map, { filePath, document }) => {
|
539
|
+
var _a;
|
540
|
+
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
541
|
+
map[filePath].push(document);
|
542
|
+
return map;
|
543
|
+
}, Object.create(null));
|
544
|
+
const getParentNode = (currentFilePath, node) => {
|
545
|
+
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
|
546
|
+
if (fragmentDefs.size === 0) {
|
547
|
+
return node;
|
548
|
+
}
|
549
|
+
// skip iteration over documents for current filepath
|
550
|
+
delete FilePathToDocumentsMap[currentFilePath];
|
551
|
+
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
552
|
+
const missingFragments = getMissingFragments({
|
553
|
+
kind: Kind.DOCUMENT,
|
554
|
+
definitions: documents,
|
555
|
+
});
|
556
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
557
|
+
if (isCurrentFileImportFragment) {
|
558
|
+
return getParentNode(filePath, {
|
559
|
+
kind: Kind.DOCUMENT,
|
560
|
+
definitions: [...node.definitions, ...documents],
|
561
|
+
});
|
562
|
+
}
|
563
|
+
}
|
564
|
+
return node;
|
565
|
+
};
|
566
|
+
return getParentNode(context.getFilename(), node);
|
567
|
+
},
|
568
|
+
}, {
|
472
569
|
category: 'Operations',
|
473
570
|
description: 'A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.',
|
474
571
|
requiresSchema: true,
|
475
572
|
requiresSiblings: true,
|
476
|
-
}, ({
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
].reduce((map, { filePath, document }) => {
|
482
|
-
var _a;
|
483
|
-
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
484
|
-
map[filePath].push(document);
|
485
|
-
return map;
|
486
|
-
}, Object.create(null));
|
487
|
-
const getParentNode = (currentFilePath, node) => {
|
488
|
-
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
|
489
|
-
if (fragmentDefs.size === 0) {
|
490
|
-
return node;
|
491
|
-
}
|
492
|
-
// skip iteration over documents for current filepath
|
493
|
-
delete FilePathToDocumentsMap[currentFilePath];
|
494
|
-
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
495
|
-
const missingFragments = getMissingFragments({
|
496
|
-
kind: Kind.DOCUMENT,
|
497
|
-
definitions: documents,
|
498
|
-
});
|
499
|
-
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
500
|
-
if (isCurrentFileImportFragment) {
|
501
|
-
return getParentNode(filePath, {
|
502
|
-
kind: Kind.DOCUMENT,
|
503
|
-
definitions: [...node.definitions, ...documents],
|
504
|
-
});
|
505
|
-
}
|
506
|
-
}
|
507
|
-
return node;
|
508
|
-
};
|
509
|
-
return getParentNode(context.getFilename(), node);
|
510
|
-
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
573
|
+
}), validationToRule({
|
574
|
+
ruleId: 'no-unused-variables',
|
575
|
+
ruleName: 'NoUnusedVariables',
|
576
|
+
getDocumentNode: handleMissingFragments,
|
577
|
+
}, {
|
511
578
|
category: 'Operations',
|
512
579
|
description: 'A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.',
|
513
580
|
requiresSchema: true,
|
514
581
|
requiresSiblings: true,
|
515
|
-
}
|
582
|
+
}), validationToRule({
|
583
|
+
ruleId: 'overlapping-fields-can-be-merged',
|
584
|
+
ruleName: 'OverlappingFieldsCanBeMerged',
|
585
|
+
}, {
|
516
586
|
category: 'Operations',
|
517
587
|
description: 'A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.',
|
518
588
|
requiresSchema: true,
|
519
|
-
}), validationToRule(
|
589
|
+
}), validationToRule({
|
590
|
+
ruleId: 'possible-fragment-spread',
|
591
|
+
ruleName: 'PossibleFragmentSpreads',
|
592
|
+
}, {
|
520
593
|
category: 'Operations',
|
521
594
|
description: 'A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.',
|
522
595
|
requiresSchema: true,
|
523
|
-
}), validationToRule(
|
596
|
+
}), validationToRule({
|
597
|
+
ruleId: 'possible-type-extension',
|
598
|
+
ruleName: 'PossibleTypeExtensions',
|
599
|
+
hasDidYouMeanSuggestions: true,
|
600
|
+
}, {
|
524
601
|
category: 'Schema',
|
525
602
|
description: 'A type extension is only valid if the type is defined and has the same kind.',
|
526
603
|
// TODO: add in graphql-eslint v4
|
527
604
|
recommended: false,
|
528
605
|
requiresSchema: true,
|
529
606
|
isDisabledForAllConfig: true,
|
530
|
-
}), validationToRule(
|
607
|
+
}), validationToRule({
|
608
|
+
ruleId: 'provided-required-arguments',
|
609
|
+
ruleName: 'ProvidedRequiredArguments',
|
610
|
+
}, {
|
531
611
|
category: ['Schema', 'Operations'],
|
532
612
|
description: 'A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.',
|
533
613
|
requiresSchema: true,
|
534
|
-
}), validationToRule(
|
614
|
+
}), validationToRule({
|
615
|
+
ruleId: 'scalar-leafs',
|
616
|
+
ruleName: 'ScalarLeafs',
|
617
|
+
hasDidYouMeanSuggestions: true,
|
618
|
+
}, {
|
535
619
|
category: 'Operations',
|
536
620
|
description: 'A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.',
|
537
621
|
requiresSchema: true,
|
538
|
-
}), validationToRule(
|
622
|
+
}), validationToRule({
|
623
|
+
ruleId: 'one-field-subscriptions',
|
624
|
+
ruleName: 'SingleFieldSubscriptions',
|
625
|
+
}, {
|
539
626
|
category: 'Operations',
|
540
627
|
description: 'A GraphQL subscription is valid only if it contains a single root field.',
|
541
628
|
requiresSchema: true,
|
542
|
-
}), validationToRule(
|
629
|
+
}), validationToRule({
|
630
|
+
ruleId: 'unique-argument-names',
|
631
|
+
ruleName: 'UniqueArgumentNames',
|
632
|
+
}, {
|
543
633
|
category: 'Operations',
|
544
634
|
description: 'A GraphQL field or directive is only valid if all supplied arguments are uniquely named.',
|
545
635
|
requiresSchema: true,
|
546
|
-
}), validationToRule(
|
636
|
+
}), validationToRule({
|
637
|
+
ruleId: 'unique-directive-names',
|
638
|
+
ruleName: 'UniqueDirectiveNames',
|
639
|
+
}, {
|
547
640
|
category: 'Schema',
|
548
641
|
description: 'A GraphQL document is only valid if all defined directives have unique names.',
|
549
|
-
}), validationToRule(
|
642
|
+
}), validationToRule({
|
643
|
+
ruleId: 'unique-directive-names-per-location',
|
644
|
+
ruleName: 'UniqueDirectivesPerLocation',
|
645
|
+
}, {
|
550
646
|
category: ['Schema', 'Operations'],
|
551
647
|
description: 'A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.',
|
552
648
|
requiresSchema: true,
|
553
|
-
}), validationToRule(
|
649
|
+
}), validationToRule({
|
650
|
+
ruleId: 'unique-enum-value-names',
|
651
|
+
ruleName: 'UniqueEnumValueNames',
|
652
|
+
}, {
|
554
653
|
category: 'Schema',
|
555
654
|
description: 'A GraphQL enum type is only valid if all its values are uniquely named.',
|
556
655
|
recommended: false,
|
557
656
|
isDisabledForAllConfig: true,
|
558
|
-
}), validationToRule(
|
657
|
+
}), validationToRule({
|
658
|
+
ruleId: 'unique-field-definition-names',
|
659
|
+
ruleName: 'UniqueFieldDefinitionNames',
|
660
|
+
}, {
|
559
661
|
category: 'Schema',
|
560
662
|
description: 'A GraphQL complex type is only valid if all its fields are uniquely named.',
|
561
|
-
}), validationToRule(
|
663
|
+
}), validationToRule({
|
664
|
+
ruleId: 'unique-input-field-names',
|
665
|
+
ruleName: 'UniqueInputFieldNames',
|
666
|
+
}, {
|
562
667
|
category: 'Operations',
|
563
668
|
description: 'A GraphQL input object value is only valid if all supplied fields are uniquely named.',
|
564
|
-
}), validationToRule(
|
669
|
+
}), validationToRule({
|
670
|
+
ruleId: 'unique-operation-types',
|
671
|
+
ruleName: 'UniqueOperationTypes',
|
672
|
+
}, {
|
565
673
|
category: 'Schema',
|
566
674
|
description: 'A GraphQL document is only valid if it has only one type per operation.',
|
567
|
-
}), validationToRule(
|
675
|
+
}), validationToRule({
|
676
|
+
ruleId: 'unique-type-names',
|
677
|
+
ruleName: 'UniqueTypeNames',
|
678
|
+
}, {
|
568
679
|
category: 'Schema',
|
569
680
|
description: 'A GraphQL document is only valid if all defined types have unique names.',
|
570
|
-
}), validationToRule(
|
681
|
+
}), validationToRule({
|
682
|
+
ruleId: 'unique-variable-names',
|
683
|
+
ruleName: 'UniqueVariableNames',
|
684
|
+
}, {
|
571
685
|
category: 'Operations',
|
572
686
|
description: 'A GraphQL operation is only valid if all its variables are uniquely named.',
|
573
687
|
requiresSchema: true,
|
574
|
-
}), validationToRule(
|
688
|
+
}), validationToRule({
|
689
|
+
ruleId: 'value-literals-of-correct-type',
|
690
|
+
ruleName: 'ValuesOfCorrectType',
|
691
|
+
hasDidYouMeanSuggestions: true,
|
692
|
+
}, {
|
575
693
|
category: 'Operations',
|
576
694
|
description: 'A GraphQL document is only valid if all value literals are of the type expected at their position.',
|
577
695
|
requiresSchema: true,
|
578
|
-
}), validationToRule(
|
696
|
+
}), validationToRule({
|
697
|
+
ruleId: 'variables-are-input-types',
|
698
|
+
ruleName: 'VariablesAreInputTypes',
|
699
|
+
}, {
|
579
700
|
category: 'Operations',
|
580
701
|
description: 'A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).',
|
581
702
|
requiresSchema: true,
|
582
|
-
}), validationToRule(
|
703
|
+
}), validationToRule({
|
704
|
+
ruleId: 'variables-in-allowed-position',
|
705
|
+
ruleName: 'VariablesInAllowedPosition',
|
706
|
+
}, {
|
583
707
|
category: 'Operations',
|
584
708
|
description: 'Variables passed to field arguments conform to type.',
|
585
709
|
requiresSchema: true,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@graphql-eslint/eslint-plugin",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.14.0-alpha-20221220004017-f1f0904",
|
4
4
|
"description": "GraphQL plugin for ESLint",
|
5
5
|
"sideEffects": false,
|
6
6
|
"peerDependencies": {
|
@@ -10,7 +10,7 @@
|
|
10
10
|
"@babel/code-frame": "^7.18.6",
|
11
11
|
"@graphql-tools/code-file-loader": "^7.3.6",
|
12
12
|
"@graphql-tools/graphql-tag-pluck": "^7.3.6",
|
13
|
-
"@graphql-tools/utils": "^
|
13
|
+
"@graphql-tools/utils": "^9.0.0",
|
14
14
|
"chalk": "^4.1.2",
|
15
15
|
"debug": "^4.3.4",
|
16
16
|
"fast-glob": "^3.2.12",
|
package/processor.d.ts
CHANGED
package/rules/alphabetize.d.ts
CHANGED
@@ -4,7 +4,7 @@ declare const valuesEnum: ['EnumTypeDefinition'];
|
|
4
4
|
declare const selectionsEnum: ('OperationDefinition' | 'FragmentDefinition')[];
|
5
5
|
declare const variablesEnum: ['OperationDefinition'];
|
6
6
|
declare const argumentsEnum: ('FieldDefinition' | 'Field' | 'DirectiveDefinition' | 'Directive')[];
|
7
|
-
export
|
7
|
+
export type AlphabetizeConfig = {
|
8
8
|
fields?: typeof fieldsEnum;
|
9
9
|
values?: typeof valuesEnum;
|
10
10
|
selections?: typeof selectionsEnum;
|
@@ -1,2 +1,2 @@
|
|
1
|
-
import
|
1
|
+
import { GraphQLESLintRule } from '../types';
|
2
2
|
export declare const GRAPHQL_JS_VALIDATIONS: Record<string, GraphQLESLintRule>;
|
package/rules/input-name.d.ts
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
import { CaseStyle as _CaseStyle } from '../utils';
|
2
2
|
import { GraphQLESLintRule } from '../types';
|
3
|
-
|
3
|
+
type CaseStyle = _CaseStyle | 'matchDocumentStyle';
|
4
4
|
declare const ACCEPTED_EXTENSIONS: ['.gql', '.graphql'];
|
5
|
-
|
5
|
+
type PropertySchema = {
|
6
6
|
style?: CaseStyle;
|
7
7
|
suffix?: string;
|
8
8
|
};
|
9
|
-
export
|
9
|
+
export type MatchDocumentFilenameRuleConfig = {
|
10
10
|
fileExtension?: typeof ACCEPTED_EXTENSIONS[number];
|
11
11
|
query?: CaseStyle | PropertySchema;
|
12
12
|
mutation?: CaseStyle | PropertySchema;
|
@@ -15,9 +15,9 @@ declare const KindToDisplayName: {
|
|
15
15
|
FragmentDefinition: string;
|
16
16
|
VariableDefinition: string;
|
17
17
|
};
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
type AllowedKind = keyof typeof KindToDisplayName;
|
19
|
+
type AllowedStyle = 'camelCase' | 'PascalCase' | 'snake_case' | 'UPPER_CASE';
|
20
|
+
type PropertySchema = {
|
21
21
|
style?: AllowedStyle;
|
22
22
|
suffix?: string;
|
23
23
|
prefix?: string;
|
@@ -25,8 +25,8 @@ declare type PropertySchema = {
|
|
25
25
|
forbiddenSuffixes?: string[];
|
26
26
|
ignorePattern?: string;
|
27
27
|
};
|
28
|
-
|
29
|
-
export
|
28
|
+
type Options = AllowedStyle | PropertySchema;
|
29
|
+
export type NamingConventionRuleConfig = {
|
30
30
|
allowLeadingUnderscore?: boolean;
|
31
31
|
allowTrailingUnderscore?: boolean;
|
32
32
|
types?: Options;
|
package/rules/no-root-type.d.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import type { GraphQLESLintRule } from '../types';
|
2
2
|
declare const ROOT_TYPES: ('mutation' | 'subscription')[];
|
3
|
-
|
3
|
+
type NoRootTypeConfig = {
|
4
4
|
disallow: typeof ROOT_TYPES;
|
5
5
|
};
|
6
6
|
declare const rule: GraphQLESLintRule<[NoRootTypeConfig]>;
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import { Kind } from 'graphql';
|
2
2
|
import type { GraphQLESLintRule } from '../types';
|
3
3
|
declare const ALLOWED_KINDS: readonly [Kind.OBJECT_TYPE_DEFINITION, Kind.INTERFACE_TYPE_DEFINITION, Kind.ENUM_TYPE_DEFINITION, Kind.SCALAR_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.UNION_TYPE_DEFINITION, Kind.DIRECTIVE_DEFINITION, Kind.FIELD_DEFINITION, Kind.INPUT_VALUE_DEFINITION, Kind.ENUM_VALUE_DEFINITION, Kind.OPERATION_DEFINITION];
|
4
|
-
|
5
|
-
export
|
4
|
+
type AllowedKind = typeof ALLOWED_KINDS[number];
|
5
|
+
export type RequireDescriptionRuleConfig = {
|
6
6
|
types?: boolean;
|
7
7
|
} & {
|
8
8
|
[key in AllowedKind]?: boolean;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { GraphQLESLintRule } from '../types';
|
2
|
-
export
|
2
|
+
export type RequireIdWhenAvailableRuleConfig = {
|
3
3
|
fieldName: string | string[];
|
4
4
|
};
|
5
5
|
declare const rule: GraphQLESLintRule<[RequireIdWhenAvailableRuleConfig], true>;
|
package/testkit.d.ts
CHANGED
@@ -2,14 +2,14 @@ import { RuleTester } from 'eslint';
|
|
2
2
|
import type { ASTKindToNode } from 'graphql';
|
3
3
|
import type { GraphQLESTreeNode } from './estree-converter';
|
4
4
|
import type { GraphQLESLintRule, ParserOptions } from './types';
|
5
|
-
export
|
5
|
+
export type GraphQLESLintRuleListener<WithTypeInfo extends boolean = false> = {
|
6
6
|
[K in keyof ASTKindToNode]?: (node: GraphQLESTreeNode<ASTKindToNode[K], WithTypeInfo>) => void;
|
7
7
|
} & Record<string, any>;
|
8
|
-
export
|
8
|
+
export type GraphQLValidTestCase<Options> = Omit<RuleTester.ValidTestCase, 'options' | 'parserOptions'> & {
|
9
9
|
options?: Options;
|
10
10
|
parserOptions?: ParserOptions;
|
11
11
|
};
|
12
|
-
export
|
12
|
+
export type GraphQLInvalidTestCase<T> = GraphQLValidTestCase<T> & {
|
13
13
|
errors: number | (RuleTester.TestCaseError | string)[];
|
14
14
|
output?: string | null;
|
15
15
|
};
|
package/types.d.ts
CHANGED
@@ -5,8 +5,8 @@ import { IExtensions, IGraphQLProject } from 'graphql-config';
|
|
5
5
|
import { GraphQLParseOptions } from '@graphql-tools/utils';
|
6
6
|
import { GraphQLESLintRuleListener } from './testkit';
|
7
7
|
import { SiblingOperations } from './documents';
|
8
|
-
export
|
9
|
-
export
|
8
|
+
export type Schema = GraphQLSchema | Error | null;
|
9
|
+
export type Pointer = string | string[];
|
10
10
|
export interface ParserOptions {
|
11
11
|
schema?: Pointer | Record<string, {
|
12
12
|
headers: Record<string, string>;
|
@@ -24,29 +24,29 @@ export interface ParserOptions {
|
|
24
24
|
skipGraphQLConfig?: boolean;
|
25
25
|
filePath: string;
|
26
26
|
}
|
27
|
-
export
|
27
|
+
export type ParserServices = {
|
28
28
|
schema: Schema;
|
29
29
|
siblingOperations: SiblingOperations;
|
30
30
|
};
|
31
|
-
export
|
31
|
+
export type GraphQLESLintParseResult = Linter.ESLintParseResult & {
|
32
32
|
services: ParserServices;
|
33
33
|
};
|
34
|
-
|
35
|
-
|
34
|
+
type Location = AST.SourceLocation | ESTree.Position;
|
35
|
+
type ReportDescriptorLocation = {
|
36
36
|
node: {
|
37
37
|
loc: Location;
|
38
38
|
};
|
39
39
|
} | {
|
40
40
|
loc: Location;
|
41
41
|
};
|
42
|
-
export
|
43
|
-
export
|
42
|
+
export type ReportDescriptor = Rule.ReportDescriptorMessage & Rule.ReportDescriptorOptions & ReportDescriptorLocation;
|
43
|
+
export type GraphQLESLintRuleContext<Options = any[]> = Omit<Rule.RuleContext, 'parserServices' | 'report' | 'options'> & {
|
44
44
|
options: Options;
|
45
45
|
report(descriptor: ReportDescriptor): void;
|
46
46
|
parserServices?: ParserServices;
|
47
47
|
};
|
48
|
-
export
|
49
|
-
export
|
48
|
+
export type CategoryType = 'Schema' | 'Operations';
|
49
|
+
export type RuleDocsInfo<T> = Omit<Rule.RuleMetaData['docs'], 'category' | 'suggestion'> & {
|
50
50
|
category: CategoryType | CategoryType[];
|
51
51
|
requiresSchema?: true;
|
52
52
|
requiresSiblings?: true;
|
@@ -62,18 +62,18 @@ export declare type RuleDocsInfo<T> = Omit<Rule.RuleMetaData['docs'], 'category'
|
|
62
62
|
graphQLJSRuleName?: string;
|
63
63
|
isDisabledForAllConfig?: true;
|
64
64
|
};
|
65
|
-
export
|
65
|
+
export type GraphQLESLintRule<Options = any[], WithTypeInfo extends boolean = false> = {
|
66
66
|
create(context: GraphQLESLintRuleContext<Options>): GraphQLESLintRuleListener<WithTypeInfo>;
|
67
67
|
meta: Omit<Rule.RuleMetaData, 'docs'> & {
|
68
68
|
docs?: RuleDocsInfo<Options>;
|
69
69
|
};
|
70
70
|
};
|
71
|
-
export
|
72
|
-
|
71
|
+
export type ValueOf<T> = T[keyof T];
|
72
|
+
type Id<T> = {} & {
|
73
73
|
[P in keyof T]: T[P];
|
74
74
|
};
|
75
|
-
|
76
|
-
export
|
75
|
+
type OmitDistributive<T, K extends PropertyKey> = T extends object ? Id<OmitRecursively<T, K>> : T;
|
76
|
+
export type OmitRecursively<T extends object, K extends PropertyKey> = Omit<{
|
77
77
|
[P in keyof T]: OmitDistributive<T[P], K>;
|
78
78
|
}, K>;
|
79
79
|
export {};
|
package/utils.d.ts
CHANGED
@@ -14,7 +14,7 @@ export declare const VIRTUAL_DOCUMENT_REGEX: RegExp;
|
|
14
14
|
export declare const CWD: string;
|
15
15
|
export declare const getTypeName: (node: any) => string;
|
16
16
|
export declare const TYPES_KINDS: readonly [Kind.OBJECT_TYPE_DEFINITION, Kind.INTERFACE_TYPE_DEFINITION, Kind.ENUM_TYPE_DEFINITION, Kind.SCALAR_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.UNION_TYPE_DEFINITION];
|
17
|
-
export
|
17
|
+
export type CaseStyle = 'camelCase' | 'PascalCase' | 'snake_case' | 'UPPER_CASE' | 'kebab-case';
|
18
18
|
export declare const camelCase: (str: string) => string;
|
19
19
|
export declare const convertCase: (style: CaseStyle, str: string) => string;
|
20
20
|
export declare function getLocation(start: Position, fieldName?: string): AST.SourceLocation;
|