@graphql-eslint/eslint-plugin 3.2.0 → 3.3.0-alpha-db2c2cb.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.
@@ -5,7 +5,7 @@
5
5
  - Category: `Operations`
6
6
  - Rule name: `@graphql-eslint/known-fragment-names`
7
7
  - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
8
- - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
8
+ - Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
9
9
 
10
10
  A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.
11
11
 
@@ -13,7 +13,7 @@ A GraphQL document is only valid if all `...Fragment` fragment spreads refer to
13
13
 
14
14
  ## Usage Examples
15
15
 
16
- ### Incorrect (fragment not defined in the document)
16
+ ### Incorrect
17
17
 
18
18
  ```graphql
19
19
  # eslint @graphql-eslint/known-fragment-names: 'error'
@@ -21,7 +21,7 @@ A GraphQL document is only valid if all `...Fragment` fragment spreads refer to
21
21
  query {
22
22
  user {
23
23
  id
24
- ...UserFields
24
+ ...UserFields # fragment not defined in the document
25
25
  }
26
26
  }
27
27
  ```
@@ -44,44 +44,23 @@ query {
44
44
  }
45
45
  ```
46
46
 
47
- ### Correct (existing import to UserFields fragment)
47
+ ### Correct (`UserFields` fragment located in a separate file)
48
48
 
49
49
  ```graphql
50
50
  # eslint @graphql-eslint/known-fragment-names: 'error'
51
51
 
52
- #import '../UserFields.gql'
53
-
52
+ # user.gql
54
53
  query {
55
54
  user {
56
55
  id
57
56
  ...UserFields
58
57
  }
59
58
  }
60
- ```
61
-
62
- ### False positive case
63
59
 
64
- For 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.
65
-
66
- ```js
67
- const USER_FIELDS = gql`
68
- fragment UserFields on User {
69
- id
70
- }
71
- `
72
-
73
- const GET_USER = /* GraphQL */ `
74
- # eslint @graphql-eslint/known-fragment-names: 'error'
75
-
76
- query User {
77
- user {
78
- ...UserFields
79
- }
80
- }
81
-
82
- # Will give false positive error 'Unknown fragment "UserFields"'
83
- ${USER_FIELDS}
84
- `
60
+ # user-fields.gql
61
+ fragment UserFields on User {
62
+ id
63
+ }
85
64
  ```
86
65
 
87
66
  ## Resources
@@ -5,7 +5,7 @@
5
5
  - Category: `Operations`
6
6
  - Rule name: `@graphql-eslint/no-undefined-variables`
7
7
  - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
8
- - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
8
+ - Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
9
9
 
10
10
  A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.
11
11
 
@@ -5,7 +5,7 @@
5
5
  - Category: `Operations`
6
6
  - Rule name: `@graphql-eslint/no-unused-variables`
7
7
  - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
8
- - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
8
+ - Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
9
9
 
10
10
  A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.
11
11
 
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');
@@ -327,14 +326,14 @@ function getLocation(loc, fieldName = '', offset) {
327
326
  };
328
327
  }
329
328
 
330
- function validateDoc(sourceNode, context, schema, documentNode, rules) {
329
+ function validateDocument(sourceNode, context, schema, documentNode, rule) {
331
330
  if (documentNode.definitions.length === 0) {
332
331
  return;
333
332
  }
334
333
  try {
335
334
  const validationErrors = schema
336
- ? graphql.validate(schema, documentNode, rules)
337
- : validate.validateSDL(documentNode, null, rules);
335
+ ? graphql.validate(schema, documentNode, [rule])
336
+ : validate.validateSDL(documentNode, null, [rule]);
338
337
  for (const error of validationErrors) {
339
338
  /*
340
339
  * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
@@ -364,34 +363,79 @@ function validateDoc(sourceNode, context, schema, documentNode, rules) {
364
363
  });
365
364
  }
366
365
  }
367
- const isGraphQLImportFile = rawSDL => {
368
- const trimmedRawSDL = rawSDL.trimLeft();
369
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
366
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
367
+ const typeInfo = new graphql.TypeInfo(schema);
368
+ const fragmentDefs = new Set();
369
+ const fragmentSpreads = new Set();
370
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
371
+ FragmentDefinition(node) {
372
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
373
+ },
374
+ FragmentSpread(node) {
375
+ const parentType = typeInfo.getParentType();
376
+ if (parentType) {
377
+ fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
378
+ }
379
+ },
380
+ });
381
+ graphql.visit(node, visitor);
382
+ return { fragmentDefs, fragmentSpreads };
370
383
  };
371
- const validationToRule = (name, ruleName, docs, getDocumentNode) => {
372
- var _a;
384
+ const getMissingFragments = (schema, node) => {
385
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
386
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
387
+ };
388
+ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
389
+ const missingFragments = getMissingFragments(schema, node);
390
+ if (missingFragments.length > 0) {
391
+ const siblings = requireSiblingsOperations(ruleId, context);
392
+ const fragmentsToAdd = [];
393
+ for (const missingFragment of missingFragments) {
394
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
395
+ const [foundFragment] = siblings
396
+ .getFragment(fragmentName)
397
+ .map(source => source.document)
398
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
399
+ if (foundFragment) {
400
+ fragmentsToAdd.push(foundFragment);
401
+ }
402
+ }
403
+ if (fragmentsToAdd.length > 0) {
404
+ // recall fn to make sure to add fragments inside fragments
405
+ return handleMissingFragments({
406
+ ruleId,
407
+ context,
408
+ schema,
409
+ node: {
410
+ kind: graphql.Kind.DOCUMENT,
411
+ definitions: [...node.definitions, ...fragmentsToAdd],
412
+ },
413
+ });
414
+ }
415
+ }
416
+ return node;
417
+ };
418
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
373
419
  let ruleFn = null;
374
420
  try {
375
421
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
376
422
  }
377
- catch (e) {
423
+ catch (_a) {
378
424
  try {
379
425
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
380
426
  }
381
- catch (e) {
427
+ catch (_b) {
382
428
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
383
429
  }
384
430
  }
385
- const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
386
431
  return {
387
- [name]: {
432
+ [ruleId]: {
388
433
  meta: {
389
434
  docs: {
390
435
  recommended: true,
391
436
  ...docs,
392
437
  graphQLJSRuleName: ruleName,
393
- requiresSchema,
394
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
438
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
395
439
  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).`,
