@graphql-eslint/eslint-plugin 3.2.0 → 3.3.0-alpha-d23e9e2.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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Category: `Schema`
4
4
  - Rule name: `@graphql-eslint/possible-type-extension`
5
- - Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
5
+ - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6
6
  - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7
7
 
8
8
  A type extension is only valid if the type is defined and has the same kind.
@@ -1,3 +1,3 @@
1
1
  import { GraphQLConfig } from 'graphql-config';
2
2
  import { ParserOptions } from './types';
3
- export declare function loadGraphqlConfig(options: ParserOptions): GraphQLConfig;
3
+ export declare function loadGraphQLConfig(options: ParserOptions): GraphQLConfig;
package/index.js CHANGED
@@ -6,14 +6,13 @@ 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');
13
12
  const lowerCase = _interopDefault(require('lodash.lowercase'));
14
13
  const depthLimit = _interopDefault(require('graphql-depth-limit'));
15
14
  const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
16
- const graphqlConfig$1 = require('graphql-config');
15
+ const graphqlConfig = require('graphql-config');
17
16
  const codeFileLoader = require('@graphql-tools/code-file-loader');
18
17
  const eslint = require('eslint');
19
18
  const codeFrame = require('@babel/code-frame');
@@ -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(context, schema = null, documentNode, rule, isSchemaToExtend = false) {
331
330
  if (documentNode.definitions.length === 0) {
332
331
  return;
333
332
  }
334
333
  try {
335
- const validationErrors = schema
336
- ? graphql.validate(schema, documentNode, rules)
337
- : validate.validateSDL(documentNode, null, rules);
334
+ const validationErrors = schema && !isSchemaToExtend
335
+ ? graphql.validate(schema, documentNode, [rule])
336
+ : validate.validateSDL(documentNode, schema, [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
@@ -359,97 +358,140 @@ function validateDoc(sourceNode, context, schema, documentNode, rules) {
359
358
  }
360
359
  catch (e) {
361
360
  context.report({
362
- node: sourceNode,
361
+ // Report on first character
362
+ loc: { column: 0, line: 1 },
363
363
  message: e.message,
364
364
  });
365
365
  }
366
366
  }
367
- const isGraphQLImportFile = rawSDL => {
368
- const trimmedRawSDL = rawSDL.trimLeft();
369
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
367
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
368
+ const typeInfo = new graphql.TypeInfo(schema);
369
+ const fragmentDefs = new Set();
370
+ const fragmentSpreads = new Set();
371
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
372
+ FragmentDefinition(node) {
373
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
374
+ },
375
+ FragmentSpread(node) {
376
+ const parentType = typeInfo.getParentType();
377
+ if (parentType) {
378
+ fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
379
+ }
380
+ },
381
+ });
382
+ graphql.visit(node, visitor);
383
+ return { fragmentDefs, fragmentSpreads };
370
384
  };
371
- const validationToRule = (name, ruleName, docs, getDocumentNode) => {
372
- var _a;
385
+ const getMissingFragments = (schema, node) => {
386
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
387
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
388
+ };
389
+ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
390
+ const missingFragments = getMissingFragments(schema, node);
391
+ if (missingFragments.length > 0) {
392
+ const siblings = requireSiblingsOperations(ruleId, context);
393
+ const fragmentsToAdd = [];
394
+ for (const missingFragment of missingFragments) {
395
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
396
+ const [foundFragment] = siblings
397
+ .getFragment(fragmentName)
398
+ .map(source => source.document)
399
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
400
+ if (foundFragment) {
401
+ fragmentsToAdd.push(foundFragment);
402
+ }
403
+ }
404
+ if (fragmentsToAdd.length > 0) {
405
+ // recall fn to make sure to add fragments inside fragments
406
+ return handleMissingFragments({
407
+ ruleId,
408
+ context,
409
+ schema,
410
+ node: {
411
+ kind: graphql.Kind.DOCUMENT,
412
+ definitions: [...node.definitions, ...fragmentsToAdd],
413
+ },
414
+ });
415
+ }
416
+ }
417
+ return node;
418
+ };
419
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
373
420
  let ruleFn = null;
374
421
  try {
375
422
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
376
423
  }
377
- catch (e) {
424
+ catch (_a) {
378
425
  try {
379
426
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
380
427
  }
381
- catch (e) {
428
+ catch (_b) {
382
429
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
383
430
  }
384
431
  }
385
- const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
386
432
  return {
387
- [name]: {
433
+ [ruleId]: {
388
434
  meta: {
389
435
  docs: {
390
436
  recommended: true,
391
437
  ...docs,
392
438
  graphQLJSRuleName: ruleName,
393
- requiresSchema,
394
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
439
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
395
440
  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
441
  },
397
442
  },
398
443
  create(context) {
444
+ if (!ruleFn) {
445
+ // eslint-disable-next-line no-console
446
+ 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...`);
447
+ return {};
448
+ }
449
+ const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
399
450
  return {
400
451
  Document(node) {
401
- if (!ruleFn) {
402
- // 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...`);
404
- return;
405
- }
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]);
452
+ const documentNode = getDocumentNode
453
+ ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
454
+ : node.rawNode();
455
+ validateDocument(context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
413
456
  },
414
457
  };
415
458
  },
416
459
  },
417
460
  };
