@graphql-eslint/eslint-plugin 2.4.0-alpha-0617395.0 → 2.4.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
@@ -15,6 +15,8 @@ const depthLimit = _interopDefault(require('graphql-depth-limit'));
15
15
  const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
16
16
  const graphqlConfig$1 = require('graphql-config');
17
17
  const codeFileLoader = require('@graphql-tools/code-file-loader');
18
+ const eslint = require('eslint');
19
+ const codeFrame = require('@babel/code-frame');
18
20
 
19
21
  /*
20
22
  * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
@@ -147,7 +149,7 @@ function getLexer(source) {
147
149
  throw new Error(`Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!`);
148
150
  }
149
151
  function extractTokens(source) {
150
- const lexer = getLexer(new graphql.Source(source));
152
+ const lexer = getLexer(source);
151
153
  const tokens = [];
152
154
  let token = lexer.advance();
153
155
  while (token && token.kind !== '<EOF>') {
@@ -242,6 +244,24 @@ const convertCase = (style, str) => {
242
244
  return lowerCase(str).replace(/ /g, '-');
243
245
  }
244
246
  };
247
+ function getLocation(loc, fieldName = '', offset) {
248
+ const { start } = loc;
249
+ /*
250
+ * ESLint has 0-based column number
251
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
252
+ */
253
+ const { offsetStart = 1, offsetEnd = 1 } = offset !== null && offset !== void 0 ? offset : {};
254
+ return {
255
+ start: {
256
+ line: start.line,
257
+ column: start.column - offsetStart,
258
+ },
259
+ end: {
260
+ line: start.line,
261
+ column: start.column - offsetEnd + fieldName.length,
262
+ },
263
+ };
264
+ }
245
265
 