396
440
  },
397
441
  },
@@ -400,56 +444,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
400
444
  Document(node) {
401
445
  if (!ruleFn) {
402
446
  // eslint-disable-next-line no-console
403
- console.warn(`You rule "${name}" depends on a GraphQL validation rule ("${ruleName}") but it's not available in the "graphql-js" version you are using. Skipping...`);
447
+ 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...`);
404
448
  return;
405
449
  }
406
- const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
407
- let documentNode;
408
- const isRealFile = fs.existsSync(context.getFilename());
409
- if (isRealFile && getDocumentNode) {
410
- documentNode = getDocumentNode(context);
411
- }
412
- validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn]);
450
+ const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
451
+ const documentNode = getDocumentNode
452
+ ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
453
+ : node.rawNode();
454
+ validateDocument(node, context, schema, documentNode, ruleFn);
413
455
  },
414
456
  };
415
457
  },
416
458
  },
417
459
  };
418
460
  };
419
- const importFiles = (context) => {
420
- const code = context.getSourceCode().text;
421
- if (!isGraphQLImportFile(code)) {
422
- return null;
423
- }
424
- // Import documents because file contains '#import' comments
425
- return _import.processImport(context.getFilename());
426
- };
427
461
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
428
462
  category: 'Operations',
429
463
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
464
+ requiresSchema: true,
430
465
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
431
466
  category: 'Operations',
432
467
  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`.',
468
+ requiresSchema: true,
433
469
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
434
470
  category: 'Operations',
435
471
  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.`,
472
+ requiresSchema: true,
436
473
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
437
474
  category: ['Schema', 'Operations'],
438
475
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
476
+ requiresSchema: true,
439
477
  }), validationToRule('known-directives', 'KnownDirectives', {
440
478
  category: ['Schema', 'Operations'],
441
479
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
480
+ requiresSchema: true,
442
481
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
443
482
  category: 'Operations',
444
483
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
484
+ requiresSchema: true,
485
+ requiresSiblings: true,
445
486
  examples: [
446
487
  {
447
- title: 'Incorrect (fragment not defined in the document)',
488
+ title: 'Incorrect',
448
489
  code: /* GraphQL */ `
449
490
  query {
450
491
  user {
451
492
  id
452
- ...UserFields
493
+ ...UserFields # fragment not defined in the document
453
494
  }
454
495
  }
455
496
  `,
@@ -471,153 +512,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
471
512
  `,
472
513
  },
473
514
  {
474
- title: 'Correct (existing import to UserFields fragment)',
515
+ title: 'Correct (`UserFields` fragment located in a separate file)',
475
516
  code: /* GraphQL */ `
476
- #import '../UserFields.gql'
477
-
517
+ # user.gql
478
518
  query {
479
519
  user {
480
520
  id
481
521
  ...UserFields
482
522
  }
483
523
  }
484
- `,
485
- },
486
- {
487
- 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.",
488
- code: `
489
- const USER_FIELDS = gql\`
490
- fragment UserFields on User {
491
- id
492
- }
493
- \`
494
-
495
- const GET_USER = /* GraphQL */ \`
496
- # eslint @graphql-eslint/known-fragment-names: 'error'
497
-
498
- query User {
499
- user {
500
- ...UserFields
501
- }
502
- }
503
524
 
504
- # Will give false positive error 'Unknown fragment "UserFields"'
505
- \${USER_FIELDS}
506
- \``,
525
+ # user-fields.gql
526
+ fragment UserFields on User {
527
+ id
528
+ }
529
+ `,
507
530
  },
508
531
  ],
509
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
532
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
510
533
  category: ['Schema', 'Operations'],
511
534
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
535
+ requiresSchema: true,
512
536
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
513
537
  category: 'Operations',
514
538
  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.`,
539
+ requiresSchema: true,
515
540
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
516
541
  category: 'Schema',
517
542
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
518
- requiresSchema: false,
519
543
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
520
544
  category: 'Operations',
521
545
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
546
+ requiresSchema: true,
522
547
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
523
548
  category: 'Operations',
524
549
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
525
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
550
+ requiresSchema: true,
551
+ requiresSiblings: true,
552
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
526
553
  category: 'Operations',
527
554
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
555
+ requiresSchema: true,
528
556
  requiresSiblings: true,
529
- }, context => {
530
- const siblings = requireSiblingsOperations('no-unused-fragments', context);
531
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
532
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
533
- .map(({ filePath, document }) => ({
534
- filePath,
535
- code: document.loc.source.body,
536
- }));
537
- const getParentNode = (filePath) => {
538
- for (const { filePath: docFilePath, code } of documents) {
539
- const isFileImported = code
540
- .split('\n')
541
- .filter(isGraphQLImportFile)
542
- .map(line => _import.parseImportLine(line.replace('#', '')))
543
- .some(o => filePath === path.join(path.dirname(docFilePath), o.from));
544
- if (!isFileImported) {
545
- continue;
546
- }
547
- // Import first file that import this file
548
- const document = _import.processImport(docFilePath);
549
- // Import most top file that import this file
550
- return getParentNode(docFilePath) || document;
557
+ }, ({ ruleId, context, schema, node }) => {
558
+ const siblings = requireSiblingsOperations(ruleId, context);
559
+ const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
560
+ var _a;
561
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
562
+ map[filePath].push(document);
563
+ return map;
564
+ }, Object.create(null));
565
+ const getParentNode = (currentFilePath, node) => {
566
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
567
+ if (fragmentDefs.size === 0) {
568
+ return node;
551
569
  }
552
- return null;
570
+ // skip iteration over documents for current filepath
571
+ delete FilePathToDocumentsMap[currentFilePath];
572
+ for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
573
+ const missingFragments = getMissingFragments(schema, {
574
+ kind: graphql.Kind.DOCUMENT,
575
+ definitions: documents,
576
+ });
577
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
578
+ if (isCurrentFileImportFragment) {
579
+ return getParentNode(filePath, {
580
+ kind: graphql.Kind.DOCUMENT,
581
+ definitions: [...node.definitions, ...documents],
582
+ });
583
+ }
584
+ }
585
+ return node;
553
586
  };
554
- return getParentNode(context.getFilename());
587
+ return getParentNode(context.getFilename(), node);
555
588
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
556
589
  category: 'Operations',
557
590
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
558
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
591
+ requiresSchema: true,
592
+ requiresSiblings: true,
593
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
559
594
  category: 'Operations',
560
595
  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.`,
596
+ requiresSchema: true,
561
597
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
562
598
  category: 'Operations',
563
599
  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.`,
600
+ requiresSchema: true,
564
601
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
565
602
  category: 'Schema',
566
603
  description: `A type extension is only valid if the type is defined and has the same kind.`,
567
- requiresSchema: false,
568
604
  recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
569
605
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
570
606
  category: ['Schema', 'Operations'],
571
607
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
608
+ requiresSchema: true,
572
609
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
573
610
  category: 'Operations',
574
611
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
612
+ requiresSchema: true,
575
613
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
576
614
  category: 'Operations',
577
615
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
616
+ requiresSchema: true,
578
617
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
579
618
  category: 'Operations',
580
619
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
620
+ requiresSchema: true,
581
621
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
582
622
  category: 'Schema',
583
623
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
584
- requiresSchema: false,
585
624
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
586
625
  category: ['Schema', 'Operations'],
587
626
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
627
+ requiresSchema: true,
588
628
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
589
629
  category: 'Schema',
590
630
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
591
- requiresSchema: false,
592
631
  recommended: false,
593
632
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
594
633
  category: 'Schema',
595
634
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
596
- requiresSchema: false,
597
635
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
598
636
  category: 'Operations',
599
637
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
600
- requiresSchema: false,
601
638
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
602
639
  category: 'Schema',
603
640
  description: `A GraphQL document is only valid if it has only one type per operation.`,
604
- requiresSchema: false,
605
641
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
606
642
  category: 'Schema',
607
643
  description: `A GraphQL document is only valid if all defined types have unique names.`,
608
- requiresSchema: false,
609
644
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
610
645
  category: 'Operations',
611
646
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
647
+ requiresSchema: true,
612
648
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
613
649
  category: 'Operations',
614
650
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
651
+ requiresSchema: true,
615
652
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
616
653
  category: 'Operations',
617
654
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
655
+ requiresSchema: true,
618
656
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
619
657
  category: 'Operations',
620
658
  description: `Variables passed to field arguments conform to type.`,
659
+ requiresSchema: true,
621
660
  }));
