@graphql-eslint/eslint-plugin 3.2.0-alpha-4ca7218.0 → 3.2.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.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 CaseStyle.camelCase:
299
+ case 'camelCase':
307
300
  return camelCase(str);
308
- case CaseStyle.pascalCase:
301
+ case 'PascalCase':
309
302
  return pascalCase(str);
310
- case CaseStyle.snakeCase:
303
+ case 'snake_case':
311
304
  return lowerCase(str).replace(/ /g, '_');
312
- case CaseStyle.upperCase:
305
+ case 'UPPER_CASE':
313
306
  return lowerCase(str).replace(/ /g, '_').toUpperCase();
314
- case CaseStyle.kebabCase:
307
+ case 'kebab-case':
315
308
  return lowerCase(str).replace(/ /g, '-');
316
309
  }
317
310
  };
@@ -334,17 +327,32 @@ function getLocation(loc, fieldName = '', offset) {
334
327
  };
335
328
  }
336
329
 
337
- function validateDocument(sourceNode, context, schema, documentNode, rule) {
330
+ function validateDoc(sourceNode, context, schema, documentNode, rules) {
338
331
  if (documentNode.definitions.length === 0) {
339
332
  return;
340
333
  }
341
334
  try {
342
335
  const validationErrors = schema
343
- ? graphql.validate(schema, documentNode, [rule])
344
- : validate.validateSDL(documentNode, null, [rule]);
336
+ ? graphql.validate(schema, documentNode, rules)
337
+ : validate.validateSDL(documentNode, null, rules);
345
338
  for (const error of validationErrors) {
339
+ /*
340
+ * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
341
+ * Example: loc.end always equal loc.start
342
+ * {
343
+ * token: {
344
+ * type: 'Name',
345
+ * loc: { start: { line: 4, column: 13 }, end: { line: 4, column: 13 } },
346
+ * value: 'veryBad',
347
+ * range: [ 40, 47 ]
348
+ * }
349
+ * }
350
+ */
351
+ const { line, column } = error.locations[0];
352
+ const ancestors = context.getAncestors();
353
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column);
346
354
  context.report({
347
- loc: getLocation({ start: error.locations[0] }),
355
+ loc: getLocation({ start: error.locations[0] }, token === null || token === void 0 ? void 0 : token.value),
348
356
  message: error.message,
349
357
  });
350
358
  }
@@ -356,79 +364,34 @@ function validateDocument(sourceNode, context, schema, documentNode, rule) {
356
364
  });
357
365
  }
358
366
  }
359
- const getFragmentDefsAndFragmentSpreads = (schema, node) => {
360
- const typeInfo = new graphql.TypeInfo(schema);
361
- const fragmentDefs = new Set();
362
- const fragmentSpreads = new Set();
363
- const visitor = graphql.visitWithTypeInfo(typeInfo, {
364
- FragmentDefinition(node) {
365
- fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
366
- },
367
- FragmentSpread(node) {
368
- const parentType = typeInfo.getParentType();
369
- if (parentType) {
370
- fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
371
- }
372
- },
373
- });
374
- graphql.visit(node, visitor);
375
- return { fragmentDefs, fragmentSpreads };
376
- };
377
- const getMissingFragments = (schema, node) => {
378
- const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
379
- return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
380
- };
381
- const handleMissingFragments = ({ 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 [foundFragment] = siblings
389
- .getFragment(fragmentName)
390
- .map(source => source.document)
391
- .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
392
- if (foundFragment) {
393
- fragmentsToAdd.push(foundFragment);
394
- }
395
- }
396
- if (fragmentsToAdd.length > 0) {
397
- // recall fn to make sure to add fragments inside fragments
398
- return handleMissingFragments({
399
- ruleId,
400
- context,
401
- schema,
402
- node: {
403
- kind: graphql.Kind.DOCUMENT,
404
- definitions: [...node.definitions, ...fragmentsToAdd],
405
- },
406
- });
407
- }
408
- }
409
- return node;
367
+ const isGraphQLImportFile = rawSDL => {
368
+ const trimmedRawSDL = rawSDL.trimLeft();
369
+ return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
410
370
  };
411
- const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
371
+ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
372
+ var _a;
412
373
  let ruleFn = null;
413
374
  try {
414
375
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
415
376
  }