418
461
  };
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
462
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
428
463
  category: 'Operations',
429
464
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
465
+ requiresSchema: true,
430
466
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
431
467
  category: 'Operations',
432
468
  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`.',
469
+ requiresSchema: true,
433
470
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
434
471
  category: 'Operations',
435
472
  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.`,
473
+ requiresSchema: true,
436
474
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
437
475
  category: ['Schema', 'Operations'],
438
476
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
477
+ requiresSchema: true,
439
478
  }), validationToRule('known-directives', 'KnownDirectives', {
440
479
  category: ['Schema', 'Operations'],
441
480
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
481
+ requiresSchema: true,
442
482
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
443
483
  category: 'Operations',
444
484
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
485
+ requiresSchema: true,
486
+ requiresSiblings: true,
445
487
  examples: [
446
488
  {
447
- title: 'Incorrect (fragment not defined in the document)',
489
+ title: 'Incorrect',
448
490
  code: /* GraphQL */ `
449
491
  query {
450
492
  user {
451
493
  id
452
- ...UserFields
494
+ ...UserFields # fragment not defined in the document
453
495
  }
454
496
  }
455
497
  `,
@@ -471,153 +513,153 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
471
513
  `,
472
514
  },
473
515
  {
474
- title: 'Correct (existing import to UserFields fragment)',
516
+ title: 'Correct (`UserFields` fragment located in a separate file)',
475
517
  code: /* GraphQL */ `
476
- #import '../UserFields.gql'
477
-
518
+ # user.gql
478
519
  query {
479
520
  user {
480
521
  id
481
522
  ...UserFields
482
523
  }
483
524
  }
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
525
 
504
- # Will give false positive error 'Unknown fragment "UserFields"'
505
- \${USER_FIELDS}
506
- \``,
526
+ # user-fields.gql
527
+ fragment UserFields on User {
528
+ id
529
+ }
530
+ `,
507
531
  },
508
532
  ],
509
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
533
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
510
534
  category: ['Schema', 'Operations'],
511
535
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
536
+ requiresSchema: true,
512
537
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
513
538
  category: 'Operations',
514
539
  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.`,
540
+ requiresSchema: true,
515
541
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
516
542
  category: 'Schema',
517
543
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
518
- requiresSchema: false,
519
544
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
520
545
  category: 'Operations',
521
546
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
547
+ requiresSchema: true,
522
548
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
523
549
  category: 'Operations',
524
550
  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', {
551
+ requiresSchema: true,
552
+ requiresSiblings: true,
553
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
526
554
  category: 'Operations',
527
555
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
556
+ requiresSchema: true,
528
557
  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;
558
+ }, ({ ruleId, context, schema, node }) => {
559
+ const siblings = requireSiblingsOperations(ruleId, context);
560
+ const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
561
+ var _a;
562
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
563
+ map[filePath].push(document);
564
+ return map;
565
+ }, Object.create(null));
566
+ const getParentNode = (currentFilePath, node) => {
567
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
568
+ if (fragmentDefs.size === 0) {
569
+ return node;
551
570
  }
552
- return null;
571
+ // skip iteration over documents for current filepath
572
+ delete FilePathToDocumentsMap[currentFilePath];
573
+ for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
574
+ const missingFragments = getMissingFragments(schema, {
575
+ kind: graphql.Kind.DOCUMENT,
576
+ definitions: documents,
577
+ });
578
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
579
+ if (isCurrentFileImportFragment) {
580
+ return getParentNode(filePath, {
581
+ kind: graphql.Kind.DOCUMENT,
582
+ definitions: [...node.definitions, ...documents],
583
+ });
584
+ }
585
+ }
586
+ return node;
553
587
  };
554
- return getParentNode(context.getFilename());
588
+ return getParentNode(context.getFilename(), node);
555
589
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
556
590
  category: 'Operations',
557
591
  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', {
592
+ requiresSchema: true,
593
+ requiresSiblings: true,
594
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
559
595
  category: 'Operations',
560
596
  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.`,
597
+ requiresSchema: true,
561
598
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
562
599
  category: 'Operations',
563
600
  description: `A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.`,
601
+ requiresSchema: true,
564
602
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
565
603
  category: 'Schema',
566
604
  description: `A type extension is only valid if the type is defined and has the same kind.`,
567
- requiresSchema: false,
568
- recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
605
+ recommended: false,
606
+ requiresSchema: true,
607
+ requiresSchemaToExtend: true,
569
608
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
570
609
  category: ['Schema', 'Operations'],
571
610
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
611
+ requiresSchema: true,
572
612
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
573
613
  category: 'Operations',
574
614
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
615
+ requiresSchema: true,
575
616
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
576
617
  category: 'Operations',
577
618
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
619
+ requiresSchema: true,
578
620
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
579
621
  category: 'Operations',
580
622
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
623
+ requiresSchema: true,
581
624
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
582
625
  category: 'Schema',
583
626
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
584
- requiresSchema: false,
585
627
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
586
628
  category: ['Schema', 'Operations'],
587
629
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
630
+ requiresSchema: true,
588
631
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
589
632
  category: 'Schema',
590
633
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
591
- requiresSchema: false,
592
634
  recommended: false,
593
635
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
594
636
  category: 'Schema',
595
637
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
596
- requiresSchema: false,
597
638
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
598
639
  category: 'Operations',
599
640
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
600
- requiresSchema: false,
601
641
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
602
642
  category: 'Schema',
603
643
  description: `A GraphQL document is only valid if it has only one type per operation.`,
604
- requiresSchema: false,
605
644
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
606
645
  category: 'Schema',
607
646
  description: `A GraphQL document is only valid if all defined types have unique names.`,
608
- requiresSchema: false,
609
647
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
610
648
  category: 'Operations',
611
649
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
650
+ requiresSchema: true,
612
651
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
613
652
  category: 'Operations',
614
653
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
654
+ requiresSchema: true,
615
655
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
616
656
  category: 'Operations',
617
657
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
658
+ requiresSchema: true,
618
659
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
619
660
  category: 'Operations',
620
661
  description: `Variables passed to field arguments conform to type.`,
662
+ requiresSchema: true,
621
663
  }));
622
664
 
623
665
  const ALPHABETIZE = 'ALPHABETIZE';
@@ -3662,35 +3704,36 @@ function getSiblingOperations(options, gqlConfig) {
3662
3704
  return siblingOperations;
3663
3705
  }
3664
3706
 
3665
- let graphqlConfig;
3666
- function loadGraphqlConfig(options) {
3707
+ let graphQLConfig;
3708
+ function loadGraphQLConfig(options) {
3667
3709
  // We don't want cache config on test environment
3668
3710
  // Otherwise schema and documents will be same for all tests
3669
- if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
3670
- return graphqlConfig;
3711
+ if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
3712
+ return graphQLConfig;
3671
3713
  }
3672
3714
  const onDiskConfig = options.skipGraphQLConfig
3673
3715
  ? null
3674
- : graphqlConfig$1.loadConfigSync({
3716
+ : graphqlConfig.loadConfigSync({
3675
3717
  throwOnEmpty: false,
3676
3718
  throwOnMissing: false,
3677
3719
  extensions: [addCodeFileLoaderExtension],
3678
3720
  });
3679
- graphqlConfig =
3721
+ const configOptions = options.projects
3722
+ ? { projects: options.projects }
3723
+ : {
3724
+ schema: (options.schema || ''),
3725
+ documents: options.documents || options.operations,
3726
+ extensions: options.extensions,
3727
+ include: options.include,
3728
+ exclude: options.exclude,
3729
+ };
3730
+ graphQLConfig =
3680
3731
  onDiskConfig ||
3681
- new graphqlConfig$1.GraphQLConfig({
3682
- config: options.projects
3683
- ? { projects: options.projects }
3684
- : {
3685
- schema: (options.schema || ''),
3686
- documents: options.documents || options.operations,
3687
- extensions: options.extensions,
3688
- include: options.include,
3689
- exclude: options.exclude,
3690
- },
3732
+ new graphqlConfig.GraphQLConfig({
3733
+ config: configOptions,
3691
3734
  filepath: 'virtual-config',
3692
3735
  }, [addCodeFileLoaderExtension]);
3693
- return graphqlConfig;
3736
+ return graphQLConfig;
3694
3737
  }
3695
3738
  const addCodeFileLoaderExtension = api => {
3696
3739
  api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
@@ -3780,7 +3823,7 @@ function parse(code, options) {
3780
3823
  return parseForESLint(code, options).ast;
3781
3824
  }
3782
3825
  function parseForESLint(code, options = {}) {
3783
- const gqlConfig = loadGraphqlConfig(options);
3826
+ const gqlConfig = loadGraphQLConfig(options);
3784
3827
  const schema = getSchema(options, gqlConfig);
3785
3828
  const parserServices = {
3786
3829
  hasTypeInfo: schema !== null,
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(context, schema = null, documentNode, rule, isSchemaToExtend = false) {
325
324
  if (documentNode.definitions.length === 0) {
326
325
  return;
327
326
  }
328
327
  try {
329
- const validationErrors = schema
330
- ? validate(schema, documentNode, rules)
331
- : validateSDL(documentNode, null, rules);
328
+ const validationErrors = schema && !isSchemaToExtend
329
+ ? validate(schema, documentNode, [rule])
330
+ : validateSDL(documentNode, schema, [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
@@ -353,97 +352,140 @@ function validateDoc(sourceNode, context, schema, documentNode, rules) {
353
352
  }
354
353
  catch (e) {
355
354
  context.report({
356
- node: sourceNode,
355
+ // Report on first character
356
+ loc: { column: 0, line: 1 },
357
357
  message: e.message,
358
358
  });
359
359
  }
360
360
  }
361
- const isGraphQLImportFile = rawSDL => {
362
- const trimmedRawSDL = rawSDL.trimLeft();
363
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
361
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
362
+ const typeInfo = new TypeInfo(schema);
363
+ const fragmentDefs = new Set();
364
+ const fragmentSpreads = new Set();
365
+ const visitor = visitWithTypeInfo(typeInfo, {
366
+ FragmentDefinition(node) {
367
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
368
+ },
369
+ FragmentSpread(node) {
370
+ const parentType = typeInfo.getParentType();
371
+ if (parentType) {
372
+ fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
373
+ }
374
+ },
375
+ });
376
+ visit(node, visitor);
377
+ return { fragmentDefs, fragmentSpreads };
364
378
  };
365
- const validationToRule = (name, ruleName, docs, getDocumentNode) => {
366
- var _a;
379
+ const getMissingFragments = (schema, node) => {
380
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
381
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
382
+ };
383
+ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
384
+ const missingFragments = getMissingFragments(schema, node);
385
+ if (missingFragments.length > 0) {
386
+ const siblings = requireSiblingsOperations(ruleId, context);
387
+ const fragmentsToAdd = [];
388
+ for (const missingFragment of missingFragments) {
389
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
390
+ const [foundFragment] = siblings
391
+ .getFragment(fragmentName)
392
+ .map(source => source.document)
393
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
394
+ if (foundFragment) {
395
+ fragmentsToAdd.push(foundFragment);
396
+ }
397
+ }
398
+ if (fragmentsToAdd.length > 0) {
399
+ // recall fn to make sure to add fragments inside fragments
400
+ return handleMissingFragments({
401
+ ruleId,
402
+ context,
403
+ schema,
404
+ node: {
405
+ kind: Kind.DOCUMENT,
406
+ definitions: [...node.definitions, ...fragmentsToAdd],
407
+ },
408
+ });
409
+ }
410
+ }
411
+ return node;
412
+ };
413
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
367
414
  let ruleFn = null;
368
415
  try {
369
416
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
370
417
  }
371
- catch (e) {
418
+ catch (_a) {
372
419
  try {
373
420
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
374
421
  }
375
- catch (e) {
422
+ catch (_b) {
376
423
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
377
424
  }
378
425
  }
379
- const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
380
426
  return {
381
- [name]: {
427
+ [ruleId]: {
382
428
  meta: {
383
429
  docs: {
384
430
  recommended: true,
385
431
  ...docs,
386
432
  graphQLJSRuleName: ruleName,
387
- requiresSchema,
388
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
433
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
389
434
  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
435
  },
391
436
  },
392
437
  create(context) {
438
+ if (!ruleFn) {
439
+ // eslint-disable-next-line no-console
440
+ 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...`);
441
+ return {};
442
+ }
443
+ const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
393
444
  return {
394
445
  Document(node) {
395
- if (!ruleFn) {
396
- // 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...`);
398
- return;
399
- }
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]);
446
+ const documentNode = getDocumentNode
447
+ ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
448
+ : node.rawNode();
449
+ validateDocument(context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
407
450
  },
