@graphql-eslint/eslint-plugin 3.2.0-alpha-45f5fcb.0 → 3.3.0-alpha-db2c2cb.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,8 +1,7 @@
1
- import { Kind, validate, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, visit, visitWithTypeInfo, GraphQLObjectType, GraphQLInterfaceType, TypeInfo, isInterfaceType, Source, GraphQLError } from 'graphql';
1
+ import { Kind, validate, TypeInfo, visitWithTypeInfo, visit, TokenKind, isScalarType, isNonNullType, isListType, isObjectType as isObjectType$1, GraphQLObjectType, GraphQLInterfaceType, isInterfaceType, Source, GraphQLError } from 'graphql';
2
2
  import { validateSDL } from 'graphql/validation/validate';
3
- import { processImport, parseImportLine } from '@graphql-tools/import';
4
3
  import { statSync, existsSync, readFileSync } from 'fs';
5
- import { dirname, join, extname, basename, relative, resolve } from 'path';
4
+ import { dirname, extname, basename, relative, resolve } from 'path';
6
5
  import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
7
6
  import lowerCase from 'lodash.lowercase';
8
7
  import depthLimit from 'graphql-depth-limit';
@@ -321,59 +320,116 @@ function getLocation(loc, fieldName = '', offset) {
321
320
  };
322
321
  }
323
322
 
324
- function extractRuleName(stack) {
325
- const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
326
- return match[1] || null;
323
+ function validateDocument(sourceNode, context, schema, documentNode, rule) {
324
+ if (documentNode.definitions.length === 0) {
325
+ return;
326
+ }
327
+ try {
328
+ const validationErrors = schema
329
+ ? validate(schema, documentNode, [rule])
330
+ : validateSDL(documentNode, null, [rule]);
331
+ for (const error of validationErrors) {
332
+ /*
333
+ * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
334
+ * Example: loc.end always equal loc.start
335
+ * {
336
+ * token: {
337
+ * type: 'Name',
338
+ * loc: { start: { line: 4, column: 13 }, end: { line: 4, column: 13 } },
339
+ * value: 'veryBad',
340
+ * range: [ 40, 47 ]
341
+ * }
342
+ * }
343
+ */
344
+ const { line, column } = error.locations[0];
345
+ const ancestors = context.getAncestors();
346
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column);
347
+ context.report({
348
+ loc: getLocation({ start: error.locations[0] }, token === null || token === void 0 ? void 0 : token.value),
349
+ message: error.message,
350
+ });
351
+ }
352
+ }
353
+ catch (e) {
354
+ context.report({
355
+ node: sourceNode,
356
+ message: e.message,
357
+ });
358
+ }
327
359
  }
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
- });
360
+ const getFragmentDefsAndFragmentSpreads = (schema, node) => {
361
+ const typeInfo = new TypeInfo(schema);
362
+ const fragmentDefs = new Set();
363
+ const fragmentSpreads = new Set();
364
+ const visitor = visitWithTypeInfo(typeInfo, {
365
+ FragmentDefinition(node) {
366
+ fragmentDefs.add(`${node.name.value}:${node.typeCondition.name.value}`);
367
+ },
368
+ FragmentSpread(node) {
369
+ const parentType = typeInfo.getParentType();
370
+ if (parentType) {
371
+ fragmentSpreads.add(`${node.name.value}:${parentType.name}`);
372
+ }
373
+ },
374
+ });
375
+ visit(node, visitor);
376
+ return { fragmentDefs, fragmentSpreads };
377
+ };
378
+ const getMissingFragments = (schema, node) => {
379
+ const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(schema, node);
380
+ return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
381
+ };
382
+ const handleMissingFragments = ({ ruleId, context, schema, node }) => {
383
+ const missingFragments = getMissingFragments(schema, node);
384
+ if (missingFragments.length > 0) {
385
+ const siblings = requireSiblingsOperations(ruleId, context);
386
+ const fragmentsToAdd = [];
387
+ for (const missingFragment of missingFragments) {
388
+ const [fragmentName, fragmentTypeName] = missingFragment.split(':');
389
+ const [foundFragment] = siblings
390
+ .getFragment(fragmentName)
391
+ .map(source => source.document)
392
+ .filter(fragment => fragment.typeCondition.name.value === fragmentTypeName);
393
+ if (foundFragment) {
394
+ fragmentsToAdd.push(foundFragment);
339
395
  }
340
396
  }
341
- catch (e) {
342
- context.report({
343
- node: sourceNode,
344
- message: e.message,
397
+ if (fragmentsToAdd.length > 0) {
398
+ // recall fn to make sure to add fragments inside fragments
399
+ return handleMissingFragments({
400
+ ruleId,
401
+ context,
402
+ schema,
403
+ node: {
404
+ kind: Kind.DOCUMENT,
405
+ definitions: [...node.definitions, ...fragmentsToAdd],
406
+ },
345
407
  });
346
408
  }
347
409
  }
348
- }
349
- const isGraphQLImportFile = rawSDL => {
350
- const trimmedRawSDL = rawSDL.trimLeft();
351
- return trimmedRawSDL.startsWith('# import') || trimmedRawSDL.startsWith('#import');
410
+ return node;
352
411
  };
353
- const validationToRule = (name, ruleName, docs, getDocumentNode) => {
354
- var _a;
412
+ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
355
413
  let ruleFn = null;
356
414
  try {
357
415
  ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
358
416
  }
359
- catch (e) {
417
+ catch (_a) {
360
418
  try {
361
419
  ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
362
420
  }
363
- catch (e) {
421
+ catch (_b) {
364
422
  ruleFn = require('graphql/validation')[`${ruleName}Rule`];
365
423
  }
366
424
  }
367
- const requiresSchema = (_a = docs.requiresSchema) !== null && _a !== void 0 ? _a : true;
368
425
  return {
369
- [name]: {
426
+ [ruleId]: {
370
427
  meta: {
371
428
  docs: {
372
429
  recommended: true,
373
430
  ...docs,
374
431
  graphQLJSRuleName: ruleName,
375
- requiresSchema,
376
- url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${name}.md`,
432
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
377
433
  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).`,
378
434
  },
379
435
  },
@@ -382,56 +438,53 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
382
438
  Document(node) {
383
439
  if (!ruleFn) {
384
440
  // eslint-disable-next-line no-console
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...`);
441
+ 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...`);
386
442
  return;
387
443
  }
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);
444
+ const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
445
+ const documentNode = getDocumentNode
446
+ ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
447
+ : node.rawNode();
448
+ validateDocument(node, context, schema, documentNode, ruleFn);
395
449
  },
396
450
  };
397
451
  },
398
452
  },
399
453
  };
400
454
  };
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
- };
409
455
  const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
410
456
  category: 'Operations',
411
457
  description: `A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.`,
458
+ requiresSchema: true,
412
459
  }), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