622
661
 
623
662
  const ALPHABETIZE = 'ALPHABETIZE';
package/index.mjs CHANGED
@@ -1,8 +1,7 @@
1
- import { Kind, validate, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, visit, visitWithTypeInfo, GraphQLObjectType, GraphQLInterfaceType, TypeInfo, isInterfaceType, Source, GraphQLError } from 'graphql';
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, join, extname, basename, relative, resolve } from 'path';
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';
@@ -321,14 +320,14 @@ function getLocation(loc, fieldName = '', offset) {
321
320
  };
322
321
  }
323
322
 
324
- function validateDoc(sourceNode, context, schema, documentNode, rules) {
323
+ function validateDocument(sourceNode, context, schema, documentNode, rule) {
325
324
  if (documentNode.definitions.length === 0) {
326
325
  return;
327
326
  }
328
327
  try {
329
328
  const validationErrors = schema
330
- ? validate(schema, documentNode, rules)
331
- : validateSDL(documentNode, null, rules);
329
+ ? validate(schema, documentNode, [rule])
330
+ : validateSDL(documentNode, null, [rule]);
332
331
  for (const error of validationErrors) {
333
332
  /*
334
333
  * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
@@ -358,34 +357,79 @@ function validateDoc(sourceNode, context, schema, documentNode, rules) {
358
357
  });
359
358
  }
360
359
  }
361
- const isGraphQLImportFile = rawSDL => {
362
- const trimmedRawSDL = rawSDL.trimLeft();
363
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
360
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
361
+ const typeInfo = new TypeInfo(schema);
362
+ const fragmentDefs = new Set();
363
+ const fragmentSpreads = new Set();
364
+ const visitor = visitWithTypeInfo(typeInfo, {
365
+ FragmentDefinition(node) {
366
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
367
+ },
368
+ FragmentSpread(node) {
369
+ const parentType = typeInfo.getParentType();
370
+ if (parentType) {
371
+ fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
372
+ }
373
+ },
374
+ });
375
+ visit(node, visitor);
376
+ return { fragmentDefs, fragmentSpreads };
364
377
  };
365
- const validationToRule = (name, ruleName, docs, getDocumentNode) => {
366
- var _a;
378
+ const getMissingFragments = (schema, node) => {
379
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
380
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
381
+ };
382
+ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
383
+ const missingFragments = getMissingFragments(schema, node);
384
+ if (missingFragments.length > 0) {
385
+ const siblings = requireSiblingsOperations(ruleId, context);
386
+ const fragmentsToAdd = [];
387
+ for (const missingFragment of missingFragments) {
388
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
389
+ const [foundFragment] = siblings
390
+ .getFragment(fragmentName)
391
+ .map(source => source.document)
392
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
393
+ if (foundFragment) {
394
+ fragmentsToAdd.push(foundFragment);
395
+ }
396
+ }
397
+ if (fragmentsToAdd.length > 0) {
398
+ // recall fn to make sure to add fragments inside fragments
399
+ return handleMissingFragments({
400
+ ruleId,
401
+ context,
402
+ schema,
403
+ node: {
404
+ kind: Kind.DOCUMENT,
405
+ definitions: [...node.definitions, ...fragmentsToAdd],
406
+ },
407
+ });
408
+ }
409
+ }
410
+ return node;
411
+ };
412
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
367
413
  let ruleFn = null;
368
414
  try {
369
415
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
370
416
  }
371
- catch (e) {
417
+ catch (_a) {
372
418
  try {
373
419
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
374
420
  }
375
- catch (e) {
421
+ catch (_b) {
376
422
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
377
423
  }
378
424
  }
379
- const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
380
425
  return {
381
- [name]: {
426
+ [ruleId]: {
382
427
  meta: {
383
428
  docs: {
384
429
  recommended: true,
385
430
  ...docs,
386
431
  graphQLJSRuleName: ruleName,
387
- requiresSchema,
388
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
432
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
389
433
  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).`,
390
434
  },
391
435
  },
@@ -394,56 +438,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
394
438
  Document(node) {
395
439
  if (!ruleFn) {
396
440
  // eslint-disable-next-line no-console
397
- console.warn(`You rule "${name}" depends on a GraphQL validation rule ("${ruleName}") but it's not available in the "graphql-js" version you are using. Skipping...`);
441
+ 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...`);
398
442
  return;
399
443
  }
400
- const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
401
- let documentNode;
402
- const isRealFile = existsSync(context.getFilename());
403
- if (isRealFile && getDocumentNode) {
404
- documentNode = getDocumentNode(context);
405
- }
406
- validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn]);
444
+ const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
445
+ const documentNode = getDocumentNode
446
+ ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
447
+ : node.rawNode();
448
+ validateDocument(node, context, schema, documentNode, ruleFn);
407
449
  },
408
450
  };
409
451
  },
410
452
  },
411
453
  };
412
454
  };
413
- const importFiles = (context) => {
414
- const code = context.getSourceCode().text;
415
- if (!isGraphQLImportFile(code)) {
416
- return null;
417
- }
418
- // Import documents because file contains '#import' comments
419
- return processImport(context.getFilename());
420
- };
421
455
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
422
456
  category: 'Operations',
423
457
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
458
+ requiresSchema: true,
424
459
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
425
460
  category: 'Operations',
426
461
  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`.',
462
+ requiresSchema: true,
427
463
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
428
464
  category: 'Operations',
429
465
  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.`,
466
+ requiresSchema: true,
430
467
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
431
468
  category: ['Schema', 'Operations'],
432
469
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
470
+ requiresSchema: true,
433
471
  }), validationToRule('known-directives', 'KnownDirectives', {
434
472
  category: ['Schema', 'Operations'],
435
473
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
474
+ requiresSchema: true,
436
475
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
437
476
  category: 'Operations',
438
477
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
478
+ requiresSchema: true,
479
+ requiresSiblings: true,
439
480
  examples: [
440
481
  {
441
- title: 'Incorrect (fragment not defined in the document)',
482
+ title: 'Incorrect',
442
483
  code: /* GraphQL */ `
443
484
  query {
444
485
  user {
445
486
  id
446
- ...UserFields
487
+ ...UserFields # fragment not defined in the document
447
488
  }
448
489
  }
449
490
  `,
@@ -465,153 +506,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
465
506
  `,
466
507
  },
467
508
  {
468
- title: 'Correct (existing import to UserFields fragment)',
509
+ title: 'Correct (`UserFields` fragment located in a separate file)',
469
510
  code: /* GraphQL */ `
470
- #import '../UserFields.gql'
471
-
511
+ # user.gql
472
512
  query {
473
513
  user {
474
514
  id
475
515
  ...UserFields
476
516
  }
477
517
  }
478
- `,
479
- },
480
- {
481
- 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.",
482
- code: `
483
- const USER_FIELDS = gql\`
484
- fragment UserFields on User {
485
- id
486
- }
487
- \`
488
-
489
- const GET_USER = /* GraphQL */ \`
490
- # eslint @graphql-eslint/known-fragment-names: 'error'
491
-
492
- query User {
493
- user {
494
- ...UserFields
495
- }
496
- }
497
518
 
498
- # Will give false positive error 'Unknown fragment "UserFields"'
499
- \${USER_FIELDS}
500
- \``,
519
+ # user-fields.gql
520
+ fragment UserFields on User {
521
+ id
522
+ }
523
+ `,
501
524
  },
502
525
  ],
503
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
526
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
504
527
  category: ['Schema', 'Operations'],
505
528
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
529
+ requiresSchema: true,
506
530
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
507
531
  category: 'Operations',
508
532
  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.`,
533
+ requiresSchema: true,
509
534
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
510
535
  category: 'Schema',
511
536
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
512
- requiresSchema: false,
513
537
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
514
538
  category: 'Operations',
515
539
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
540
+ requiresSchema: true,
516
541
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
517
542
  category: 'Operations',
518
543
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
519
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
544
+ requiresSchema: true,
545
+ requiresSiblings: true,
546
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
520
547
  category: 'Operations',
521
548
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
549
+ requiresSchema: true,
522
550
  requiresSiblings: true,
523
- }, context => {
524
- const siblings = requireSiblingsOperations('no-unused-fragments', context);
525
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
526
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
527
- .map(({ filePath, document }) => ({
528
- filePath,
529
- code: document.loc.source.body,
530
- }));
531
- const getParentNode = (filePath) => {
532
- for (const { filePath: docFilePath, code } of documents) {
533
- const isFileImported = code
534
- .split('\n')
535
- .filter(isGraphQLImportFile)
536
- .map(line => parseImportLine(line.replace('#', '')))
537
- .some(o => filePath === join(dirname(docFilePath), o.from));
538
- if (!isFileImported) {
539
- continue;
540
- }
541
- // Import first file that import this file
542
- const document = processImport(docFilePath);
543
- // Import most top file that import this file
544
- return getParentNode(docFilePath) || document;
551
+ }, ({ ruleId, context, schema, node }) => {
552
+ const siblings = requireSiblingsOperations(ruleId, context);
553
+ const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
554
+ var _a;
555
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
556
+ map[filePath].push(document);
557
+ return map;
558
+ }, Object.create(null));
559
+ const getParentNode = (currentFilePath, node) => {
560
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
561
+ if (fragmentDefs.size === 0) {
562
+ return node;
545
563
  }
546
- return null;
564
+ // skip iteration over documents for current filepath
565
+ delete FilePathToDocumentsMap[currentFilePath];
566
+ for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
567
+ const missingFragments = getMissingFragments(schema, {
568
+ kind: Kind.DOCUMENT,
569
+ definitions: documents,
570
+ });
571
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
572
+ if (isCurrentFileImportFragment) {
573
+ return getParentNode(filePath, {
574
+ kind: Kind.DOCUMENT,
575
+ definitions: [...node.definitions, ...documents],
576
+ });
577
+ }
578
+ }
579
+ return node;
547
580
  };
548
- return getParentNode(context.getFilename());
581
+ return getParentNode(context.getFilename(), node);
549
582
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
550
583
  category: 'Operations',
551
584
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
552
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
585
+ requiresSchema: true,
586
+ requiresSiblings: true,
587
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
553
588
  category: 'Operations',
554
589
  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.`,
590
+ requiresSchema: true,
555
591
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
556
592
  category: 'Operations',
557
593
  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.`,
594
+ requiresSchema: true,
558
595
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
559
596
  category: 'Schema',
560
597
  description: `A type extension is only valid if the type is defined and has the same kind.`,
561
- requiresSchema: false,
562
598
  recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
563
599
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
564
600
  category: ['Schema', 'Operations'],
565
601
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
602
+ requiresSchema: true,
566
603
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
567
604
  category: 'Operations',
568
605
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
606
+ requiresSchema: true,
569
607
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
570
608
  category: 'Operations',
571
609
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
610
+ requiresSchema: true,
572
611
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
573
612
  category: 'Operations',
574
613
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
614
+ requiresSchema: true,
575
615
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
576
616
  category: 'Schema',
577
617
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
578
- requiresSchema: false,
579
618
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
580
619
  category: ['Schema', 'Operations'],
581
620
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
621
+ requiresSchema: true,
582
622
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
583
623
  category: 'Schema',
584
624
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
585
- requiresSchema: false,
586
625
  recommended: false,
587
626
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
588
627
  category: 'Schema',
589
628
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
590
- requiresSchema: false,
591
629
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
592
630
  category: 'Operations',
593
631
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
594
- requiresSchema: false,
595
632
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
596
633
  category: 'Schema',
597
634
  description: `A GraphQL document is only valid if it has only one type per operation.`,
598
- requiresSchema: false,
599
635
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
600
636
  category: 'Schema',
601
637
  description: `A GraphQL document is only valid if all defined types have unique names.`,
602
- requiresSchema: false,
603
638
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
604
639
  category: 'Operations',
605
640
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
641
+ requiresSchema: true,
606
642
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
607
643
  category: 'Operations',
608
644
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
645
+ requiresSchema: true,
609
646
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
610
647
  category: 'Operations',
611
648
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
649
+ requiresSchema: true,
612
650
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
613
651
  category: 'Operations',
614
652
  description: `Variables passed to field arguments conform to type.`,
653
+ requiresSchema: true,
615
654
  }));