416
- catch (_a) {
377
+ catch (e) {
417
378
  try {
418
379
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
419
380
  }
420
- catch (_b) {
381
+ catch (e) {
421
382
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
422
383
  }
423
384
  }
385
+ const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
424
386
  return {
425
- [ruleId]: {
387
+ [name]: {
426
388
  meta: {
427
389
  docs: {
428
390
  recommended: true,
429
391
  ...docs,
430
392
  graphQLJSRuleName: ruleName,
431
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
393
+ requiresSchema,
394
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
432
395
  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).`,
433
396
  },
434
397
  },
@@ -437,53 +400,56 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
437
400
  Document(node) {
438
401
  if (!ruleFn) {
439
402
  // eslint-disable-next-line no-console
440
- console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
403
+ 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...`);
441
404
  return;
442
405
  }
443
- const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
444
- const documentNode = getDocumentNode
445
- ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
446
- : node.rawNode();
447
- validateDocument(node, context, schema, documentNode, ruleFn);
406
+ const schema = requiresSchema ? requireGraphQLSchemaFromContext(name, context) : null;
407
+ let documentNode;
408
+ const isRealFile = fs.existsSync(context.getFilename());
409
+ if (isRealFile && getDocumentNode) {
410
+ documentNode = getDocumentNode(context);
411
+ }
412
+ validateDoc(node, context, schema, documentNode || node.rawNode(), [ruleFn]);
448
413
  },
449
414
  };
450
415
  },
451
416
  },
452
417
  };
453
418
  };
419
+ const importFiles = (context) => {
420
+ const code = context.getSourceCode().text;
421
+ if (!isGraphQLImportFile(code)) {
422
+ return null;
423
+ }
424
+ // Import documents because file contains '#import' comments
425
+ return _import.processImport(context.getFilename());
426
+ };
454
427
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
455
428
  category: 'Operations',
456
429
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
457
- requiresSchema: true,
458
430
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
459
431
  category: 'Operations',
460
432
  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`.',
461
- requiresSchema: true,
462
433
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
463
434
  category: 'Operations',
464
435
  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.`,
465
- requiresSchema: true,
466
436
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
467
437
  category: ['Schema', 'Operations'],
468
438
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
469
- requiresSchema: true,
470
439
  }), validationToRule('known-directives', 'KnownDirectives', {
471
440
  category: ['Schema', 'Operations'],
472
441
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
473
- requiresSchema: true,
474
442
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
475
443
  category: 'Operations',
476
444
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
477
- requiresSchema: true,
478
- requiresSiblings: true,
479
445
  examples: [
480
446
  {
481
- title: 'Incorrect',
447
+ title: 'Incorrect (fragment not defined in the document)',
482
448
  code: /* GraphQL */ `
483
449
  query {
484
450
  user {
485
451
  id
486
- ...UserFields # fragment not defined in the document
452
+ ...UserFields
487
453
  }
488
454
  }
489
455
  `,
@@ -505,151 +471,153 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
505
471
  `,
506
472
  },
507
473
  {
508
- title: 'Correct (`UserFields` fragment located in a separate file)',
474
+ title: 'Correct (existing import to UserFields fragment)',
509
475
  code: /* GraphQL */ `
510
- # user.gql
476
+ #import '../UserFields.gql'
477
+
511
478
  query {
512
479
  user {
513
480
  id
514
481
  ...UserFields
515
482
  }
516
483
  }
517
-
518
- # user-fields.gql
519
- fragment UserFields on User {
520
- id
521
- }
522
484
  `,
523
485
  },
486
+ {
487
+ title: "False positive case\n\nFor extracting documents from code under the hood we use [graphql-tag-pluck](https://graphql-tools.com/docs/graphql-tag-pluck) that [don't support string interpolation](https://stackoverflow.com/questions/62749847/graphql-codegen-dynamic-fields-with-interpolation/62751311#62751311) for this moment.",
488
+ code: `
489
+ const USER_FIELDS = gql\`
490
+ fragment UserFields on User {
491
+ id
492
+ }
493
+ \`
494
+
495
+ const GET_USER = /* GraphQL */ \`
496
+ # eslint @graphql-eslint/known-fragment-names: 'error'
497
+
498
+ query User {
499
+ user {
500
+ ...UserFields
501
+ }
502
+ }
503
+
504
+ # Will give false positive error 'Unknown fragment "UserFields"'
505
+ \${USER_FIELDS}
506
+ \``,
507
+ },
524
508
  ],
525
- }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
509
+ }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
526
510
  category: ['Schema', 'Operations'],