246
266
  function extractRuleName(stack) {
247
267
  const match = (stack || '').match(/validation[/\\\\]rules[/\\\\](.*?)\.js:/) || [];
@@ -255,7 +275,7 @@ function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName
255
275
  for (const error of validationErrors) {
256
276
  const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
257
277
  context.report({
258
- loc: error.locations[0],
278
+ loc: getLocation({ start: error.locations[0] }),
259
279
  message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
260
280
  });
261
281
  }
@@ -292,6 +312,7 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
292
312
  meta: {
293
313
  docs: {
294
314
  ...docs,
315
+ graphQLJSRuleName: ruleName,
295
316
  category: 'Validation',
296
317
  recommended: true,
297
318
  requiresSchema,
@@ -591,7 +612,7 @@ const rule = {
591
612
  ],
592
613
  },
593
614
  messages: {
594
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}".',
615
+ [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
595
616
  },
596
617
  schema: {
597
618
  type: 'array',
@@ -648,16 +669,9 @@ const rule = {
648
669
  for (const node of nodes) {
649
670
  const currName = node.name.value;
650
671
  if (prevName && prevName > currName) {
651
- const { start, end } = node.name.loc;
652
672
  const isVariableNode = node.kind === graphql.Kind.VARIABLE;
653
673
  context.report({
654
- loc: {
655
- start: {
656
- line: start.line,
657
- column: start.column - (isVariableNode ? 2 : 1),
658
- },
659
- end,
660
- },
674
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
661
675
  messageId: ALPHABETIZE,
662
676
  data: isVariableNode
663
677
  ? {
@@ -723,35 +737,22 @@ const rule = {
723
737
  };
724
738
 
725
739
  const AVOID_DUPLICATE_FIELDS = 'AVOID_DUPLICATE_FIELDS';
726
- const ensureUnique = () => {
727
- const set = new Set();
728
- return {
729
- add: (item, onError) => {
730
- if (set.has(item)) {
731
- onError();
732
- }
733
- else {
734
- set.add(item);
735
- }
736
- },
737
- };
738
- };
739
740
  const rule$1 = {
740
741
  meta: {
741
742
  type: 'suggestion',
742
743
  docs: {
743
- 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.`,
744
745
  category: 'Stylistic Issues',
745
746
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
746
747
  examples: [
747
748
  {
748
749
  title: 'Incorrect',
749
750
  code: /* GraphQL */ `
750
- query getUserDetails {
751
+ query {
751
752
  user {
752
- name # first
753
+ name
753
754
  email
754
- name # second
755
+ name # duplicate field
755
756
  }
756
757
  }
757
758
  `,
@@ -759,7 +760,7 @@ const rule$1 = {
759
760
  {
760
761
  title: 'Incorrect',
761
762
  code: /* GraphQL */ `
762
- query getUsers {
763
+ query {
763
764
  users(
764
765
  first: 100
765
766
  skip: 50
@@ -774,9 +775,11 @@ const rule$1 = {
774
775
  {
775
776
  title: 'Incorrect',
776
777
  code: /* GraphQL */ `
777
- query getUsers($first: Int!, $first: Int!) {
778
- # Duplicate variable
779
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
778
+ query (
779
+ $first: Int!
780
+ $first: Int! # duplicate variable
781
+ ) {
782
+ users(first: $first, skip: 50) {
780
783
  id
781
784
  }
782
785
  }
@@ -785,58 +788,47 @@ const rule$1 = {
785
788
  ],
786
789
  },
787
790
  messages: {
788
- [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
791
+ [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
789
792
  },
790
793
  schema: [],
791
794
  },
792
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
+ }
793
813
  return {
794
814
  OperationDefinition(node) {
795
- const uniqueCheck = ensureUnique();
796
- for (const arg of node.variableDefinitions || []) {
797
- uniqueCheck.add(arg.variable.name.value, () => {
798
- context.report({
799
- messageId: AVOID_DUPLICATE_FIELDS,
800
- data: {
801
- type: 'Operation variable',
802
- fieldName: arg.variable.name.value,
803
- },
804
- node: arg,
805
- });
806
- });
815
+ const set = new Set();
816
+ for (const varDef of node.variableDefinitions) {
817
+ checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
807
818
  }
808
819
  },
809
820
  Field(node) {
810
- const uniqueCheck = ensureUnique();
811
- for (const arg of node.arguments || []) {
812
- uniqueCheck.add(arg.name.value, () => {
813
- context.report({
814
- messageId: AVOID_DUPLICATE_FIELDS,
815
- data: {
816
- type: 'Field argument',
817
- fieldName: arg.name.value,
818
- },
819
- node: arg,
820
- });
821
- });
821
+ const set = new Set();
822
+ for (const arg of node.arguments) {
823
+ checkNode(set, arg.name.value, 'Field argument', arg);
822
824
  }
823
825
  },
824
826
  SelectionSet(node) {
825
827
  var _a;
826
- const uniqueCheck = ensureUnique();
827
- for (const selection of node.selections || []) {
828
+ const set = new Set();
829
+ for (const selection of node.selections) {
828
830
  if (selection.kind === graphql.Kind.FIELD) {
829
- const nameToCheck = ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value;
830
- uniqueCheck.add(nameToCheck, () => {
831
- context.report({
832
- messageId: AVOID_DUPLICATE_FIELDS,
833
- data: {
834
- type: 'Field',
835
- fieldName: nameToCheck,
836
- },
837
- node: selection,
838
- });
839
- });
831
+ checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
840
832
  }
841
833
  }
842
834
  },
@@ -967,16 +959,20 @@ const rule$3 = {
967
959
  if (!mutationType) {
968
960
  return {};
969
961
  }
970
- const selector = `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${graphql.Kind.FIELD_DEFINITION}`;
962
+ const selector = [
963
+ `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
964
+ '>',
965
+ graphql.Kind.FIELD_DEFINITION,
966
+ graphql.Kind.NAMED_TYPE,
967
+ ].join(' ');
971
968
  return {
972
969
  [selector](node) {
973
- const rawNode = node.rawNode();
974
- const typeName = getTypeName(rawNode);
970
+ const typeName = node.name.value;
975
971
  const graphQLType = schema.getType(typeName);
976
972
  if (graphql.isScalarType(graphQLType)) {
977
973
  context.report({
978
- node,
979
- message: `Unexpected scalar result type "${typeName}".`,
974
+ loc: getLocation(node.loc, typeName),
975
+ message: `Unexpected scalar result type "${typeName}"`,
980
976
  });
981
977
  }
982
978
  },
@@ -1025,23 +1021,13 @@ const rule$4 = {
1025
1021
  for (const field of node.fields) {
1026
1022
  const fieldName = field.name.value;
1027
1023
  if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
1028
- const { start } = field.loc;
1029
1024
  context.report({
1030
1025
  data: {
1031
1026
  fieldName,
1032
1027
  typeName,
1033
1028
  },
1034
1029
  messageId: AVOID_TYPENAME_PREFIX,
1035
- loc: {
1036
- start: {
1037
- line: start.line,
1038
- column: start.column - 1,
1039
- },
1040
- end: {
1041
- line: start.line,
1042
- column: start.column - 1 + lowerTypeName.length,
1043
- },
1044
- },
1030
+ loc: getLocation(field.loc, lowerTypeName),
1045
1031
  });
1046
1032
  }
1047
1033
  }
@@ -1101,7 +1087,7 @@ const rule$5 = {
1101
1087
  '[description.type="StringValue"]': node => {
1102
1088
  if (node.description.block !== (style === 'block')) {
1103
1089
  context.report({
1104
- node: node.description,
1090
+ loc: getLocation(node.description.loc),
1105
1091
  message: `Unexpected ${wrongDescriptionType} description`,
1106
1092
  });
1107
1093
  }
@@ -1188,10 +1174,11 @@ const rule$6 = {
1188
1174
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1189
1175
  const listeners = {
1190
1176
  'FieldDefinition > InputValueDefinition': node => {
1191
- if (node.name.value !== 'input' && shouldCheckType(node.parent.parent)) {
1177
+ const name = node.name.value;
1178
+ if (name !== 'input' && shouldCheckType(node.parent.parent)) {
1192
1179
  context.report({
1193
- node: node.name,
1194
- message: `Input "${node.name.value}" should be called "input"`,
1180
+ loc: getLocation(node.loc, name),
1181
+ message: `Input "${name}" should be called "input"`,
1195
1182
  });
1196
1183
  }
1197
1184
  },
@@ -1208,11 +1195,12 @@ const rule$6 = {
1208
1195
  const inputValueNode = findInputType(node);
1209
1196
  if (shouldCheckType(inputValueNode.parent.parent)) {
1210
1197
  const mutationName = `${inputValueNode.parent.name.value}Input`;
1198
+ const name = node.name.value;
1211
1199
  if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
1212
- node.name.value.toLowerCase() !== mutationName.toLowerCase()) {
1200
+ name.toLowerCase() !== mutationName.toLowerCase()) {
1213
1201
  context.report({
1214
- node,
1215
- message: `InputType "${node.name.value}" name should be "${mutationName}"`,
1202
+ loc: getLocation(node.loc, name),
1203
+ message: `InputType "${name}" name should be "${mutationName}"`,
1216
1204
  });
1217
1205
  }
1218
1206
  }
@@ -1369,7 +1357,8 @@ const rule$7 = {
1369
1357
  var _a;
1370
1358
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1371
1359
  context.report({
1372
- node: documentNode,
1360
+ // Report on first character
1361
+ loc: { column: 0, line: 1 },
1373
1362
  messageId: MATCH_EXTENSION,
1374
1363
  data: {
1375
1364
  fileExtension,
@@ -1401,7 +1390,8 @@ const rule$7 = {
1401
1390
  const filenameWithExtension = filename + expectedExtension;
1402
1391
  if (expectedFilename !== filenameWithExtension) {
1403
1392
  context.report({
1404
- node: documentNode,
1393
+ // Report on first character
1394
+ loc: { column: 0, line: 1 },
1405
1395
  messageId: MATCH_STYLE,
1406
1396
  data: {
1407
1397
  expectedFilename,
@@ -1599,7 +1589,7 @@ const rule$8 = {
1599
1589
  });
1600
1590
  if (result.ok === false) {
1601
1591
  context.report({
1602
- node,
1592
+ loc: getLocation(node.loc, node.value),
1603
1593
  message: result.errorMessage,
1604
1594
  data: {
1605
1595
  prefix,
@@ -1626,10 +1616,16 @@ const rule$8 = {
1626
1616
  return {
1627
1617
  Name: node => {
1628
1618
  if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
1629
- 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
+ });
1630
1623
  }
1631
1624
  if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
1632
- 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
+ });
1633
1629
  }
1634
1630
  },
1635
1631
  ObjectTypeDefinition: node => {
@@ -1743,28 +1739,14 @@ const rule$9 = {
1743
1739
  },
1744
1740
  create(context) {
1745
1741
  return {
1746
- OperationDefinition(node) {
1747
- var _a;
1748
- const isAnonymous = (((_a = node.name) === null || _a === void 0 ? void 0 : _a.value) || '').length === 0;
1749
- if (isAnonymous) {
1750
- const { start } = node.loc;
1751
- context.report({
1752
- loc: {
1753
- start: {
1754
- column: start.column - 1,
1755
- line: start.line,
1756
- },
1757
- end: {
1758
- column: start.column - 1 + node.operation.length,
1759
- line: start.line,
1760
- },
1761
- },
1762
- data: {
1763
- operation: node.operation,
1764
- },
1765
- messageId: NO_ANONYMOUS_OPERATIONS,
1766
- });
1767
- }
1742
+ 'OperationDefinition[name=undefined]'(node) {
1743
+ context.report({
1744
+ loc: getLocation(node.loc, node.operation),
1745
+ data: {
1746
+ operation: node.operation,
1747
+ },
1748
+ messageId: NO_ANONYMOUS_OPERATIONS,
1749
+ });
1768
1750
  },
1769
1751
  };
1770
1752
  },
@@ -1871,8 +1853,8 @@ const rule$b = {
1871
1853
  mutation {
1872
1854
  changeSomething(
1873
1855
  type: OLD # This is deprecated, so you'll get an error
1874
- ) {
1875
- ...
1856
+ ) {
1857
+ ...
1876
1858
  }
1877
1859
  }
1878
1860
  `,
@@ -1910,8 +1892,9 @@ const rule$b = {
1910
1892
  const typeInfo = node.typeInfo();
1911
1893
  if (typeInfo && typeInfo.enumValue) {
1912
1894
  if (typeInfo.enumValue.deprecationReason) {
1895
+ const enumValueName = node.value;
1913
1896
  context.report({
1914
- loc: node.loc,
1897
+ loc: getLocation(node.loc, enumValueName),
1915
1898
  messageId: NO_DEPRECATED,
1916
1899
  data: {
1917
1900
  type: 'enum value',
@@ -1926,8 +1909,9 @@ const rule$b = {
1926
1909
  const typeInfo = node.typeInfo();
1927
1910
  if (typeInfo && typeInfo.fieldDef) {
1928
1911
  if (typeInfo.fieldDef.deprecationReason) {
1912
+ const fieldName = node.name.value;
1929
1913
  context.report({
1930
- loc: node.loc,
1914
+ loc: getLocation(node.loc, fieldName),
1931
1915
  messageId: NO_DEPRECATED,
1932
1916
  data: {
1933
1917
  type: 'field',
@@ -2003,10 +1987,7 @@ const rule$c = {
2003
1987
  if (!isEslintComment && line !== prev.line && next.kind === graphql.TokenKind.NAME && linesAfter < 2) {
2004
1988
  context.report({
2005
1989
  messageId: HASHTAG_COMMENT,
2006
- loc: {
2007
- start: { line, column },
2008
- end: { line, column },
2009
- },
1990
+ loc: getLocation({ start: { line, column } }),
2010
1991
  });
2011
1992
  }
2012
1993
  }
@@ -2135,7 +2116,7 @@ const rule$e = {
2135
2116
  const typeName = node.name.value;
2136
2117
  if (!reachableTypes.has(typeName)) {
2137
2118
  context.report({
2138
- node,
2119
+ loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === graphql.Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
2139
2120
  messageId: UNREACHABLE_TYPE,
2140
2121
  data: { typeName },
2141
2122
  fix: fixer => fixer.remove(node),
@@ -2233,7 +2214,7 @@ const rule$f = {
2233
2214
  return;
2234
2215
  }
2235
2216
  context.report({
2236
- node,
2217
+ loc: getLocation(node.loc, fieldName),
2237
2218
  messageId: UNUSED_FIELD,
2238
2219
  data: { fieldName },
2239
2220
  fix(fixer) {
@@ -2388,10 +2369,10 @@ const rule$g = {
2388
2369
  ],
2389
2370
  },
2390
2371
  messages: {
2391
- [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date.',
2392
- [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY".',
2393
- [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date.',
2394
- [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',
2395
2376
  },
2396
2377
  schema: [
2397
2378
  {
@@ -2412,13 +2393,16 @@ const rule$g = {
2412
2393
  const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
2413
2394
  const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
2414
2395
  if (!deletionDateNode) {
2415
- 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
+ });
2416
2400
  return;
2417
2401
  }
2418
2402
  const deletionDate = valueFromNode(deletionDateNode.value);
2419
2403
  const isValidDate = DATE_REGEX.test(deletionDate);
2420
2404
  if (!isValidDate) {
2421
- context.report({ node: node.name, messageId: MESSAGE_INVALID_FORMAT });
2405
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
2422
2406
  return;
2423
2407
  }
2424
2408
  let [day, month, year] = deletionDate.split('/');
@@ -2427,7 +2411,7 @@ const rule$g = {
2427
2411
  const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
2428
2412
  if (Number.isNaN(deletionDateInMS)) {
2429
2413
  context.report({
2430
- node: node.name,
2414
+ node: deletionDateNode.value,
2431
2415
  messageId: MESSAGE_INVALID_DATE,
2432
2416
  data: {
2433
2417
  deletionDate,
@@ -2438,7 +2422,7 @@ const rule$g = {
2438
2422
  const canRemove = Date.now() > deletionDateInMS;
2439
2423
  if (canRemove) {
2440
2424
  context.report({
2441
- node: node.name,
2425
+ node,
2442
2426
  messageId: MESSAGE_CAN_BE_REMOVED,
2443
2427
  data: {
2444
2428
  nodeName: node.parent.name.value,
@@ -2489,17 +2473,15 @@ const rule$h = {
2489
2473
  },
2490
2474
  create(context) {
2491
2475
  return {
2492
- Directive(node) {
2493
- if (node && node.name && node.name.value === 'deprecated') {
2494
- const args = node.arguments || [];
2495
- const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2496
- const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2497
- if (!value) {
2498
- context.report({
2499
- node: node.name,
2500
- message: 'Directive "@deprecated" must have a reason!',
2501
- });
2502
- }
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
+ });
2503
2485
  }
2504
2486
  },
2505
2487
  };
@@ -2522,20 +2504,8 @@ const DESCRIBABLE_NODES = [
2522
2504
  function verifyRule(context, node) {
2523
2505
  if (node) {
2524
2506
  if (!node.description || !node.description.value || node.description.value.trim().length === 0) {
2525
- const { start, end } = ('name' in node ? node.name : node).loc;
2526
2507
  context.report({
2527
- loc: {
2528
- start: {
2529
- line: start.line,
2530
- column: start.column - 1,
2531
- },
2532
- end: {
2533
- line: end.line,
2534
- column:
2535
- // node.name don't exist on SchemaDefinition
2536
- 'name' in node ? end.column - 1 + node.name.value.length : end.column,
2537
- },
2538
- },
2508
+ loc: getLocation(('name' in node ? node.name : node).loc, 'name' in node ? node.name.value : 'schema'),
2539
2509
  messageId: REQUIRE_DESCRIPTION_ERROR,
2540
2510
  data: {
2541
2511
  nodeType: node.kind,
@@ -2656,18 +2626,22 @@ const rule$j = {
2656
2626
  if (!mutationType || !queryType) {
2657
2627
  return {};
2658
2628
  }
2659
- 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(' ');
2660
2635
  return {
2661
2636
  [selector](node) {
2662
- const rawNode = node.rawNode();
2663
- const typeName = getTypeName(rawNode);
2637
+ const typeName = node.name.value;
2664
2638
  const graphQLType = schema.getType(typeName);
2665
2639
  if (graphql.isObjectType(graphQLType)) {
2666
2640
  const { fields } = graphQLType.astNode;
2667
2641
  const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
2668
2642
  if (!hasQueryType) {
2669
2643
  context.report({
2670
- node,
2644
+ loc: getLocation(node.loc, typeName),
2671
2645
  message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
2672
2646
  });
2673
2647
  }
@@ -3006,7 +2980,7 @@ const rule$l = {
3006
2980
  getDocument: () => document,
3007
2981
  reportError: (error) => {
3008
2982
  context.report({
3009
- loc: error.locations[0],
2983
+ loc: getLocation({ start: error.locations[0] }),
3010
2984
  message: error.message,
3011
2985
  });
3012
2986
  },
@@ -3162,15 +3136,16 @@ const rule$m = {
3162
3136
  }
3163
3137
  return isValidIdName && isValidIdType;
3164
3138
  });
3139
+ const typeName = node.name.value;
3165
3140
  // Usually, there should be only one unique identifier field per type.
3166
3141
  // Some clients allow multiple fields to be used. If more people need this,
3167
3142
  // we can extend this rule later.
3168
3143
  if (validIds.length !== 1) {
3169
3144
  context.report({
3170
- node,
3171
- 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 }}`,
3172
3147
  data: {
3173
- nodeName: node.name.value,
3148
+ typeName,
3174
3149
  acceptedNamesString: options.acceptedIdNames.join(','),
3175
3150
  acceptedTypesString: options.acceptedIdTypes.join(','),
3176
3151
  },
@@ -3184,11 +3159,7 @@ const rule$m = {
3184
3159
  const RULE_NAME$3 = 'unique-fragment-name';
3185
3160
  const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
3186
3161
  const checkNode = (context, node, ruleName, messageId) => {
3187
- var _a;
3188
- const documentName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
3189
- if (!documentName) {
3190
- return;
3191
- }
3162
+ const documentName = node.name.value;
3192
3163
  const siblings = requireSiblingsOperations(ruleName, context);
3193
3164
  const siblingDocuments = node.kind === graphql.Kind.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName);
3194
3165
  const filepath = context.getFilename();
@@ -3199,7 +3170,6 @@ const checkNode = (context, node, ruleName, messageId) => {
3199
3170
  return isSameName && !isSamePath;
3200
3171
  });
3201
3172
  if (conflictingDocuments.length > 0) {
3202
- const { start, end } = node.name.loc;
3203
3173
  context.report({
3204
3174
  messageId,
3205
3175
  data: {
@@ -3208,16 +3178,7 @@ const checkNode = (context, node, ruleName, messageId) => {
3208
3178
  .map(f => `\t${path.relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
3209
3179
  .join('\n'),
3210
3180
  },
3211
- loc: {
3212
- start: {
3213
- line: start.line,
3214
- column: start.column - 1,
3215
- },
3216
- end: {
3217
- line: end.line,
3218
- column: end.column - 1,
3219
- },
3220
- },
3181
+ loc: getLocation(node.name.loc, documentName),
3221
3182
  });
3222
3183
  }
3223
3184
  };
@@ -3334,7 +3295,7 @@ const rule$o = {
3334
3295
  },
3335
3296
  create(context) {
3336
3297
  return {
3337
- OperationDefinition(node) {
3298
+ 'OperationDefinition[name!=undefined]'(node) {
3338
3299
  checkNode(context, node, RULE_NAME$4, UNIQUE_OPERATION_NAME);
3339
3300
  },
3340
3301
  };
@@ -3732,7 +3693,7 @@ function parseForESLint(code, options = {}) {
3732
3693
  noLocation: false,
3733
3694
  });
3734
3695
  const { rootTree, comments } = convertToESTree(graphqlAst.document, schema ? new graphql.TypeInfo(schema) : null);
3735
- const tokens = extractTokens(code);
3696
+ const tokens = extractTokens(new graphql.Source(code, filePath));
3736
3697
  return {
3737
3698
  services: parserServices,
3738
3699
  parserServices,
@@ -3764,22 +3725,110 @@ function parseForESLint(code, options = {}) {
3764
3725
  }
3765
3726
  }
3766
3727
 
3767
- class GraphQLRuleTester extends require('eslint').RuleTester {
3728
+ class GraphQLRuleTester extends eslint.RuleTester {
3768
3729
  constructor(parserOptions = {}) {
3769
- super({
3730
+ const config = {
3770
3731
  parser: require.resolve('@graphql-eslint/eslint-plugin'),
3771
3732
  parserOptions: {
3772
3733
  ...parserOptions,
3773
3734
  skipGraphQLConfig: true,
3774
3735
  },
3775
- });
3736
+ };
3737
+ super(config);
3738
+ this.config = config;
3776
3739
  }
3777
3740
  fromMockFile(path$1) {
3778
3741
  return fs.readFileSync(path.resolve(__dirname, `../tests/mocks/${path$1}`), 'utf-8');
3779
3742
  }
3780
3743
  runGraphQLTests(name, rule, tests) {
3781
- 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);
3763
+ // Skip snapshot testing if `expect` variable is not defined
3764
+ if (typeof expect === 'undefined') {
3765
+ return;
3766
+ }
3767
+ const linter = new eslint.Linter();
3768
+ linter.defineRule(name, rule);
3769
+ for (const testCase of tests.invalid) {
3770
+ const verifyConfig = getVerifyConfig(name, this.config, testCase);
3771
+ defineParser(linter, verifyConfig.parser);
3772
+ const { code, filename } = testCase;
3773
+ const messages = linter.verify(code, verifyConfig, { filename });
3774
+ for (const message of messages) {
3775
+ if (message.fatal) {
3776
+ throw new Error(message.message);
3777
+ }
3778
+ const messageForSnapshot = visualizeEslintMessage(code, message);
3779
+ // eslint-disable-next-line no-undef
3780
+ expect(messageForSnapshot).toMatchSnapshot();
3781
+ }
3782
+ }
3783
+ }
3784
+ }
3785
+ function getVerifyConfig(ruleId, testerConfig, testCase) {
3786
+ const { options, parserOptions, parser = testerConfig.parser } = testCase;
3787
+ return {
3788
+ ...testerConfig,
3789
+ parser,
3790
+ parserOptions: {
3791
+ ...testerConfig.parserOptions,
3792
+ ...parserOptions,
3793
+ },
3794
+ rules: {
3795
+ [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
3796
+ },
3797
+ };
3798
+ }
3799
+ const parsers = new WeakMap();
3800
+ function defineParser(linter, parser) {
3801
+ if (!parser) {
3802
+ return;
3803
+ }
3804
+ if (!parsers.has(linter)) {
3805
+ parsers.set(linter, new Set());
3782
3806
  }
3807
+ const defined = parsers.get(linter);
3808
+ if (!defined.has(parser)) {
3809
+ defined.add(parser);
3810
+ linter.defineParser(parser, require(parser));
3811
+ }
3812
+ }
3813
+ function visualizeEslintMessage(text, result) {
3814
+ const { line, column, endLine, endColumn, message } = result;
3815
+ const location = {
3816
+ start: {
3817
+ line,
3818
+ column,
3819
+ },
3820
+ };
3821
+ if (typeof endLine === 'number' && typeof endColumn === 'number') {
3822
+ location.end = {
3823
+ line: endLine,
3824
+ column: endColumn,
3825
+ };
3826
+ }
3827
+ return codeFrame.codeFrameColumns(text, location, {
3828
+ linesAbove: Number.POSITIVE_INFINITY,
3829
+ linesBelow: Number.POSITIVE_INFINITY,
3830
+ message,
3831
+ });
3783
3832
  }
3784
3833
 
3785
3834
  exports.GraphQLRuleTester = GraphQLRuleTester;