413
460
  category: 'Operations',
414
461
  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`.',
462
+ requiresSchema: true,
415
463
  }), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
416
464
  category: 'Operations',
417
465
  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.`,
466
+ requiresSchema: true,
418
467
  }), validationToRule('known-argument-names', 'KnownArgumentNames', {
419
468
  category: ['Schema', 'Operations'],
420
469
  description: `A GraphQL field is only valid if all supplied arguments are defined by that field.`,
470
+ requiresSchema: true,
421
471
  }), validationToRule('known-directives', 'KnownDirectives', {
422
472
  category: ['Schema', 'Operations'],
423
473
  description: `A GraphQL document is only valid if all \`@directives\` are known by the schema and legally positioned.`,
474
+ requiresSchema: true,
424
475
  }), validationToRule('known-fragment-names', 'KnownFragmentNames', {
425
476
  category: 'Operations',
426
477
  description: `A GraphQL document is only valid if all \`...Fragment\` fragment spreads refer to fragments defined in the same document.`,
478
+ requiresSchema: true,
479
+ requiresSiblings: true,
427
480
  examples: [
428
481
  {
429
- title: 'Incorrect (fragment not defined in the document)',
482
+ title: 'Incorrect',
430
483
  code: /* GraphQL */ `
431
484
  query {
432
485
  user {
433
486
  id
434
- ...UserFields
487
+ ...UserFields # fragment not defined in the document
435
488
  }
436
489
  }
437
490
  `,
@@ -453,153 +506,151 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
453
506
  `,
454
507
  },
455
508
  {
456
- title: 'Correct (existing import to UserFields fragment)',
509
+ title: 'Correct (`UserFields` fragment located in a separate file)',
457
510
  code: /* GraphQL */ `
458
- #import '../UserFields.gql'
459
-
511
+ # user.gql
460
512
  query {
461
513
  user {
462
514
  id
463
515
  ...UserFields
464
516
  }
465
517
  }
466
- `,
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
518
 
486
- # Will give false positive error 'Unknown fragment "UserFields"'
487
- \${USER_FIELDS}
488
- \``,
519
+ # user-fields.gql
520
+ fragment UserFields on User {
521
+ id
522
+ }
523
+ `,
489
524
  },
490
525
  ],
491
- }, importFiles), validationToRule('known-type-names', 'KnownTypeNames', {
526
+ }, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
492
527
  category: ['Schema', 'Operations'],
493
528
  description: `A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.`,
529
+ requiresSchema: true,
494
530
  }), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
495
531
  category: 'Operations',
496
532
  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.`,
533
+ requiresSchema: true,
497
534
  }), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
498
535
  category: 'Schema',
499
536
  description: `A GraphQL document is only valid if it contains only one schema definition.`,
500
- requiresSchema: false,
501
537
  }), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
