@graphql-eslint/eslint-plugin 2.3.2-alpha-6c8a706.0 → 2.3.2

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.
@@ -14,11 +14,11 @@ Checks for duplicate fields in selection set, variables in operation definition,
14
14
  ```graphql
15
15
  # eslint @graphql-eslint/avoid-duplicate-fields: 'error'
16
16
 
17
- query getUserDetails {
17
+ query {
18
18
  user {
19
- name # first
19
+ name
20
20
  email
21
- name # second
21
+ name # duplicate field
22
22
  }
23
23
  }
24
24
  ```
@@ -28,7 +28,7 @@ query getUserDetails {
28
28
  ```graphql
29
29
  # eslint @graphql-eslint/avoid-duplicate-fields: 'error'
30
30
 
31
- query getUsers {
31
+ query {
32
32
  users(
33
33
  first: 100
34
34
  skip: 50
@@ -45,9 +45,11 @@ query getUsers {
45
45
  ```graphql
46
46
  # eslint @graphql-eslint/avoid-duplicate-fields: 'error'
47
47
 
48
- query getUsers($first: Int!, $first: Int!) {
49
- # Duplicate variable
50
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
48
+ query (
49
+ $first: Int!
50
+ $first: Int! # duplicate variable
51
+ ) {
52
+ users(first: $first, skip: 50) {
51
53
  id
52
54
  }
53
55
  }
package/index.js CHANGED
@@ -612,7 +612,7 @@ const rule = {
612
612
  ],
613
613
  },
614
614
  messages: {
615
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}".',
615
+ [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
616
616
  },
617
617
  schema: {
618
618
  type: 'array',
@@ -669,16 +669,9 @@ const rule = {
669
669
  for (const node of nodes) {
670
670
  const currName = node.name.value;
671
671
  if (prevName && prevName > currName) {
672
- const { start, end } = node.name.loc;
673
672
  const isVariableNode = node.kind === graphql.Kind.VARIABLE;
674
673
  context.report({
675
- loc: {
676
- start: {
677
- line: start.line,
678
- column: start.column - (isVariableNode ? 2 : 1),
679
- },
680
- end,
681
- },
674
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
682
675
  messageId: ALPHABETIZE,
683
676
  data: isVariableNode
684
677
  ? {
@@ -744,35 +737,22 @@ const rule = {
744
737
  };
745
738
 
746
739
  const AVOID_DUPLICATE_FIELDS = 'AVOID_DUPLICATE_FIELDS';
747
- const ensureUnique = () => {
748
- const set = new Set();
749
- return {
750
- add: (item, onError) => {
751
- if (set.has(item)) {
752
- onError();
753
- }
754
- else {
755
- set.add(item);
756
- }
757
- },
758
- };
759
- };
760
740
  const rule$1 = {
761
741
  meta: {
762
742
  type: 'suggestion',
763
743
  docs: {
764
- description: 'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
744
+ description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
765
745
  category: 'Stylistic Issues',
766
746
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
767
747
  examples: [
768
748
  {
769
749
  title: 'Incorrect',
770
750
  code: /* GraphQL */ `
771
- query getUserDetails {
751
+ query {
772
752
  user {
773
- name # first
753
+ name
774
754
  email
775
- name # second
755
+ name # duplicate field
776
756
  }
777
757
  }
778
758
  `,
@@ -780,7 +760,7 @@ const rule$1 = {
780
760
  {
781
761
  title: 'Incorrect',
782
762
  code: /* GraphQL */ `
783
- query getUsers {
763
+ query {
784
764
  users(
785
765
  first: 100
786
766
  skip: 50
@@ -795,9 +775,11 @@ const rule$1 = {
795
775
  {
796
776
  title: 'Incorrect',
797
777
  code: /* GraphQL */ `
798
- query getUsers($first: Int!, $first: Int!) {
799
- # Duplicate variable
800
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
778
+ query (
779
+ $first: Int!
780
+ $first: Int! # duplicate variable
781
+ ) {
782
+ users(first: $first, skip: 50) {
801
783
  id
802
784
  }
803
785
  }
@@ -806,58 +788,47 @@ const rule$1 = {
806
788
  ],
807
789
  },
808
790
  messages: {
809
- [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
791
+ [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
810
792
  },
811
793
  schema: [],
812
794
  },
813
795
  create(context) {
796
+ function checkNode(usedFields, fieldName, type, node) {
797
+ if (usedFields.has(fieldName)) {
798
+ context.report({
799
+ loc: getLocation((node.kind === graphql.Kind.FIELD && node.alias ? node.alias : node).loc, fieldName, {
800
+ offsetEnd: node.kind === graphql.Kind.VARIABLE_DEFINITION ? 0 : 1,
801
+ }),
802
+ messageId: AVOID_DUPLICATE_FIELDS,
803
+ data: {
804
+ type,
805
+ fieldName,
806
+ },
807
+ });
808
+ }
809
+ else {
810
+ usedFields.add(fieldName);
811
+ }
812
+ }
814
813
  return {
815
814
  OperationDefinition(node) {
816
- const uniqueCheck = ensureUnique();
817
- for (const arg of node.variableDefinitions || []) {
818
- uniqueCheck.add(arg.variable.name.value, () => {
819
- context.report({
820
- messageId: AVOID_DUPLICATE_FIELDS,
821
- data: {
822
- type: 'Operation variable',
823
- fieldName: arg.variable.name.value,
824
- },
825
- node: arg,
826
- });
827
- });
815
+ const set = new Set();
816
+ for (const varDef of node.variableDefinitions) {
817
+ checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
828
818
  }
829
819
  },
830
820
  Field(node) {
831
- const uniqueCheck = ensureUnique();
832
- for (const arg of node.arguments || []) {
833
- uniqueCheck.add(arg.name.value, () => {
834
- context.report({
835
- messageId: AVOID_DUPLICATE_FIELDS,
836
- data: {
837
- type: 'Field argument',
838
- fieldName: arg.name.value,
839
- },
840
- node: arg,
841
- });
842
- });
821
+ const set = new Set();
822
+ for (const arg of node.arguments) {
823
+ checkNode(set, arg.name.value, 'Field argument', arg);
843
824
  }
844
825
  },
845
826
  SelectionSet(node) {
846
827
  var _a;
847
- const uniqueCheck = ensureUnique();
848
- for (const selection of node.selections || []) {
828
+ const set = new Set();
829
+ for (const selection of node.selections) {
849
830
  if (selection.kind === graphql.Kind.FIELD) {
850
- const nameToCheck = ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value;
851
- uniqueCheck.add(nameToCheck, () => {
852
- context.report({
853
- messageId: AVOID_DUPLICATE_FIELDS,
854
- data: {
855
- type: 'Field',
856
- fieldName: nameToCheck,
857
- },
858
- node: selection,
859
- });
860
- });
831
+ checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
861
832
  }
862
833
  }
863
834
  },
@@ -1116,7 +1087,7 @@ const rule$5 = {
1116
1087
  '[description.type="StringValue"]': node => {
1117
1088
  if (node.description.block !== (style === 'block')) {
1118
1089
  context.report({
1119
- node: node.description,
1090
+ loc: getLocation(node.description.loc),
1120
1091
  message: `Unexpected ${wrongDescriptionType} description`,
1121
1092
  });
1122
1093
  }
@@ -1203,10 +1174,11 @@ const rule$6 = {
1203
1174
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1204
1175
  const listeners = {
1205
1176
  'FieldDefinition > InputValueDefinition': node => {
1206
- if (node.name.value !== 'input' && shouldCheckType(node.parent.parent)) {
1177
+ const name = node.name.value;
1178
+ if (name !== 'input' && shouldCheckType(node.parent.parent)) {
1207
1179
  context.report({
1208
- node: node.name,
1209
- message: `Input "${node.name.value}" should be called "input"`,
1180
+ loc: getLocation(node.loc, name),
1181
+ message: `Input "${name}" should be called "input"`,
1210
1182
  });
1211
1183
  }
1212
1184
  },
@@ -1223,11 +1195,12 @@ const rule$6 = {
1223
1195
  const inputValueNode = findInputType(node);
1224
1196
  if (shouldCheckType(inputValueNode.parent.parent)) {
1225
1197
  const mutationName = `${inputValueNode.parent.name.value}Input`;
1198
+ const name = node.name.value;
1226
1199
  if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
1227
- node.name.value.toLowerCase() !== mutationName.toLowerCase()) {
1200
+ name.toLowerCase() !== mutationName.toLowerCase()) {
1228
1201
  context.report({
1229
- node,
1230
- message: `InputType "${node.name.value}" name should be "${mutationName}"`,
1202
+ loc: getLocation(node.loc, name),
1203
+ message: `InputType "${name}" name should be "${mutationName}"`,
1231
1204
  });
1232
1205
  }
1233
1206
  }
@@ -1616,7 +1589,7 @@ const rule$8 = {
1616
1589
  });
1617
1590
  if (result.ok === false) {
1618
1591
  context.report({
1619
- node,
1592
+ loc: getLocation(node.loc, node.value),
1620
1593
  message: result.errorMessage,
1621
1594
  data: {
1622
1595
  prefix,
@@ -1643,10 +1616,16 @@ const rule$8 = {
1643
1616
  return {
1644
1617
  Name: node => {
1645
1618
  if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
1646
- context.report({ node, message: 'Leading underscores are not allowed' });
1619
+ context.report({
1620
+ loc: getLocation(node.loc, node.value),
1621
+ message: 'Leading underscores are not allowed',
1622
+ });
1647
1623
  }
1648
1624
  if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
1649
- context.report({ node, message: 'Trailing underscores are not allowed' });
1625
+ context.report({
1626
+ loc: getLocation(node.loc, node.value),
1627
+ message: 'Trailing underscores are not allowed',
1628
+ });
1650
1629
  }
1651
1630
  },
1652
1631
  ObjectTypeDefinition: node => {
@@ -1874,8 +1853,8 @@ const rule$b = {
1874
1853
  mutation {
1875
1854
  changeSomething(
1876
1855
  type: OLD # This is deprecated, so you'll get an error
1877
- ) {
1878
- ...
1856
+ ) {
1857
+ ...
1879
1858
  }
1880
1859
  }
1881
1860
  `,
@@ -1913,8 +1892,9 @@ const rule$b = {
1913
1892
  const typeInfo = node.typeInfo();
1914
1893
  if (typeInfo && typeInfo.enumValue) {
1915
1894
  if (typeInfo.enumValue.isDeprecated) {
1895
+ const enumValueName = node.value;
1916
1896
  context.report({
1917
- loc: node.loc,
1897
+ loc: getLocation(node.loc, enumValueName),
1918
1898
  messageId: NO_DEPRECATED,
1919
1899
  data: {
1920
1900
  type: 'enum value',
@@ -1929,8 +1909,9 @@ const rule$b = {
1929
1909
  const typeInfo = node.typeInfo();
1930
1910
  if (typeInfo && typeInfo.fieldDef) {
1931
1911
  if (typeInfo.fieldDef.isDeprecated) {
1912
+ const fieldName = node.name.value;
1932
1913
  context.report({
1933
- loc: node.loc,
1914
+ loc: getLocation(node.loc, fieldName),
1934
1915
  messageId: NO_DEPRECATED,
1935
1916
  data: {
1936
1917
  type: 'field',
@@ -2006,10 +1987,7 @@ const rule$c = {
2006
1987
  if (!isEslintComment && line !== prev.line && next.kind === graphql.TokenKind.NAME && linesAfter < 2) {
2007
1988
  context.report({
2008
1989
  messageId: HASHTAG_COMMENT,
2009
- loc: {
2010
- start: { line, column },
2011
- end: { line, column },
2012
- },
1990
+ loc: getLocation({ start: { line, column } }),
2013
1991
  });
2014
1992
  }
2015
1993
  }
@@ -2138,7 +2116,7 @@ const rule$e = {
2138
2116
  const typeName = node.name.value;
2139
2117
  if (!reachableTypes.has(typeName)) {
2140
2118
  context.report({
2141
- node,
2119
+ loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === graphql.Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
2142
2120
  messageId: UNREACHABLE_TYPE,
2143
2121
  data: { typeName },
2144
2122
  fix: fixer => fixer.remove(node),
@@ -2236,7 +2214,7 @@ const rule$f = {
2236
2214
  return;
2237
2215
  }
2238
2216
  context.report({
2239
- node,
2217
+ loc: getLocation(node.loc, fieldName),
2240
2218
  messageId: UNUSED_FIELD,
2241
2219
  data: { fieldName },
2242
2220
  fix(fixer) {
@@ -2391,10 +2369,10 @@ const rule$g = {
2391
2369
  ],
2392
2370
  },
2393
2371
  messages: {
2394
- [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date.',
2395
- [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY".',
2396
- [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date.',
2397
- [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed.',
2372
+ [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
2373
+ [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
2374
+ [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
2375
+ [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
2398
2376
  },
2399
2377
  schema: [
2400
2378
  {
@@ -2415,13 +2393,16 @@ const rule$g = {
2415
2393
  const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
2416
2394
  const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
2417
2395
  if (!deletionDateNode) {
2418
- context.report({ node: node.name, messageId: MESSAGE_REQUIRE_DATE });
2396
+ context.report({
2397
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2398
+ messageId: MESSAGE_REQUIRE_DATE,
2399
+ });
2419
2400
  return;
2420
2401
  }
2421
2402
  const deletionDate = valueFromNode(deletionDateNode.value);
2422
2403
  const isValidDate = DATE_REGEX.test(deletionDate);
2423
2404
  if (!isValidDate) {
2424
- context.report({ node: node.name, messageId: MESSAGE_INVALID_FORMAT });
2405
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
2425
2406
  return;
2426
2407
  }
2427
2408
  let [day, month, year] = deletionDate.split('/');
@@ -2430,7 +2411,7 @@ const rule$g = {
2430
2411
  const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
2431
2412
  if (Number.isNaN(deletionDateInMS)) {
2432
2413
  context.report({
2433
- node: node.name,
2414
+ node: deletionDateNode.value,
2434
2415
  messageId: MESSAGE_INVALID_DATE,
2435
2416
  data: {
2436
2417
  deletionDate,
@@ -2441,7 +2422,7 @@ const rule$g = {
2441
2422
  const canRemove = Date.now() > deletionDateInMS;
2442
2423
  if (canRemove) {
2443
2424
  context.report({
2444
- node: node.name,
2425
+ node,
2445
2426
  messageId: MESSAGE_CAN_BE_REMOVED,
2446
2427
  data: {
2447
2428
  nodeName: node.parent.name.value,
@@ -2492,17 +2473,15 @@ const rule$h = {
2492
2473
  },
2493
2474
  create(context) {
2494
2475
  return {
2495
- Directive(node) {
2496
- if (node && node.name && node.name.value === 'deprecated') {
2497
- const args = node.arguments || [];
2498
- const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2499
- const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2500
- if (!value) {
2501
- context.report({
2502
- node: node.name,
2503
- message: 'Directive "@deprecated" must have a reason!',
2504
- });
2505
- }
2476
+ 'Directive[name.value=deprecated]'(node) {
2477
+ const args = node.arguments || [];
2478
+ const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2479
+ const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2480
+ if (!value) {
2481
+ context.report({
2482
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2483
+ message: 'Directive "@deprecated" must have a reason!',
2484
+ });
2506
2485
  }
2507
2486
  },
2508
2487
  };
@@ -2647,18 +2626,22 @@ const rule$j = {
2647
2626
  if (!mutationType || !queryType) {
2648
2627
  return {};
2649
2628
  }
2650
- const selector = `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${graphql.Kind.FIELD_DEFINITION}`;
2629
+ const selector = [
2630
+ `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2631
+ '>',
2632
+ graphql.Kind.FIELD_DEFINITION,
2633
+ graphql.Kind.NAMED_TYPE,
2634
+ ].join(' ');
2651
2635
  return {
2652
2636
  [selector](node) {
2653
- const rawNode = node.rawNode();
2654
- const typeName = getTypeName(rawNode);
2637
+ const typeName = node.name.value;
2655
2638
  const graphQLType = schema.getType(typeName);
2656
2639
  if (graphql.isObjectType(graphQLType)) {
2657
2640
  const { fields } = graphQLType.astNode;
2658
2641
  const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
2659
2642
  if (!hasQueryType) {
2660
2643
  context.report({
2661
- node,
2644
+ loc: getLocation(node.loc, typeName),
2662
2645
  message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
2663
2646
  });
2664
2647
  }
@@ -3153,15 +3136,16 @@ const rule$m = {
3153
3136
  }
3154
3137
  return isValidIdName && isValidIdType;
3155
3138
  });
3139
+ const typeName = node.name.value;
3156
3140
  // Usually, there should be only one unique identifier field per type.
3157
3141
  // Some clients allow multiple fields to be used. If more people need this,
3158
3142
  // we can extend this rule later.
3159
3143
  if (validIds.length !== 1) {
3160
3144
  context.report({
3161
- node,
3162
- message: '{{nodeName}} must have exactly one non-nullable unique identifier. Accepted name(s): {{acceptedNamesString}} ; Accepted type(s): {{acceptedTypesString}}',
3145
+ loc: getLocation(node.name.loc, typeName),
3146
+ message: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }} ; Accepted type(s): {{ acceptedTypesString }}`,
3163
3147
  data: {
3164
- nodeName: node.name.value,
3148
+ typeName,
3165
3149
  acceptedNamesString: options.acceptedIdNames.join(','),
3166
3150
  acceptedTypesString: options.acceptedIdTypes.join(','),
3167
3151
  },
@@ -3757,7 +3741,25 @@ class GraphQLRuleTester extends eslint.RuleTester {
3757
3741
  return fs.readFileSync(path.resolve(__dirname, `../tests/mocks/${path$1}`), 'utf-8');
3758
3742
  }
3759
3743
  runGraphQLTests(name, rule, tests) {
3760
- super.run(name, rule, tests);
3744
+ const ruleTests = eslint.Linter.version.startsWith('8')
3745
+ ? tests
3746
+ : {
3747
+ valid: tests.valid.map(test => {
3748
+ if (typeof test === 'string') {
3749
+ return test;
3750
+ }
3751
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3752
+ const { name, ...testCaseOptions } = test;
3753
+ return testCaseOptions;
3754
+ }),
3755
+ invalid: tests.invalid.map(test => {
3756
+ // ESLint 7 throws an error on CI - Unexpected top-level property "name"
3757
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3758
+ const { name, ...testCaseOptions } = test;
3759
+ return testCaseOptions;
3760
+ }),
3761
+ };
3762
+ super.run(name, rule, ruleTests);
3761
3763
  // Skip snapshot testing if `expect` variable is not defined
3762
3764
  if (typeof expect === 'undefined') {
3763
3765
  return;
package/index.mjs CHANGED
@@ -606,7 +606,7 @@ const rule = {
606
606
  ],
607
607
  },
608
608
  messages: {
609
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}".',
609
+ [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
610
610
  },
611
611
  schema: {
612
612
  type: 'array',
@@ -663,16 +663,9 @@ const rule = {
663
663
  for (const node of nodes) {
664
664
  const currName = node.name.value;
665
665
  if (prevName && prevName > currName) {
666
- const { start, end } = node.name.loc;
667
666
  const isVariableNode = node.kind === Kind.VARIABLE;
668
667
  context.report({
669
- loc: {
670
- start: {
671
- line: start.line,
672
- column: start.column - (isVariableNode ? 2 : 1),
673
- },
674
- end,
675
- },
668
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
676
669
  messageId: ALPHABETIZE,
677
670
  data: isVariableNode
678
671
  ? {
@@ -738,35 +731,22 @@ const rule = {
738
731
  };
739
732
 
740
733
  const AVOID_DUPLICATE_FIELDS = 'AVOID_DUPLICATE_FIELDS';
741
- const ensureUnique = () => {
742
- const set = new Set();
743
- return {
744
- add: (item, onError) => {
745
- if (set.has(item)) {
746
- onError();
747
- }
748
- else {
749
- set.add(item);
750
- }
751
- },
752
- };
753
- };
754
734
  const rule$1 = {
755
735
  meta: {
756
736
  type: 'suggestion',
757
737
  docs: {
758
- description: 'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
738
+ description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
759
739
  category: 'Stylistic Issues',
760
740
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
761
741
  examples: [
762
742
  {
763
743
  title: 'Incorrect',
764
744
  code: /* GraphQL */ `
765
- query getUserDetails {
745
+ query {
766
746
  user {
767
- name # first
747
+ name
768
748
  email
769
- name # second
749
+ name # duplicate field
770
750
  }
771
751
  }
772
752
  `,
@@ -774,7 +754,7 @@ const rule$1 = {
774
754
  {
775
755
  title: 'Incorrect',
776
756
  code: /* GraphQL */ `
777
- query getUsers {
757
+ query {
778
758
  users(
779
759
  first: 100
780
760
  skip: 50
@@ -789,9 +769,11 @@ const rule$1 = {
789
769
  {
790
770
  title: 'Incorrect',
791
771
  code: /* GraphQL */ `
792
- query getUsers($first: Int!, $first: Int!) {
793
- # Duplicate variable
794
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
772
+ query (
773
+ $first: Int!
774
+ $first: Int! # duplicate variable
775
+ ) {
776
+ users(first: $first, skip: 50) {
795
777
  id
796
778
  }
797
779
  }
@@ -800,58 +782,47 @@ const rule$1 = {
800
782
  ],
801
783
  },
802
784
  messages: {
803
- [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
785
+ [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
804
786
  },
805
787
  schema: [],
806
788
  },
807
789
  create(context) {
790
+ function checkNode(usedFields, fieldName, type, node) {
791
+ if (usedFields.has(fieldName)) {
792
+ context.report({
793
+ loc: getLocation((node.kind === Kind.FIELD && node.alias ? node.alias : node).loc, fieldName, {
794
+ offsetEnd: node.kind === Kind.VARIABLE_DEFINITION ? 0 : 1,
795
+ }),
796
+ messageId: AVOID_DUPLICATE_FIELDS,
797
+ data: {
798
+ type,
799
+ fieldName,
800
+ },
801
+ });
802
+ }
803
+ else {
804
+ usedFields.add(fieldName);
805
+ }
806
+ }
808
807
  return {
809
808
  OperationDefinition(node) {
810
- const uniqueCheck = ensureUnique();
811
- for (const arg of node.variableDefinitions || []) {
812
- uniqueCheck.add(arg.variable.name.value, () => {
813
- context.report({
814
- messageId: AVOID_DUPLICATE_FIELDS,
815
- data: {
816
- type: 'Operation variable',
817
- fieldName: arg.variable.name.value,
818
- },
819
- node: arg,
820
- });
821
- });
809
+ const set = new Set();
810
+ for (const varDef of node.variableDefinitions) {
811
+ checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
822
812
  }
823
813
  },
824
814
  Field(node) {
825
- const uniqueCheck = ensureUnique();
826
- for (const arg of node.arguments || []) {
827
- uniqueCheck.add(arg.name.value, () => {
828
- context.report({
829
- messageId: AVOID_DUPLICATE_FIELDS,
830
- data: {
831
- type: 'Field argument',
832
- fieldName: arg.name.value,
833
- },
834
- node: arg,
835
- });
836
- });
815
+ const set = new Set();
816
+ for (const arg of node.arguments) {
817
+ checkNode(set, arg.name.value, 'Field argument', arg);
837
818
  }
838
819
  },
839
820
  SelectionSet(node) {
840
821
  var _a;
841
- const uniqueCheck = ensureUnique();
842
- for (const selection of node.selections || []) {
822
+ const set = new Set();
823
+ for (const selection of node.selections) {
843
824
  if (selection.kind === Kind.FIELD) {
844
- const nameToCheck = ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value;
845
- uniqueCheck.add(nameToCheck, () => {
846
- context.report({
847
- messageId: AVOID_DUPLICATE_FIELDS,
848
- data: {
849
- type: 'Field',
850
- fieldName: nameToCheck,
851
- },
852
- node: selection,
853
- });
854
- });
825
+ checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
855
826
  }
856
827
  }
857
828
  },
@@ -1110,7 +1081,7 @@ const rule$5 = {
1110
1081
  '[description.type="StringValue"]': node => {
1111
1082
  if (node.description.block !== (style === 'block')) {
1112
1083
  context.report({
1113
- node: node.description,
1084
+ loc: getLocation(node.description.loc),
1114
1085
  message: `Unexpected ${wrongDescriptionType} description`,
1115
1086
  });
1116
1087
  }
@@ -1197,10 +1168,11 @@ const rule$6 = {
1197
1168
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1198
1169
  const listeners = {
1199
1170
  'FieldDefinition > InputValueDefinition': node => {
1200
- if (node.name.value !== 'input' && shouldCheckType(node.parent.parent)) {
1171
+ const name = node.name.value;
1172
+ if (name !== 'input' && shouldCheckType(node.parent.parent)) {
1201
1173
  context.report({
1202
- node: node.name,
1203
- message: `Input "${node.name.value}" should be called "input"`,
1174
+ loc: getLocation(node.loc, name),
1175
+ message: `Input "${name}" should be called "input"`,
1204
1176
  });
1205
1177
  }
1206
1178
  },
@@ -1217,11 +1189,12 @@ const rule$6 = {
1217
1189
  const inputValueNode = findInputType(node);
1218
1190
  if (shouldCheckType(inputValueNode.parent.parent)) {
1219
1191
  const mutationName = `${inputValueNode.parent.name.value}Input`;
1192
+ const name = node.name.value;
1220
1193
  if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
1221
- node.name.value.toLowerCase() !== mutationName.toLowerCase()) {
1194
+ name.toLowerCase() !== mutationName.toLowerCase()) {
1222
1195
  context.report({
1223
- node,
1224
- message: `InputType "${node.name.value}" name should be "${mutationName}"`,
1196
+ loc: getLocation(node.loc, name),
1197
+ message: `InputType "${name}" name should be "${mutationName}"`,
1225
1198
  });
1226
1199
  }
1227
1200
  }
@@ -1610,7 +1583,7 @@ const rule$8 = {
1610
1583
  });
1611
1584
  if (result.ok === false) {
1612
1585
  context.report({
1613
- node,
1586
+ loc: getLocation(node.loc, node.value),
1614
1587
  message: result.errorMessage,
1615
1588
  data: {
1616
1589
  prefix,
@@ -1637,10 +1610,16 @@ const rule$8 = {
1637
1610
  return {
1638
1611
  Name: node => {
1639
1612
  if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
1640
- context.report({ node, message: 'Leading underscores are not allowed' });
1613
+ context.report({
1614
+ loc: getLocation(node.loc, node.value),
1615
+ message: 'Leading underscores are not allowed',
1616
+ });
1641
1617
  }
1642
1618
  if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
1643
- context.report({ node, message: 'Trailing underscores are not allowed' });
1619
+ context.report({
1620
+ loc: getLocation(node.loc, node.value),
1621
+ message: 'Trailing underscores are not allowed',
1622
+ });
1644
1623
  }
1645
1624
  },
1646
1625
  ObjectTypeDefinition: node => {
@@ -1868,8 +1847,8 @@ const rule$b = {
1868
1847
  mutation {
1869
1848
  changeSomething(
1870
1849
  type: OLD # This is deprecated, so you'll get an error
1871
- ) {
1872
- ...
1850
+ ) {
1851
+ ...
1873
1852
  }
1874
1853
  }
1875
1854
  `,
@@ -1907,8 +1886,9 @@ const rule$b = {
1907
1886
  const typeInfo = node.typeInfo();
1908
1887
  if (typeInfo && typeInfo.enumValue) {
1909
1888
  if (typeInfo.enumValue.isDeprecated) {
1889
+ const enumValueName = node.value;
1910
1890
  context.report({
1911
- loc: node.loc,
1891
+ loc: getLocation(node.loc, enumValueName),
1912
1892
  messageId: NO_DEPRECATED,
1913
1893
  data: {
1914
1894
  type: 'enum value',
@@ -1923,8 +1903,9 @@ const rule$b = {
1923
1903
  const typeInfo = node.typeInfo();
1924
1904
  if (typeInfo && typeInfo.fieldDef) {
1925
1905
  if (typeInfo.fieldDef.isDeprecated) {
1906
+ const fieldName = node.name.value;
1926
1907
  context.report({
1927
- loc: node.loc,
1908
+ loc: getLocation(node.loc, fieldName),
1928
1909
  messageId: NO_DEPRECATED,
1929
1910
  data: {
1930
1911
  type: 'field',
@@ -2000,10 +1981,7 @@ const rule$c = {
2000
1981
  if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
2001
1982
  context.report({
2002
1983
  messageId: HASHTAG_COMMENT,
2003
- loc: {
2004
- start: { line, column },
2005
- end: { line, column },
2006
- },
1984
+ loc: getLocation({ start: { line, column } }),
2007
1985
  });
2008
1986
  }
2009
1987
  }
@@ -2132,7 +2110,7 @@ const rule$e = {
2132
2110
  const typeName = node.name.value;
2133
2111
  if (!reachableTypes.has(typeName)) {
2134
2112
  context.report({
2135
- node,
2113
+ loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
2136
2114
  messageId: UNREACHABLE_TYPE,
2137
2115
  data: { typeName },
2138
2116
  fix: fixer => fixer.remove(node),
@@ -2230,7 +2208,7 @@ const rule$f = {
2230
2208
  return;
2231
2209
  }
2232
2210
  context.report({
2233
- node,
2211
+ loc: getLocation(node.loc, fieldName),
2234
2212
  messageId: UNUSED_FIELD,
2235
2213
  data: { fieldName },
2236
2214
  fix(fixer) {
@@ -2385,10 +2363,10 @@ const rule$g = {
2385
2363
  ],
2386
2364
  },
2387
2365
  messages: {
2388
- [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date.',
2389
- [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY".',
2390
- [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date.',
2391
- [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed.',
2366
+ [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
2367
+ [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
2368
+ [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
2369
+ [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
2392
2370
  },
2393
2371
  schema: [
2394
2372
  {
@@ -2409,13 +2387,16 @@ const rule$g = {
2409
2387
  const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
2410
2388
  const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
2411
2389
  if (!deletionDateNode) {
2412
- context.report({ node: node.name, messageId: MESSAGE_REQUIRE_DATE });
2390
+ context.report({
2391
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2392
+ messageId: MESSAGE_REQUIRE_DATE,
2393
+ });
2413
2394
  return;
2414
2395
  }
2415
2396
  const deletionDate = valueFromNode(deletionDateNode.value);
2416
2397
  const isValidDate = DATE_REGEX.test(deletionDate);
2417
2398
  if (!isValidDate) {
2418
- context.report({ node: node.name, messageId: MESSAGE_INVALID_FORMAT });
2399
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
2419
2400
  return;
2420
2401
  }
2421
2402
  let [day, month, year] = deletionDate.split('/');
@@ -2424,7 +2405,7 @@ const rule$g = {
2424
2405
  const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
2425
2406
  if (Number.isNaN(deletionDateInMS)) {
2426
2407
  context.report({
2427
- node: node.name,
2408
+ node: deletionDateNode.value,
2428
2409
  messageId: MESSAGE_INVALID_DATE,
2429
2410
  data: {
2430
2411
  deletionDate,
@@ -2435,7 +2416,7 @@ const rule$g = {
2435
2416
  const canRemove = Date.now() > deletionDateInMS;
2436
2417
  if (canRemove) {
2437
2418
  context.report({
2438
- node: node.name,
2419
+ node,
2439
2420
  messageId: MESSAGE_CAN_BE_REMOVED,
2440
2421
  data: {
2441
2422
  nodeName: node.parent.name.value,
@@ -2486,17 +2467,15 @@ const rule$h = {
2486
2467
  },
2487
2468
  create(context) {
2488
2469
  return {
2489
- Directive(node) {
2490
- if (node && node.name && node.name.value === 'deprecated') {
2491
- const args = node.arguments || [];
2492
- const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2493
- const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2494
- if (!value) {
2495
- context.report({
2496
- node: node.name,
2497
- message: 'Directive "@deprecated" must have a reason!',
2498
- });
2499
- }
2470
+ 'Directive[name.value=deprecated]'(node) {
2471
+ const args = node.arguments || [];
2472
+ const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2473
+ const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2474
+ if (!value) {
2475
+ context.report({
2476
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2477
+ message: 'Directive "@deprecated" must have a reason!',
2478
+ });
2500
2479
  }
2501
2480
  },
2502
2481
  };
@@ -2641,18 +2620,22 @@ const rule$j = {
2641
2620
  if (!mutationType || !queryType) {
2642
2621
  return {};
2643
2622
  }
2644
- const selector = `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${Kind.FIELD_DEFINITION}`;
2623
+ const selector = [
2624
+ `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2625
+ '>',
2626
+ Kind.FIELD_DEFINITION,
2627
+ Kind.NAMED_TYPE,
2628
+ ].join(' ');
2645
2629
  return {
2646
2630
  [selector](node) {
2647
- const rawNode = node.rawNode();
2648
- const typeName = getTypeName(rawNode);
2631
+ const typeName = node.name.value;
2649
2632
  const graphQLType = schema.getType(typeName);
2650
2633
  if (isObjectType$1(graphQLType)) {
2651
2634
  const { fields } = graphQLType.astNode;
2652
2635
  const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
2653
2636
  if (!hasQueryType) {
2654
2637
  context.report({
2655
- node,
2638
+ loc: getLocation(node.loc, typeName),
2656
2639
  message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
2657
2640
  });
2658
2641
  }
@@ -3147,15 +3130,16 @@ const rule$m = {
3147
3130
  }
3148
3131
  return isValidIdName && isValidIdType;
3149
3132
  });
3133
+ const typeName = node.name.value;
3150
3134
  // Usually, there should be only one unique identifier field per type.
3151
3135
  // Some clients allow multiple fields to be used. If more people need this,
3152
3136
  // we can extend this rule later.
3153
3137
  if (validIds.length !== 1) {
3154
3138
  context.report({
3155
- node,
3156
- message: '{{nodeName}} must have exactly one non-nullable unique identifier. Accepted name(s): {{acceptedNamesString}} ; Accepted type(s): {{acceptedTypesString}}',
3139
+ loc: getLocation(node.name.loc, typeName),
3140
+ message: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }} ; Accepted type(s): {{ acceptedTypesString }}`,
3157
3141
  data: {
3158
- nodeName: node.name.value,
3142
+ typeName,
3159
3143
  acceptedNamesString: options.acceptedIdNames.join(','),
3160
3144
  acceptedTypesString: options.acceptedIdTypes.join(','),
3161
3145
  },
@@ -3751,7 +3735,25 @@ class GraphQLRuleTester extends RuleTester {
3751
3735
  return readFileSync(resolve(__dirname, `../tests/mocks/${path}`), 'utf-8');
3752
3736
  }
3753
3737
  runGraphQLTests(name, rule, tests) {
3754
- super.run(name, rule, tests);
3738
+ const ruleTests = Linter.version.startsWith('8')
3739
+ ? tests
3740
+ : {
3741
+ valid: tests.valid.map(test => {
3742
+ if (typeof test === 'string') {
3743
+ return test;
3744
+ }
3745
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3746
+ const { name, ...testCaseOptions } = test;
3747
+ return testCaseOptions;
3748
+ }),
3749
+ invalid: tests.invalid.map(test => {
3750
+ // ESLint 7 throws an error on CI - Unexpected top-level property "name"
3751
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3752
+ const { name, ...testCaseOptions } = test;
3753
+ return testCaseOptions;
3754
+ }),
3755
+ };
3756
+ super.run(name, rule, ruleTests);
3755
3757
  // Skip snapshot testing if `expect` variable is not defined
3756
3758
  if (typeof expect === 'undefined') {
3757
3759
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphql-eslint/eslint-plugin",
3
- "version": "2.3.2-alpha-6c8a706.0",
3
+ "version": "2.3.2",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
@@ -1,3 +1,3 @@
1
1
  import { GraphQLESLintRule } from '../types';
2
- declare const rule: GraphQLESLintRule<[], false>;
2
+ declare const rule: GraphQLESLintRule;
3
3
  export default rule;
package/rules/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export declare const rules: {
6
6
  variables?: "OperationDefinition"[];
7
7
  arguments?: ("Field" | "Directive" | "FieldDefinition" | "DirectiveDefinition")[];
8
8
  }], false>;
9
- 'avoid-duplicate-fields': import("..").GraphQLESLintRule<[], false>;
9
+ 'avoid-duplicate-fields': import("..").GraphQLESLintRule<any[], false>;
10
10
  'avoid-operation-name-prefix': import("..").GraphQLESLintRule<import("./avoid-operation-name-prefix").AvoidOperationNamePrefixConfig, false>;
11
11
  'avoid-scalar-result-type-on-mutation': import("..").GraphQLESLintRule<any[], false>;
12
12
  'avoid-typename-prefix': import("..").GraphQLESLintRule<any[], false>;
package/testkit.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare type GraphQLESLintRuleListener<WithTypeInfo extends boolean = fal
6
6
  [K in keyof ASTKindToNode]?: (node: GraphQLESTreeNode<ASTKindToNode[K], WithTypeInfo>) => void;
7
7
  } & Record<string, any>;
8
8
  export declare type GraphQLValidTestCase<Options> = Omit<RuleTester.ValidTestCase, 'options' | 'parserOptions'> & {
9
+ name: string;
9
10
  options?: Options;
10
11
  parserOptions?: ParserOptions;
11
12
  };