@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.
- 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/docs/rules/possible-type-extension.md +1 -1
- package/graphql-config.d.ts +1 -1
- package/index.js +162 -119
- package/index.mjs +161 -118
- package/package.json +10 -3
- package/rules/graphql-js-validation.d.ts +2 -5
- package/types.d.ts +3 -2
@@ -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
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
- Category: `Schema`
|
4
4
|
- Rule name: `@graphql-eslint/possible-type-extension`
|
5
|
-
- Requires 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.
|
package/graphql-config.d.ts
CHANGED
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
|
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
|
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,
|
337
|
-
: validate.validateSDL(documentNode,
|
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
|
-
|
361
|
+
// Report on first character
|
362
|
+
loc: { column: 0, line: 1 },
|
363
363
|
message: e.message,
|
364
364
|
});
|
365
365
|
}
|
366
366
|
}
|
367
|
-
const
|
368
|
-
const
|
369
|
-
|
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
|
372
|
-
|
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 (
|
424
|
+
catch (_a) {
|
378
425
|
try {
|
379
426
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
380
427
|
}
|
381
|
-
catch (
|
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
|
-
[
|
433
|
+
[ruleId]: {
|
388
434
|
meta: {
|
389
435
|
docs: {
|
390
436
|
recommended: true,
|
391
437
|
...docs,
|
392
438
|
graphQLJSRuleName: ruleName,
|
393
|
-
|
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
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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
|
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 (
|
516
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
475
517
|
code: /* GraphQL */ `
|
476
|
-
#
|
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
|
-
|
505
|
-
|
506
|
-
|
526
|
+
# user-fields.gql
|
527
|
+
fragment UserFields on User {
|
528
|
+
id
|
529
|
+
}
|
530
|
+
`,
|
507
531
|
},
|
508
532
|
],
|
509
|
-
},
|
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
|
-
|
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(
|
531
|
-
const
|
532
|
-
|
533
|
-
|
534
|
-
filePath
|
535
|
-
|
536
|
-
}));
|
537
|
-
const getParentNode = (
|
538
|
-
|
539
|
-
|
540
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
568
|
-
|
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
|
3666
|
-
function
|
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' &&
|
3670
|
-
return
|
3711
|
+
if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
|
3712
|
+
return graphQLConfig;
|
3671
3713
|
}
|
3672
3714
|
const onDiskConfig = options.skipGraphQLConfig
|
3673
3715
|
? null
|
3674
|
-
: graphqlConfig
|
3716
|
+
: graphqlConfig.loadConfigSync({
|
3675
3717
|
throwOnEmpty: false,
|
3676
3718
|
throwOnMissing: false,
|
3677
3719
|
extensions: [addCodeFileLoaderExtension],
|
3678
3720
|
});
|
3679
|
-
|
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
|
3682
|
-
config:
|
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
|
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 =
|
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,
|
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';
|
@@ -321,14 +320,14 @@ function getLocation(loc, fieldName = '', offset) {
|
|
321
320
|
};
|
322
321
|
}
|
323
322
|
|
324
|
-
function
|
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,
|
331
|
-
: validateSDL(documentNode,
|
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
|
-
|
355
|
+
// Report on first character
|
356
|
+
loc: { column: 0, line: 1 },
|
357
357
|
message: e.message,
|
358
358
|
});
|
359
359
|
}
|
360
360
|
}
|
361
|
-
const
|
362
|
-
const
|
363
|
-
|
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
|
366
|
-
|
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 (
|
418
|
+
catch (_a) {
|
372
419
|
try {
|
373
420
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
374
421
|
}
|
375
|
-
catch (
|
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
|
-
[
|
427
|
+
[ruleId]: {
|
382
428
|
meta: {
|
383
429
|
docs: {
|
384
430
|
recommended: true,
|
385
431
|
...docs,
|
386
432
|
graphQLJSRuleName: ruleName,
|
387
|
-
|
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
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
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
|
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 (
|
510
|
+
title: 'Correct (`UserFields` fragment located in a separate file)',
|
469
511
|
code: /* GraphQL */ `
|
470
|
-
#
|
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
|
-
|
499
|
-
|
500
|
-
|
520
|
+
# user-fields.gql
|
521
|
+
fragment UserFields on User {
|
522
|
+
id
|
523
|
+
}
|
524
|
+
`,
|
501
525
|
},
|
502
526
|
],
|
503
|
-
},
|
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
|
-
|
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(
|
525
|
-
const
|
526
|
-
|
527
|
-
|
528
|
-
filePath
|
529
|
-
|
530
|
-
}));
|
531
|
-
const getParentNode = (
|
532
|
-
|
533
|
-
|
534
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
562
|
-
|
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
|
3660
|
-
function
|
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' &&
|
3664
|
-
return
|
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
|
-
|
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:
|
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
|
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 =
|
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.
|
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/
|
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 {
|
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>): 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?:
|
53
|
-
requiresSiblings?:
|
52
|
+
requiresSchema?: true;
|
53
|
+
requiresSiblings?: true;
|
54
|
+
requiresSchemaToExtend?: true;
|
54
55
|
examples?: {
|
55
56
|
title: string;
|
56
57
|
code: string;
|