527
511
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
528
- requiresSchema: true,
529
512
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
530
513
  category: 'Operations',
531
514
  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.`,
532
- requiresSchema: true,
533
515
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
534
516
  category: 'Schema',
535
517
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
518
+ requiresSchema: false,
536
519
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
537
520
  category: 'Operations',
538
521
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
539
- requiresSchema: true,
540
522
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
541
523
  category: 'Operations',
542
524
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
543
- requiresSchema: true,
544
- requiresSiblings: true,
545
- }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
525
+ }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
546
526
  category: 'Operations',
547
527
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
548
- requiresSchema: true,
549
528
  requiresSiblings: true,
550
- }, ({ ruleId, context, schema, node }) => {
551
- const siblings = requireSiblingsOperations(ruleId, context);
552
- const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
553
- var _a;
554
- (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
555
- map[filePath].push(document);
556
- return map;
557
- }, Object.create(null));
558
- const getParentNode = (currentFilePath, node) => {
559
- const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
560
- if (fragmentDefs.size === 0) {
561
- return node;
529
+ }, context => {
530
+ const siblings = requireSiblingsOperations('no-unused-fragments', context);
531
+ const documents = [...siblings.getOperations(), ...siblings.getFragments()]
532
+ .filter(({ document }) => isGraphQLImportFile(document.loc.source.body))
533
+ .map(({ filePath, document }) => ({
534
+ filePath,
535
+ code: document.loc.source.body,
536
+ }));
537
+ const getParentNode = (filePath) => {
538
+ for (const { filePath: docFilePath, code } of documents) {
539
+ const isFileImported = code
540
+ .split('\n')
541
+ .filter(isGraphQLImportFile)
542
+ .map(line => _import.parseImportLine(line.replace('#', '')))
543
+ .some(o => filePath === path.join(path.dirname(docFilePath), o.from));
544
+ if (!isFileImported) {
545
+ continue;
546
+ }
547
+ // Import first file that import this file
548
+ const document = _import.processImport(docFilePath);
549
+ // Import most top file that import this file
550
+ return getParentNode(docFilePath) || document;
562
551
  }
563
- // skip iteration over documents for current filepath
564
- delete FilePathToDocumentsMap[currentFilePath];
565
- for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
566
- const missingFragments = getMissingFragments(schema, {
567
- kind: graphql.Kind.DOCUMENT,
568
- definitions: documents,
569
- });
570
- const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
571
- if (isCurrentFileImportFragment) {
572
- return getParentNode(filePath, {
573
- kind: graphql.Kind.DOCUMENT,
574
- definitions: [...node.definitions, ...documents],
575
- });
576
- }
577
- }
578
- return node;
552
+ return null;
579
553
  };
580
- return getParentNode(context.getFilename(), node);
554
+ return getParentNode(context.getFilename());
581
555
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
582
556
  category: 'Operations',
583
557
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
584
- requiresSchema: true,
585
- requiresSiblings: true,
586
- }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
558
+ }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
587
559
  category: 'Operations',
588
560
  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.`,
589
- requiresSchema: true,
590
561
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
591
562
  category: 'Operations',
