@graphql-eslint/eslint-plugin 3.2.0-alpha-6aa2721.0 → 3.2.0-alpha-45f5fcb.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 +30 -9
- package/docs/rules/no-undefined-variables.md +1 -1
- package/docs/rules/no-unused-variables.md +1 -1
- package/index.js +181 -209
- package/index.mjs +183 -211
- package/package.json +4 -10
- package/rules/graphql-js-validation.d.ts +5 -2
- package/rules/index.d.ts +2 -129
- package/rules/match-document-filename.d.ts +3 -3
- package/rules/naming-convention.d.ts +1 -1
- package/types.d.ts +2 -2
- package/utils.d.ts +1 -7
package/index.js
CHANGED
@@ -6,6 +6,7 @@ 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');
|
9
10
|
const fs = require('fs');
|
10
11
|
const path = require('path');
|
11
12
|
const utils = require('@graphql-tools/utils');
|
@@ -285,14 +286,6 @@ const TYPES_KINDS = [
|
|
285
286
|
graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
286
287
|
graphql.Kind.UNION_TYPE_DEFINITION,
|
287
288
|
];
|
288
|
-
var CaseStyle;
|
289
|
-
(function (CaseStyle) {
|
290
|
-
CaseStyle["camelCase"] = "camelCase";
|
291
|
-
CaseStyle["pascalCase"] = "PascalCase";
|
292
|
-
CaseStyle["snakeCase"] = "snake_case";
|
293
|
-
CaseStyle["upperCase"] = "UPPER_CASE";
|
294
|
-
CaseStyle["kebabCase"] = "kebab-case";
|
295
|
-
})(CaseStyle || (CaseStyle = {}));
|
296
289
|
const pascalCase = (str) => lowerCase(str)
|
297
290
|
.split(' ')
|
298
291
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
@@ -303,15 +296,15 @@ const camelCase = (str) => {
|
|
303
296
|
};
|
304
297
|
const convertCase = (style, str) => {
|
305
298
|
switch (style) {
|
306
|
-
case
|
299
|
+
case 'camelCase':
|
307
300
|
return camelCase(str);
|
308
|
-
case
|
301
|
+
case 'PascalCase':
|
309
302
|
return pascalCase(str);
|
310
|
-
case
|
303
|
+
case 'snake_case':
|
311
304
|
return lowerCase(str).replace(/ /g, '_');
|
312
|
-
case
|
305
|
+
case 'UPPER_CASE':
|
313
306
|
return lowerCase(str).replace(/ /g, '_').toUpperCase();
|
314
|
-
case
|
307
|
+
case 'kebab-case':
|
315
308
|
return lowerCase(str).replace(/ /g, '-');
|
316
309
|
}
|
317
310
|
};
|
@@ -334,99 +327,59 @@ function getLocation(loc, fieldName = '', offset) {
|
|
334
327
|
};
|
335
328
|
}
|
336
329
|
|
337
|
-
function
|
338
|
-
|
339
|
-
|
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) {
|
346
|
-
context.report({
|
347
|
-
loc: getLocation({ start: error.locations[0] }),
|
348
|
-
message: error.message,
|
349
|
-
});
|
350
|
-
}
|
351
|
-
}
|
352
|
-
catch (e) {
|
353
|
-
context.report({
|
354
|
-
node: sourceNode,
|
355
|
-
message: e.message,
|
356
|
-
});
|
357
|
-
}
|
330
|
+
function extractRuleName(stack) {
|
331
|
+
const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
|
332
|
+
return match[1] || null;
|
358
333
|
}
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
|
334
|
+
function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
|
335
|
+
var _a;
|
336
|
+
if (((_a = documentNode === null || documentNode === void 0 ? void 0 : documentNode.definitions) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
337
|
+
try {
|
338
|
+
const validationErrors = schema ? graphql.validate(schema, documentNode, rules) : validate.validateSDL(documentNode, null, rules);
|
339
|
+
for (const error of validationErrors) {
|
340
|
+
const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
|
341
|
+
context.report({
|
342
|
+
loc: getLocation({ start: error.locations[0] }),
|
343
|
+
message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
|
344
|
+
});
|
371
345
|
}
|
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 = ({ ruleId, context, schema, node }) => {
|
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
|
-
fragmentsToAdd.push(fragments[0]);
|
393
346
|
}
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
context,
|
399
|
-
schema,
|
400
|
-
node: {
|
401
|
-
kind: graphql.Kind.DOCUMENT,
|
402
|
-
definitions: [...node.definitions, ...fragmentsToAdd],
|
403
|
-
},
|
347
|
+
catch (e) {
|
348
|
+
context.report({
|
349
|
+
node: sourceNode,
|
350
|
+
message: e.message,
|
404
351
|
});
|
405
352
|
}
|
406
353
|
}
|
407
|
-
|
354
|
+
}
|
355
|
+
const isGraphQLImportFile = rawSDL => {
|
356
|
+
const trimmedRawSDL = rawSDL.trimLeft();
|
357
|
+
return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
|
408
358
|
};
|
409
|
-
const validationToRule = (
|
359
|
+
const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
360
|
+
var _a;
|
410
361
|
let ruleFn = null;
|
411
362
|
try {
|
412
363
|
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
413
364
|
}
|
414
|
-
catch (
|
365
|
+
catch (e) {
|
415
366
|
try {
|
416
367
|
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
417
368
|
}
|
418
|
-
catch (
|
369
|
+
catch (e) {
|
419
370
|
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
420
371
|
}
|
421
372
|
}
|
373
|
+
const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
|
422
374
|
return {
|
423
|
-
[
|
375
|
+
[name]: {
|
424
376
|
meta: {
|
425
377
|
docs: {
|
426
378
|
recommended: true,
|
427
379
|
...docs,
|
428
380
|
graphQLJSRuleName: ruleName,
|
429
|
-
|
381
|
+
requiresSchema,
|
382
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
|
430
383
|
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).`,
|
431
384
|
},
|
432
385
|
},
|
@@ -435,53 +388,56 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
|
|
435
388
|
Document(node) {
|
436
389
|
if (!ruleFn) {
|
437
390
|
// eslint-disable-next-line no-console
|
438
|
-
console.warn(`You rule "${
|
391
|
+
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
392
|
return;
|
440
393
|
}
|
441
|
-
const schema =
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
394
|
+
const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
|
395
|
+
let documentNode;
|
396
|
+
const isRealFile = fs.existsSync(context.getFilename());
|
397
|
+
if (isRealFile && getDocumentNode) {
|
398
|
+
documentNode = getDocumentNode(context);
|
399
|
+
}
|
400
|
+
validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn], ruleName);
|
446
401
|
},
|
447
402
|
};
|
448
403
|
},
|
449
404
|
},
|
450
405
|
};
|
451
406
|
};
|
407
|
+
const importFiles = (context) => {
|
408
|
+
const code = context.getSourceCode().text;
|
409
|
+
if (!isGraphQLImportFile(code)) {
|
410
|
+
return null;
|
411
|
+
}
|
412
|
+
// Import documents because file contains '#import' comments
|
413
|
+
return _import.processImport(context.getFilename());
|
414
|
+
};
|
452
415
|
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
453
416
|
category: 'Operations',
|
454
417
|
description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
|
455
|
-
requiresSchema: true,
|
456
418
|
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
457
419
|
category: 'Operations',
|
458
420
|
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`.',
|
459
|
-
requiresSchema: true,
|
460
421
|
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
461
422
|
category: 'Operations',
|
462
423
|
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.`,
|
463
|
-
requiresSchema: true,
|
464
424
|
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
465
425
|
category: ['Schema', 'Operations'],
|
466
426
|
description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
|
467
|
-
requiresSchema: true,
|
468
427
|
}), validationToRule('known-directives', 'KnownDirectives', {
|
469
428
|
category: ['Schema', 'Operations'],
|
470
429
|
description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
|
471
|
-
requiresSchema: true,
|
472
430
|
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
473
431
|
category: 'Operations',
|
474
432
|
description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
|
475
|
-
requiresSchema: true,
|
476
|
-
requiresSiblings: true,
|
477
433
|
examples: [
|
478
434
|
{
|
479
|
-
title: 'Incorrect',
|
435
|
+
title: 'Incorrect (fragment not defined in the document)',
|
480
436
|
code: /* GraphQL */ `
|
481
437
|
query {
|
482
438
|
user {
|
483
439
|
id
|
484
|
-
...UserFields
|
440
|
+
...UserFields
|
485
441
|
}
|
486
442
|
}
|
487
443
|
`,
|
@@ -503,151 +459,153 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
|
|
503
459
|
`,
|
504
460
|
},
|
505
461
|
{
|
506
|
-
title: 'Correct (
|
462
|
+
title: 'Correct (existing import to UserFields fragment)',
|
507
463
|
code: /* GraphQL */ `
|
508
|
-
#
|
464
|
+
#import '../UserFields.gql'
|
465
|
+
|
509
466
|
query {
|
510
467
|
user {
|
511
468
|
id
|
512
469
|
...UserFields
|
513
470
|
}
|
514
471
|
}
|
515
|
-
|
516
|
-
# user-fields.gql
|
517
|
-
fragment UserFields on User {
|
518
|
-
id
|
519
|
-
}
|
520
472
|
`,
|
521
473
|
},
|
474
|
+
{
|
475
|
+
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.",
|
476
|
+
code: `
|
477
|
+
const USER_FIELDS = gql\`
|
478
|
+
fragment UserFields on User {
|
479
|
+
id
|
480
|
+
}
|
481
|
+
\`
|
482
|
+
|
483
|
+
const GET_USER = /* GraphQL */ \`
|
484
|
+
# eslint @graphql-eslint/known-fragment-names: 'error'
|
485
|
+
|
486
|
+
query User {
|
487
|
+
user {
|
488
|
+
...UserFields
|
489
|
+
}
|
490
|
+
}
|
491
|
+
|
492
|
+
# Will give false positive error 'Unknown fragment "UserFields"'
|
493
|
+
\${USER_FIELDS}
|
494
|
+
\``,
|
495
|
+
},
|
522
496
|
],
|
523
|
-
},
|
497
|
+
}, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
|
524
498
|
category: ['Schema', 'Operations'],
|
525
499
|
description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
|
526
|
-
requiresSchema: true,
|
527
500
|
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
528
501
|
category: 'Operations',
|
529
502
|
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.`,
|
530
|
-
requiresSchema: true,
|
531
503
|
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
532
504
|
category: 'Schema',
|
533
505
|
description: `A GraphQL document is only valid if it contains only one schema definition.`,
|
506
|
+
requiresSchema: false,
|
534
507
|
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
535
508
|
category: 'Operations',
|
536
509
|
description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
|
537
|
-
requiresSchema: true,
|
538
510
|
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
539
511
|
category: 'Operations',
|
540
512
|
description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
|
541
|
-
|
542
|
-
requiresSiblings: true,
|
543
|
-
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
513
|
+
}, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
544
514
|
category: 'Operations',
|
545
515
|
description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
|
546
|
-
requiresSchema: true,
|
547
516
|
requiresSiblings: true,
|
548
|
-
},
|
549
|
-
const siblings = requireSiblingsOperations(
|
550
|
-
const
|
551
|
-
|
552
|
-
(
|
553
|
-
|
554
|
-
|
555
|
-
}
|
556
|
-
const getParentNode = (
|
557
|
-
const {
|
558
|
-
|
559
|
-
|
517
|
+
}, context => {
|
518
|
+
const siblings = requireSiblingsOperations('no-unused-fragments', context);
|
519
|
+
const documents = [...siblings.getOperations(), ...siblings.getFragments()]
|
520
|
+
.filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
|
521
|
+
.map(({ filePath, document }) => ({
|
522
|
+
filePath,
|
523
|
+
code: document.loc.source.body,
|
524
|
+
}));
|
525
|
+
const getParentNode = (filePath) => {
|
526
|
+
for (const { filePath: docFilePath, code } of documents) {
|
527
|
+
const isFileImported = code
|
528
|
+
.split('\n')
|
529
|
+
.filter(isGraphQLImportFile)
|
530
|
+
.map(line => _import.parseImportLine(line.replace('#', '')))
|
531
|
+
.some(o => filePath === path.join(path.dirname(docFilePath), o.from));
|
532
|
+
if (!isFileImported) {
|
533
|
+
continue;
|
534
|
+
}
|
535
|
+
// Import first file that import this file
|
536
|
+
const document = _import.processImport(docFilePath);
|
537
|
+
// Import most top file that import this file
|
538
|
+
return getParentNode(docFilePath) || document;
|
560
539
|
}
|
561
|
-
|
562
|
-
delete FilePathToDocumentsMap[currentFilePath];
|
563
|
-
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
564
|
-
const missingFragments = getMissingFragments(schema, {
|
565
|
-
kind: graphql.Kind.DOCUMENT,
|
566
|
-
definitions: documents,
|
567
|
-
});
|
568
|
-
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
569
|
-
if (isCurrentFileImportFragment) {
|
570
|
-
return getParentNode(filePath, {
|
571
|
-
kind: graphql.Kind.DOCUMENT,
|
572
|
-
definitions: [...node.definitions, ...documents],
|
573
|
-
});
|
574
|
-
}
|
575
|
-
}
|
576
|
-
return node;
|
540
|
+
return null;
|
577
541
|
};
|
578
|
-
return getParentNode(context.getFilename()
|
542
|
+
return getParentNode(context.getFilename());
|
579
543
|
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
580
544
|
category: 'Operations',
|
581
545
|
description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
|
582
|
-
|
583
|
-
requiresSiblings: true,
|
584
|
-
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
546
|
+
}, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
585
547
|
category: 'Operations',
|
586
548
|
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.`,
|
587
|
-
requiresSchema: true,
|
588
549
|
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
589
550
|
category: 'Operations',
|
590
551
|
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.`,
|
591
|
-
requiresSchema: true,
|
592
552
|
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
593
553
|
category: 'Schema',
|
594
554
|
description: `A type extension is only valid if the type is defined and has the same kind.`,
|
555
|
+
requiresSchema: false,
|
595
556
|
recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
|
596
557
|
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
597
558
|
category: ['Schema', 'Operations'],
|
598
559
|
description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
|
599
|
-
requiresSchema: true,
|
600
560
|
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
601
561
|
category: 'Operations',
|
602
562
|
description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
|
603
|
-
requiresSchema: true,
|
604
563
|
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
605
564
|
category: 'Operations',
|
606
565
|
description: `A GraphQL subscription is valid only if it contains a single root field.`,
|
607
|
-
requiresSchema: true,
|
608
566
|
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
609
567
|
category: 'Operations',
|
610
568
|
description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
|
611
|
-
requiresSchema: true,
|
612
569
|
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
613
570
|
category: 'Schema',
|
614
571
|
description: `A GraphQL document is only valid if all defined directives have unique names.`,
|
572
|
+
requiresSchema: false,
|
615
573
|
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
616
574
|
category: ['Schema', 'Operations'],
|
617
575
|
description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
|
618
|
-
requiresSchema: true,
|
619
576
|
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
620
577
|
category: 'Schema',
|
621
578
|
description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
|
579
|
+
requiresSchema: false,
|
622
580
|
recommended: false,
|
623
581
|
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
624
582
|
category: 'Schema',
|
625
583
|
description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
|
584
|
+
requiresSchema: false,
|
626
585
|
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
627
586
|
category: 'Operations',
|
628
587
|
description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
|
588
|
+
requiresSchema: false,
|
629
589
|
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
630
590
|
category: 'Schema',
|
631
591
|
description: `A GraphQL document is only valid if it has only one type per operation.`,
|
592
|
+
requiresSchema: false,
|
632
593
|
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
633
594
|
category: 'Schema',
|
634
595
|
description: `A GraphQL document is only valid if all defined types have unique names.`,
|
596
|
+
requiresSchema: false,
|
635
597
|
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
636
598
|
category: 'Operations',
|
637
599
|
description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
|
638
|
-
requiresSchema: true,
|
639
600
|
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
640
601
|
category: 'Operations',
|
641
602
|
description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
|
642
|
-
requiresSchema: true,
|
643
603
|
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
644
604
|
category: 'Operations',
|
645
605
|
description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
|
646
|
-
requiresSchema: true,
|
647
606
|
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
648
607
|
category: 'Operations',
|
649
608
|
description: `Variables passed to field arguments conform to type.`,
|
650
|
-
requiresSchema: true,
|
651
609
|
}));
|
652
610
|
|
653
611
|
const ALPHABETIZE = 'ALPHABETIZE';
|
@@ -1081,13 +1039,7 @@ const rule$2 = {
|
|
1081
1039
|
const MATCH_EXTENSION = 'MATCH_EXTENSION';
|
1082
1040
|
const MATCH_STYLE = 'MATCH_STYLE';
|
1083
1041
|
const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
|
1084
|
-
const CASE_STYLES = [
|
1085
|
-
CaseStyle.camelCase,
|
1086
|
-
CaseStyle.pascalCase,
|
1087
|
-
CaseStyle.snakeCase,
|
1088
|
-
CaseStyle.upperCase,
|
1089
|
-
CaseStyle.kebabCase,
|
1090
|
-
];
|
1042
|
+
const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case'];
|
1091
1043
|
const schemaOption = {
|
1092
1044
|
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1093
1045
|
};
|
@@ -1111,7 +1063,7 @@ const rule$3 = {
|
|
1111
1063
|
},
|
1112
1064
|
{
|
1113
1065
|
title: 'Correct',
|
1114
|
-
usage: [{ query:
|
1066
|
+
usage: [{ query: 'snake_case' }],
|
1115
1067
|
code: /* GraphQL */ `
|
1116
1068
|
# user_by_id.gql
|
1117
1069
|
query UserById {
|
@@ -1125,7 +1077,7 @@ const rule$3 = {
|
|
1125
1077
|
},
|
1126
1078
|
{
|
1127
1079
|
title: 'Correct',
|
1128
|
-
usage: [{ fragment: { style:
|
1080
|
+
usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
|
1129
1081
|
code: /* GraphQL */ `
|
1130
1082
|
# user-fields.fragment.gql
|
1131
1083
|
fragment user_fields on User {
|
@@ -1136,7 +1088,7 @@ const rule$3 = {
|
|
1136
1088
|
},
|
1137
1089
|
{
|
1138
1090
|
title: 'Correct',
|
1139
|
-
usage: [{ mutation: { style:
|
1091
|
+
usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
|
1140
1092
|
code: /* GraphQL */ `
|
1141
1093
|
# DeleteUserMutation.gql
|
1142
1094
|
mutation DELETE_USER {
|
@@ -1156,7 +1108,7 @@ const rule$3 = {
|
|
1156
1108
|
},
|
1157
1109
|
{
|
1158
1110
|
title: 'Incorrect',
|
1159
|
-
usage: [{ query:
|
1111
|
+
usage: [{ query: 'PascalCase' }],
|
1160
1112
|
code: /* GraphQL */ `
|
1161
1113
|
# user-by-id.gql
|
1162
1114
|
query UserById {
|
@@ -1171,10 +1123,10 @@ const rule$3 = {
|
|
1171
1123
|
],
|
1172
1124
|
configOptions: [
|
1173
1125
|
{
|
1174
|
-
query:
|
1175
|
-
mutation:
|
1176
|
-
subscription:
|
1177
|
-
fragment:
|
1126
|
+
query: 'kebab-case',
|
1127
|
+
mutation: 'kebab-case',
|
1128
|
+
subscription: 'kebab-case',
|
1129
|
+
fragment: 'kebab-case',
|
1178
1130
|
},
|
1179
1131
|
],
|
1180
1132
|
},
|
@@ -1191,25 +1143,22 @@ const rule$3 = {
|
|
1191
1143
|
asObject: {
|
1192
1144
|
type: 'object',
|
1193
1145
|
additionalProperties: false,
|
1146
|
+
minProperties: 1,
|
1194
1147
|
properties: {
|
1195
|
-
style: {
|
1196
|
-
|
1197
|
-
},
|
1198
|
-
suffix: {
|
1199
|
-
type: 'string',
|
1200
|
-
},
|
1148
|
+
style: { enum: CASE_STYLES },
|
1149
|
+
suffix: { type: 'string' },
|
1201
1150
|
},
|
1202
1151
|
},
|
1203
1152
|
},
|
1204
1153
|
type: 'array',
|
1154
|
+
minItems: 1,
|
1205
1155
|
maxItems: 1,
|
1206
1156
|
items: {
|
1207
1157
|
type: 'object',
|
1208
1158
|
additionalProperties: false,
|
1159
|
+
minProperties: 1,
|
1209
1160
|
properties: {
|
1210
|
-
fileExtension: {
|
1211
|
-
enum: ACCEPTED_EXTENSIONS,
|
1212
|
-
},
|
1161
|
+
fileExtension: { enum: ACCEPTED_EXTENSIONS },
|
1213
1162
|
query: schemaOption,
|
1214
1163
|
mutation: schemaOption,
|
1215
1164
|
subscription: schemaOption,
|
@@ -1264,7 +1213,7 @@ const rule$3 = {
|
|
1264
1213
|
option = { style: option };
|
1265
1214
|
}
|
1266
1215
|
const expectedExtension = options.fileExtension || fileExtension;
|
1267
|
-
const expectedFilename = convertCase(option.style, docName) + (option.suffix || '') + expectedExtension;
|
1216
|
+
const expectedFilename = (option.style ? convertCase(option.style, docName) : filename) + (option.suffix || '') + expectedExtension;
|
1268
1217
|
const filenameWithExtension = filename + expectedExtension;
|
1269
1218
|
if (expectedFilename !== filenameWithExtension) {
|
1270
1219
|
context.report({
|
@@ -1416,6 +1365,7 @@ const rule$4 = {
|
|
1416
1365
|
],
|
1417
1366
|
},
|
1418
1367
|
},
|
1368
|
+
hasSuggestions: true,
|
1419
1369
|
schema: {
|
1420
1370
|
definitions: {
|
1421
1371
|
asString: {
|
@@ -1490,65 +1440,87 @@ const rule$4 = {
|
|
1490
1440
|
const style = restOptions[kind] || types;
|
1491
1441
|
return typeof style === 'object' ? style : { style };
|
1492
1442
|
}
|
1493
|
-
const checkNode = (selector) => (
|
1494
|
-
const { name } =
|
1495
|
-
if (!
|
1443
|
+
const checkNode = (selector) => (n) => {
|
1444
|
+
const { name: node } = n.kind === graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
|
1445
|
+
if (!node) {
|
1496
1446
|
return;
|
1497
1447
|
}
|
1498
1448
|
const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
|
1499
|
-
const nodeType = KindToDisplayName[
|
1500
|
-
const nodeName =
|
1501
|
-
const
|
1502
|
-
if (
|
1449
|
+
const nodeType = KindToDisplayName[n.kind] || n.kind;
|
1450
|
+
const nodeName = node.value;
|
1451
|
+
const error = getError();
|
1452
|
+
if (error) {
|
1453
|
+
const { errorMessage, renameToName } = error;
|
1503
1454
|
context.report({
|
1504
|
-
loc: getLocation(
|
1455
|
+
loc: getLocation(node.loc, node.value),
|
1505
1456
|
message: `${nodeType} "${nodeName}" should ${errorMessage}`,
|
1457
|
+
suggest: [
|
1458
|
+
{
|
1459
|
+
desc: `Rename to "${renameToName}"`,
|
1460
|
+
fix: fixer => fixer.replaceText(node, renameToName),
|
1461
|
+
},
|
1462
|
+
],
|
1506
1463
|
});
|
1507
1464
|
}
|
1508
|
-
function
|
1509
|
-
|
1510
|
-
if (allowLeadingUnderscore) {
|
1511
|
-
name = name.replace(/^_*/, '');
|
1512
|
-
}
|
1513
|
-
if (allowTrailingUnderscore) {
|
1514
|
-
name = name.replace(/_*$/, '');
|
1515
|
-
}
|
1465
|
+
function getError() {
|
1466
|
+
const name = nodeName.replace(/(^_+)|(_+$)/g, '');
|
1516
1467
|
if (prefix && !name.startsWith(prefix)) {
|
1517
|
-
return
|
1468
|
+
return {
|
1469
|
+
errorMessage: `have "${prefix}" prefix`,
|
1470
|
+
renameToName: prefix + nodeName,
|
1471
|
+
};
|
1518
1472
|
}
|
1519
1473
|
if (suffix && !name.endsWith(suffix)) {
|
1520
|
-
return
|
1474
|
+
return {
|
1475
|
+
errorMessage: `have "${suffix}" suffix`,
|
1476
|
+
renameToName: nodeName + suffix,
|
1477
|
+
};
|
1521
1478
|
}
|
1522
1479
|
const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
|
1523
1480
|
if (forbiddenPrefix) {
|
1524
|
-
return
|
1481
|
+
return {
|
1482
|
+
errorMessage: `not have "${forbiddenPrefix}" prefix`,
|
1483
|
+
renameToName: nodeName.replace(new RegExp(`^${forbiddenPrefix}`), ''),
|
1484
|
+
};
|
1525
1485
|
}
|
1526
1486
|
const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
|
1527
1487
|
if (forbiddenSuffix) {
|
1528
|
-
return
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1488
|
+
return {
|
1489
|
+
errorMessage: `not have "${forbiddenSuffix}" suffix`,
|
1490
|
+
renameToName: nodeName.replace(new RegExp(`${forbiddenSuffix}$`), ''),
|
1491
|
+
};
|
1532
1492
|
}
|
1533
1493
|
const caseRegex = StyleToRegex[style];
|
1534
1494
|
if (caseRegex && !caseRegex.test(name)) {
|
1535
|
-
return
|
1495
|
+
return {
|
1496
|
+
errorMessage: `be in ${style} format`,
|
1497
|
+
renameToName: convertCase(style, nodeName),
|
1498
|
+
};
|
1536
1499
|
}
|
1537
1500
|
}
|
1538
1501
|
};
|
1539
|
-
const checkUnderscore = (node) => {
|
1502
|
+
const checkUnderscore = (isLeading) => (node) => {
|
1540
1503
|
const name = node.value;
|
1504
|
+
const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
|
1541
1505
|
context.report({
|
1542
1506
|
loc: getLocation(node.loc, name),
|
1543
|
-
message: `${
|
1507
|
+
message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
|
1508
|
+
suggest: [
|
1509
|
+
{
|
1510
|
+
desc: `Rename to "${renameToName}"`,
|
1511
|
+
fix: fixer => fixer.replaceText(node, renameToName),
|
1512
|
+
},
|
1513
|
+
],
|
1544
1514
|
});
|
1545
1515
|
};
|
1546
1516
|
const listeners = {};
|
1547
1517
|
if (!allowLeadingUnderscore) {
|
1548
|
-
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1518
|
+
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1519
|
+
checkUnderscore(true);
|
1549
1520
|
}
|
1550
1521
|
if (!allowTrailingUnderscore) {
|
1551
|
-
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1522
|
+
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1523
|
+
checkUnderscore(false);
|
1552
1524
|
}
|
1553
1525
|
const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean));
|
1554
1526
|
for (const selector of selectors) {
|