616
655
 
617
656
  const ALPHABETIZE = 'ALPHABETIZE';
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@graphql-eslint/eslint-plugin",
3
- "version": "3.2.0",
3
+ "version": "3.3.0-alpha-db2c2cb.0",
4
+ "description": "GraphQL plugin for ESLint",
4
5
  "sideEffects": false,
5
6
  "peerDependencies": {
6
7
  "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
@@ -9,13 +10,18 @@
9
10
  "@babel/code-frame": "7.16.0",
10
11
  "@graphql-tools/code-file-loader": "7.2.3",
11
12
  "@graphql-tools/graphql-tag-pluck": "7.1.4",
12
- "@graphql-tools/import": "6.6.2",
13
13
  "@graphql-tools/utils": "8.5.4",
14
14
  "graphql-config": "4.1.0",
15
15
  "graphql-depth-limit": "1.1.0",
16
16
  "lodash.lowercase": "4.3.0"
17
17
  },
18
18
  "repository": "https://github.com/dotansimha/graphql-eslint",
19
+ "keywords": [
20
+ "eslint",
21
+ "eslintplugin",
22
+ "eslint-plugin",
23
+ "graphql"
24
+ ],
19
25
  "author": "Dotan Simha <dotansimha@gmail.com>",
20
26
  "license": "MIT",