502
538
  category: 'Operations',
503
539
  description: `A GraphQL fragment is only valid when it does not have cycles in fragments usage.`,
540
+ requiresSchema: true,
504
541
  }), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
505
542
  category: 'Operations',
506
543
  description: `A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.`,
507
- }, importFiles), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
544
+ requiresSchema: true,
545
+ requiresSiblings: true,
546
+ }, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
508
547
  category: 'Operations',
509
548
  description: `A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.`,
549
+ requiresSchema: true,
510
550
  requiresSiblings: true,
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;
551
+ }, ({ ruleId, context, schema, node }) => {
552
+ const siblings = requireSiblingsOperations(ruleId, context);
553
+ const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
554
+ var _a;
555
+ (_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
556
+ map[filePath].push(document);
557
+ return map;
558
+ }, Object.create(null));
559
+ const getParentNode = (currentFilePath, node) => {
560
+ const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(schema, node);
561
+ if (fragmentDefs.size === 0) {
562
+ return node;
533
563
  }
534
- return null;
564
+ // skip iteration over documents for current filepath
565
+ delete FilePathToDocumentsMap[currentFilePath];
566
+ for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
567
+ const missingFragments = getMissingFragments(schema, {
568
+ kind: Kind.DOCUMENT,
569
+ definitions: documents,
570
+ });
571
+ const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
572
+ if (isCurrentFileImportFragment) {
573
+ return getParentNode(filePath, {
574
+ kind: Kind.DOCUMENT,
575
+ definitions: [...node.definitions, ...documents],
576
+ });
577
+ }
578
+ }
579
+ return node;
535
580
  };
536
- return getParentNode(context.getFilename());
581
+ return getParentNode(context.getFilename(), node);
537
582
  }), validationToRule('no-unused-variables', 'NoUnusedVariables', {
538
583
  category: 'Operations',
539
584
  description: `A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.`,
540
- }, importFiles), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
585
+ requiresSchema: true,
586
+ requiresSiblings: true,
587
+ }, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
541
588
  category: 'Operations',
542
589
  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.`,
590
+ requiresSchema: true,
543
591
  }), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
544
592
  category: 'Operations',
545
593
  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.`,
594
+ requiresSchema: true,
546
595
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
547
596
  category: 'Schema',
548
597
  description: `A type extension is only valid if the type is defined and has the same kind.`,
549
- requiresSchema: false,
550
598
  recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
551
599
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
552
600
  category: ['Schema', 'Operations'],
553
601
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
602
+ requiresSchema: true,
554
603
  }), validationToRule('scalar-leafs', 'ScalarLeafs', {
555
604
  category: 'Operations',
556
605
  description: `A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.`,
606
+ requiresSchema: true,
557
607
  }), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