408
451
  };
409
452
  },
410
453
  },
411
454
  };
412
455
  };
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
456
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
422
457
  category: 'Operations',
423
458
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
459
+ requiresSchema: true,
424
460
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
425
461
  category: 'Operations',
426
462
  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`.',
463
+ requiresSchema: true,
427
464
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
428
465
  category: 'Operations',
429
466
  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.`,
467
+ requiresSchema: true,
430
468
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
431
469
  category: ['Schema', 'Operations'],
432
470
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
471
+ requiresSchema: true,
433
472
  }), validationToRule('known-directives', 'KnownDirectives', {
434
473
  category: ['Schema', 'Operations'],
435
474
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
475
+ requiresSchema: true,
436
476
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
437
477
  category: 'Operations',
438
478
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
479
+ requiresSchema: true,
480
+ requiresSiblings: true,
439
481
  examples: [
440
482
  {
441
- title: 'Incorrect (fragment not defined in the document)',
483
+ title: 'Incorrect',
442
484
  code: /* GraphQL */ `
443
485
  query {
444
486
  user {
445
487
  id
446
- ...UserFields
488
+ ...UserFields # fragment not defined in the document
447
489
  }
448
490
  }
449
491
  `,
@@ -465,153 +507,153 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
465
507
  `,
466
508
  },
467
509
  {
468
- title: 'Correct (existing import to UserFields fragment)',
510
+ title: 'Correct (`UserFields` fragment located in a separate file)',
469
511
  code: /* GraphQL */ `
470
- #import '../UserFields.gql'
471
-
512
+ # user.gql
472
513
  query {
473
514
  user {
474
515
  id
475
516
  ...UserFields
476
517
  }
477
518
  }
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
519
 
498
- # Will give false positive error 'Unknown fragment "UserFields"'
499
- \${USER_FIELDS}
500
- \``,
520
+ # user-fields.gql
521
+ fragment UserFields on User {
522
+ id
523
+ }
524
+ `,
501
525
  },
502
526
  ],
503
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
527
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
504
528
  category: ['Schema', 'Operations'],
505
529
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
530
+ requiresSchema: true,
506
531
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
507
532
  category: 'Operations',
508
533
  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.`,
534
+ requiresSchema: true,
509
535
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
510
536
  category: 'Schema',
511
537
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
512
- requiresSchema: false,
513
538
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
514
539
  category: 'Operations',
515
540
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
541
+ requiresSchema: true,
516
542
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
517
543
  category: 'Operations',
518
544
  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', {
545
+ requiresSchema: true,
546
+ requiresSiblings: true,
547
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
520
548
  category: 'Operations',
521
549
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
550
+ requiresSchema: true,
522
551
  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;
552
+ }, ({ ruleId, context, schema, node }) => {
553
+ const siblings = requireSiblingsOperations(ruleId, context);
554
+ const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
555
+ var _a;
556
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
557
+ map[filePath].push(document);
558
+ return map;
559
+ }, Object.create(null));
560
+ const getParentNode = (currentFilePath, node) => {
561
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
562
+ if (fragmentDefs.size === 0) {
563
+ return node;
545
564
  }
546
- return null;
565
+ // skip iteration over documents for current filepath
566
+ delete FilePathToDocumentsMap[currentFilePath];
567
+ for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
568
+ const missingFragments = getMissingFragments(schema, {
569
+ kind: Kind.DOCUMENT,
570
+ definitions: documents,
571
+ });
572
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
573
+ if (isCurrentFileImportFragment) {
574
+ return getParentNode(filePath, {
575
+ kind: Kind.DOCUMENT,
576
+ definitions: [...node.definitions, ...documents],
577
+ });
578
+ }
579
+ }
580
+ return node;
547
581
  };
548
- return getParentNode(context.getFilename());
582
+ return getParentNode(context.getFilename(), node);
549
583
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
550
584
  category: 'Operations',
551
585
  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', {
586
+ requiresSchema: true,
587
+ requiresSiblings: true,
588
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
553
589
  category: 'Operations',
554
590
  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.`,
591
+ requiresSchema: true,
555
592
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
556
593
  category: 'Operations',
557
594
  description: `A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.`,
595
+ requiresSchema: true,
558
596
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
559
597
  category: 'Schema',
560
598
  description: `A type extension is only valid if the type is defined and has the same kind.`,
561
- requiresSchema: false,
562
- recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
599
+ recommended: false,
600
+ requiresSchema: true,
601
+ requiresSchemaToExtend: true,
563
602
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
564
603
  category: ['Schema', 'Operations'],
565
604
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
605
+ requiresSchema: true,
566
606
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
567
607
  category: 'Operations',
568
608
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
609
+ requiresSchema: true,
569
610
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
570
611
  category: 'Operations',
571
612
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
613
+ requiresSchema: true,
572
614
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
573
615
  category: 'Operations',
574
616
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
617
+ requiresSchema: true,
575
618
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
576
619
  category: 'Schema',
577
620
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
578
- requiresSchema: false,
579
621
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
580
622
  category: ['Schema', 'Operations'],
581
623
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
624
+ requiresSchema: true,
582
625
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
583
626
  category: 'Schema',
584
627
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
585
- requiresSchema: false,
586
628
  recommended: false,
587
629
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
588
630
  category: 'Schema',
589
631
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
590
- requiresSchema: false,
591
632
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
592
633
  category: 'Operations',
593
634
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
594
- requiresSchema: false,
595
635
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
596
636
  category: 'Schema',
597
637
  description: `A GraphQL document is only valid if it has only one type per operation.`,
598
- requiresSchema: false,
599
638
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
600
639
  category: 'Schema',
601
640
  description: `A GraphQL document is only valid if all defined types have unique names.`,
602
- requiresSchema: false,
603
641
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
604
642
  category: 'Operations',
605
643
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
644
+ requiresSchema: true,
606
645
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
607
646
  category: 'Operations',
608
647
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
648
+ requiresSchema: true,
609
649
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
610
650
  category: 'Operations',
611
651
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
652
+ requiresSchema: true,
612
653
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
613
654
  category: 'Operations',
614
655
  description: `Variables passed to field arguments conform to type.`,
656
+ requiresSchema: true,
615
657
  }));
616
658
 
617
659
  const ALPHABETIZE = 'ALPHABETIZE';
@@ -3656,12 +3698,12 @@ function getSiblingOperations(options, gqlConfig) {
3656
3698
  return siblingOperations;
3657
3699
  }
3658
3700
 
3659
- let graphqlConfig;
3660
- function loadGraphqlConfig(options) {
3701
+ let graphQLConfig;
3702
+ function loadGraphQLConfig(options) {
3661
3703
  // We don't want cache config on test environment
3662
3704
  // Otherwise schema and documents will be same for all tests
3663
- if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
3664
- return graphqlConfig;
3705
+ if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
3706
+ return graphQLConfig;
3665
3707
  }
3666
3708
  const onDiskConfig = options.skipGraphQLConfig
3667
3709
  ? null
@@ -3670,21 +3712,22 @@ function loadGraphqlConfig(options) {
3670
3712
  throwOnMissing: false,
3671
3713
  extensions: [addCodeFileLoaderExtension],
3672
3714
  });
3673
- graphqlConfig =
3715
+ const configOptions = options.projects
3716
+ ? { projects: options.projects }
3717
+ : {
3718
+ schema: (options.schema || ''),
3719
+ documents: options.documents || options.operations,
3720
+ extensions: options.extensions,
3721
+ include: options.include,
3722
+ exclude: options.exclude,
3723
+ };
3724
+ graphQLConfig =
3674
3725
  onDiskConfig ||
3675
3726
  new GraphQLConfig({
3676
- config: options.projects
3677
- ? { projects: options.projects }
3678
- : {
3679
- schema: (options.schema || ''),
3680
- documents: options.documents || options.operations,
3681
- extensions: options.extensions,
3682
- include: options.include,
3683
- exclude: options.exclude,
3684
- },
3727
+ config: configOptions,
3685
3728
  filepath: 'virtual-config',
3686
3729
  }, [addCodeFileLoaderExtension]);
3687
- return graphqlConfig;
3730
+ return graphQLConfig;
3688
3731
  }
3689
3732
  const addCodeFileLoaderExtension = api => {
3690
3733
  api.loaders.schema.register(new CodeFileLoader());
@@ -3774,7 +3817,7 @@ function parse(code, options) {
3774
3817
  return parseForESLint(code, options).ast;
3775
3818
  }
3776
3819
  function parseForESLint(code, options = {}) {
3777
- const gqlConfig = loadGraphqlConfig(options);
3820
+ const gqlConfig = loadGraphQLConfig(options);
3778
3821
  const schema = getSchema(options, gqlConfig);
3779
3822
  const parserServices = {
3780
3823
  hasTypeInfo: schema !== null,
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-d23e9e2.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
- "@graphql-tools/utils": "8.5.4",
13
+ "@graphql-tools/utils": "8.5.5",
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",
@@ -25,6 +31,7 @@
25
31
  "definition": "index.d.ts"
26
32
  },
27
33
  "exports": {
34
+ "./package.json": "./package.json",
28
35
  ".": {
29
36
  "require": "./index.js",
30
37
  "import": "./index.mjs"
@@ -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,9 @@ 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
+ requiresSchemaToExtend?: true;
54
55
  examples?: {
55
56
  title: string;
56
57
  code: string;