592
563
  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.`,
593
- requiresSchema: true,
594
564
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
595
565
  category: 'Schema',
596
566
  description: `A type extension is only valid if the type is defined and has the same kind.`,
567
+ requiresSchema: false,
597
568
  recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
598
569
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
599
570
  category: ['Schema', 'Operations'],
600
571
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
601
- requiresSchema: true,
602
572
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
603
573
  category: 'Operations',
604
574
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
605
- requiresSchema: true,
606
575
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
607
576
  category: 'Operations',
608
577
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
609
- requiresSchema: true,
610
578
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
611
579
  category: 'Operations',
612
580
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
613
- requiresSchema: true,
614
581
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
615
582
  category: 'Schema',
616
583
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
584
+ requiresSchema: false,
617
585
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
618
586
  category: ['Schema', 'Operations'],
619
587
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
620
- requiresSchema: true,
621
588
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
622
589
  category: 'Schema',
623
590
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
591
+ requiresSchema: false,
624
592
  recommended: false,
625
593
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
626
594
  category: 'Schema',
627
595
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
596
+ requiresSchema: false,
628
597
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
629
598
  category: 'Operations',
630
599
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
600
+ requiresSchema: false,
631
601
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
632
602
  category: 'Schema',
633
603
  description: `A GraphQL document is only valid if it has only one type per operation.`,
604
+ requiresSchema: false,
634
605
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
635
606
  category: 'Schema',
636
607
  description: `A GraphQL document is only valid if all defined types have unique names.`,
608
+ requiresSchema: false,
637
609
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
638
610
  category: 'Operations',
639
611
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
640
- requiresSchema: true,
641
612
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
642
613
  category: 'Operations',
643
614
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
644
- requiresSchema: true,
645
615
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
646
616
  category: 'Operations',
647
617
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
648
- requiresSchema: true,
649
618
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
650
619
  category: 'Operations',
651
620
  description: `Variables passed to field arguments conform to type.`,
652
- requiresSchema: true,
653
621
  }));
654
622
 
655
623
  const ALPHABETIZE = 'ALPHABETIZE';
@@ -1083,13 +1051,7 @@ const rule$2 = {
1083
1051
  const MATCH_EXTENSION = 'MATCH_EXTENSION';
1084
1052
  const MATCH_STYLE = 'MATCH_STYLE';
1085
1053
  const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
1086
- const CASE_STYLES = [
1087
- CaseStyle.camelCase,
1088
- CaseStyle.pascalCase,
1089
- CaseStyle.snakeCase,
1090
- CaseStyle.upperCase,
1091
- CaseStyle.kebabCase,
1092
- ];
1054
+ const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case'];
1093
1055
  const schemaOption = {
1094
1056
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1095
1057
  };
@@ -1113,7 +1075,7 @@ const rule$3 = {
1113
1075
  },
1114
1076
  {
1115
1077
  title: 'Correct',
1116
- usage: [{ query: CaseStyle.snakeCase }],
1078
+ usage: [{ query: 'snake_case' }],
1117
1079
  code: /* GraphQL */ `
1118
1080
  # user_by_id.gql