558
608
  category: 'Operations',
559
609
  description: `A GraphQL subscription is valid only if it contains a single root field.`,
610
+ requiresSchema: true,
560
611
  }), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
561
612
  category: 'Operations',
562
613
  description: `A GraphQL field or directive is only valid if all supplied arguments are uniquely named.`,
614
+ requiresSchema: true,
563
615
  }), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
564
616
  category: 'Schema',
565
617
  description: `A GraphQL document is only valid if all defined directives have unique names.`,
566
- requiresSchema: false,
567
618
  }), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
568
619
  category: ['Schema', 'Operations'],
569
620
  description: `A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.`,
621
+ requiresSchema: true,
570
622
  }), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
571
623
  category: 'Schema',
572
624
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
573
- requiresSchema: false,
574
625
  recommended: false,
575
626
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
576
627
  category: 'Schema',
577
628
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
578
- requiresSchema: false,
579
629
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
580
630
  category: 'Operations',
581
631
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
582
- requiresSchema: false,
583
632
  }), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
584
633
  category: 'Schema',
585
634
  description: `A GraphQL document is only valid if it has only one type per operation.`,
586
- requiresSchema: false,
587
635
  }), validationToRule('unique-type-names', 'UniqueTypeNames', {
588
636
  category: 'Schema',
589
637
  description: `A GraphQL document is only valid if all defined types have unique names.`,
590
- requiresSchema: false,
591
638
  }), validationToRule('unique-variable-names', 'UniqueVariableNames', {
592
639
  category: 'Operations',
593
640
  description: `A GraphQL operation is only valid if all its variables are uniquely named.`,
641
+ requiresSchema: true,
594
642
  }), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
595
643
  category: 'Operations',
596
644
  description: `A GraphQL document is only valid if all value literals are of the type expected at their position.`,
645
+ requiresSchema: true,
597
646
  }), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
598
647
  category: 'Operations',
599
648
  description: `A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).`,
649
+ requiresSchema: true,
600
650
  }), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
601
651
  category: 'Operations',
602
652
  description: `Variables passed to field arguments conform to type.`,
653
+ requiresSchema: true,
603
654
  }));
604
655
 
605
656
  const ALPHABETIZE = 'ALPHABETIZE';
