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