@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/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import { Kind, validate, TypeInfo, visitWithTypeInfo, visit, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, isInterfaceType, Source, GraphQLError } from 'graphql';
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 CaseStyle.camelCase:
293
+ case 'camelCase':
301
294
  return camelCase(str);
302
- case CaseStyle.pascalCase:
295
+ case 'PascalCase':
303
296
  return pascalCase(str);
304
- case CaseStyle.snakeCase:
297
+ case 'snake_case':
305
298
  return lowerCase(str).replace(/ /g, '_');
306
- case CaseStyle.upperCase:
299
+ case 'UPPER_CASE':
307
300
  return lowerCase(str).replace(/ /g, '_').toUpperCase();
308
- case CaseStyle.kebabCase:
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 validateDocument(sourceNode, context, schema, documentNode, rule) {
332
- if (documentNode.definitions.length === 0) {
333
- return;
334
- }
335
- try {
336
- const validationErrors = schema
337
- ? validate(schema, documentNode, [rule])
338
- : validateSDL(documentNode, null, [rule]);
339
- for (const error of validationErrors) {
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
- const getFragmentDefsAndFragmentSpreads = (schema, node) => {
354
- const typeInfo = new TypeInfo(schema);
355
- const fragmentDefs = new Set();
356
- const fragmentSpreads = new Set();
357
- const visitor = visitWithTypeInfo(typeInfo, {
358
- FragmentDefinition(node) {
359
- fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
360
- },
361
- FragmentSpread(node) {
362
- const parentType = typeInfo.getParentType();
363
- if (parentType) {
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
- if (fragmentsToAdd.length > 0) {
389
- // recall fn to make sure to add fragments inside fragments
390
- return handleMissingFragments({
391
- ruleId,
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
- return node;
348
+ }
349
+ const isGraphQLImportFile = rawSDL => {
350
+ const trimmedRawSDL = rawSDL.trimLeft();
351
+ return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
402
352
  };
403
- const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
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 (_a) {
359
+ catch (e) {
409
360
  try {
410
361
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
411
362
  }
412
- catch (_b) {
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
- [ruleId]: {
369
+ [name]: {
418
370
  meta: {
419
371
  docs: {
420
372
  recommended: true,
421
373
  ...docs,
422
374
  graphQLJSRuleName: ruleName,
423
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
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 "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
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 = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
436
- const documentNode = getDocumentNode
437
- ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
438
- : node.rawNode();
439
- validateDocument(node, context, schema, documentNode, ruleFn);
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 # fragment not defined in the document
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 (`UserFields` fragment located in a separate file)',
456
+ title: 'Correct (existing import to UserFields fragment)',
501
457
  code: /* GraphQL */ `
502
- # user.gql
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
- }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
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
- requiresSchema: true,
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
- }, ({ ruleId, context, schema, node }) => {
543
- const siblings = requireSiblingsOperations(ruleId, context);
544
- const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
545
- var _a;
546
- (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
547
- map[filePath].push(document);
548
- return map;
549
- }, Object.create(null));
550
- const getParentNode = (currentFilePath, node) => {
551
- const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
552
- if (fragmentDefs.size === 0) {
553
- return node;
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
- // skip iteration over documents for current filepath
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(), node);
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
- requiresSchema: true,
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: CaseStyle.snakeCase }],
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: CaseStyle.kebabCase, suffix: '.fragment' } }],
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: CaseStyle.pascalCase, suffix: 'Mutation' } }],
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: CaseStyle.pascalCase }],
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: CaseStyle.kebabCase,
1169
- mutation: CaseStyle.kebabCase,
1170
- subscription: CaseStyle.kebabCase,
1171
- fragment: CaseStyle.kebabCase,
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
- enum: CASE_STYLES,
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) => (node) => {
1488
- const { name } = node.kind === Kind.VARIABLE_DEFINITION ? node.variable : node;
1489
- if (!name) {
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[node.kind] || node.kind;
1494
- const nodeName = name.value;
1495
- const errorMessage = getErrorMessage();
1496
- if (errorMessage) {
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(name.loc, name.value),
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 getErrorMessage() {
1503
- let name = nodeName;
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 `have "${prefix}" prefix`;
1462
+ return {
1463
+ errorMessage: `have "${prefix}" prefix`,
1464
+ renameToName: prefix + nodeName,
1465
+ };
1512
1466
  }
1513
1467
  if (suffix && !name.endsWith(suffix)) {
1514
- return `have "${suffix}" suffix`;
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 `not have "${forbiddenPrefix}" prefix`;
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 `not have "${forbiddenSuffix}" suffix`;
1523
- }
1524
- if (style && !ALLOWED_STYLES.includes(style)) {
1525
- return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`;
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 `be in ${style} format`;
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: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`,
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])'] = checkUnderscore;
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])'] = checkUnderscore;
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) {