@@ -1445,13 +1496,16 @@ const rule$4 = {
1445
1496
  const error = getError();
1446
1497
  if (error) {
1447
1498
  const { errorMessage, renameToName } = error;
1499
+ const [leadingUnderscore] = nodeName.match(/^_*/);
1500
+ const [trailingUnderscore] = nodeName.match(/_*$/);
1501
+ const suggestedName = leadingUnderscore + renameToName + trailingUnderscore;
1448
1502
  context.report({
1449
1503
  loc: getLocation(node.loc, node.value),
1450
1504
  message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1451
1505
  suggest: [
1452
1506
  {
1453
- desc: `Rename to "${renameToName}"`,
1454
- fix: fixer => fixer.replaceText(node, renameToName),
1507
+ desc: `Rename to "${suggestedName}"`,
1508
+ fix: fixer => fixer.replaceText(node, suggestedName),
1455
1509
  },
1456
1510
  ],
1457
1511
  });
@@ -1461,34 +1515,34 @@ const rule$4 = {
1461
1515
  if (prefix && !name.startsWith(prefix)) {
1462
1516
  return {
1463
1517
  errorMessage: `have "${prefix}" prefix`,
1464
- renameToName: prefix + nodeName,
1518
+ renameToName: prefix + name,
1465
1519
  };
1466
1520
  }
1467
1521
  if (suffix && !name.endsWith(suffix)) {
1468
1522
  return {
1469
1523
  errorMessage: `have "${suffix}" suffix`,
1470
- renameToName: nodeName + suffix,
1524
+ renameToName: name + suffix,
1471
1525
  };
1472
1526
  }
1473
1527
  const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
1474
1528
  if (forbiddenPrefix) {
1475
1529
  return {
1476
1530
  errorMessage: `not have "${forbiddenPrefix}" prefix`,
1477
- renameToName: nodeName.replace(new RegExp(`^${forbiddenPrefix}`), ''),
1531
+ renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
1478
1532
  };
1479
1533
  }
1480
1534
  const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
1481
1535
  if (forbiddenSuffix) {
1482
1536
  return {
1483
1537
  errorMessage: `not have "${forbiddenSuffix}" suffix`,
1484
- renameToName: nodeName.replace(new RegExp(`${forbiddenSuffix}$`), ''),
1538
+ renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
1485
1539
  };
1486
1540
  }
1487
1541
  const caseRegex = StyleToRegex[style];
1488
1542
  if (caseRegex && !caseRegex.test(name)) {
1489
1543
  return {
1490
1544
  errorMessage: `be in ${style} format`,
1491
- renameToName: convertCase(style, nodeName),
1545
+ renameToName: convertCase(style, name),
1492
1546
  };
1493
1547
  }
1494
1548
  }
@@ -2857,9 +2911,22 @@ const rule$j = {
2857
2911
  recommended: true,
2858
2912
  },
2859
2913
  messages: {
2860
- [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 }}".`,
2914
+ [REQUIRE_ID_WHEN_AVAILABLE]: [
2915
+ `Field {{ fieldName }} must be selected when it's available on a type. Please make sure to include it in your selection set!`,
2916
+ `If you are using fragments, make sure that all used fragments {{ checkedFragments }}specifies the field {{ fieldName }}.`,
2917
+ ].join('\n'),
2861
2918
  },
2862
2919
  schema: {
2920
+ definitions: {
2921
+ asString: {
2922
+ type: 'string',
2923
+ },
2924
+ asArray: {
2925
+ type: 'array',
2926
+ minItems: 1,
2927
+ uniqueItems: true,
2928
+ },
2929
+ },
2863
2930
  type: 'array',
2864
2931
  maxItems: 1,
2865
2932
  items: {
@@ -2867,7 +2934,7 @@ const rule$j = {
2867
2934
  additionalProperties: false,
2868
2935
  properties: {
2869
2936
  fieldName: {
2870
- type: 'string',
2937
+ oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
2871
2938
  default: DEFAULT_ID_FIELD_NAME,
2872
2939
  },
2873
2940
  },
@@ -2875,69 +2942,64 @@ const rule$j = {
2875
2942
  },
2876
2943
  },
2877
2944
  create(context) {
2945
+ requireGraphQLSchemaFromContext('require-id-when-available', context);
2946
+ const siblings = requireSiblingsOperations('require-id-when-available', context);
2947
+ const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
2948
+ const idNames = Array.isArray(fieldName) ? fieldName : [fieldName];
2949
+ const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
2878
2950
  return {
2879
2951
  SelectionSet(node) {
2880
2952
  var _a, _b;
2881
- requireGraphQLSchemaFromContext('require-id-when-available', context);
2882
- const siblings = requireSiblingsOperations('require-id-when-available', context);
2883
- const fieldName = (context.options[0] || {}).fieldName || DEFAULT_ID_FIELD_NAME;
2884
- if (!node.selections || node.selections.length === 0) {
2953
+ const typeInfo = node.typeInfo();
2954
+ if (!typeInfo.gqlType) {
2885
2955
  return;
2886
2956
  }
2887
- const typeInfo = node.typeInfo();
2888
- if (typeInfo && typeInfo.gqlType) {
2889
- const rawType = getBaseType(typeInfo.gqlType);
2890
- if (rawType instanceof GraphQLObjectType || rawType instanceof GraphQLInterfaceType) {
2891
- const fields = rawType.getFields();
2892
- const hasIdFieldInType = !!fields[fieldName];
2893
- const checkedFragmentSpreads = new Set();
2894
- if (hasIdFieldInType) {
2895
- let found = false;
2896
- for (const selection of node.selections) {
2897
- if (selection.kind === 'Field' && selection.name.value === fieldName) {
2898
- found = true;
2899
- }
2900
- else if (selection.kind === 'InlineFragment') {
2901
- found = (((_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2902
- }
2903
- else if (selection.kind === 'FragmentSpread') {
2904
- const foundSpread = siblings.getFragment(selection.name.value);
2905
- if (foundSpread[0]) {
2906
- checkedFragmentSpreads.add(foundSpread[0].document.name.value);
2907
- found = (((_b = foundSpread[0].document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections) || []).some(s => s.kind === 'Field' && s.name.value === fieldName);
2908
- }
2909
- }
2910
- if (found) {
2911
- break;
2912
- }
2913
- }
2914
- const { parent } = node;
2915
- const hasIdFieldInInterfaceSelectionSet = parent &&
2916
- parent.kind === 'InlineFragment' &&
2917
- parent.parent &&
2918
- parent.parent.kind === 'SelectionSet' &&
2919
- parent.parent.selections.some(s => s.kind === 'Field' && s.name.value === fieldName);
2920
- if (!found && !hasIdFieldInInterfaceSelectionSet) {
2921
- context.report({
2922
- loc: {
2923
- start: {
2924
- line: node.loc.start.line,
2925
- column: node.loc.start.column - 1,
2926
- },
2927
- end: {
2928
- line: node.loc.end.line,
2929
- column: node.loc.end.column - 1,
2930
- },
2931
- },
2932
- messageId: REQUIRE_ID_WHEN_AVAILABLE,
2933
- data: {
2934
- checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${Array.from(checkedFragmentSpreads).join(', ')})`,
2935
- fieldName,
2936
- },
2937
- });
2938
- }
2957
+ const rawType = getBaseType(typeInfo.gqlType);
2958
+ const isObjectType = rawType instanceof GraphQLObjectType;
2959
+ const isInterfaceType = rawType instanceof GraphQLInterfaceType;
2960
+ if (!isObjectType && !isInterfaceType) {
2961
+ return;
2962
+ }
2963
+ const fields = rawType.getFields();
2964
+ const hasIdFieldInType = idNames.some(name => fields[name]);
2965
+ if (!hasIdFieldInType) {
2966
+ return;
2967
+ }
2968
+ const checkedFragmentSpreads = new Set();
2969
+ let found = false;
2970
+ for (const selection of node.selections) {
2971
+ if (isFound(selection)) {
2972
+ found = true;
2973
+ }
2974
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
2975
+ found = (_a = selection.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.some(s => isFound(s));
2976
+ }
2977
+ else if (selection.kind === Kind.FRAGMENT_SPREAD) {
2978
+ const [foundSpread] = siblings.getFragment(selection.name.value);
2979
+ if (foundSpread) {
2980
+ checkedFragmentSpreads.add(foundSpread.document.name.value);
2981
+ found = (_b = foundSpread.document.selectionSet) === null || _b === void 0 ? void 0 : _b.selections.some(s => isFound(s));
2939
2982
  }
2940
2983
  }
2984
+ if (found) {
2985
+ break;
2986
+ }
2987
+ }
2988
+ const { parent } = node;
2989
+ const hasIdFieldInInterfaceSelectionSet = parent &&
2990
+ parent.kind === Kind.INLINE_FRAGMENT &&
2991
+ parent.parent &&
2992
+ parent.parent.kind === Kind.SELECTION_SET &&
2993
+ parent.parent.selections.some(s => isFound(s));
2994
+ if (!found && !hasIdFieldInInterfaceSelectionSet) {
2995
+ context.report({
2996
+ loc: getLocation(node.loc),
2997
+ messageId: REQUIRE_ID_WHEN_AVAILABLE,
2998
+ data: {
2999
+ checkedFragments: checkedFragmentSpreads.size === 0 ? '' : `(${[...checkedFragmentSpreads].join(', ')}) `,
3000
+ fieldName: idNames.map(name => `"${name}"`).join(' or '),
3001
+ },
3002
+ });
2941
3003
  }
2942
3004
  },
2943
3005
  };
@@ -3029,7 +3091,7 @@ const rule$k = {
3029
3091
  // eslint-disable-next-line no-console
3030
3092
  console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3031
3093
  }
3032
- const maxDepth = context.options[0].maxDepth;
3094
+ const { maxDepth } = context.options[0];
3033
3095
  const ignore = context.options[0].ignore || [];
3034
3096
  const checkFn = depthLimit(maxDepth, { ignore });
3035
3097
  return {