@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.
- package/docs/rules/known-fragment-names.md +9 -30
- package/docs/rules/no-undefined-variables.md +1 -1
- package/docs/rules/no-unused-variables.md +1 -1
- package/index.js +120 -95
- package/index.mjs +122 -97
- package/package.json +1 -2
- package/rules/graphql-js-validation.d.ts +2 -5
@@ -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: `
|
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
|
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 (
|
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
|
-
#
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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: `
|
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: `
|
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
|
339
|
-
|
340
|
-
|
341
|
-
}
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
-
|
358
|
-
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
|
364
|
-
const
|
365
|
-
|
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 (
|
413
|
+
catch (_b) {
|
374
414
|
try {
|
375
415
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
376
416
|
}
|
377
|
-
catch (
|
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
|
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
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
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
|
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 (
|
506
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
471
507
|
code: /* GraphQL */ `
|
472
|
-
#
|
473
|
-
|
508
|
+
# user.gql
|
474
509
|
query {
|
475
510
|
user {
|
476
511
|
id
|
477
512
|
...UserFields
|
478
513
|
}
|
479
514
|
}
|
480
|
-
`,
|
481
|
-
},
|
482
|
-
{
|
483
|
-
title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
|
484
|
-
code: `
|
485
|
-
const USER_FIELDS = gql\`
|
486
|
-
fragment UserFields on User {
|
487
|
-
id
|
488
|
-
}
|
489
|
-
\`
|
490
|
-
|
491
|
-
const GET_USER = /* GraphQL */ \`
|
492
|
-
# eslint @graphql-eslint/known-fragment-names: 'error'
|
493
|
-
|
494
|
-
query User {
|
495
|
-
user {
|
496
|
-
...UserFields
|
497
|
-
}
|
498
|
-
}
|
499
515
|
|
500
|
-
|
501
|
-
|
502
|
-
|
516
|
+
# user-fields.gql
|
517
|
+
fragment UserFields on User {
|
518
|
+
id
|
519
|
+
}
|
520
|
+
`,
|
503
521
|
},
|
504
522
|
],
|
505
|
-
},
|
523
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
506
524
|
category: ['Schema', 'Operations'],
|
507
525
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
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
|
-
|
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(
|
527
|
-
const
|
528
|
-
|
529
|
-
|
530
|
-
filePath
|
531
|
-
|
532
|
-
}));
|
533
|
-
const getParentNode = (
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
.filter(isGraphQLImportFile)
|
538
|
-
.map(line => _import.parseImportLine(line.replace('#', '')))
|
539
|
-
.some(o => filePath === path.join(path.dirname(docFilePath), o.from));
|
540
|
-
if (!isFileImported) {
|
541
|
-
continue;
|
542
|
-
}
|
543
|
-
// Import first file that import this file
|
544
|
-
const document = _import.processImport(docFilePath);
|
545
|
-
// Import most top file that import this file
|
546
|
-
return getParentNode(docFilePath) || document;
|
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
|
-
|
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
|
-
|
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,
|
1
|
+
import { Kind, validate, TypeInfo, visitWithTypeInfo, visit, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, isInterfaceType, Source, GraphQLError } from 'graphql';
|
2
2
|
import { validateSDL } from 'graphql/validation/validate';
|
3
|
-
import { processImport, parseImportLine } from '@graphql-tools/import';
|
4
3
|
import { statSync, existsSync, readFileSync } from 'fs';
|
5
|
-
import { dirname,
|
4
|
+
import { dirname, extname, basename, relative, resolve } from 'path';
|
6
5
|
import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
|
7
6
|
import lowerCase from 'lodash.lowercase';
|
8
7
|
import depthLimit from 'graphql-depth-limit';
|
@@ -329,34 +328,75 @@ function getLocation(loc, fieldName = '', offset) {
|
|
329
328
|
};
|
330
329
|
}
|
331
330
|
|
332
|
-
function
|
333
|
-
|
334
|
-
|
335
|
-
}
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
352
|
-
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
|
358
|
-
const
|
359
|
-
|
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 (
|
407
|
+
catch (_b) {
|
368
408
|
try {
|
369
409
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
370
410
|
}
|
371
|
-
catch (
|
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
|
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
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
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 (
|
500
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
465
501
|
code: /* GraphQL */ `
|
466
|
-
#
|
467
|
-
|
502
|
+
# user.gql
|
468
503
|
query {
|
469
504
|
user {
|
470
505
|
id
|
471
506
|
...UserFields
|
472
507
|
}
|
473
508
|
}
|
474
|
-
`,
|
475
|
-
},
|
476
|
-
{
|
477
|
-
title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
|
478
|
-
code: `
|
479
|
-
const USER_FIELDS = gql\`
|
480
|
-
fragment UserFields on User {
|
481
|
-
id
|
482
|
-
}
|
483
|
-
\`
|
484
|
-
|
485
|
-
const GET_USER = /* GraphQL */ \`
|
486
|
-
# eslint @graphql-eslint/known-fragment-names: 'error'
|
487
|
-
|
488
|
-
query User {
|
489
|
-
user {
|
490
|
-
...UserFields
|
491
|
-
}
|
492
|
-
}
|
493
509
|
|
494
|
-
|
495
|
-
|
496
|
-
|
510
|
+
# user-fields.gql
|
511
|
+
fragment UserFields on User {
|
512
|
+
id
|
513
|
+
}
|
514
|
+
`,
|
497
515
|
},
|
498
516
|
],
|
499
|
-
},
|
517
|
+
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
500
518
|
category: ['Schema', 'Operations'],
|
501
519
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
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
|
-
|
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(
|
521
|
-
const
|
522
|
-
|
523
|
-
|
524
|
-
filePath
|
525
|
-
|
526
|
-
}));
|
527
|
-
const getParentNode = (
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
.filter(isGraphQLImportFile)
|
532
|
-
.map(line => parseImportLine(line.replace('#', '')))
|
533
|
-
.some(o => filePath === join(dirname(docFilePath), o.from));
|
534
|
-
if (!isFileImported) {
|
535
|
-
continue;
|
536
|
-
}
|
537
|
-
// Import first file that import this file
|
538
|
-
const document = processImport(docFilePath);
|
539
|
-
// Import most top file that import this file
|
540
|
-
return getParentNode(docFilePath) || document;
|
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
|
-
|
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
|
-
|
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.
|
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 {
|
2
|
-
|
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>;
|