@graphql-eslint/eslint-plugin 3.1.0 → 3.2.0-alpha-2e742a6.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');
@@ -335,34 +334,75 @@ function getLocation(loc, fieldName = '', offset) {
335
334
  };
336
335
  }
337
336
 
338
- function extractRuleName(stack) {
339
- const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
340
- return match[1] || null;
341
- }
342
- function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
343
- var _a;
344
- if (((_a = documentNode === null || documentNode === void 0 ? void 0 : documentNode.definitions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
345
- try {
346
- const validationErrors = schema ? graphql.validate(schema, documentNode, rules) : validate.validateSDL(documentNode, null, rules);
347
- for (const error of validationErrors) {
348
- const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
349
- context.report({
350
- loc: getLocation({ start: error.locations[0] }),
351
- message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
352
- });
353
- }
354
- }
355
- catch (e) {
337
+ function validateDoc(sourceNode, context, schema, documentNode, rule) {
338
+ if (documentNode.definitions.length === 0) {
339
+ return;
340
+ }
341
+ try {
342
+ const validationErrors = schema
343
+ ? graphql.validate(schema, documentNode, [rule])
344
+ : validate.validateSDL(documentNode, null, [rule]);
345
+ for (const error of validationErrors) {
356
346
  context.report({
357
- node: sourceNode,
358
- message: e.message,
347
+ loc: getLocation({ start: error.locations[0] }),
348
+ message: error.message,
359
349
  });
360
350
  }
361
351
  }
352
+ catch (e) {
353
+ context.report({
354
+ node: sourceNode,
355
+ message: e.message,
356
+ });
357
+ }
362
358
  }
363
- const isGraphQLImportFile = rawSDL => {
364
- const trimmedRawSDL = rawSDL.trimLeft();
365
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
359
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
360
+ const typeInfo = new graphql.TypeInfo(schema);
361
+ const fragmentDefs = new Set();
362
+ const fragmentSpreads = new Set();
363
+ const visitor = graphql.visitWithTypeInfo(typeInfo, {
364
+ FragmentDefinition(node) {
365
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
366
+ },
367
+ FragmentSpread(node) {
368
+ const fieldDef = typeInfo.getFieldDef();
369
+ if (fieldDef) {
370
+ fragmentSpreads.add(`${node.name.value}:${typeInfo.getParentType().name}`);
371
+ }
372
+ },
373
+ });
374
+ graphql.visit(node, visitor);
375
+ return { fragmentDefs, fragmentSpreads };
376
+ };
377
+ const getMissingFragments = (schema, node) => {
378
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
379
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
380
+ };
381
+ const handleMissingFragments = ({ schema, node, ruleId, context }) => {
382
+ const missingFragments = getMissingFragments(schema, node);
383
+ if (missingFragments.length > 0) {
384
+ const siblings = requireSiblingsOperations(ruleId, context);
385
+ const fragmentsToAdd = [];
386
+ for (const missingFragment of missingFragments) {
387
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
388
+ const fragments = siblings
389
+ .getFragment(fragmentName)
390
+ .map(source => source.document)
391
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
392
+ if (fragments.length > 1) {
393
+ // eslint-disable-next-line no-console
394
+ console.warn(`You have ${fragments.length} fragments that have same name ${fragmentName} and same type ${fragmentTypeName}. That can provoke unexpected result for "${ruleId}" rule.`);
395
+ }
396
+ fragmentsToAdd.push(fragments[0]);
397
+ }
398
+ if (fragmentsToAdd.length > 0) {
399
+ return {
400
+ kind: graphql.Kind.DOCUMENT,
401
+ definitions: [...node.definitions, ...fragmentsToAdd],
402
+ };
403
+ }
404
+ }
405
+ return node;
366
406
  };
367
407
  const validationToRule = (name, ruleName, docs, getDocumentNode) => {
368
408
  var _a;
@@ -370,11 +410,11 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
370
410
  try {
371
411
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
372
412
  }
373
- catch (e) {
413
+ catch (_b) {
374
414
  try {
375
415
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
376
416
  }
377
- catch (e) {
417
+ catch (_c) {
378
418
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
379
419
  }
380
420
  }
@@ -396,30 +436,25 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
396
436
  Document(node) {
397
437
  if (!ruleFn) {
398
438
  // eslint-disable-next-line no-console
399
- 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...`);
439
+ 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...`);
400
440
  return;
401
441
  }
402
442
  const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
403
- let documentNode;
404
- const isRealFile = fs.existsSync(context.getFilename());
405
- if (isRealFile && getDocumentNode) {
406
- documentNode = getDocumentNode(context);
407
- }
408
- validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
443
+ const documentNode = node.rawNode();
444
+ validateDoc(node, context, schema, getDocumentNode
445
+ ? getDocumentNode({
446
+ schema,
447
+ node: documentNode,
448
+ ruleId: name,
449
+ context,
450
+ })
451
+ : documentNode, ruleFn);
409
452
  },
410
453
  };
411
454
  },
412
455
  },
413
456
  };
414
457
  };
415
- const importFiles = (context) => {
416
- const code = context.getSourceCode().text;
417
- if (!isGraphQLImportFile(code)) {
418
- return null;
419
- }
420
- // Import documents because file contains '#import' comments
421
- return _import.processImport(context.getFilename());
422
- };
423
458
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
424
459
  category: 'Operations',
425
460
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
@@ -438,14 +473,15 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
438
473
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
439
474
  category: 'Operations',
440
475
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
476
+ requiresSiblings: true,
441
477
  examples: [
442
478
  {
443
- title: 'Incorrect (fragment not defined in the document)',
479
+ title: 'Incorrect',
444
480
  code: /* GraphQL */ `
445
481
  query {
446
482
  user {
447
483
  id
448
- ...UserFields
484
+ ...UserFields # fragment not defined in the document
449
485
  }
450
486
  }
451
487
  `,
@@ -467,42 +503,24 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
467
503
  `,
468
504
  },
469
505
  {
470
- title: 'Correct (existing import to UserFields fragment)',
506
+ title: 'Correct (`UserFields` fragment located in a separate file)',
471
507
  code: /* GraphQL */ `
472
- #import '../UserFields.gql'
473
-
508
+ # user.gql
474
509
  query {
475
510
  user {
476
511
  id
477
512
  ...UserFields
478
513
  }
479
514
  }
480
- `,
481
- },
482
- {
483
- title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
484
- code: `
485
- const USER_FIELDS = gql\`
486
- fragment UserFields on User {
487
- id
488
- }
489
- \`
490
-
491
- const GET_USER = /* GraphQL */ \`
492
- # eslint @graphql-eslint/known-fragment-names: 'error'
493
-
494
- query User {
495
- user {
496
- ...UserFields
497
- }
498
- }
499
515
 
500
- # Will give false positive error 'Unknown fragment "UserFields"'
501
- \${USER_FIELDS}
502
- \``,
516
+ # user-fields.gql
517
+ fragment UserFields on User {
518
+ id
519
+ }
520
+ `,
503
521
  },
504
522
  ],
505
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
523
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
506
524
  category: ['Schema', 'Operations'],
507
525
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
508
526
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
@@ -518,40 +536,47 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
518
536
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
519
537
  category: 'Operations',
520
538
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
521
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
539
+ requiresSiblings: true,
540
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
522
541
  category: 'Operations',
523
542
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
524
543
  requiresSiblings: true,
525
- }, context => {
526
- const siblings = requireSiblingsOperations('no-unused-fragments', context);
527
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
528
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
529
- .map(({ filePath, document }) => ({
530
- filePath,
531
- code: document.loc.source.body,
532
- }));
533
- const getParentNode = (filePath) => {
534
- for (const { filePath: docFilePath, code } of documents) {
535
- const isFileImported = code
536
- .split('\n')
537
- .filter(isGraphQLImportFile)
538
- .map(line => _import.parseImportLine(line.replace('#', '')))
539
- .some(o => filePath === path.join(path.dirname(docFilePath), o.from));
540
- if (!isFileImported) {
541
- continue;
542
- }
543
- // Import first file that import this file
544
- const document = _import.processImport(docFilePath);
545
- // Import most top file that import this file
546
- return getParentNode(docFilePath) || document;
544
+ }, ({ context, node, schema, ruleId }) => {
545
+ const siblings = requireSiblingsOperations(ruleId, context);
546
+ const filePathForDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
547
+ var _a;
548
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
549
+ map[filePath].push(document);
550
+ return map;
551
+ }, Object.create(null));
552
+ const getParentNode = (currentFilePath, node) => {
553
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
554
+ if (fragmentDefs.size === 0) {
555
+ return node;
547
556
  }
548
- return null;
557
+ // skip iteration over documents for current filepath
558
+ delete filePathForDocumentsMap[currentFilePath];
559
+ for (const [filePath, documents] of Object.entries(filePathForDocumentsMap)) {
560
+ const missingFragments = getMissingFragments(schema, {
561
+ kind: graphql.Kind.DOCUMENT,
562
+ definitions: documents,
563
+ });
564
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
565
+ if (isCurrentFileImportFragment) {
566
+ return getParentNode(filePath, {
567
+ kind: graphql.Kind.DOCUMENT,
568
+ definitions: [...node.definitions, ...documents],
569
+ });
570
+ }
571
+ }
572
+ return node;
549
573
  };
550
- return getParentNode(context.getFilename());
574
+ return getParentNode(context.getFilename(), node);
551
575
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
552
576
  category: 'Operations',
553
577
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
554
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
578
+ requiresSiblings: true,
579
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
555
580
  category: 'Operations',
556
581
  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.`,
557
582
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
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';
@@ -329,34 +328,75 @@ function getLocation(loc, fieldName = '', offset) {
329
328
  };
330
329
  }
331
330
 
332
- function extractRuleName(stack) {
333
- const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
334
- return match[1] || null;
335
- }
336
- function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
337
- var _a;
338
- if (((_a = documentNode === null || documentNode === void 0 ? void 0 : documentNode.definitions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
339
- try {
340
- const validationErrors = schema ? validate(schema, documentNode, rules) : validateSDL(documentNode, null, rules);
341
- for (const error of validationErrors) {
342
- const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
343
- context.report({
344
- loc: getLocation({ start: error.locations[0] }),
345
- message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
346
- });
347
- }
348
- }
349
- catch (e) {
331
+ function validateDoc(sourceNode, context, schema, documentNode, rule) {
332
+ if (documentNode.definitions.length === 0) {
333
+ return;
334
+ }
335
+ try {
336
+ const validationErrors = schema
337
+ ? validate(schema, documentNode, [rule])
338
+ : validateSDL(documentNode, null, [rule]);
339
+ for (const error of validationErrors) {
350
340
  context.report({
351
- node: sourceNode,
352
- message: e.message,
341
+ loc: getLocation({ start: error.locations[0] }),
342
+ message: error.message,
353
343
  });
354
344
  }
355
345
  }
346
+ catch (e) {
347
+ context.report({
348
+ node: sourceNode,
349
+ message: e.message,
350
+ });
351
+ }
356
352
  }
357
- const isGraphQLImportFile = rawSDL => {
358
- const trimmedRawSDL = rawSDL.trimLeft();
359
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
353
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
354
+ const typeInfo = new TypeInfo(schema);
355
+ const fragmentDefs = new Set();
356
+ const fragmentSpreads = new Set();
357
+ const visitor = visitWithTypeInfo(typeInfo, {
358
+ FragmentDefinition(node) {
359
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
360
+ },
361
+ FragmentSpread(node) {
362
+ const fieldDef = typeInfo.getFieldDef();
363
+ if (fieldDef) {
364
+ fragmentSpreads.add(`${node.name.value}:${typeInfo.getParentType().name}`);
365
+ }
366
+ },
367
+ });
368
+ visit(node, visitor);
369
+ return { fragmentDefs, fragmentSpreads };
370
+ };
371
+ const getMissingFragments = (schema, node) => {
372
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
373
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
374
+ };
375
+ const handleMissingFragments = ({ schema, node, ruleId, context }) => {
376
+ const missingFragments = getMissingFragments(schema, node);
377
+ if (missingFragments.length > 0) {
378
+ const siblings = requireSiblingsOperations(ruleId, context);
379
+ const fragmentsToAdd = [];
380
+ for (const missingFragment of missingFragments) {
381
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
382
+ const fragments = siblings
383
+ .getFragment(fragmentName)
384
+ .map(source => source.document)
385
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
386
+ if (fragments.length > 1) {
387
+ // eslint-disable-next-line no-console
388
+ console.warn(`You have ${fragments.length} fragments that have same name ${fragmentName} and same type ${fragmentTypeName}. That can provoke unexpected result for "${ruleId}" rule.`);
389
+ }
390
+ fragmentsToAdd.push(fragments[0]);
391
+ }
392
+ if (fragmentsToAdd.length > 0) {
393
+ return {
394
+ kind: Kind.DOCUMENT,
395
+ definitions: [...node.definitions, ...fragmentsToAdd],
396
+ };
397
+ }
398
+ }
399
+ return node;
360
400
  };
361
401
  const validationToRule = (name, ruleName, docs, getDocumentNode) => {
362
402
  var _a;
@@ -364,11 +404,11 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
364
404
  try {
365
405
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
366
406
  }
367
- catch (e) {
407
+ catch (_b) {
368
408
  try {
369
409
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
370
410
  }
371
- catch (e) {
411
+ catch (_c) {
372
412
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
373
413
  }
374
414
  }
@@ -390,30 +430,25 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
390
430
  Document(node) {
391
431
  if (!ruleFn) {
392
432
  // eslint-disable-next-line no-console
393
- 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...`);
433
+ 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...`);
394
434
  return;
395
435
  }
396
436
  const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
397
- let documentNode;
398
- const isRealFile = existsSync(context.getFilename());
399
- if (isRealFile && getDocumentNode) {
400
- documentNode = getDocumentNode(context);
401
- }
402
- validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
437
+ const documentNode = node.rawNode();
438
+ validateDoc(node, context, schema, getDocumentNode
439
+ ? getDocumentNode({
440
+ schema,
441
+ node: documentNode,
442
+ ruleId: name,
443
+ context,
444
+ })
445
+ : documentNode, ruleFn);
403
446
  },
404
447
  };
405
448
  },
406
449
  },
407
450
  };
408
451
  };
409
- const importFiles = (context) => {
410
- const code = context.getSourceCode().text;
411
- if (!isGraphQLImportFile(code)) {
412
- return null;
413
- }
414
- // Import documents because file contains '#import' comments
415
- return processImport(context.getFilename());
416
- };
417
452
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
418
453
  category: 'Operations',
419
454
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
@@ -432,14 +467,15 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
432
467
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
433
468
  category: 'Operations',
434
469
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
470
+ requiresSiblings: true,
435
471
  examples: [
436
472
  {
437
- title: 'Incorrect (fragment not defined in the document)',
473
+ title: 'Incorrect',
438
474
  code: /* GraphQL */ `
439
475
  query {
440
476
  user {
441
477
  id
442
- ...UserFields
478
+ ...UserFields # fragment not defined in the document
443
479
  }
444
480
  }
445
481
  `,
@@ -461,42 +497,24 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
461
497
  `,
462
498
  },
463
499
  {
464
- title: 'Correct (existing import to UserFields fragment)',
500
+ title: 'Correct (`UserFields` fragment located in a separate file)',
465
501
  code: /* GraphQL */ `
466
- #import '../UserFields.gql'
467
-
502
+ # user.gql
468
503
  query {
469
504
  user {
470
505
  id
471
506
  ...UserFields
472
507
  }
473
508
  }
474
- `,
475
- },
476
- {
477
- title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
478
- code: `
479
- const USER_FIELDS = gql\`
480
- fragment UserFields on User {
481
- id
482
- }
483
- \`
484
-
485
- const GET_USER = /* GraphQL */ \`
486
- # eslint @graphql-eslint/known-fragment-names: 'error'
487
-
488
- query User {
489
- user {
490
- ...UserFields
491
- }
492
- }
493
509
 
494
- # Will give false positive error 'Unknown fragment "UserFields"'
495
- \${USER_FIELDS}
496
- \``,
510
+ # user-fields.gql
511
+ fragment UserFields on User {
512
+ id
513
+ }
514
+ `,
497
515
  },
498
516
  ],
499
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
517
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
500
518
  category: ['Schema', 'Operations'],
501
519
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
502
520
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
@@ -512,40 +530,47 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
512
530
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
513
531
  category: 'Operations',
514
532
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
515
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
533
+ requiresSiblings: true,
534
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
516
535
  category: 'Operations',
517
536
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
518
537
  requiresSiblings: true,
519
- }, context => {
520
- const siblings = requireSiblingsOperations('no-unused-fragments', context);
521
- const documents = [...siblings.getOperations(), ...siblings.getFragments()]
522
- .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
523
- .map(({ filePath, document }) => ({
524
- filePath,
525
- code: document.loc.source.body,
526
- }));
527
- const getParentNode = (filePath) => {
528
- for (const { filePath: docFilePath, code } of documents) {
529
- const isFileImported = code
530
- .split('\n')
531
- .filter(isGraphQLImportFile)
532
- .map(line => parseImportLine(line.replace('#', '')))
533
- .some(o => filePath === join(dirname(docFilePath), o.from));
534
- if (!isFileImported) {
535
- continue;
536
- }
537
- // Import first file that import this file
538
- const document = processImport(docFilePath);
539
- // Import most top file that import this file
540
- return getParentNode(docFilePath) || document;
538
+ }, ({ context, node, schema, ruleId }) => {
539
+ const siblings = requireSiblingsOperations(ruleId, context);
540
+ const filePathForDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
541
+ var _a;
542
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
543
+ map[filePath].push(document);
544
+ return map;
545
+ }, Object.create(null));
546
+ const getParentNode = (currentFilePath, node) => {
547
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
548
+ if (fragmentDefs.size === 0) {
549
+ return node;
541
550
  }
542
- return null;
551
+ // skip iteration over documents for current filepath
552
+ delete filePathForDocumentsMap[currentFilePath];
553
+ for (const [filePath, documents] of Object.entries(filePathForDocumentsMap)) {
554
+ const missingFragments = getMissingFragments(schema, {
555
+ kind: Kind.DOCUMENT,
556
+ definitions: documents,
557
+ });
558
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
559
+ if (isCurrentFileImportFragment) {
560
+ return getParentNode(filePath, {
561
+ kind: Kind.DOCUMENT,
562
+ definitions: [...node.definitions, ...documents],
563
+ });
564
+ }
565
+ }
566
+ return node;
543
567
  };
544
- return getParentNode(context.getFilename());
568
+ return getParentNode(context.getFilename(), node);
545
569
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
546
570
  category: 'Operations',
547
571
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
548
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
572
+ requiresSiblings: true,
573
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
549
574
  category: 'Operations',
550
575
  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.`,
551
576
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphql-eslint/eslint-plugin",
3
- "version": "3.1.0",
3
+ "version": "3.2.0-alpha-2e742a6.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "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,7 +9,6 @@
9
9
  "@babel/code-frame": "7.16.0",
10
10
  "@graphql-tools/code-file-loader": "7.2.2",
11
11
  "@graphql-tools/graphql-tag-pluck": "7.1.4",
12
- "@graphql-tools/import": "6.6.1",
13
12
  "@graphql-tools/utils": "8.5.3",
14
13
  "graphql-config": "4.1.0",
15
14
  "graphql-depth-limit": "1.1.0",
@@ -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>, ruleName?: string | null): 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>;