@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.mjs
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
import { Kind, validate, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1,
|
1
|
+
import { Kind, validate, TypeInfo, visitWithTypeInfo, visit, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, isInterfaceType, Source, GraphQLError } from 'graphql';
|
2
2
|
import { validateSDL } from 'graphql/validation/validate';
|
3
|
-
import { processImport, parseImportLine } from '@graphql-tools/import';
|
4
3
|
import { statSync, existsSync, readFileSync } from 'fs';
|
5
|
-
import { dirname,
|
4
|
+
import { dirname, extname, basename, relative, resolve } from 'path';
|
6
5
|
import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
|
7
6
|
import lowerCase from 'lodash.lowercase';
|
8
7
|
import depthLimit from 'graphql-depth-limit';
|
@@ -329,59 +328,99 @@ function getLocation(loc, fieldName = '', offset) {
|
|
329
328
|
};
|
330
329
|
}
|
331
330
|
|
332
|
-
function
|
333
|
-
|
334
|
-
|
331
|
+
function validateDocument(sourceNode, context, schema, documentNode, rule) {
|
332
|
+
if (documentNode.definitions.length === 0) {
|
333
|
+
return;
|
334
|
+
}
|
335
|
+
try {
|
336
|
+
const validationErrors = schema
|
337
|
+
? validate(schema, documentNode, [rule])
|
338
|
+
: validateSDL(documentNode, null, [rule]);
|
339
|
+
for (const error of validationErrors) {
|
340
|
+
context.report({
|
341
|
+
loc: getLocation({ start: error.locations[0] }),
|
342
|
+
message: error.message,
|
343
|
+
});
|
344
|
+
}
|
345
|
+
}
|
346
|
+
catch (e) {
|
347
|
+
context.report({
|
348
|
+
node: sourceNode,
|
349
|
+
message: e.message,
|
350
|
+
});
|
351
|
+
}
|
335
352
|
}
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
353
|
+
const getFragmentDefsAndFragmentSpreads = (schema, node) => {
|
354
|
+
const typeInfo = new TypeInfo(schema);
|
355
|
+
const fragmentDefs = new Set();
|
356
|
+
const fragmentSpreads = new Set();
|
357
|
+
const visitor = visitWithTypeInfo(typeInfo, {
|
358
|
+
FragmentDefinition(node) {
|
359
|
+
fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
|
360
|
+
},
|
361
|
+
FragmentSpread(node) {
|
362
|
+
const parentType = typeInfo.getParentType();
|
363
|
+
if (parentType) {
|
364
|
+
fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
|
347
365
|
}
|
366
|
+
},
|
367
|
+
});
|
368
|
+
visit(node, visitor);
|
369
|
+
return { fragmentDefs, fragmentSpreads };
|
370
|
+
};
|
371
|
+
const getMissingFragments = (schema, node) => {
|
372
|
+
const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
|
373
|
+
return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
|
374
|
+
};
|
375
|
+
const handleMissingFragments = ({ ruleId, context, schema, node }) => {
|
376
|
+
const missingFragments = getMissingFragments(schema, node);
|
377
|
+
if (missingFragments.length > 0) {
|
378
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
379
|
+
const fragmentsToAdd = [];
|
380
|
+
for (const missingFragment of missingFragments) {
|
381
|
+
const [fragmentName, fragmentTypeName] = missingFragment.split(':');
|
382
|
+
const fragments = siblings
|
383
|
+
.getFragment(fragmentName)
|
384
|
+
.map(source => source.document)
|
385
|
+
.filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
|
386
|
+
fragmentsToAdd.push(fragments[0]);
|
348
387
|
}
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
388
|
+
if (fragmentsToAdd.length > 0) {
|
389
|
+
// recall fn to make sure to add fragments inside fragments
|
390
|
+
return handleMissingFragments({
|
391
|
+
ruleId,
|
392
|
+
context,
|
393
|
+
schema,
|
394
|
+
node: {
|
395
|
+
kind: Kind.DOCUMENT,
|
396
|
+
definitions: [...node.definitions, ...fragmentsToAdd],
|
397
|
+
},
|
353
398
|
});
|
354
399
|
}
|
355
400
|
}
|
356
|
-
|
357
|
-
const isGraphQLImportFile = rawSDL => {
|
358
|
-
const trimmedRawSDL = rawSDL.trimLeft();
|
359
|
-
return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
|
401
|
+
return node;
|
360
402
|
};
|
361
|
-
const validationToRule = (
|
362
|
-
var _a;
|
403
|
+
const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
363
404
|
let ruleFn = null;
|
364
405
|
try {
|
365
406
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
366
407
|
}
|
367
|
-
catch (
|
408
|
+
catch (_a) {
|
368
409
|
try {
|
369
410
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
370
411
|
}
|
371
|
-
catch (
|
412
|
+
catch (_b) {
|
372
413
|
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
373
414
|
}
|
374
415
|
}
|
375
|
-
const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
|
376
416
|
return {
|
377
|
-
[
|
417
|
+
[ruleId]: {
|
378
418
|
meta: {
|
379
419
|
docs: {
|
380
420
|
recommended: true,
|
381
421
|
...docs,
|
382
422
|
graphQLJSRuleName: ruleName,
|
383
|
-
|
384
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
|
423
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
|
385
424
|
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).`,
|
386
425
|
},
|
387
426
|
},
|
@@ -390,56 +429,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
|
390
429
|
Document(node) {
|
391
430
|
if (!ruleFn) {
|
392
431
|
// eslint-disable-next-line no-console
|
393
|
-
console.warn(`You rule "${
|
432
|
+
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...`);
|
394
433
|
return;
|
395
434
|
}
|
396
|
-
const schema = requiresSchema ? requireGraphQLSchemaFromContext(
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
}
|
402
|
-
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
|
435
|
+
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
436
|
+
const documentNode = getDocumentNode
|
437
|
+
? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
|
438
|
+
: node.rawNode();
|
439
|
+
validateDocument(node, context, schema, documentNode, ruleFn);
|
403
440
|
},
|
404
441
|
};
|
405
442
|
},
|
406
443
|
},
|
407
444
|
};
|
408
445
|
};
|
409
|
-
const importFiles = (context) => {
|
410
|
-
const code = context.getSourceCode().text;
|
411
|
-
if (!isGraphQLImportFile(code)) {
|
412
|
-
return null;
|
413
|
-
}
|
414
|
-
// Import documents because file contains '#import' comments
|
415
|
-
return processImport(context.getFilename());
|
416
|
-
};
|
417
446
|
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
418
447
|
category: 'Operations',
|
419
448
|
description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
|
449
|
+
requiresSchema: true,
|
420
450
|
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
421
451
|
category: 'Operations',
|
422
452
|
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`.',
|
453
|
+
requiresSchema: true,
|
423
454
|
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
424
455
|
category: 'Operations',
|
425
456
|
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.`,
|
457
|
+
requiresSchema: true,
|
426
458
|
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
427
459
|
category: ['Schema', 'Operations'],
|
428
460
|
description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
|
461
|
+
requiresSchema: true,
|
429
462
|
}), validationToRule('known-directives', 'KnownDirectives', {
|
430
463
|
category: ['Schema', 'Operations'],
|
431
464
|
description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
|
465
|
+
requiresSchema: true,
|
432
466
|
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
433
467
|
category: 'Operations',
|
434
468
|
description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
|
469
|
+
requiresSchema: true,
|
470
|
+
requiresSiblings: true,
|
435
471
|
examples: [
|
436
472
|
{
|
437
|
-
title: 'Incorrect
|
473
|
+
title: 'Incorrect',
|
438
474
|
code: /* GraphQL */ `
|
439
475
|
query {
|
440
476
|
user {
|
441
477
|
id
|
442
|
-
...UserFields
|
478
|
+
...UserFields # fragment not defined in the document
|
443
479
|
}
|
444
480
|
}
|
445
481
|
`,
|
@@ -461,153 +497,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
461
497
|
`,
|
462
498
|
},
|
463
499
|
{
|
464
|
-
title: 'Correct (
|
500
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
465
501
|
code: /* GraphQL */ `
|
466
|
-
#
|
467
|
-
|
502
|
+
# user.gql
|
468
503
|
query {
|
469
504
|
user {
|
470
505
|
id
|
471
506
|
...UserFields
|
472
507
|
}
|
473
508
|
}
|
474
|
-
`,
|
475
|
-
},
|
476
|
-
{
|
477
|
-
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.",
|
478
|
-
code: `
|
479
|
-
const USER_FIELDS = gql\`
|
480
|
-
fragment UserFields on User {
|
481
|
-
id
|
482
|
-
}
|
483
|
-
\`
|
484
|
-
|
485
|
-
const GET_USER = /* GraphQL */ \`
|
486
|
-
# eslint @graphql-eslint/known-fragment-names: 'error'
|
487
|
-
|
488
|
-
query User {
|
489
|
-
user {
|
490
|
-
...UserFields
|
491
|
-
}
|
492
|
-
}
|
493
509
|
|
494
|
-
|
495
|
-
|
496
|
-
|
510
|
+
# user-fields.gql
|
511
|
+
fragment UserFields on User {
|
512
|
+
id
|
513
|
+
}
|
514
|
+
`,
|
497
515
|
},
|
498
516
|
],
|
499
|
-
},
|
517
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
500
518
|
category: ['Schema', 'Operations'],
|
501
519
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
520
|
+
requiresSchema: true,
|
502
521
|
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
503
522
|
category: 'Operations',
|
504
523
|
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.`,
|
524
|
+
requiresSchema: true,
|
505
525
|
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
506
526
|
category: 'Schema',
|
507
527
|
description: `A GraphQL document is only valid if it contains only one schema definition.`,
|
508
|
-
requiresSchema: false,
|
509
528
|
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
510
529
|
category: 'Operations',
|
511
530
|
description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
|
531
|
+
requiresSchema: true,
|
512
532
|
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
513
533
|
category: 'Operations',
|
514
534
|
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
|
515
|
-
|
535
|
+
requiresSchema: true,
|
536
|
+
requiresSiblings: true,
|
537
|
+
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
516
538
|
category: 'Operations',
|
517
539
|
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
|
540
|
+
requiresSchema: true,
|
518
541
|
requiresSiblings: true,
|
519
|
-
}, context => {
|
520
|
-
const siblings = requireSiblingsOperations(
|
521
|
-
const
|
522
|
-
|
523
|
-
|
524
|
-
filePath
|
525
|
-
|
526
|
-
}));
|
527
|
-
const getParentNode = (
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
.filter(isGraphQLImportFile)
|
532
|
-
.map(line => parseImportLine(line.replace('#', '')))
|
533
|
-
.some(o => filePath === join(dirname(docFilePath), o.from));
|
534
|
-
if (!isFileImported) {
|
535
|
-
continue;
|
536
|
-
}
|
537
|
-
// Import first file that import this file
|
538
|
-
const document = processImport(docFilePath);
|
539
|
-
// Import most top file that import this file
|
540
|
-
return getParentNode(docFilePath) || document;
|
542
|
+
}, ({ ruleId, context, schema, node }) => {
|
543
|
+
const siblings = requireSiblingsOperations(ruleId, context);
|
544
|
+
const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].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(schema, node);
|
552
|
+
if (fragmentDefs.size === 0) {
|
553
|
+
return node;
|
541
554
|
}
|
542
|
-
|
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(schema, {
|
559
|
+
kind: Kind.DOCUMENT,
|
560
|
+
definitions: documents,
|
561
|
+
});
|
562
|
+
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
563
|
+
if (isCurrentFileImportFragment) {
|
564
|
+
return getParentNode(filePath, {
|
565
|
+
kind: Kind.DOCUMENT,
|
566
|
+
definitions: [...node.definitions, ...documents],
|
567
|
+
});
|
568
|
+
}
|
569
|
+
}
|
570
|
+
return node;
|
543
571
|
};
|
544
|
-
return getParentNode(context.getFilename());
|
572
|
+
return getParentNode(context.getFilename(), node);
|
545
573
|
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
546
574
|
category: 'Operations',
|
547
575
|
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
|
548
|
-
|
576
|
+
requiresSchema: true,
|
577
|
+
requiresSiblings: true,
|
578
|
+
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
549
579
|
category: 'Operations',
|
550
580
|
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.`,
|
581
|
+
requiresSchema: true,
|
551
582
|
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
552
583
|
category: 'Operations',
|
553
584
|
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.`,
|
585
|
+
requiresSchema: true,
|
554
586
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
555
587
|
category: 'Schema',
|
556
588
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
557
|
-
requiresSchema: false,
|
558
589
|
recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
|
559
590
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
560
591
|
category: ['Schema', 'Operations'],
|
561
592
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
593
|
+
requiresSchema: true,
|
562
594
|
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
563
595
|
category: 'Operations',
|
564
596
|
description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
|
597
|
+
requiresSchema: true,
|
565
598
|
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
566
599
|
category: 'Operations',
|
567
600
|
description: `A GraphQL subscription is valid only if it contains a single root field.`,
|
601
|
+
requiresSchema: true,
|
568
602
|
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
569
603
|
category: 'Operations',
|
570
604
|
description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
|
605
|
+
requiresSchema: true,
|
571
606
|
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
572
607
|
category: 'Schema',
|
573
608
|
description: `A GraphQL document is only valid if all defined directives have unique names.`,
|
574
|
-
requiresSchema: false,
|
575
609
|
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
576
610
|
category: ['Schema', 'Operations'],
|
577
611
|
description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
|
612
|
+
requiresSchema: true,
|
578
613
|
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
579
614
|
category: 'Schema',
|
580
615
|
description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
|
581
|
-
requiresSchema: false,
|
582
616
|
recommended: false,
|
583
617
|
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
584
618
|
category: 'Schema',
|
585
619
|
description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
|
586
|
-
requiresSchema: false,
|
587
620
|
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
588
621
|
category: 'Operations',
|
589
622
|
description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
|
590
|
-
requiresSchema: false,
|
591
623
|
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
592
624
|
category: 'Schema',
|
593
625
|
description: `A GraphQL document is only valid if it has only one type per operation.`,
|
594
|
-
requiresSchema: false,
|
595
626
|
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
596
627
|
category: 'Schema',
|
597
628
|
description: `A GraphQL document is only valid if all defined types have unique names.`,
|
598
|
-
requiresSchema: false,
|
599
629
|
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
600
630
|
category: 'Operations',
|
601
631
|
description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
|
632
|
+
requiresSchema: true,
|
602
633
|
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
603
634
|
category: 'Operations',
|
604
635
|
description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
|
636
|
+
requiresSchema: true,
|
605
637
|
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
606
638
|
category: 'Operations',
|
607
639
|
description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
|
640
|
+
requiresSchema: true,
|
608
641
|
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
609
642
|
category: 'Operations',
|
610
643
|
description: `Variables passed to field arguments conform to type.`,
|
644
|
+
requiresSchema: true,
|
611
645
|
}));
|
612
646
|
|
613
647
|
const ALPHABETIZE = 'ALPHABETIZE';
|
@@ -1839,7 +1873,7 @@ const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
|
|
1839
1873
|
const rule$9 = {
|
1840
1874
|
meta: {
|
1841
1875
|
messages: {
|
1842
|
-
[HASHTAG_COMMENT]:
|
1876
|
+
[HASHTAG_COMMENT]: `Using hashtag (#) for adding GraphQL descriptions is not allowed. Prefer using """ for multiline, or " for a single line description.`,
|
1843
1877
|
},
|
1844
1878
|
docs: {
|
1845
1879
|
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.',
|
@@ -1886,14 +1920,15 @@ const rule$9 = {
|
|
1886
1920
|
schema: [],
|
1887
1921
|
},
|
1888
1922
|
create(context) {
|
1923
|
+
const selector = `${Kind.DOCUMENT}[definitions.0.kind!=/^(${Kind.OPERATION_DEFINITION}|${Kind.FRAGMENT_DEFINITION})$/]`;
|
1889
1924
|
return {
|
1890
|
-
|
1925
|
+
[selector](node) {
|
1891
1926
|
const rawNode = node.rawNode();
|
1892
1927
|
let token = rawNode.loc.startToken;
|
1893
1928
|
while (token !== null) {
|
1894
1929
|
const { kind, prev, next, value, line, column } = token;
|
1895
1930
|
if (kind === TokenKind.COMMENT && prev && next) {
|
1896
|
-
const isEslintComment = value.
|
1931
|
+
const isEslintComment = value.trimStart().startsWith('eslint');
|
1897
1932
|
const linesAfter = next.line - line;
|
1898
1933
|
if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
|
1899
1934
|
context.report({
|
@@ -2100,7 +2135,22 @@ const rule$c = {
|
|
2100
2135
|
};
|
2101
2136
|
|
2102
2137
|
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
|
2103
|
-
const
|
2138
|
+
const RULE_ID = 'no-unreachable-types';
|
2139
|
+
const KINDS = [
|
2140
|
+
Kind.DIRECTIVE_DEFINITION,
|
2141
|
+
Kind.OBJECT_TYPE_DEFINITION,
|
2142
|
+
Kind.OBJECT_TYPE_EXTENSION,
|
2143
|
+
Kind.INTERFACE_TYPE_DEFINITION,
|
2144
|
+
Kind.INTERFACE_TYPE_EXTENSION,
|
2145
|
+
Kind.SCALAR_TYPE_DEFINITION,
|
2146
|
+
Kind.SCALAR_TYPE_EXTENSION,
|
2147
|
+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
2148
|
+
Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
2149
|
+
Kind.UNION_TYPE_DEFINITION,
|
2150
|
+
Kind.UNION_TYPE_EXTENSION,
|
2151
|
+
Kind.ENUM_TYPE_DEFINITION,
|
2152
|
+
Kind.ENUM_TYPE_EXTENSION,
|
2153
|
+
];
|
2104
2154
|
const rule$d = {
|
2105
2155
|
meta: {
|
2106
2156
|
messages: {
|
@@ -2109,7 +2159,7 @@ const rule$d = {
|
|
2109
2159
|
docs: {
|
2110
2160
|
description: `Requires all types to be reachable at some level by root level fields.`,
|
2111
2161
|
category: 'Schema',
|
2112
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${
|
2162
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
2113
2163
|
requiresSchema: true,
|
2114
2164
|
examples: [
|
2115
2165
|
{
|
@@ -2141,43 +2191,36 @@ const rule$d = {
|
|
2141
2191
|
],
|
2142
2192
|
recommended: true,
|
2143
2193
|
},
|
2144
|
-
fixable: 'code',
|
2145
2194
|
type: 'suggestion',
|
2146
2195
|
schema: [],
|
2196
|
+
hasSuggestions: true,
|
2147
2197
|
},
|
2148
2198
|
create(context) {
|
2149
|
-
const reachableTypes = requireReachableTypesFromContext(
|
2150
|
-
|
2151
|
-
const typeName = node.name.value;
|
2152
|
-
if (!reachableTypes.has(typeName)) {
|
2153
|
-
context.report({
|
2154
|
-
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
|
2155
|
-
messageId: UNREACHABLE_TYPE,
|
2156
|
-
data: { typeName },
|
2157
|
-
fix: fixer => fixer.remove(node),
|
2158
|
-
});
|
2159
|
-
}
|
2160
|
-
}
|
2199
|
+
const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
|
2200
|
+
const selector = KINDS.join(',');
|
2161
2201
|
return {
|
2162
|
-
|
2163
|
-
|
2164
|
-
|
2165
|
-
|
2166
|
-
|
2167
|
-
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
|
2173
|
-
|
2174
|
-
|
2202
|
+
[selector](node) {
|
2203
|
+
const typeName = node.name.value;
|
2204
|
+
if (!reachableTypes.has(typeName)) {
|
2205
|
+
context.report({
|
2206
|
+
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
|
2207
|
+
messageId: UNREACHABLE_TYPE,
|
2208
|
+
data: { typeName },
|
2209
|
+
suggest: [
|
2210
|
+
{
|
2211
|
+
desc: `Remove ${typeName}`,
|
2212
|
+
fix: fixer => fixer.remove(node),
|
2213
|
+
},
|
2214
|
+
],
|
2215
|
+
});
|
2216
|
+
}
|
2217
|
+
},
|
2175
2218
|
};
|
2176
2219
|
},
|
2177
2220
|
};
|
2178
2221
|
|
2179
2222
|
const UNUSED_FIELD = 'UNUSED_FIELD';
|
2180
|
-
const
|
2223
|
+
const RULE_ID$1 = 'no-unused-fields';
|
2181
2224
|
const rule$e = {
|
2182
2225
|
meta: {
|
2183
2226
|
messages: {
|
@@ -2186,7 +2229,7 @@ const rule$e = {
|
|
2186
2229
|
docs: {
|
2187
2230
|
description: `Requires all fields to be used at some level by siblings operations.`,
|
2188
2231
|
category: 'Schema',
|
2189
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${
|
2232
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
|
2190
2233
|
requiresSiblings: true,
|
2191
2234
|
requiresSchema: true,
|
2192
2235
|
examples: [
|
@@ -2233,12 +2276,12 @@ const rule$e = {
|
|
2233
2276
|
},
|
2234
2277
|
],
|
2235
2278
|
},
|
2236
|
-
fixable: 'code',
|
2237
2279
|
type: 'suggestion',
|
2238
2280
|
schema: [],
|
2281
|
+
hasSuggestions: true,
|
2239
2282
|
},
|
2240
2283
|
create(context) {
|
2241
|
-
const usedFields = requireUsedFieldsFromContext(
|
2284
|
+
const usedFields = requireUsedFieldsFromContext(RULE_ID$1, context);
|
2242
2285
|
return {
|
2243
2286
|
FieldDefinition(node) {
|
2244
2287
|
var _a;
|
@@ -2252,22 +2295,18 @@ const rule$e = {
|
|
2252
2295
|
loc: getLocation(node.loc, fieldName),
|
2253
2296
|
messageId: UNUSED_FIELD,
|
2254
2297
|
data: { fieldName },
|
2255
|
-
|
2256
|
-
|
2257
|
-
|
2258
|
-
|
2259
|
-
|
2260
|
-
|
2261
|
-
|
2262
|
-
|
2263
|
-
|
2264
|
-
|
2265
|
-
|
2266
|
-
|
2267
|
-
}
|
2268
|
-
// Remove whitespace before token
|
2269
|
-
return fixer.removeRange([tokenBefore.range[1], node.range[1]]);
|
2270
|
-
},
|
2298
|
+
suggest: [
|
2299
|
+
{
|
2300
|
+
desc: `Remove "${fieldName}" field`,
|
2301
|
+
fix(fixer) {
|
2302
|
+
const sourceCode = context.getSourceCode();
|
2303
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
2304
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
2305
|
+
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
|
2306
|
+
return isEmptyType ? fixer.remove(node.parent) : fixer.remove(node);
|
2307
|
+
},
|
2308
|
+
},
|
2309
|
+
],
|
2271
2310
|
});
|
2272
2311
|
},
|
2273
2312
|
};
|
@@ -2630,14 +2669,14 @@ const rule$h = {
|
|
2630
2669
|
},
|
2631
2670
|
};
|
2632
2671
|
|
2633
|
-
const RULE_NAME
|
2672
|
+
const RULE_NAME = 'require-field-of-type-query-in-mutation-result';
|
2634
2673
|
const rule$i = {
|
2635
2674
|
meta: {
|
2636
2675
|
type: 'suggestion',
|
2637
2676
|
docs: {
|
2638
2677
|
category: 'Schema',
|
2639
2678
|
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`.',
|
2640
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME
|
2679
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME}.md`,
|
2641
2680
|
requiresSchema: true,
|
2642
2681
|
examples: [
|
2643
2682
|
{
|
@@ -2672,7 +2711,7 @@ const rule$i = {
|
|
2672
2711
|
schema: [],
|
2673
2712
|
},
|
2674
2713
|
create(context) {
|
2675
|
-
const schema = requireGraphQLSchemaFromContext(RULE_NAME
|
2714
|
+
const schema = requireGraphQLSchemaFromContext(RULE_NAME, context);
|
2676
2715
|
const mutationType = schema.getMutationType();
|
2677
2716
|
const queryType = schema.getQueryType();
|
2678
2717
|
if (!mutationType || !queryType) {
|
@@ -2846,22 +2885,9 @@ const rule$j = {
|
|
2846
2885
|
recommended: true,
|
2847
2886
|
},
|
2848
2887
|
messages: {
|
2849
|
-
[REQUIRE_ID_WHEN_AVAILABLE]:
|
2850
|
-
`Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
|
2851
|
-
`If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
|
2852
|
-
].join('\n'),
|
2888
|
+
[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 }}".`,
|
2853
2889
|
},
|
2854
2890
|
schema: {
|
2855
|
-
definitions: {
|
2856
|
-
asString: {
|
2857
|
-
type: 'string',
|
2858
|
-
},
|
2859
|
-
asArray: {
|
2860
|
-
type: 'array',
|
2861
|
-
minItems: 1,
|
2862
|
-
uniqueItems: true,
|
2863
|
-
},
|
2864
|
-
},
|
2865
2891
|
type: 'array',
|
2866
2892
|
maxItems: 1,
|
2867
2893
|
items: {
|
@@ -2869,7 +2895,7 @@ const rule$j = {
|
|
2869
2895
|
additionalProperties: false,
|
2870
2896
|
properties: {
|
2871
2897
|
fieldName: {
|
2872
|
-
|
2898
|
+
type: 'string',
|
2873
2899
|
default: DEFAULT_ID_FIELD_NAME,
|
2874
2900
|
},
|
2875
2901
|
},
|
@@ -2877,64 +2903,69 @@ const rule$j = {
|
|
2877
2903
|
},
|
2878
2904
|
},
|
2879
2905
|
create(context) {
|
2880
|
-
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
2881
|
-
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2882
|
-
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2883
|
-
const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
|
2884
|
-
const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
|
2885
2906
|
return {
|
2886
2907
|
SelectionSet(node) {
|
2887
2908
|
var _a, _b;
|
2888
|
-
|
2889
|
-
|
2890
|
-
|
2891
|
-
|
2892
|
-
const rawType = getBaseType(typeInfo.gqlType);
|
2893
|
-
const isObjectType = rawType instanceof GraphQLObjectType;
|
2894
|
-
const isInterfaceType = rawType instanceof GraphQLInterfaceType;
|
2895
|
-
if (!isObjectType && !isInterfaceType) {
|
2909
|
+
requireGraphQLSchemaFromContext('require-id-when-available', context);
|
2910
|
+
const siblings = requireSiblingsOperations('require-id-when-available', context);
|
2911
|
+
const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
|
2912
|
+
if (!node.selections || node.selections.length === 0) {
|
2896
2913
|
return;
|
2897
2914
|
}
|
2898
|
-
const
|
2899
|
-
|
2900
|
-
|
2901
|
-
|
2902
|
-
|
2903
|
-
|
2904
|
-
|
2905
|
-
|
2906
|
-
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
2911
|
-
|
2912
|
-
|
2913
|
-
|
2914
|
-
|
2915
|
-
|
2916
|
-
|
2915
|
+
const typeInfo = node.typeInfo();
|
2916
|
+
if (typeInfo && typeInfo.gqlType) {
|
2917
|
+
const rawType = getBaseType(typeInfo.gqlType);
|
2918
|
+
if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) {
|
2919
|
+
const fields = rawType.getFields();
|
2920
|
+
const hasIdFieldInType = !!fields[fieldName];
|
2921
|
+
const checkedFragmentSpreads = new Set();
|
2922
|
+
if (hasIdFieldInType) {
|
2923
|
+
let found = false;
|
2924
|
+
for (const selection of node.selections) {
|
2925
|
+
if (selection.kind === 'Field' && selection.name.value === fieldName) {
|
2926
|
+
found = true;
|
2927
|
+
}
|
2928
|
+
else if (selection.kind === 'InlineFragment') {
|
2929
|
+
found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2930
|
+
}
|
2931
|
+
else if (selection.kind === 'FragmentSpread') {
|
2932
|
+
const foundSpread = siblings.getFragment(selection.name.value);
|
2933
|
+
if (foundSpread[0]) {
|
2934
|
+
checkedFragmentSpreads.add(foundSpread[0].document.name.value);
|
2935
|
+
found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2936
|
+
}
|
2937
|
+
}
|
2938
|
+
if (found) {
|
2939
|
+
break;
|
2940
|
+
}
|
2941
|
+
}
|
2942
|
+
const { parent } = node;
|
2943
|
+
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2944
|
+
parent.kind === 'InlineFragment' &&
|
2945
|
+
parent.parent &&
|
2946
|
+
parent.parent.kind === 'SelectionSet' &&
|
2947
|
+
parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
|
2948
|
+
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2949
|
+
context.report({
|
2950
|
+
loc: {
|
2951
|
+
start: {
|
2952
|
+
line: node.loc.start.line,
|
2953
|
+
column: node.loc.start.column - 1,
|
2954
|
+
},
|
2955
|
+
end: {
|
2956
|
+
line: node.loc.end.line,
|
2957
|
+
column: node.loc.end.column - 1,
|
2958
|
+
},
|
2959
|
+
},
|
2960
|
+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2961
|
+
data: {
|
2962
|
+
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
|
2963
|
+
fieldName,
|
2964
|
+
},
|
2965
|
+
});
|
2966
|
+
}
|
2917
2967
|
}
|
2918
2968
|
}
|
2919
|
-
if (found) {
|
2920
|
-
break;
|
2921
|
-
}
|
2922
|
-
}
|
2923
|
-
const { parent } = node;
|
2924
|
-
const hasIdFieldInInterfaceSelectionSet = parent &&
|
2925
|
-
parent.kind === Kind.INLINE_FRAGMENT &&
|
2926
|
-
parent.parent &&
|
2927
|
-
parent.parent.kind === Kind.SELECTION_SET &&
|
2928
|
-
parent.parent.selections.some(s => isFound(s));
|
2929
|
-
if (!found && !hasIdFieldInInterfaceSelectionSet) {
|
2930
|
-
context.report({
|
2931
|
-
loc: getLocation(node.loc),
|
2932
|
-
messageId: REQUIRE_ID_WHEN_AVAILABLE,
|
2933
|
-
data: {
|
2934
|
-
checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
|
2935
|
-
fieldName: idNames.map(name => `"${name}"`).join(' or '),
|
2936
|
-
},
|
2937
|
-
});
|
2938
2969
|
}
|
2939
2970
|
},
|
2940
2971
|
};
|
@@ -3026,7 +3057,7 @@ const rule$k = {
|
|
3026
3057
|
// eslint-disable-next-line no-console
|
3027
3058
|
console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3028
3059
|
}
|
3029
|
-
const
|
3060
|
+
const maxDepth = context.options[0].maxDepth;
|
3030
3061
|
const ignore = context.options[0].ignore || [];
|
3031
3062
|
const checkFn = depthLimit(maxDepth, { ignore });
|
3032
3063
|
return {
|
@@ -3223,7 +3254,7 @@ const rule$l = {
|
|
3223
3254
|
},
|
3224
3255
|
};
|
3225
3256
|
|
3226
|
-
const RULE_NAME$
|
3257
|
+
const RULE_NAME$1 = 'unique-fragment-name';
|
3227
3258
|
const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
|
3228
3259
|
const checkNode = (context, node, ruleName, messageId) => {
|
3229
3260
|
const documentName = node.name.value;
|
@@ -3255,7 +3286,7 @@ const rule$m = {
|
|
3255
3286
|
docs: {
|
3256
3287
|
category: 'Operations',
|
3257
3288
|
description: `Enforce unique fragment names across your project.`,
|
3258
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$
|
3289
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$1}.md`,
|
3259
3290
|
requiresSiblings: true,
|
3260
3291
|
examples: [
|
3261
3292
|
{
|
@@ -3300,13 +3331,13 @@ const rule$m = {
|
|
3300
3331
|
create(context) {
|
3301
3332
|
return {
|
3302
3333
|
FragmentDefinition(node) {
|
3303
|
-
checkNode(context, node, RULE_NAME$
|
3334
|
+
checkNode(context, node, RULE_NAME$1, UNIQUE_FRAGMENT_NAME);
|
3304
3335
|
},
|
3305
3336
|
};
|
3306
3337
|
},
|
3307
3338
|
};
|
3308
3339
|
|
3309
|
-
const RULE_NAME$
|
3340
|
+
const RULE_NAME$2 = 'unique-operation-name';
|
3310
3341
|
const UNIQUE_OPERATION_NAME = 'UNIQUE_OPERATION_NAME';
|
3311
3342
|
const rule$n = {
|
3312
3343
|
meta: {
|
@@ -3314,7 +3345,7 @@ const rule$n = {
|
|
3314
3345
|
docs: {
|
3315
3346
|
category: 'Operations',
|
3316
3347
|
description: `Enforce unique operation names across your project.`,
|
3317
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$
|
3348
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_NAME$2}.md`,
|
3318
3349
|
requiresSiblings: true,
|
3319
3350
|
examples: [
|
3320
3351
|
{
|
@@ -3363,7 +3394,7 @@ const rule$n = {
|
|
3363
3394
|
create(context) {
|
3364
3395
|
return {
|
3365
3396
|
'OperationDefinition[name!=undefined]'(node) {
|
3366
|
-
checkNode(context, node, RULE_NAME$
|
3397
|
+
checkNode(context, node, RULE_NAME$2, UNIQUE_OPERATION_NAME);
|
3367
3398
|
},
|
3368
3399
|
};
|
3369
3400
|
},
|