@graphql-eslint/eslint-plugin 3.1.0-alpha-878c908.0 → 3.2.0-alpha-6aa2721.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/docs/README.md +57 -58
- package/docs/rules/known-fragment-names.md +9 -30
- package/docs/rules/no-undefined-variables.md +1 -1
- package/docs/rules/no-unreachable-types.md +0 -2
- package/docs/rules/no-unused-fields.md +0 -2
- package/docs/rules/no-unused-variables.md +1 -1
- package/docs/rules/require-id-when-available.md +1 -16
- package/index.js +267 -236
- package/index.mjs +269 -238
- package/package.json +8 -2
- package/rules/graphql-js-validation.d.ts +2 -5
- package/types.d.ts +2 -2
package/index.js
CHANGED
@@ -6,7 +6,6 @@ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'defau
|
|
6
6
|
|
7
7
|
const graphql = require('graphql');
|
8
8
|
const validate = require('graphql/validation/validate');
|
9
|
-
const _import = require('@graphql-tools/import');
|
10
9
|
const fs = require('fs');
|
11
10
|
const path = require('path');
|
12
11
|
const utils = require('@graphql-tools/utils');
|
@@ -335,59 +334,99 @@ function getLocation(loc, fieldName = '', offset) {
|
|
335
334
|
};
|
336
335
|
}
|
337
336
|
|
338
|
-
function
|
339
|
-
|
340
|
-
|
337
|
+
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
338
|
+
if (documentNode.definitions.length === 0) {
|
339
|
+
return;
|
340
|
+
}
|
341
|
+
try {
|
342
|
+
const validationErrors = schema
|
343
|
+
? graphql.validate(schema, documentNode, [rule])
|
344
|
+
: validate.validateSDL(documentNode, null, [rule]);
|
345
|
+
for (const error of validationErrors) {
|
346
|
+
context.report({
|
347
|
+
loc: getLocation({ start: error.locations[0] }),
|
348
|
+
message: error.message,
|
349
|
+
});
|
350
|
+
}
|
351
|
+
}
|
352
|
+
catch (e) {
|
353
|
+
context.report({
|
354
|
+
node: sourceNode,
|
355
|
+
message: e.message,
|
356
|
+
});
|
357
|
+
}
|
341
358
|
}
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
359
|
+
const getFragmentDefsAndFragmentSpreads = (schema, node) => {
|
360
|
+
const typeInfo = new graphql.TypeInfo(schema);
|
361
|
+
const fragmentDefs = new Set();
|
362
|
+
const fragmentSpreads = new Set();
|
363
|
+
const visitor = graphql.visitWithTypeInfo(typeInfo, {
|
364
|
+
FragmentDefinition(node) {
|
365
|
+
fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
|
366
|
+
},
|
367
|
+
FragmentSpread(node) {
|
368
|
+
const parentType = typeInfo.getParentType();
|
369
|
+
if (parentType) {
|
370
|
+
fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
|
353
371
|
}
|
372
|
+
},
|
373
|
+
});
|
374
|
+
graphql.visit(node, visitor);
|
375
|
+
return { fragmentDefs, fragmentSpreads };
|
376
|
+
};
|
377
|
+
const getMissingFragments = (schema, node) => {
|
378
|
+
const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
|
379
|
+
return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
|
380
|
+
};
|
381
|
+
const handleMissingFragments = ({ ruleId, context, schema, node }) => {
|
382
|
+
const missingFragments = getMissingFragments(schema, node);
|
383
|
+
if (missingFragments.length > 0) {
|
384
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
385
|
+
const fragmentsToAdd = [];
|
386
|
+
for (const missingFragment of missingFragments) {
|
387
|
+
const [fragmentName, fragmentTypeName] = missingFragment.split(':');
|
388
|
+
const fragments = siblings
|
389
|
+
.getFragment(fragmentName)
|
390
|
+
.map(source => source.document)
|
391
|
+
.filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
|
392
|
+
fragmentsToAdd.push(fragments[0]);
|
354
393
|
}
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
394
|
+
if (fragmentsToAdd.length > 0) {
|
395
|
+
// recall fn to make sure to add fragments inside fragments
|
396
|
+
return handleMissingFragments({
|
397
|
+
ruleId,
|
398
|
+
context,
|
399
|
+
schema,
|
400
|
+
node: {
|
401
|
+
kind: graphql.Kind.DOCUMENT,
|
402
|
+
definitions: [...node.definitions, ...fragmentsToAdd],
|
403
|
+
},
|
359
404
|
});
|
360
405
|
}
|
361
406
|
}
|
362
|
-
|
363
|
-
const isGraphQLImportFile = rawSDL => {
|
364
|
-
const trimmedRawSDL = rawSDL.trimLeft();
|
365
|
-
return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
|
407
|
+
return node;
|
366
408
|
};
|
367
|
-
const validationToRule = (
|
368
|
-
var _a;
|
409
|
+
const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
369
410
|
let ruleFn = null;
|
370
411
|
try {
|
371
412
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
372
413
|
}
|
373
|
-
catch (
|
414
|
+
catch (_a) {
|
374
415
|
try {
|
375
416
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
376
417
|
}
|
377
|
-
catch (
|
418
|
+
catch (_b) {
|
378
419
|
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
379
420
|
}
|
380
421
|
}
|
381
|
-
const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
|
382
422
|
return {
|
383
|
-
[
|
423
|
+
[ruleId]: {
|
384
424
|
meta: {
|
385
425
|
docs: {
|
386
426
|
recommended: true,
|
387
427
|
...docs,
|
388
428
|
graphQLJSRuleName: ruleName,
|
389
|
-
|
390
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
|
429
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
|
391
430
|
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function. [You can find its source code here](https://github.com/graphql/graphql-js/blob/main/src/validation/rules/${ruleName}Rule.ts).`,
|
392
431
|
},
|
393
432
|
},
|
@@ -396,56 +435,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
|
396
435
|
Document(node) {
|
397
436
|
if (!ruleFn) {
|
398
437
|
// eslint-disable-next-line no-console
|
399
|
-
console.warn(`You rule "${
|
438
|
+
console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
|
400
439
|
return;
|
401
440
|
}
|
402
|
-
const schema = requiresSchema ? requireGraphQLSchemaFromContext(
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
}
|
408
|
-
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
|
441
|
+
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
442
|
+
const documentNode = getDocumentNode
|
443
|
+
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
444
|
+
: node.rawNode();
|
445
|
+
validateDocument(node, context, schema, documentNode, ruleFn);
|
409
446
|
},
|
410
447
|
};
|
411
448
|
},
|
412
449
|
},
|
413
450
|
};
|
414
451
|
};
|
415
|
-
const importFiles = (context) => {
|
416
|
-
const code = context.getSourceCode().text;
|
417
|
-
if (!isGraphQLImportFile(code)) {
|
418
|
-
return null;
|
419
|
-
}
|
420
|
-
// Import documents because file contains '#import' comments
|
421
|
-
return _import.processImport(context.getFilename());
|
422
|
-
};
|
423
452
|
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
424
453
|
category: 'Operations',
|
425
454
|
description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
|
455
|
+
requiresSchema: true,
|
426
456
|
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
427
457
|
category: 'Operations',
|
428
458
|
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`.',
|
459
|
+
requiresSchema: true,
|
429
460
|
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
430
461
|
category: 'Operations',
|
431
462
|
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.`,
|
463
|
+
requiresSchema: true,
|
432
464
|
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
433
465
|
category: ['Schema', 'Operations'],
|
434
466
|
description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
|
467
|
+
requiresSchema: true,
|
435
468
|
}), validationToRule('known-directives', 'KnownDirectives', {
|
436
469
|
category: ['Schema', 'Operations'],
|
437
470
|
description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
|
471
|
+
requiresSchema: true,
|
438
472
|
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
439
473
|
category: 'Operations',
|
440
474
|
description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
|
475
|
+
requiresSchema: true,
|
476
|
+
requiresSiblings: true,
|
441
477
|
examples: [
|
442
478
|
{
|
443
|
-
title: 'Incorrect
|
479
|
+
title: 'Incorrect',
|
444
480
|
code: /* GraphQL */ `
|
445
481
|
query {
|
446
482
|
user {
|
447
483
|
id
|
448
|
-
...UserFields
|
484
|
+
...UserFields # fragment not defined in the document
|
449
485
|
}
|
450
486
|
}
|
451
487
|
`,
|
@@ -467,153 +503,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
467
503
|
`,
|
468
504
|
},
|
469
505
|
{
|
470
|
-
title: 'Correct (
|
506
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
471
507
|
code: /* GraphQL */ `
|
472
|
-
#
|
473
|
-
|
508
|
+
# user.gql
|
474
509
|
query {
|
475
510
|
user {
|
476
511
|
id
|
477
512
|
...UserFields
|
478
513
|
}
|
479
514
|
}
|
480
|
-
`,
|
481
|
-
},
|
482
|
-
{
|
483
|
-
title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
|
484
|
-
code: `
|
485
|
-
const USER_FIELDS = gql\`
|
486
|
-
fragment UserFields on User {
|
487
|
-
id
|
488
|
-
}
|
489
|
-
\`
|
490
|
-
|
491
|
-
const GET_USER = /* GraphQL */ \`
|
492
|
-
# eslint @graphql-eslint/known-fragment-names: 'error'
|
493
|
-
|
494
|
-
query User {
|
495
|
-
user {
|
496
|
-
...UserFields
|
497
|
-
}
|
498
|
-
}
|
499
515
|
|
500
|
-
|
501
|
-
|
502
|
-
|
516
|
+
# user-fields.gql
|
517
|
+
fragment UserFields on User {
|
518
|
+
id
|
519
|
+
}
|
520
|
+
`,
|
503
521
|
},
|
504
522
|
],
|
505
|
-
},
|
523
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
506
524
|
category: ['Schema', 'Operations'],
|
507
525
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
526
|
+
requiresSchema: true,
|
508
527
|
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
509
528
|
category: 'Operations',
|
510
529
|
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.`,
|
530
|
+
requiresSchema: true,
|
511
531
|
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
512
532
|
category: 'Schema',
|
513
533
|
description: `A GraphQL document is only valid if it contains only one schema definition.`,
|
514
|
-
requiresSchema: false,
|
515
534
|
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
516
535
|
category: 'Operations',
|
517
536
|
description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
|
537
|
+
requiresSchema: true,
|
518
538
|
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
519
539
|
category: 'Operations',
|
520
540
|
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
|
521
|
-
|
541
|
+
requiresSchema: true,
|
542
|
+
requiresSiblings: true,
|
543
|
+
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
522
544
|
category: 'Operations',
|
523
545
|
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
|
546
|
+
requiresSchema: true,
|
524
547
|
requiresSiblings: true,
|
525
|
-
}, context => {
|
526
|
-
const siblings = requireSiblingsOperations(
|
527
|
-
const
|
528
|
-
|
529
|
-
|
530
|
-
filePath
|
531
|
-
|
532
|
-
}));
|
533
|
-
const getParentNode = (
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
.filter(isGraphQLImportFile)
|
538
|
-
.map(line => _import.parseImportLine(line.replace('#', '')))
|
539
|
-
.some(o => filePath === path.join(path.dirname(docFilePath), o.from));
|
540
|
-
if (!isFileImported) {
|
541
|
-
continue;
|
542
|
-
}
|
543
|
-
// Import first file that import this file
|
544
|
-
const document = _import.processImport(docFilePath);
|
545
|
-
// Import most top file that import this file
|
546
|
-
return getParentNode(docFilePath) || document;
|
548
|
+
}, ({ ruleId, context, schema, node }) => {
|
549
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
550
|
+
const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
|
551
|
+
var _a;
|
552
|
+
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
553
|
+
map[filePath].push(document);
|
554
|
+
return map;
|
555
|
+
}, Object.create(null));
|
556
|
+
const getParentNode = (currentFilePath, node) => {
|
557
|
+
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
|
558
|
+
if (fragmentDefs.size === 0) {
|
559
|
+
return node;
|
547
560
|
}
|
548
|
-
|
561
|
+
// skip iteration over documents for current filepath
|
562
|
+
delete FilePathToDocumentsMap[currentFilePath];
|
563
|
+
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
564
|
+
const missingFragments = getMissingFragments(schema, {
|
565
|
+
kind: graphql.Kind.DOCUMENT,
|
566
|
+
definitions: documents,
|
567
|
+
});
|
568
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
569
|
+
if (isCurrentFileImportFragment) {
|
570
|
+
return getParentNode(filePath, {
|
571
|
+
kind: graphql.Kind.DOCUMENT,
|
572
|
+
definitions: [...node.definitions, ...documents],
|
573
|
+
});
|
574
|
+
}
|
575
|
+
}
|
576
|
+
return node;
|
549
577
|
};
|
550
|
-
return getParentNode(context.getFilename());
|
578
|
+
return getParentNode(context.getFilename(), node);
|
551
579
|
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
552
580
|
category: 'Operations',
|
553
581
|
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
|
554
|
-
|
582
|
+
requiresSchema: true,
|
583
|
+
requiresSiblings: true,
|
584
|
+
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
555
585
|
category: 'Operations',
|
556
586
|
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.`,
|
587
|
+
requiresSchema: true,
|
557
588
|
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
558
589
|
category: 'Operations',
|
559
590
|
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.`,
|
591
|
+
requiresSchema: true,
|
560
592
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
561
593
|
category: 'Schema',
|
562
594
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
563
|
-
requiresSchema: false,
|
564
595
|
recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
|
565
596
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
566
597
|
category: ['Schema', 'Operations'],
|
567
598
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
599
|
+
requiresSchema: true,
|
568
600
|
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
569
601
|
category: 'Operations',
|
570
602
|
description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
|
603
|
+
requiresSchema: true,
|
571
604
|
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
572
605
|
category: 'Operations',
|
573
606
|
description: `A GraphQL subscription is valid only if it contains a single root field.`,
|
607
|
+
requiresSchema: true,
|
574
608
|
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
575
609
|
category: 'Operations',
|
576
610
|
description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
|
611
|
+
requiresSchema: true,
|
577
612
|
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
578
613
|
category: 'Schema',
|
579
614
|
description: `A GraphQL document is only valid if all defined directives have unique names.`,
|
580
|
-
requiresSchema: false,
|
581
615
|
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
582
616
|
category: ['Schema', 'Operations'],
|
583
617
|
description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
|
618
|
+
requiresSchema: true,
|
584
619
|
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
585
620
|
category: 'Schema',
|
586
621
|
description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
|
587
|
-
requiresSchema: false,
|
588
622
|
recommended: false,
|
589
623
|
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
590
624
|
category: 'Schema',
|
591
625
|
description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
|
592
|
-
requiresSchema: false,
|
593
626
|
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
594
627
|
category: 'Operations',
|
595
628
|
description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
|
596
|
-
requiresSchema: false,
|
597
629
|
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
598
630
|
category: 'Schema',
|
599
631
|
description: `A GraphQL document is only valid if it has only one type per operation.`,
|
600
|
-
requiresSchema: false,
|
601
632
|
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
602
633
|
category: 'Schema',
|
603
634
|
description: `A GraphQL document is only valid if all defined types have unique names.`,
|
604
|
-
requiresSchema: false,
|
605
635
|
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
606
636
|
category: 'Operations',
|
607
637
|
description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
|
638
|
+
requiresSchema: true,
|
608
639
|
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
609
640
|
category: 'Operations',
|
610
641
|
description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
|
642
|
+
requiresSchema: true,
|
611
643
|
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
612
644
|
category: 'Operations',
|
613
645
|
description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
|
646
|
+
requiresSchema: true,
|
614
647
|
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
615
648
|
category: 'Operations',
|
616
649
|
description: `Variables passed to field arguments conform to type.`,
|
650
|
+
requiresSchema: true,
|
617
651
|
}));
|
618
652
|
|
619
653
|
const ALPHABETIZE = 'ALPHABETIZE';
|
@@ -1845,7 +1879,7 @@ const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
|
|
1845
1879
|
const rule$9 = {
|
1846
1880
|
meta: {
|
1847
1881
|
messages: {
|
1848
|
-
[HASHTAG_COMMENT]:
|
1882
|
+
[HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
|
1849
1883
|
},
|
1850
1884
|
docs: {
|
1851
1885
|
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.',
|
@@ -1892,14 +1926,15 @@ const rule$9 = {
|
|
1892
1926
|
schema: [],
|
1893
1927
|
},
|
1894
1928
|
create(context) {
|
1929
|
+
const selector = `${graphql.Kind.DOCUMENT}[definitions.0.kind!=/^(${graphql.Kind.OPERATION_DEFINITION}|${graphql.Kind.FRAGMENT_DEFINITION})$/]`;
|
1895
1930
|
return {
|
1896
|
-
|
1931
|
+
[selector](node) {
|
1897
1932
|
const rawNode = node.rawNode();
|
1898
1933
|
let token = rawNode.loc.startToken;
|
1899
1934
|
while (token !== null) {
|
1900
1935
|
const { kind, prev, next, value, line, column } = token;
|
1901
1936
|
if (kind === graphql.TokenKind.COMMENT && prev && next) {
|
1902
|
-
const isEslintComment = value.
|
1937
|
+
const isEslintComment = value.trimStart().startsWith('eslint');
|
1903
1938
|
const linesAfter = next.line - line;
|
1904
1939
|
if (!isEslintComment && line !== prev.line && next.kind === graphql.TokenKind.NAME && linesAfter < 2) {
|
1905
1940
|
context.report({
|
@@ -2106,7 +2141,22 @@ const rule$c = {
|
|
2106
2141
|
};
|
2107
2142
|
|
2108
2143
|
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
|
2109
|
-
const
|
2144
|
+
const RULE_ID = 'no-unreachable-types';
|
2145
|
+
const KINDS = [
|
2146
|
+
graphql.Kind.DIRECTIVE_DEFINITION,
|
2147
|
+
graphql.Kind.OBJECT_TYPE_DEFINITION,
|
2148
|
+
graphql.Kind.OBJECT_TYPE_EXTENSION,
|
2149
|
+
graphql.Kind.INTERFACE_TYPE_DEFINITION,
|
2150
|
+
graphql.Kind.INTERFACE_TYPE_EXTENSION,
|
2151
|
+
graphql.Kind.SCALAR_TYPE_DEFINITION,
|
2152
|
+
graphql.Kind.SCALAR_TYPE_EXTENSION,
|
2153
|
+
graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
2154
|
+
graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
2155
|
+
graphql.Kind.UNION_TYPE_DEFINITION,
|
2156
|
+
graphql.Kind.UNION_TYPE_EXTENSION,
|
2157
|
+
graphql.Kind.ENUM_TYPE_DEFINITION,
|
2158
|
+
graphql.Kind.ENUM_TYPE_EXTENSION,
|
2159
|
+
];
|
2110
2160
|
const rule$d = {
|
2111
2161
|
meta: {
|
2112
2162
|
messages: {
|
@@ -2115,7 +2165,7 @@ const rule$d = {
|
|
2115
2165
|
docs: {
|
2116
2166
|
description: `Requires all types to be reachable at some level by root level fields.`,
|
2117
2167
|
category: 'Schema',
|
2118
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${
|
2168
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
2119
2169
|
requiresSchema: true,
|
2120
2170
|
examples: [
|
2121
2171
|
{
|
@@ -2147,43 +2197,36 @@ const rule$d = {
|
|
2147
2197
|
],
|
2148
2198
|
recommended: true,
|
2149
2199
|
},
|
2150
|
-
fixable: 'code',
|
2151
2200
|
type: 'suggestion',
|
2152
2201
|
schema: [],
|
2202
|
+
hasSuggestions: true,
|
2153
2203
|
},
|
2154
2204
|
create(context) {
|
2155
|
-
const reachableTypes = requireReachableTypesFromContext(
|
2156
|
-
|
2157
|
-
const typeName = node.name.value;
|
2158
|
-
if (!reachableTypes.has(typeName)) {
|
2159
|
-
context.report({
|
2160
|
-
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === graphql.Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
|
2161
|
-
messageId: UNREACHABLE_TYPE,
|
2162
|
-
data: { typeName },
|
2163
|
-
fix: fixer => fixer.remove(node),
|
2164
|
-
});
|
2165
|
-
}
|
2166
|
-
}
|
2205
|
+
const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
|
2206
|
+
const selector = KINDS.join(',');
|
2167
2207
|
return {
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
|
2173
|
-
|
2174
|
-
|
2175
|
-
|
2176
|
-
|
2177
|
-
|
2178
|
-
|
2179
|
-
|
2180
|
-
|
2208
|
+
[selector](node) {
|
2209
|
+
const typeName = node.name.value;
|
2210
|
+
if (!reachableTypes.has(typeName)) {
|
2211
|
+
context.report({
|
2212
|
+
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === graphql.Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
|
2213
|
+
messageId: UNREACHABLE_TYPE,
|
2214
|
+
data: { typeName },
|
2215
|
+
suggest: [
|
2216
|
+
{
|
2217
|
+
desc: `Remove ${typeName}`,
|
2218
|
+
fix: fixer => fixer.remove(node),
|
2219
|
+
},
|
2220
|
+
],
|
2221
|
+
});
|
2222
|
+
}
|
2223
|
+
},
|
2181
2224
|
};
|
2182
2225
|
},
|
2183
2226
|
};
|
2184
2227
|
|
2185
2228
|
const UNUSED_FIELD = 'UNUSED_FIELD';
|
2186
|
-
const
|
2229
|
+
const RULE_ID$1 = 'no-unused-fields';
|
2187
2230
|
const rule$e = {
|
2188
2231
|
meta: {
|
2189
2232
|
messages: {
|
@@ -2192,7 +2235,7 @@ const rule$e = {
|
|
2192
2235
|
docs: {
|
2193
2236
|
description: `Requires all fields to be used at some level by siblings operations.`,
|
2194
2237
|
category: 'Schema',
|
2195
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${
|
2238
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
|
2196
2239
|
requiresSiblings: true,
|
2197
2240
|
requiresSchema: true,
|
2198
2241
|
examples: [
|
@@ -2239,12 +2282,12 @@ const rule$e = {
|
|
2239
2282
|
},
|
2240
2283
|
],
|
2241
2284
|
},
|
2242
|
-
fixable: 'code',
|
2243
2285
|
type: 'suggestion',
|
2244
2286
|
schema: [],
|
2287
|
+
hasSuggestions: true,
|
2245
2288
|
},
|
2246
2289
|
create(context) {
|
2247
|
-
const usedFields = requireUsedFieldsFromContext(
|
2290
|
+
const usedFields = requireUsedFieldsFromContext(RULE_ID$1, context);
|
2248
2291
|
return {
|
2249
2292
|
FieldDefinition(node) {
|
2250
2293
|
var _a;
|
@@ -2258,22 +2301,18 @@ const rule$e = {
|
|
2258
2301
|
loc: getLocation(node.loc, fieldName),
|
2259
2302
|
messageId: UNUSED_FIELD,
|
2260
2303
|
data: { fieldName },
|
2261
|
-
|
2262
|
-
|
2263
|
-
|
2264
|
-
|
2265
|
-
|
2266
|
-
|
2267
|
-
|
2268
|
-
|
2269
|
-
|
2270
|
-
|
2271
|
-
|
2272
|
-
|
2273
|
-
}
|
2274
|
-
// Remove whitespace before token
|
2275
|
-
return fixer.removeRange([tokenBefore.range[1], node.range[1]]);
|
2276
|
-
},
|
2304
|
+
suggest: [
|
2305
|
+
{
|
2306
|
+
desc: `Remove "${fieldName}" field`,
|
2307
|
+
fix(fixer) {
|
2308
|
+
const sourceCode = context.getSourceCode();
|
2309
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
2310
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
2311
|
+
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
|
2312
|
+
return isEmptyType ? fixer.remove(node.parent) : fixer.remove(node);
|
2313
|
+
},
|
2314
|
+
},
|
2315
|
+
],
|
2277
2316
|
});
|
2278
2317
|
},
|
2279
2318
|
};
|
@@ -2636,14 +2675,14 @@ const rule$h = {
|
|
2636
2675
|
},
|
2637
2676
|
};
|
2638
2677
|
|
2639
|
-
const RULE_NAME
|
2678
|
+
const RULE_NAME = 'require-field-of-type-query-in-mutation-result';
|
2640
2679
|
const rule$i = {
|
2641
2680
|
meta: {
|
2642
2681
|
type: 'suggestion',
|
2643
2682
|
docs: {
|
2644
2683
|
category: 'Schema',
|
2645
2684
|
description: 'Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.\n> Currently, no errors are reported for result type `union`, `interface` and `scalar`.',
|
2646
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME
|
2685
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`,
|
2647
2686
|
requiresSchema: true,
|
2648
2687
|
examples: [
|
2649
2688
|
{
|
@@ -2678,7 +2717,7 @@ const rule$i = {
|
|
2678
2717
|
schema: [],
|
2679
2718
|
},
|
2680
2719
|
create(context) {
|
2681
|
-
const schema = requireGraphQLSchemaFromContext(RULE_NAME
|
2720
|
+
const schema = requireGraphQLSchemaFromContext(RULE_NAME, context);
|
2682
2721
|
const mutationType = schema.getMutationType();
|
2683
2722
|
const queryType = schema.getQueryType();
|
2684
2723
|
if (!mutationType || !queryType) {
|
@@ -2852,22 +2891,9 @@ const rule$j = {
|
|
2852
2891
|
recommended: true,
|
2853
2892
|
},
|
2854
2893
|
messages: {
|
2855
|
-
[REQUIRE_ID_WHEN_AVAILABLE]:
|
2856
|
-
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2857
|
-
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2858
|
-
].join('\n'),
|
2894
|
+
[REQUIRE_ID_WHEN_AVAILABLE]: `Field "{{ fieldName }}" must be selected when it's available on a type. Please make sure to include it in your selection set!\nIf you are using fragments, make sure that all used fragments {{ checkedFragments }} specifies the field "{{ fieldName }}".`,
|
2859
2895
|
},
|
2860
2896
|
schema: {
|
2861
|
-
definitions: {
|
2862
|
-
asString: {
|
2863
|
-
type: 'string',
|
2864
|
-
},
|
2865
|
-
asArray: {
|
2866
|
-
type: 'array',
|
2867
|
-
minItems: 1,
|
2868
|
-
uniqueItems: true,
|
2869
|
-
},
|
2870
|
-
},
|
2871
2897
|
type: 'array',
|
2872
2898
|
maxItems: 1,
|
2873
2899
|
items: {
|
@@ -2875,7 +2901,7 @@ const rule$j = {
|
|
2875
2901
|
additionalProperties: false,
|
2876
2902
|
properties: {
|
2877
2903
|
fieldName: {
|
2878
|
-
|
2904
|
+
type: 'string',
|
2879
2905
|
default: DEFAULT_ID_FIELD_NAME,
|
2880
2906
|
},
|
2881
2907
|
},
|
@@ -2883,64 +2909,69 @@ const rule$j = {
|
|
2883
2909
|
},
|
2884
2910
|
},
|
2885
2911
|
create(context) {
|
2886
|
-
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
2887
|
-
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2888
|
-
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2889
|
-
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
2890
|
-
const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
|
2891
2912
|
return {
|
2892
2913
|
SelectionSet(node) {
|
2893
2914
|
var _a, _b;
|
2894
|
-
|
2895
|
-
|
2896
|
-
|
2897
|
-
|
2898
|
-
const rawType = getBaseType(typeInfo.gqlType);
|
2899
|
-
const isObjectType = rawType instanceof graphql.GraphQLObjectType;
|
2900
|
-
const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
|
2901
|
-
if (!isObjectType && !isInterfaceType) {
|
2915
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
2916
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2917
|
+
const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
|
2918
|
+
if (!node.selections || node.selections.length === 0) {
|
2902
2919
|
return;
|
2903
2920
|
}
|
2904
|
-
const
|
2905
|
-
|
2906
|
-
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
2911
|
-
|
2912
|
-
|
2913
|
-
|
2914
|
-
|
2915
|
-
|
2916
|
-
|
2917
|
-
|
2918
|
-
|
2919
|
-
|
2920
|
-
|
2921
|
-
|
2922
|
-
|
2921
|
+
const typeInfo = node.typeInfo();
|
2922
|
+
if (typeInfo && typeInfo.gqlType) {
|
2923
|
+
const rawType = getBaseType(typeInfo.gqlType);
|
2924
|
+
if (rawType instanceof graphql.GraphQLObjectType || rawType instanceof graphql.GraphQLInterfaceType) {
|
2925
|
+
const fields = rawType.getFields();
|
2926
|
+
const hasIdFieldInType = !!fields[fieldName];
|
2927
|
+
const checkedFragmentSpreads = new Set();
|
2928
|
+
if (hasIdFieldInType) {
|
2929
|
+
let found = false;
|
2930
|
+
for (const selection of node.selections) {
|
2931
|
+
if (selection.kind === 'Field' && selection.name.value === fieldName) {
|
2932
|
+
found = true;
|
2933
|
+
}
|
2934
|
+
else if (selection.kind === 'InlineFragment') {
|
2935
|
+
found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2936
|
+
}
|
2937
|
+
else if (selection.kind === 'FragmentSpread') {
|
2938
|
+
const foundSpread = siblings.getFragment(selection.name.value);
|
2939
|
+
if (foundSpread[0]) {
|
2940
|
+
checkedFragmentSpreads.add(foundSpread[0].document.name.value);
|
2941
|
+
found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2942
|
+
}
|
2943
|
+
}
|
2944
|
+
if (found) {
|
2945
|
+
break;
|
2946
|
+
}
|
2947
|
+
}
|
2948
|
+
const { parent } = node;
|
2949
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2950
|
+
parent.kind === 'InlineFragment' &&
|
2951
|
+
parent.parent &&
|
2952
|
+
parent.parent.kind === 'SelectionSet' &&
|
2953
|
+
parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2954
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2955
|
+
context.report({
|
2956
|
+
loc: {
|
2957
|
+
start: {
|
2958
|
+
line: node.loc.start.line,
|
2959
|
+
column: node.loc.start.column - 1,
|
2960
|
+
},
|
2961
|
+
end: {
|
2962
|
+
line: node.loc.end.line,
|
2963
|
+
column: node.loc.end.column - 1,
|
2964
|
+
},
|
2965
|
+
},
|
2966
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2967
|
+
data: {
|
2968
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
|
2969
|
+
fieldName,
|
2970
|
+
},
|
2971
|
+
});
|
2972
|
+
}
|
2923
2973
|
}
|
2924
2974
|
}
|
2925
|
-
if (found) {
|
2926
|
-
break;
|
2927
|
-
}
|
2928
|
-
}
|
2929
|
-
const { parent } = node;
|
2930
|
-
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2931
|
-
parent.kind === graphql.Kind.INLINE_FRAGMENT &&
|
2932
|
-
parent.parent &&
|
2933
|
-
parent.parent.kind === graphql.Kind.SELECTION_SET &&
|
2934
|
-
parent.parent.selections.some(s => isFound(s));
|
2935
|
-
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2936
|
-
context.report({
|
2937
|
-
loc: getLocation(node.loc),
|
2938
|
-
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2939
|
-
data: {
|
2940
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
2941
|
-
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
2942
|
-
},
|
2943
|
-
});
|
2944
2975
|
}
|
2945
2976
|
},
|
2946
2977
|
};
|
@@ -3032,7 +3063,7 @@ const rule$k = {
|
|
3032
3063
|
// eslint-disable-next-line no-console
|
3033
3064
|
console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3034
3065
|
}
|
3035
|
-
const
|
3066
|
+
const maxDepth = context.options[0].maxDepth;
|
3036
3067
|
const ignore = context.options[0].ignore || [];
|
3037
3068
|
const checkFn = depthLimit(maxDepth, { ignore });
|
3038
3069
|
return {
|
@@ -3229,7 +3260,7 @@ const rule$l = {
|
|
3229
3260
|
},
|
3230
3261
|
};
|
3231
3262
|
|
3232
|
-
const RULE_NAME$
|
3263
|
+
const RULE_NAME$1 = 'unique-fragment-name';
|
3233
3264
|
const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
|
3234
3265
|
const checkNode = (context, node, ruleName, messageId) => {
|
3235
3266
|
const documentName = node.name.value;
|
@@ -3261,7 +3292,7 @@ const rule$m = {
|
|
3261
3292
|
docs: {
|
3262
3293
|
category: 'Operations',
|
3263
3294
|
description: `Enforce unique fragment names across your project.`,
|
3264
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$
|
3295
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$1}.md`,
|
3265
3296
|
requiresSiblings: true,
|
3266
3297
|
examples: [
|
3267
3298
|
{
|
@@ -3306,13 +3337,13 @@ const rule$m = {
|
|
3306
3337
|
create(context) {
|
3307
3338
|
return {
|
3308
3339
|
FragmentDefinition(node) {
|
3309
|
-
checkNode(context, node, RULE_NAME$
|
3340
|
+
checkNode(context, node, RULE_NAME$1, UNIQUE_FRAGMENT_NAME);
|
3310
3341
|
},
|
3311
3342
|
};
|
3312
3343
|
},
|
3313
3344
|
};
|
3314
3345
|
|
3315
|
-
const RULE_NAME$
|
3346
|
+
const RULE_NAME$2 = 'unique-operation-name';
|
3316
3347
|
const UNIQUE_OPERATION_NAME = 'UNIQUE_OPERATION_NAME';
|
3317
3348
|
const rule$n = {
|
3318
3349
|
meta: {
|
@@ -3320,7 +3351,7 @@ const rule$n = {
|
|
3320
3351
|
docs: {
|
3321
3352
|
category: 'Operations',
|
3322
3353
|
description: `Enforce unique operation names across your project.`,
|
3323
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$
|
3354
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$2}.md`,
|
3324
3355
|
requiresSiblings: true,
|
3325
3356
|
examples: [
|
3326
3357
|
{
|
@@ -3369,7 +3400,7 @@ const rule$n = {
|
|
3369
3400
|
create(context) {
|
3370
3401
|
return {
|
3371
3402
|
'OperationDefinition[name!=undefined]'(node) {
|
3372
|
-
checkNode(context, node, RULE_NAME$
|
3403
|
+
checkNode(context, node, RULE_NAME$2, UNIQUE_OPERATION_NAME);
|
3373
3404
|
},
|
3374
3405
|
};
|
3375
3406
|
},
|