21
27
  "main": "index.js",
@@ -1,5 +1,2 @@
1
- import { GraphQLSchema, DocumentNode, ASTNode, ValidationRule } from 'graphql';
2
- import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
3
- import { GraphQLESTreeNode } from '../estree-parser';
4
- export declare function validateDoc(sourceNode: GraphQLESTreeNode<ASTNode>, context: GraphQLESLintRuleContext, schema: GraphQLSchema | null, documentNode: DocumentNode, rules: ReadonlyArray<ValidationRule>): void;
5
- export declare const GRAPHQL_JS_VALIDATIONS: Record<string, GraphQLESLintRule<any[], false>>;
1
+ import { GraphQLESLintRule } from '../types';
2
+ export declare const GRAPHQL_JS_VALIDATIONS: Record<string, GraphQLESLintRule>;
package/types.d.ts CHANGED
@@ -49,8 +49,8 @@ export declare type CategoryType = 'Schema' | 'Operations';
49
49
  export declare type RuleDocsInfo<T> = {
50
50
  docs: Omit<Rule.RuleMetaData['docs'], 'category'> & {
51
51
  category: CategoryType | CategoryType[];
52
- requiresSchema?: boolean;
53
- requiresSiblings?: boolean;
52
+ requiresSchema?: true;
53
+ requiresSiblings?: true;
54
54
  examples?: {
55
55
  title: string;
56
56
  code: string;