1119
1081
  query UserById {
@@ -1127,7 +1089,7 @@ const rule$3 = {
1127
1089
  },
1128
1090
  {
1129
1091
  title: 'Correct',
1130
- usage: [{ fragment: { style: CaseStyle.kebabCase, suffix: '.fragment' } }],
1092
+ usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
1131
1093
  code: /* GraphQL */ `
1132
1094
  # user-fields.fragment.gql
1133
1095
  fragment user_fields on User {
@@ -1138,7 +1100,7 @@ const rule$3 = {
1138
1100
  },
1139
1101
  {
1140
1102
  title: 'Correct',
1141
- usage: [{ mutation: { style: CaseStyle.pascalCase, suffix: 'Mutation' } }],
1103
+ usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
1142
1104
  code: /* GraphQL */ `
1143
1105
  # DeleteUserMutation.gql
1144
1106
  mutation DELETE_USER {
@@ -1158,7 +1120,7 @@ const rule$3 = {
1158
1120
  },
1159
1121
  {
1160
1122
  title: 'Incorrect',
1161
- usage: [{ query: CaseStyle.pascalCase }],
1123
+ usage: [{ query: 'PascalCase' }],
1162
1124
  code: /* GraphQL */ `
1163
1125
  # user-by-id.gql
1164
1126
  query UserById {
@@ -1173,10 +1135,10 @@ const rule$3 = {
1173
1135
  ],
1174
1136
  configOptions: [
1175
1137
  {
1176
- query: CaseStyle.kebabCase,
1177
- mutation: CaseStyle.kebabCase,
1178
- subscription: CaseStyle.kebabCase,
1179
- fragment: CaseStyle.kebabCase,
1138
+ query: 'kebab-case',
1139
+ mutation: 'kebab-case',
1140
+ subscription: 'kebab-case',
1141
+ fragment: 'kebab-case',
1180
1142
  },
1181
1143
  ],
1182
1144
  },
@@ -1193,25 +1155,22 @@ const rule$3 = {
1193
1155
  asObject: {
1194
1156
  type: 'object',
1195
1157
  additionalProperties: false,
1158
+ minProperties: 1,
1196
1159
  properties: {
1197
- style: {
1198
- enum: CASE_STYLES,
1199
- },
1200
- suffix: {
1201
- type: 'string',
1202
- },
1160
+ style: { enum: CASE_STYLES },
1161
+ suffix: { type: 'string' },
1203
1162
  },
1204
1163
  },
1205
1164
  },
1206
1165
  type: 'array',
1166
+ minItems: 1,
1207
1167
  maxItems: 1,
1208
1168
  items: {
1209
1169
  type: 'object',
1210
1170
  additionalProperties: false,
1171
+ minProperties: 1,
1211
1172
  properties: {
1212
- fileExtension: {
1213
- enum: ACCEPTED_EXTENSIONS,
1214
- },
1173
+ fileExtension: { enum: ACCEPTED_EXTENSIONS },
1215
1174
  query: schemaOption,
1216
1175
  mutation: schemaOption,
1217
1176
  subscription: schemaOption,
@@ -1266,7 +1225,7 @@ const rule$3 = {
1266
1225
  option = { style: option };
1267
1226
  }
1268
1227
  const expectedExtension = options.fileExtension || fileExtension;
1269
- const expectedFilename = convertCase(option.style, docName) + (option.suffix || '') + expectedExtension;
1228
+ const expectedFilename = (option.style ? convertCase(option.style, docName) : filename) + (option.suffix || '') + expectedExtension;
1270
1229
  const filenameWithExtension = filename + expectedExtension;
1271
1230
  if (expectedFilename !== filenameWithExtension) {
1272
1231
  context.report({
@@ -1418,6 +1377,7 @@ const rule$4 = {
1418
1377
  ],
1419
1378
  },
1420
1379
  },
1380
+ hasSuggestions: true,
1421
1381
  schema: {
1422
1382
  definitions: {
1423
1383
  asString: {
@@ -1492,65 +1452,90 @@ const rule$4 = {
1492
1452
  const style = restOptions[kind] || types;
1493
1453
  return typeof style === 'object' ? style : { style };
1494
1454
  }
1495
- const checkNode = (selector) => (node) => {
1496
- const { name } = node.kind === graphql.Kind.VARIABLE_DEFINITION ? node.variable : node;
1497
- if (!name) {
1455
+ const checkNode = (selector) => (n) => {
1456
+ const { name: node } = n.kind === graphql.Kind.VARIABLE_DEFINITION ? n.variable : n;
1457
+ if (!node) {
1498
1458
  return;
1499
1459
  }
1500
1460
  const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
1501
- const nodeType = KindToDisplayName[node.kind] || node.kind;
1502
- const nodeName = name.value;
1503
- const errorMessage = getErrorMessage();
1504
- if (errorMessage) {
1461
+ const nodeType = KindToDisplayName[n.kind] || n.kind;
1462
+ const nodeName = node.value;
1463
+ const error = getError();
1464
+ if (error) {
1465
+ const { errorMessage, renameToName } = error;
1466
+ const [leadingUnderscore] = nodeName.match(/^_*/);
1467
+ const [trailingUnderscore] = nodeName.match(/_*$/);
1468
+ const suggestedName = leadingUnderscore + renameToName + trailingUnderscore;
1505
1469
  context.report({
1506
- loc: getLocation(name.loc, name.value),
1470
+ loc: getLocation(node.loc, node.value),
1507
1471
  message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1472
+ suggest: [
1473
+ {
1474
+ desc: `Rename to "${suggestedName}"`,
1475
+ fix: fixer => fixer.replaceText(node, suggestedName),
1476
+ },
1477
+ ],
1508
1478
  });
1509
1479
  }
1510
- function getErrorMessage() {
1511
- let name = nodeName;
1512
- if (allowLeadingUnderscore) {
1513
- name = name.replace(/^_*/, '');
1514
- }
1515
- if (allowTrailingUnderscore) {
1516
- name = name.replace(/_*$/, '');
1517
- }
1480
+ function getError() {
1481
+ const name = nodeName.replace(/(^_+)|(_+$)/g, '');
1518
1482
  if (prefix && !name.startsWith(prefix)) {
1519
- return `have "${prefix}" prefix`;
1483
+ return {
1484
+ errorMessage: `have "${prefix}" prefix`,
1485
+ renameToName: prefix + name,
1486
+ };
1520
1487
  }
1521
1488
  if (suffix && !name.endsWith(suffix)) {
1522
- return `have "${suffix}" suffix`;
1489
+ return {
1490
+ errorMessage: `have "${suffix}" suffix`,
1491
+ renameToName: name + suffix,
1492
+ };
1523
1493
  }
1524
1494
  const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
1525
1495
  if (forbiddenPrefix) {
1526
- return `not have "${forbiddenPrefix}" prefix`;
1496
+ return {
1497
+ errorMessage: `not have "${forbiddenPrefix}" prefix`,
1498
+ renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
1499
+ };
1527
1500
  }
1528
1501
  const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
1529
1502
  if (forbiddenSuffix) {
1530
- return `not have "${forbiddenSuffix}" suffix`;
1531
- }
1532
- if (style && !ALLOWED_STYLES.includes(style)) {
1533
- return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`;
1503
+ return {
1504
+ errorMessage: `not have "${forbiddenSuffix}" suffix`,
1505
+ renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
1506
+ };
1534
1507
  }
1535
1508
  const caseRegex = StyleToRegex[style];
1536
1509
  if (caseRegex && !caseRegex.test(name)) {
1537
- return `be in ${style} format`;
1510
+ return {
1511
+ errorMessage: `be in ${style} format`,
1512
+ renameToName: convertCase(style, name),
1513
+ };
1538
1514
  }
1539
1515
  }
1540
1516
  };
1541
- const checkUnderscore = (node) => {
1517
+ const checkUnderscore = (isLeading) => (node) => {
1542
1518
  const name = node.value;
1519
+ const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
1543
1520
  context.report({
1544
1521
  loc: getLocation(node.loc, name),
1545
- message: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`,
1522
+ message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
1523
+ suggest: [
1524
+ {
1525
+ desc: `Rename to "${renameToName}"`,
1526
+ fix: fixer => fixer.replaceText(node, renameToName),
1527
+ },
1528
+ ],
1546
1529
  });
1547
1530
  };
1548
1531
  const listeners = {};
1549
1532
  if (!allowLeadingUnderscore) {
1550
- listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1533
+ listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
1534
+ checkUnderscore(true);
1551
1535
  }
1552
1536
  if (!allowTrailingUnderscore) {
1553
- listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1537
+ listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
1538
+ checkUnderscore(false);
1554
1539
  }
1555
1540
  const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean));
1556
1541
  for (const selector of selectors) {
@@ -2893,9 +2878,22 @@ const rule$j = {
2893
2878
  recommended: true,
2894
2879
  },
2895
2880
  messages: {
2896
- [REQUIRE_ID_WHEN_AVAILABLE]: `Field "{{ fieldName }}" must be selected when it's available on a type. Please make sure to include it in your selection set!\nIf you are using fragments, make sure that all used fragments {{ checkedFragments }} specifies the field "{{ fieldName }}".`,
2881
+ [REQUIRE_ID_WHEN_AVAILABLE]: [
2882
+ `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
2883
+ `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
2884
+ ].join('\n'),
2897
2885
  },
2898
2886
  schema: {
2887
+ definitions: {
2888
+ asString: {
2889
+ type: 'string',
2890
+ },
2891
+ asArray: {
2892
+ type: 'array',
2893
+ minItems: 1,
2894
+ uniqueItems: true,
2895
+ },
2896
+ },
2899
2897
  type: 'array',
2900
2898
  maxItems: 1,
2901
2899
  items: {
@@ -2903,7 +2901,7 @@ const rule$j = {
2903
2901
  additionalProperties: false,
2904
2902
  properties: {
2905
2903
  fieldName: {
2906
- type: 'string',
2904
+ oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
2907
2905
  default: DEFAULT_ID_FIELD_NAME,
2908
2906
  },
2909
2907
  },
@@ -2911,69 +2909,64 @@ const rule$j = {
2911
2909
  },
2912
2910
  },
2913
2911
  create(context) {
2912
+ requireGraphQLSchemaFromContext('require-id-when-available', context);
2913
+ const siblings = requireSiblingsOperations('require-id-when-available', context);
2914
+ const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
2915
+ const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
2916
+ const isFound = (s) => s.kind === graphql.Kind.FIELD && idNames.includes(s.name.value);
2914
2917
  return {
2915
2918
  SelectionSet(node) {
2916
2919
  var _a, _b;
2917
- requireGraphQLSchemaFromContext('require-id-when-available', context);
2918
- const siblings = requireSiblingsOperations('require-id-when-available', context);
2919
- const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
2920
- if (!node.selections || node.selections.length === 0) {
2920
+ const typeInfo = node.typeInfo();
2921
+ if (!typeInfo.gqlType) {
2921
2922
  return;
2922
2923
  }
2923
- const typeInfo = node.typeInfo();
2924
- if (typeInfo && typeInfo.gqlType) {
2925
- const rawType = getBaseType(typeInfo.gqlType);
2926
- if (rawType instanceof graphql.GraphQLObjectType || rawType instanceof graphql.GraphQLInterfaceType) {
2927
- const fields = rawType.getFields();
2928
- const hasIdFieldInType = !!fields[fieldName];
2929
- const checkedFragmentSpreads = new Set();
2930
- if (hasIdFieldInType) {
2931
- let found = false;
2932
- for (const selection of node.selections) {
2933
- if (selection.kind === 'Field' && selection.name.value === fieldName) {
2934
- found = true;
2935
- }
2936
- else if (selection.kind === 'InlineFragment') {
2937
- found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2938
- }
2939
- else if (selection.kind === 'FragmentSpread') {
2940
- const foundSpread = siblings.getFragment(selection.name.value);
2941
- if (foundSpread[0]) {
2942
- checkedFragmentSpreads.add(foundSpread[0].document.name.value);
2943
- found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2944
- }
2945
- }
2946
- if (found) {
2947
- break;
2948
- }
2949
- }
2950
- const { parent } = node;
2951
- const hasIdFieldInInterfaceSelectionSet = parent &&
2952
- parent.kind === 'InlineFragment' &&
2953
- parent.parent &&
2954
- parent.parent.kind === 'SelectionSet' &&
2955
- parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
2956
- if (!found && !hasIdFieldInInterfaceSelectionSet) {
2957
- context.report({
2958
- loc: {
2959
- start: {
2960
- line: node.loc.start.line,
2961
- column: node.loc.start.column - 1,
2962
- },
2963
- end: {
2964
- line: node.loc.end.line,
2965
- column: node.loc.end.column - 1,
2966
- },
2967
- },
2968
- messageId: REQUIRE_ID_WHEN_AVAILABLE,
2969
- data: {
2970
- checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
2971
- fieldName,
2972
- },
2973
- });
2974
- }
2924
+ const rawType = getBaseType(typeInfo.gqlType);
2925
+ const isObjectType = rawType instanceof graphql.GraphQLObjectType;
2926
+ const isInterfaceType = rawType instanceof graphql.GraphQLInterfaceType;
2927
+ if (!isObjectType && !isInterfaceType) {
2928
+ return;
2929
+ }
2930
+ const fields = rawType.getFields();
2931
+ const hasIdFieldInType = idNames.some(name => fields[name]);
2932
+ if (!hasIdFieldInType) {
2933
+ return;
2934
+ }
2935
+ const checkedFragmentSpreads = new Set();
2936
+ let found = false;
2937
+ for (const selection of node.selections) {
2938
+ if (isFound(selection)) {
2939
+ found = true;
2940
+ }
2941
+ else if (selection.kind === graphql.Kind.INLINE_FRAGMENT) {
2942
+ found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
2943
+ }
2944
+ else if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
2945
+ const [foundSpread] = siblings.getFragment(selection.name.value);
2946
+ if (foundSpread) {
2947
+ checkedFragmentSpreads.add(foundSpread.document.name.value);
2948
+ found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
2975
2949
  }
2976
2950
  }
2951
+ if (found) {
2952
+ break;
2953
+ }
2954
+ }
2955
+ const { parent } = node;
2956
+ const hasIdFieldInInterfaceSelectionSet = parent &&
2957
+ parent.kind === graphql.Kind.INLINE_FRAGMENT &&
2958
+ parent.parent &&
2959
+ parent.parent.kind === graphql.Kind.SELECTION_SET &&
2960
+ parent.parent.selections.some(s => isFound(s));
2961
+ if (!found && !hasIdFieldInInterfaceSelectionSet) {
2962
+ context.report({
2963
+ loc: getLocation(node.loc),
2964
+ messageId: REQUIRE_ID_WHEN_AVAILABLE,
2965
+ data: {
2966
+ checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
2967
+ fieldName: idNames.map(name => `"${name}"`).join(' or '),
2968
+ },
2969
+ });
2977
2970
  }
2978
2971
  },
2979
2972
  };
@@ -3065,7 +3058,7 @@ const rule$k = {
3065
3058
  // eslint-disable-next-line no-console
3066
3059
  console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3067
3060
  }
3068
- const maxDepth = context.options[0].maxDepth;
3061
+ const { maxDepth } = context.options[0];
3069
3062
  const ignore = context.options[0].ignore || [];
3070
3063
  const checkFn = depthLimit(maxDepth, { ignore });
3071
3064
  return {