@graphql-eslint/eslint-plugin 2.4.0-alpha-60dfe26.0 → 3.0.0-alpha-5388f29.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
@@ -9,6 +9,8 @@ import depthLimit from 'graphql-depth-limit';
9
9
  import { parseCode } from '@graphql-tools/graphql-tag-pluck';
10
10
  import { loadConfigSync, GraphQLConfig } from 'graphql-config';
11
11
  import { CodeFileLoader } from '@graphql-tools/code-file-loader';
12
+ import { RuleTester, Linter } from 'eslint';
13
+ import { codeFrameColumns } from '@babel/code-frame';
12
14
 
13
15
  /*
14
16
  * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
@@ -27,7 +29,34 @@ const recommendedConfig = {
27
29
  '@graphql-eslint/known-type-names': 'error',
28
30
  '@graphql-eslint/lone-anonymous-operation': 'error',
29
31
  '@graphql-eslint/lone-schema-definition': 'error',
30
- '@graphql-eslint/naming-convention': 'error',
32
+ '@graphql-eslint/naming-convention': [
33
+ 'error',
34
+ {
35
+ types: 'PascalCase',
36
+ fields: 'camelCase',
37
+ overrides: {
38
+ EnumValueDefinition: 'UPPER_CASE',
39
+ OperationDefinition: {
40
+ style: 'PascalCase',
41
+ forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
42
+ forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
43
+ },
44
+ FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] },
45
+ 'FieldDefinition[parent.name.value=Query]': {
46
+ forbiddenPrefixes: ['query', 'get'],
47
+ forbiddenSuffixes: ['Query'],
48
+ },
49
+ 'FieldDefinition[parent.name.value=Mutation]': {
50
+ forbiddenPrefixes: ['mutation'],
51
+ forbiddenSuffixes: ['Mutation'],
52
+ },
53
+ 'FieldDefinition[parent.name.value=Subscription]': {
54
+ forbiddenPrefixes: ['subscription'],
55
+ forbiddenSuffixes: ['Subscription'],
56
+ },
57
+ },
58
+ },
59
+ ],
31
60
  '@graphql-eslint/no-anonymous-operations': 'error',
32
61
  '@graphql-eslint/no-case-insensitive-enum-values-duplicates': 'error',
33
62
  '@graphql-eslint/no-fragment-cycles': 'error',
@@ -203,9 +232,14 @@ const loaderCache = new Proxy(Object.create(null), {
203
232
  return true;
204
233
  },
205
234
  });
206
- const isObjectType = (node) => [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION].includes(node.type);
207
- const isQueryType = (node) => isObjectType(node) && node.name.value === 'Query';
208
- const isMutationType = (node) => isObjectType(node) && node.name.value === 'Mutation';
235
+ const TYPES_KINDS = [
236
+ Kind.OBJECT_TYPE_DEFINITION,
237
+ Kind.INTERFACE_TYPE_DEFINITION,
238
+ Kind.ENUM_TYPE_DEFINITION,
239
+ Kind.SCALAR_TYPE_DEFINITION,
240
+ Kind.INPUT_OBJECT_TYPE_DEFINITION,
241
+ Kind.UNION_TYPE_DEFINITION,
242
+ ];
209
243
  var CaseStyle;
210
244
  (function (CaseStyle) {
211
245
  CaseStyle["camelCase"] = "camelCase";
@@ -236,9 +270,27 @@ const convertCase = (style, str) => {
236
270
  return lowerCase(str).replace(/ /g, '-');
237
271
  }
238
272
  };
273
+ function getLocation(loc, fieldName = '', offset) {
274
+ const { start } = loc;
275
+ /*
276
+ * ESLint has 0-based column number
277
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
278
+ */
279
+ const { offsetStart = 1, offsetEnd = 1 } = offset !== null && offset !== void 0 ? offset : {};
280
+ return {
281
+ start: {
282
+ line: start.line,
283
+ column: start.column - offsetStart,
284
+ },
285
+ end: {
286
+ line: start.line,
287
+ column: start.column - offsetEnd + fieldName.length,
288
+ },
289
+ };
290
+ }
239
291
 
240
292
  function extractRuleName(stack) {
241
- const match = (stack || '').match(/validation[/\\\\]rules[/\\\\](.*?)\.js:/) || [];
293
+ const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
242
294
  return match[1] || null;
243
295
  }
244
296
  function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
@@ -249,7 +301,7 @@ function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName
249
301
  for (const error of validationErrors) {
250
302
  const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
251
303
  context.report({
252
- loc: error.locations[0],
304
+ loc: getLocation({ start: error.locations[0] }),
253
305
  message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
254
306
  });
255
307
  }
@@ -286,6 +338,7 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
286
338
  meta: {
287
339
  docs: {
288
340
  ...docs,
341
+ graphQLJSRuleName: ruleName,
289
342
  category: 'Validation',
290
343
  recommended: true,
291
344
  requiresSchema,
@@ -585,7 +638,7 @@ const rule = {
585
638
  ],
586
639
  },
587
640
  messages: {
588
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}".',
641
+ [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
589
642
  },
590
643
  schema: {
591
644
  type: 'array',
@@ -598,38 +651,48 @@ const rule = {
598
651
  properties: {
599
652
  fields: {
600
653
  type: 'array',
601
- contains: {
654
+ uniqueItems: true,
655
+ minItems: 1,
656
+ items: {
602
657
  enum: fieldsEnum,
603
658
  },
604
- description: 'Fields of `type`, `interface`, and `input`.',
659
+ description: 'Fields of `type`, `interface`, and `input`',
605
660
  },
606
661
  values: {
607
662
  type: 'array',
608
- contains: {
663
+ uniqueItems: true,
664
+ minItems: 1,
665
+ items: {
609
666
  enum: valuesEnum,
610
667
  },
611
- description: 'Values of `enum`.',
668
+ description: 'Values of `enum`',
612
669
  },
613
670
  selections: {
614
671
  type: 'array',
615
- contains: {
672
+ uniqueItems: true,
673
+ minItems: 1,
674
+ items: {
616
675
  enum: selectionsEnum,
617
676
  },
618
- description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`.',
677
+ description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
619
678
  },
620
679
  variables: {
621
680
  type: 'array',
622
- contains: {
681
+ uniqueItems: true,
682
+ minItems: 1,
683
+ items: {
623
684
  enum: variablesEnum,
624
685
  },
625
- description: 'Variables of operations (`query`, `mutation` and `subscription`).',
686
+ description: 'Variables of operations (`query`, `mutation` and `subscription`)',
626
687
  },
627
688
  arguments: {
628
689
  type: 'array',
629
- contains: {
690
+ uniqueItems: true,
691
+ minItems: 1,
692
+ items: {
630
693
  enum: argumentsEnum,
631
694
  },
632
- description: 'Arguments of fields and directives.',
695
+ description: 'Arguments of fields and directives',
633
696
  },
634
697
  },
635
698
  },
@@ -642,16 +705,9 @@ const rule = {
642
705
  for (const node of nodes) {
643
706
  const currName = node.name.value;
644
707
  if (prevName && prevName > currName) {
645
- const { start, end } = node.name.loc;
646
708
  const isVariableNode = node.kind === Kind.VARIABLE;
647
709
  context.report({
648
- loc: {
649
- start: {
650
- line: start.line,
651
- column: start.column - (isVariableNode ? 2 : 1),
652
- },
653
- end,
654
- },
710
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
655
711
  messageId: ALPHABETIZE,
656
712
  data: isVariableNode
657
713
  ? {
@@ -667,7 +723,7 @@ const rule = {
667
723
  const opts = context.options[0];
668
724
  const fields = new Set((_a = opts.fields) !== null && _a !== void 0 ? _a : []);
669
725
  const listeners = {};
670
- const fieldsSelector = [
726
+ const kinds = [
671
727
  fields.has(Kind.OBJECT_TYPE_DEFINITION) && [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION],
672
728
  fields.has(Kind.INTERFACE_TYPE_DEFINITION) && [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION],
673
729
  fields.has(Kind.INPUT_OBJECT_TYPE_DEFINITION) && [
@@ -675,8 +731,9 @@ const rule = {
675
731
  Kind.INPUT_OBJECT_TYPE_EXTENSION,
676
732
  ],
677
733
  ]
678
- .flat()
679
- .join(',');
734
+ .filter(Boolean)
735
+ .flat();
736
+ const fieldsSelector = kinds.join(',');
680
737
  const hasEnumValues = ((_b = opts.values) === null || _b === void 0 ? void 0 : _b[0]) === Kind.ENUM_TYPE_DEFINITION;
681
738
  const selectionsSelector = (_c = opts.selections) === null || _c === void 0 ? void 0 : _c.join(',');
682
739
  const hasVariables = ((_d = opts.variables) === null || _d === void 0 ? void 0 : _d[0]) === Kind.OPERATION_DEFINITION;
@@ -717,35 +774,22 @@ const rule = {
717
774
  };
718
775
 
719
776
  const AVOID_DUPLICATE_FIELDS = 'AVOID_DUPLICATE_FIELDS';
720
- const ensureUnique = () => {
721
- const set = new Set();
722
- return {
723
- add: (item, onError) => {
724
- if (set.has(item)) {
725
- onError();
726
- }
727
- else {
728
- set.add(item);
729
- }
730
- },
731
- };
732
- };
733
777
  const rule$1 = {
734
778
  meta: {
735
779
  type: 'suggestion',
736
780
  docs: {
737
- description: 'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
781
+ description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
738
782
  category: 'Stylistic Issues',
739
783
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
740
784
  examples: [
741
785
  {
742
786
  title: 'Incorrect',
743
787
  code: /* GraphQL */ `
744
- query getUserDetails {
788
+ query {
745
789
  user {
746
- name # first
790
+ name
747
791
  email
748
- name # second
792
+ name # duplicate field
749
793
  }
750
794
  }
751
795
  `,
@@ -753,7 +797,7 @@ const rule$1 = {
753
797
  {
754
798
  title: 'Incorrect',
755
799
  code: /* GraphQL */ `
756
- query getUsers {
800
+ query {
757
801
  users(
758
802
  first: 100
759
803
  skip: 50
@@ -768,9 +812,11 @@ const rule$1 = {
768
812
  {
769
813
  title: 'Incorrect',
770
814
  code: /* GraphQL */ `
771
- query getUsers($first: Int!, $first: Int!) {
772
- # Duplicate variable
773
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
815
+ query (
816
+ $first: Int!
817
+ $first: Int! # duplicate variable
818
+ ) {
819
+ users(first: $first, skip: 50) {
774
820
  id
775
821
  }
776
822
  }
@@ -779,58 +825,47 @@ const rule$1 = {
779
825
  ],
780
826
  },
781
827
  messages: {
782
- [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
828
+ [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
783
829
  },
784
830
  schema: [],
785
831
  },
786
832
  create(context) {
833
+ function checkNode(usedFields, fieldName, type, node) {
834
+ if (usedFields.has(fieldName)) {
835
+ context.report({
836
+ loc: getLocation((node.kind === Kind.FIELD && node.alias ? node.alias : node).loc, fieldName, {
837
+ offsetEnd: node.kind === Kind.VARIABLE_DEFINITION ? 0 : 1,
838
+ }),
839
+ messageId: AVOID_DUPLICATE_FIELDS,
840
+ data: {
841
+ type,
842
+ fieldName,
843
+ },
844
+ });
845
+ }
846
+ else {
847
+ usedFields.add(fieldName);
848
+ }
849
+ }
787
850
  return {
788
851
  OperationDefinition(node) {
789
- const uniqueCheck = ensureUnique();
790
- for (const arg of node.variableDefinitions || []) {
791
- uniqueCheck.add(arg.variable.name.value, () => {
792
- context.report({
793
- messageId: AVOID_DUPLICATE_FIELDS,
794
- data: {
795
- type: 'Operation variable',
796
- fieldName: arg.variable.name.value,
797
- },
798
- node: arg,
799
- });
800
- });
852
+ const set = new Set();
853
+ for (const varDef of node.variableDefinitions) {
854
+ checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
801
855
  }
802
856
  },
803
857
  Field(node) {
804
- const uniqueCheck = ensureUnique();
805
- for (const arg of node.arguments || []) {
806
- uniqueCheck.add(arg.name.value, () => {
807
- context.report({
808
- messageId: AVOID_DUPLICATE_FIELDS,
809
- data: {
810
- type: 'Field argument',
811
- fieldName: arg.name.value,
812
- },
813
- node: arg,
814
- });
815
- });
858
+ const set = new Set();
859
+ for (const arg of node.arguments) {
860
+ checkNode(set, arg.name.value, 'Field argument', arg);
816
861
  }
817
862
  },
818
863
  SelectionSet(node) {
819
864
  var _a;
820
- const uniqueCheck = ensureUnique();
821
- for (const selection of node.selections || []) {
865
+ const set = new Set();
866
+ for (const selection of node.selections) {
822
867
  if (selection.kind === Kind.FIELD) {
823
- const nameToCheck = ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value;
824
- uniqueCheck.add(nameToCheck, () => {
825
- context.report({
826
- messageId: AVOID_DUPLICATE_FIELDS,
827
- data: {
828
- type: 'Field',
829
- fieldName: nameToCheck,
830
- },
831
- node: selection,
832
- });
833
- });
868
+ checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
834
869
  }
835
870
  }
836
871
  },
@@ -961,16 +996,20 @@ const rule$3 = {
961
996
  if (!mutationType) {
962
997
  return {};
963
998
  }
964
- const selector = `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${Kind.FIELD_DEFINITION}`;
999
+ const selector = [
1000
+ `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
1001
+ '>',
1002
+ Kind.FIELD_DEFINITION,
1003
+ Kind.NAMED_TYPE,
1004
+ ].join(' ');
965
1005
  return {
966
1006
  [selector](node) {
967
- const rawNode = node.rawNode();
968
- const typeName = getTypeName(rawNode);
1007
+ const typeName = node.name.value;
969
1008
  const graphQLType = schema.getType(typeName);
970
1009
  if (isScalarType(graphQLType)) {
971
1010
  context.report({
972
- node,
973
- message: `Unexpected scalar result type "${typeName}".`,
1011
+ loc: getLocation(node.loc, typeName),
1012
+ message: `Unexpected scalar result type "${typeName}"`,
974
1013
  });
975
1014
  }
976
1015
  },
@@ -1019,23 +1058,13 @@ const rule$4 = {
1019
1058
  for (const field of node.fields) {
1020
1059
  const fieldName = field.name.value;
1021
1060
  if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
1022
- const { start } = field.loc;
1023
1061
  context.report({
1024
1062
  data: {
1025
1063
  fieldName,
1026
1064
  typeName,
1027
1065
  },
1028
1066
  messageId: AVOID_TYPENAME_PREFIX,
1029
- loc: {
1030
- start: {
1031
- line: start.line,
1032
- column: start.column - 1,
1033
- },
1034
- end: {
1035
- line: start.line,
1036
- column: start.column - 1 + lowerTypeName.length,
1037
- },
1038
- },
1067
+ loc: getLocation(field.loc, lowerTypeName),
1039
1068
  });
1040
1069
  }
1041
1070
  }
@@ -1095,7 +1124,7 @@ const rule$5 = {
1095
1124
  '[description.type="StringValue"]': node => {
1096
1125
  if (node.description.block !== (style === 'block')) {
1097
1126
  context.report({
1098
- node: node.description,
1127
+ loc: getLocation(node.description.loc),
1099
1128
  message: `Unexpected ${wrongDescriptionType} description`,
1100
1129
  });
1101
1130
  }
@@ -1104,6 +1133,9 @@ const rule$5 = {
1104
1133
  },
1105
1134
  };
1106
1135
 
1136
+ const isObjectType = (node) => [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION].includes(node.type);
1137
+ const isQueryType = (node) => isObjectType(node) && node.name.value === 'Query';
1138
+ const isMutationType = (node) => isObjectType(node) && node.name.value === 'Mutation';
1107
1139
  const rule$6 = {
1108
1140
  meta: {
1109
1141
  type: 'suggestion',
@@ -1144,6 +1176,7 @@ const rule$6 = {
1144
1176
  schema: [
1145
1177
  {
1146
1178
  type: 'object',
1179
+ additionalProperties: false,
1147
1180
  properties: {
1148
1181
  checkInputType: {
1149
1182
  type: 'boolean',
@@ -1166,35 +1199,34 @@ const rule$6 = {
1166
1199
  description: 'Apply the rule to Mutations',
1167
1200
  },
1168
1201
  },
1169
- additionalProperties: false,
1170
1202
  },
1171
1203
  ],
1172
1204
  },
1173
1205
  create(context) {
1174
- var _a;
1175
1206
  const options = {
1176
- caseSensitiveInputType: true,
1177
1207
  checkInputType: false,
1178
- checkMutations: true,
1208
+ caseSensitiveInputType: true,
1179
1209
  checkQueries: false,
1180
- ...(_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a[0],
1210
+ checkMutations: true,
1211
+ ...context.options[0],
1181
1212
  };
1182
1213
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1183
1214
  const listeners = {
1184
- 'FieldDefinition > InputValueDefinition': node => {
1185
- if (node.name.value !== 'input' && shouldCheckType(node.parent.parent)) {
1215
+ 'FieldDefinition > InputValueDefinition[name.value!=input]'(node) {
1216
+ if (shouldCheckType(node.parent.parent)) {
1217
+ const name = node.name.value;
1186
1218
  context.report({
1187
- node: node.name,
1188
- message: `Input "${node.name.value}" should be called "input"`,
1219
+ loc: getLocation(node.loc, name),
1220
+ message: `Input "${name}" should be called "input"`,
1189
1221
  });
1190
1222
  }
1191
1223
  },
1192
1224
  };
1193
- if (options === null || options === void 0 ? void 0 : options.checkInputType) {
1194
- listeners['FieldDefinition > InputValueDefinition NamedType'] = node => {
1225
+ if (options.checkInputType) {
1226
+ listeners['FieldDefinition > InputValueDefinition NamedType'] = (node) => {
1195
1227
  const findInputType = item => {
1196
1228
  let currentNode = item;
1197
- while (currentNode.type !== 'InputValueDefinition') {
1229
+ while (currentNode.type !== Kind.INPUT_VALUE_DEFINITION) {
1198
1230
  currentNode = currentNode.parent;
1199
1231
  }
1200
1232
  return currentNode;
@@ -1202,11 +1234,12 @@ const rule$6 = {
1202
1234
  const inputValueNode = findInputType(node);
1203
1235
  if (shouldCheckType(inputValueNode.parent.parent)) {
1204
1236
  const mutationName = `${inputValueNode.parent.name.value}Input`;
1237
+ const name = node.name.value;
1205
1238
  if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
1206
- node.name.value.toLowerCase() !== mutationName.toLowerCase()) {
1239
+ name.toLowerCase() !== mutationName.toLowerCase()) {
1207
1240
  context.report({
1208
- node,
1209
- message: `InputType "${node.name.value}" name should be "${mutationName}"`,
1241
+ loc: getLocation(node.loc, name),
1242
+ message: `InputType "${name}" name should be "${mutationName}"`,
1210
1243
  });
1211
1244
  }
1212
1245
  }
@@ -1363,7 +1396,8 @@ const rule$7 = {
1363
1396
  var _a;
1364
1397
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1365
1398
  context.report({
1366
- node: documentNode,
1399
+ // Report on first character
1400
+ loc: { column: 0, line: 1 },
1367
1401
  messageId: MATCH_EXTENSION,
1368
1402
  data: {
1369
1403
  fileExtension,
@@ -1395,7 +1429,8 @@ const rule$7 = {
1395
1429
  const filenameWithExtension = filename + expectedExtension;
1396
1430
  if (expectedFilename !== filenameWithExtension) {
1397
1431
  context.report({
1398
- node: documentNode,
1432
+ // Report on first character
1433
+ loc: { column: 0, line: 1 },
1399
1434
  messageId: MATCH_STYLE,
1400
1435
  data: {
1401
1436
  expectedFilename,
@@ -1408,69 +1443,40 @@ const rule$7 = {
1408
1443
  },
1409
1444
  };
1410
1445
 
1411
- const formats = {
1412
- camelCase: /^[a-z][^_]*$/g,
1413
- PascalCase: /^[A-Z][^_]*$/g,
1414
- snake_case: /^[a-z_][a-z0-9_]*$/g,
1415
- UPPER_CASE: /^[A-Z_][A-Z0-9_]*$/g,
1416
- };
1417
- const acceptedStyles = [
1418
- 'camelCase',
1419
- 'PascalCase',
1420
- 'snake_case',
1421
- 'UPPER_CASE',
1446
+ const FIELDS_KINDS = [
1447
+ Kind.FIELD_DEFINITION,
1448
+ Kind.INPUT_VALUE_DEFINITION,
1449
+ Kind.VARIABLE_DEFINITION,
1450
+ Kind.ARGUMENT,
1451
+ Kind.DIRECTIVE_DEFINITION,
1422
1452
  ];
1423
- function checkNameFormat(params) {
1424
- const { value, style, leadingUnderscore, trailingUnderscore, suffix, prefix, forbiddenPrefixes, forbiddenSuffixes, } = params;
1425
- let name = value;
1426
- if (leadingUnderscore === 'allow') {
1427
- [, name] = name.match(/^_*(.*)$/);
1428
- }
1429
- if (trailingUnderscore === 'allow') {
1430
- name = name.replace(/_*$/, '');
1431
- }
1432
- if (prefix && !name.startsWith(prefix)) {
1433
- return {
1434
- ok: false,
1435
- errorMessage: '{{nodeType}} name "{{nodeName}}" should have "{{prefix}}" prefix',
1436
- };
1437
- }
1438
- if (suffix && !name.endsWith(suffix)) {
1439
- return {
1440
- ok: false,
1441
- errorMessage: '{{nodeType}} name "{{nodeName}}" should have "{{suffix}}" suffix',
1442
- };
1443
- }
1444
- if (style && !acceptedStyles.includes(style)) {
1445
- return {
1446
- ok: false,
1447
- errorMessage: `{{nodeType}} name "{{nodeName}}" should be in one of the following options: ${acceptedStyles.join(',')}`,
1448
- };
1449
- }
1450
- if (forbiddenPrefixes.some(forbiddenPrefix => name.startsWith(forbiddenPrefix))) {
1451
- return {
1452
- ok: false,
1453
- errorMessage: '{{nodeType}} "{{nodeName}}" should not have one of the following prefix(es): {{forbiddenPrefixes}}',
1454
- };
1455
- }
1456
- if (forbiddenSuffixes.some(forbiddenSuffix => name.endsWith(forbiddenSuffix))) {
1457
- return {
1458
- ok: false,
1459
- errorMessage: '{{nodeType}} "{{nodeName}}" should not have one of the following suffix(es): {{forbiddenSuffixes}}',
1460
- };
1461
- }
1462
- if (!formats[style]) {
1463
- return { ok: true };
1464
- }
1465
- const ok = new RegExp(formats[style]).test(name);
1466
- if (ok) {
1467
- return { ok: true };
1468
- }
1469
- return {
1470
- ok: false,
1471
- errorMessage: '{{nodeType}} name "{{nodeName}}" should be in {{format}} format',
1472
- };
1473
- }
1453
+ const KindToDisplayName = {
1454
+ // types
1455
+ [Kind.OBJECT_TYPE_DEFINITION]: 'Type',
1456
+ [Kind.INTERFACE_TYPE_DEFINITION]: 'Interface',
1457
+ [Kind.ENUM_TYPE_DEFINITION]: 'Enumerator',
1458
+ [Kind.SCALAR_TYPE_DEFINITION]: 'Scalar',
1459
+ [Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'Input type',
1460
+ [Kind.UNION_TYPE_DEFINITION]: 'Union',
1461
+ // fields
1462
+ [Kind.FIELD_DEFINITION]: 'Field',
1463
+ [Kind.INPUT_VALUE_DEFINITION]: 'Input property',
1464
+ [Kind.VARIABLE_DEFINITION]: 'Variable',
1465
+ [Kind.ARGUMENT]: 'Argument',
1466
+ [Kind.DIRECTIVE_DEFINITION]: 'Directive',
1467
+ // rest
1468
+ [Kind.ENUM_VALUE_DEFINITION]: 'Enumeration value',
1469
+ [Kind.OPERATION_DEFINITION]: 'Operation',
1470
+ [Kind.FRAGMENT_DEFINITION]: 'Fragment',
1471
+ };
1472
+ const StyleToRegex = {
1473
+ camelCase: /^[a-z][\dA-Za-z]*$/,
1474
+ PascalCase: /^[A-Z][\dA-Za-z]*$/,
1475
+ snake_case: /^[a-z][\d_a-z]*[\da-z]$/,
1476
+ UPPER_CASE: /^[A-Z][\dA-Z_]*[\dA-Z]$/,
1477
+ };
1478
+ const ALLOWED_KINDS = Object.keys(KindToDisplayName).sort();
1479
+ const ALLOWED_STYLES = Object.keys(StyleToRegex);
1474
1480
  const schemaOption$1 = {
1475
1481
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1476
1482
  };
@@ -1485,89 +1491,120 @@ const rule$8 = {
1485
1491
  examples: [
1486
1492
  {
1487
1493
  title: 'Incorrect',
1488
- usage: [{ ObjectTypeDefinition: 'PascalCase' }],
1494
+ usage: [{ types: 'PascalCase', fields: 'camelCase' }],
1489
1495
  code: /* GraphQL */ `
1490
- type someTypeName {
1491
- f: String!
1496
+ type user {
1497
+ first_name: String!
1492
1498
  }
1493
1499
  `,
1494
1500
  },
1495
1501
  {
1496
1502
  title: 'Correct',
1497
- usage: [{ FieldDefinition: 'camelCase', ObjectTypeDefinition: 'PascalCase' }],
1503
+ usage: [{ types: 'PascalCase', fields: 'camelCase' }],
1498
1504
  code: /* GraphQL */ `
1499
- type SomeTypeName {
1500
- someFieldName: String
1505
+ type User {
1506
+ firstName: String
1501
1507
  }
1502
1508
  `,
1503
1509
  },
1504
1510
  ],
1511
+ optionsForConfig: [
1512
+ {
1513
+ types: 'PascalCase',
1514
+ fields: 'camelCase',
1515
+ overrides: {
1516
+ EnumValueDefinition: 'UPPER_CASE',
1517
+ OperationDefinition: {
1518
+ style: 'PascalCase',
1519
+ forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
1520
+ forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
1521
+ },
1522
+ FragmentDefinition: {
1523
+ style: 'PascalCase',
1524
+ forbiddenPrefixes: ['Fragment'],
1525
+ forbiddenSuffixes: ['Fragment'],
1526
+ },
1527
+ 'FieldDefinition[parent.name.value=Query]': {
1528
+ forbiddenPrefixes: ['query', 'get'],
1529
+ forbiddenSuffixes: ['Query'],
1530
+ },
1531
+ 'FieldDefinition[parent.name.value=Mutation]': {
1532
+ forbiddenPrefixes: ['mutation'],
1533
+ forbiddenSuffixes: ['Mutation'],
1534
+ },
1535
+ 'FieldDefinition[parent.name.value=Subscription]': {
1536
+ forbiddenPrefixes: ['subscription'],
1537
+ forbiddenSuffixes: ['Subscription'],
1538
+ },
1539
+ },
1540
+ },
1541
+ ],
1505
1542
  },
1506
1543
  schema: {
1507
1544
  definitions: {
1508
1545
  asString: {
1509
- type: 'string',
1510
- description: `One of: ${acceptedStyles.map(t => `\`${t}\``).join(', ')}`,
1511
- enum: acceptedStyles,
1546
+ enum: ALLOWED_STYLES,
1547
+ description: `One of: ${ALLOWED_STYLES.map(t => `\`${t}\``).join(', ')}`,
1512
1548
  },
1513
1549
  asObject: {
1514
1550
  type: 'object',
1551
+ additionalProperties: false,
1515
1552
  properties: {
1516
- style: {
1517
- type: 'string',
1518
- enum: acceptedStyles,
1519
- },
1520
- prefix: {
1521
- type: 'string',
1522
- },
1523
- suffix: {
1524
- type: 'string',
1525
- },
1553
+ style: { enum: ALLOWED_STYLES },
1554
+ prefix: { type: 'string' },
1555
+ suffix: { type: 'string' },
1526
1556
  forbiddenPrefixes: {
1527
- additionalItems: false,
1528
1557
  type: 'array',
1558
+ uniqueItems: true,
1529
1559
  minItems: 1,
1530
- items: {
1531
- type: 'string',
1532
- },
1560
+ items: { type: 'string' },
1533
1561
  },
1534
1562
  forbiddenSuffixes: {
1535
- additionalItems: false,
1536
1563
  type: 'array',
1564
+ uniqueItems: true,
1537
1565
  minItems: 1,
1538
- items: {
1539
- type: 'string',
1540
- },
1566
+ items: { type: 'string' },
1541
1567
  },
1542
1568
  },
1543
1569
  },
1544
1570
  },
1545
- $schema: 'http://json-schema.org/draft-04/schema#',
1546
1571
  type: 'array',
1572
+ maxItems: 1,
1547
1573
  items: {
1548
1574
  type: 'object',
1575
+ additionalProperties: false,
1549
1576
  properties: {
1550
- [Kind.FIELD_DEFINITION]: schemaOption$1,
1551
- [Kind.INPUT_OBJECT_TYPE_DEFINITION]: schemaOption$1,
1552
- [Kind.ENUM_VALUE_DEFINITION]: schemaOption$1,
1553
- [Kind.INPUT_VALUE_DEFINITION]: schemaOption$1,
1554
- [Kind.OBJECT_TYPE_DEFINITION]: schemaOption$1,
1555
- [Kind.INTERFACE_TYPE_DEFINITION]: schemaOption$1,
1556
- [Kind.ENUM_TYPE_DEFINITION]: schemaOption$1,
1557
- [Kind.UNION_TYPE_DEFINITION]: schemaOption$1,
1558
- [Kind.SCALAR_TYPE_DEFINITION]: schemaOption$1,
1559
- [Kind.OPERATION_DEFINITION]: schemaOption$1,
1560
- [Kind.FRAGMENT_DEFINITION]: schemaOption$1,
1561
- QueryDefinition: schemaOption$1,
1562
- leadingUnderscore: {
1563
- type: 'string',
1564
- enum: ['allow', 'forbid'],
1565
- default: 'forbid',
1577
+ types: {
1578
+ ...schemaOption$1,
1579
+ description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
1566
1580
  },
1567
- trailingUnderscore: {
1568
- type: 'string',
1569
- enum: ['allow', 'forbid'],
1570
- default: 'forbid',
1581
+ fields: {
1582
+ ...schemaOption$1,
1583
+ description: `Includes:\n\n${FIELDS_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
1584
+ },
1585
+ allowLeadingUnderscore: {
1586
+ type: 'boolean',
1587
+ default: false,
1588
+ },
1589
+ allowTrailingUnderscore: {
1590
+ type: 'boolean',
1591
+ default: false,
1592
+ },
1593
+ overrides: {
1594
+ type: 'object',
1595
+ additionalProperties: false,
1596
+ description: [
1597
+ 'May contain the following `ASTNode` names:',
1598
+ '',
1599
+ ...ALLOWED_KINDS.map(kind => `- \`${kind}\``),
1600
+ '',
1601
+ "> It's also possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts with `ASTNode` name",
1602
+ '>',
1603
+ '> Example: pattern property `FieldDefinition[parent.name.value=Query]` will match only fields for type `Query`',
1604
+ ].join('\n'),
1605
+ patternProperties: {
1606
+ [`^(${ALLOWED_KINDS.join('|')})(.+)?$`]: schemaOption$1,
1607
+ },
1571
1608
  },
1572
1609
  },
1573
1610
  },
@@ -1575,130 +1612,83 @@ const rule$8 = {
1575
1612
  },
1576
1613
  create(context) {
1577
1614
  const options = {
1578
- leadingUnderscore: 'forbid',
1579
- trailingUnderscore: 'forbid',
1580
- ...(context.options[0] || {}),
1615
+ overrides: {},
1616
+ ...context.options[0],
1581
1617
  };
1582
- const checkNode = (node, property, nodeType) => {
1583
- const { style, suffix = '', prefix = '', forbiddenPrefixes = [], forbiddenSuffixes = [] } = property;
1584
- const result = checkNameFormat({
1585
- value: node.value,
1586
- style,
1587
- leadingUnderscore: options.leadingUnderscore,
1588
- trailingUnderscore: options.trailingUnderscore,
1589
- prefix,
1590
- suffix,
1591
- forbiddenPrefixes,
1592
- forbiddenSuffixes,
1593
- });
1594
- if (result.ok === false) {
1618
+ function normalisePropertyOption(kind) {
1619
+ let style = options.overrides[kind];
1620
+ if (!style) {
1621
+ style = TYPES_KINDS.includes(kind) ? options.types : options.fields;
1622
+ }
1623
+ return typeof style === 'object' ? style : { style };
1624
+ }
1625
+ const checkNode = (selector) => (node) => {
1626
+ const { name } = node.kind === Kind.VARIABLE_DEFINITION ? node.variable : node;
1627
+ if (!name) {
1628
+ return;
1629
+ }
1630
+ const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
1631
+ const nodeType = KindToDisplayName[node.kind] || node.kind;
1632
+ const nodeName = name.value;
1633
+ const errorMessage = getErrorMessage();
1634
+ if (errorMessage) {
1595
1635
  context.report({
1596
- node,
1597
- message: result.errorMessage,
1598
- data: {
1599
- prefix,
1600
- suffix,
1601
- format: style,
1602
- forbiddenPrefixes: forbiddenPrefixes.join(', '),
1603
- forbiddenSuffixes: forbiddenSuffixes.join(', '),
1604
- nodeType,
1605
- nodeName: node.value,
1606
- },
1636
+ loc: getLocation(name.loc, name.value),
1637
+ message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1607
1638
  });
1608
1639
  }
1609
- };
1610
- const normalisePropertyOption = (value) => {
1611
- if (typeof value === 'object') {
1612
- return value;
1613
- }
1614
- return {
1615
- style: value,
1616
- prefix: '',
1617
- suffix: '',
1618
- };
1619
- };
1620
- return {
1621
- Name: node => {
1622
- if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
1623
- context.report({ node, message: 'Leading underscores are not allowed' });
1640
+ function getErrorMessage() {
1641
+ let name = nodeName;
1642
+ if (options.allowLeadingUnderscore) {
1643
+ name = name.replace(/^_*/, '');
1624
1644
  }
1625
- if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
1626
- context.report({ node, message: 'Trailing underscores are not allowed' });
1645
+ if (options.allowTrailingUnderscore) {
1646
+ name = name.replace(/_*$/, '');
1627
1647
  }
1628
- },
1629
- ObjectTypeDefinition: node => {
1630
- if (options.ObjectTypeDefinition) {
1631
- const property = normalisePropertyOption(options.ObjectTypeDefinition);
1632
- checkNode(node.name, property, 'Type');
1648
+ if (prefix && !name.startsWith(prefix)) {
1649
+ return `have "${prefix}" prefix`;
1633
1650
  }
1634
- },
1635
- InterfaceTypeDefinition: node => {
1636
- if (options.InterfaceTypeDefinition) {
1637
- const property = normalisePropertyOption(options.InterfaceTypeDefinition);
1638
- checkNode(node.name, property, 'Interface');
1651
+ if (suffix && !name.endsWith(suffix)) {
1652
+ return `have "${suffix}" suffix`;
1639
1653
  }
1640
- },
1641
- EnumTypeDefinition: node => {
1642
- if (options.EnumTypeDefinition) {
1643
- const property = normalisePropertyOption(options.EnumTypeDefinition);
1644
- checkNode(node.name, property, 'Enumerator');
1654
+ const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
1655
+ if (forbiddenPrefix) {
1656
+ return `not have "${forbiddenPrefix}" prefix`;
1645
1657
  }
1646
- },
1647
- InputObjectTypeDefinition: node => {
1648
- if (options.InputObjectTypeDefinition) {
1649
- const property = normalisePropertyOption(options.InputObjectTypeDefinition);
1650
- checkNode(node.name, property, 'Input type');
1658
+ const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
1659
+ if (forbiddenSuffix) {
1660
+ return `not have "${forbiddenSuffix}" suffix`;
1651
1661
  }
1652
- },
1653
- FieldDefinition: (node) => {
1654
- if (options.QueryDefinition && isQueryType(node.parent)) {
1655
- const property = normalisePropertyOption(options.QueryDefinition);
1656
- checkNode(node.name, property, 'Query');
1662
+ if (style && !ALLOWED_STYLES.includes(style)) {
1663
+ return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`;
1657
1664
  }
1658
- if (options.FieldDefinition && !isQueryType(node.parent)) {
1659
- const property = normalisePropertyOption(options.FieldDefinition);
1660
- checkNode(node.name, property, 'Field');
1661
- }
1662
- },
1663
- EnumValueDefinition: node => {
1664
- if (options.EnumValueDefinition) {
1665
- const property = normalisePropertyOption(options.EnumValueDefinition);
1666
- checkNode(node.name, property, 'Enumeration value');
1667
- }
1668
- },
1669
- InputValueDefinition: node => {
1670
- if (options.InputValueDefinition) {
1671
- const property = normalisePropertyOption(options.InputValueDefinition);
1672
- checkNode(node.name, property, 'Input property');
1665
+ const caseRegex = StyleToRegex[style];
1666
+ if (caseRegex && !caseRegex.test(name)) {
1667
+ return `be in ${style} format`;
1673
1668
  }
1674
- },
1675
- OperationDefinition: node => {
1676
- if (options.OperationDefinition) {
1677
- const property = normalisePropertyOption(options.OperationDefinition);
1678
- if (node.name) {
1679
- checkNode(node.name, property, 'Operation');
1680
- }
1681
- }
1682
- },
1683
- FragmentDefinition: node => {
1684
- if (options.FragmentDefinition) {
1685
- const property = normalisePropertyOption(options.FragmentDefinition);
1686
- checkNode(node.name, property, 'Fragment');
1687
- }
1688
- },
1689
- ScalarTypeDefinition: node => {
1690
- if (options.ScalarTypeDefinition) {
1691
- const property = normalisePropertyOption(options.ScalarTypeDefinition);
1692
- checkNode(node.name, property, 'Scalar');
1693
- }
1694
- },
1695
- UnionTypeDefinition: node => {
1696
- if (options.UnionTypeDefinition) {
1697
- const property = normalisePropertyOption(options.UnionTypeDefinition);
1698
- checkNode(node.name, property, 'Union');
1699
- }
1700
- },
1669
+ }
1670
+ };
1671
+ const checkUnderscore = (node) => {
1672
+ const name = node.value;
1673
+ context.report({
1674
+ loc: getLocation(node.loc, name),
1675
+ message: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`,
1676
+ });
1701
1677
  };
1678
+ const listeners = {};
1679
+ if (!options.allowLeadingUnderscore) {
1680
+ listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1681
+ }
1682
+ if (!options.allowTrailingUnderscore) {
1683
+ listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1684
+ }
1685
+ const selectors = new Set([options.types && TYPES_KINDS, options.fields && FIELDS_KINDS, Object.keys(options.overrides)]
1686
+ .flat()
1687
+ .filter(Boolean));
1688
+ for (const selector of selectors) {
1689
+ listeners[selector] = checkNode(selector);
1690
+ }
1691
+ return listeners;
1702
1692
  },
1703
1693
  };
1704
1694
 
@@ -1737,28 +1727,14 @@ const rule$9 = {
1737
1727
  },
1738
1728
  create(context) {
1739
1729
  return {
1740
- OperationDefinition(node) {
1741
- var _a;
1742
- const isAnonymous = (((_a = node.name) === null || _a === void 0 ? void 0 : _a.value) || '').length === 0;
1743
- if (isAnonymous) {
1744
- const { start } = node.loc;
1745
- context.report({
1746
- loc: {
1747
- start: {
1748
- column: start.column - 1,
1749
- line: start.line,
1750
- },
1751
- end: {
1752
- column: start.column - 1 + node.operation.length,
1753
- line: start.line,
1754
- },
1755
- },
1756
- data: {
1757
- operation: node.operation,
1758
- },
1759
- messageId: NO_ANONYMOUS_OPERATIONS,
1760
- });
1761
- }
1730
+ 'OperationDefinition[name=undefined]'(node) {
1731
+ context.report({
1732
+ loc: getLocation(node.loc, node.operation),
1733
+ data: {
1734
+ operation: node.operation,
1735
+ },
1736
+ messageId: NO_ANONYMOUS_OPERATIONS,
1737
+ });
1762
1738
  },
1763
1739
  };
1764
1740
  },
@@ -1865,8 +1841,8 @@ const rule$b = {
1865
1841
  mutation {
1866
1842
  changeSomething(
1867
1843
  type: OLD # This is deprecated, so you'll get an error
1868
- ) {
1869
- ...
1844
+ ) {
1845
+ ...
1870
1846
  }
1871
1847
  }
1872
1848
  `,
@@ -1903,9 +1879,10 @@ const rule$b = {
1903
1879
  requireGraphQLSchemaFromContext('no-deprecated', context);
1904
1880
  const typeInfo = node.typeInfo();
1905
1881
  if (typeInfo && typeInfo.enumValue) {
1906
- if (typeInfo.enumValue.deprecationReason) {
1882
+ if (typeInfo.enumValue.isDeprecated) {
1883
+ const enumValueName = node.value;
1907
1884
  context.report({
1908
- loc: node.loc,
1885
+ loc: getLocation(node.loc, enumValueName),
1909
1886
  messageId: NO_DEPRECATED,
1910
1887
  data: {
1911
1888
  type: 'enum value',
@@ -1919,9 +1896,10 @@ const rule$b = {
1919
1896
  requireGraphQLSchemaFromContext('no-deprecated', context);
1920
1897
  const typeInfo = node.typeInfo();
1921
1898
  if (typeInfo && typeInfo.fieldDef) {
1922
- if (typeInfo.fieldDef.deprecationReason) {
1899
+ if (typeInfo.fieldDef.isDeprecated) {
1900
+ const fieldName = node.name.value;
1923
1901
  context.report({
1924
- loc: node.loc,
1902
+ loc: getLocation(node.loc, fieldName),
1925
1903
  messageId: NO_DEPRECATED,
1926
1904
  data: {
1927
1905
  type: 'field',
@@ -1997,10 +1975,7 @@ const rule$c = {
1997
1975
  if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
1998
1976
  context.report({
1999
1977
  messageId: HASHTAG_COMMENT,
2000
- loc: {
2001
- start: { line, column },
2002
- end: { line, column },
2003
- },
1978
+ loc: getLocation({ start: { line, column } }),
2004
1979
  });
2005
1980
  }
2006
1981
  }
@@ -2129,7 +2104,7 @@ const rule$e = {
2129
2104
  const typeName = node.name.value;
2130
2105
  if (!reachableTypes.has(typeName)) {
2131
2106
  context.report({
2132
- node,
2107
+ loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
2133
2108
  messageId: UNREACHABLE_TYPE,
2134
2109
  data: { typeName },
2135
2110
  fix: fixer => fixer.remove(node),
@@ -2227,7 +2202,7 @@ const rule$f = {
2227
2202
  return;
2228
2203
  }
2229
2204
  context.report({
2230
- node,
2205
+ loc: getLocation(node.loc, fieldName),
2231
2206
  messageId: UNUSED_FIELD,
2232
2207
  data: { fieldName },
2233
2208
  fix(fixer) {
@@ -2339,6 +2314,7 @@ function convertDescription(node) {
2339
2314
  return [];
2340
2315
  }
2341
2316
 
2317
+ // eslint-disable-next-line unicorn/better-regex
2342
2318
  const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
2343
2319
  const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
2344
2320
  const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
@@ -2382,10 +2358,10 @@ const rule$g = {
2382
2358
  ],
2383
2359
  },
2384
2360
  messages: {
2385
- [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date.',
2386
- [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY".',
2387
- [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date.',
2388
- [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed.',
2361
+ [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
2362
+ [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
2363
+ [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
2364
+ [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
2389
2365
  },
2390
2366
  schema: [
2391
2367
  {
@@ -2406,13 +2382,16 @@ const rule$g = {
2406
2382
  const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
2407
2383
  const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
2408
2384
  if (!deletionDateNode) {
2409
- context.report({ node: node.name, messageId: MESSAGE_REQUIRE_DATE });
2385
+ context.report({
2386
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2387
+ messageId: MESSAGE_REQUIRE_DATE,
2388
+ });
2410
2389
  return;
2411
2390
  }
2412
2391
  const deletionDate = valueFromNode(deletionDateNode.value);
2413
2392
  const isValidDate = DATE_REGEX.test(deletionDate);
2414
2393
  if (!isValidDate) {
2415
- context.report({ node: node.name, messageId: MESSAGE_INVALID_FORMAT });
2394
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
2416
2395
  return;
2417
2396
  }
2418
2397
  let [day, month, year] = deletionDate.split('/');
@@ -2421,7 +2400,7 @@ const rule$g = {
2421
2400
  const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
2422
2401
  if (Number.isNaN(deletionDateInMS)) {
2423
2402
  context.report({
2424
- node: node.name,
2403
+ node: deletionDateNode.value,
2425
2404
  messageId: MESSAGE_INVALID_DATE,
2426
2405
  data: {
2427
2406
  deletionDate,
@@ -2432,7 +2411,7 @@ const rule$g = {
2432
2411
  const canRemove = Date.now() > deletionDateInMS;
2433
2412
  if (canRemove) {
2434
2413
  context.report({
2435
- node: node.name,
2414
+ node,
2436
2415
  messageId: MESSAGE_CAN_BE_REMOVED,
2437
2416
  data: {
2438
2417
  nodeName: node.parent.name.value,
@@ -2483,17 +2462,15 @@ const rule$h = {
2483
2462
  },
2484
2463
  create(context) {
2485
2464
  return {
2486
- Directive(node) {
2487
- if (node && node.name && node.name.value === 'deprecated') {
2488
- const args = node.arguments || [];
2489
- const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2490
- const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2491
- if (!value) {
2492
- context.report({
2493
- node: node.name,
2494
- message: 'Directive "@deprecated" must have a reason!',
2495
- });
2496
- }
2465
+ 'Directive[name.value=deprecated]'(node) {
2466
+ const args = node.arguments || [];
2467
+ const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2468
+ const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2469
+ if (!value) {
2470
+ context.report({
2471
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2472
+ message: 'Directive "@deprecated" must have a reason!',
2473
+ });
2497
2474
  }
2498
2475
  },
2499
2476
  };
@@ -2516,20 +2493,8 @@ const DESCRIBABLE_NODES = [
2516
2493
  function verifyRule(context, node) {
2517
2494
  if (node) {
2518
2495
  if (!node.description || !node.description.value || node.description.value.trim().length === 0) {
2519
- const { start, end } = ('name' in node ? node.name : node).loc;
2520
2496
  context.report({
2521
- loc: {
2522
- start: {
2523
- line: start.line,
2524
- column: start.column - 1,
2525
- },
2526
- end: {
2527
- line: end.line,
2528
- column:
2529
- // node.name don't exist on SchemaDefinition
2530
- 'name' in node ? end.column - 1 + node.name.value.length : end.column,
2531
- },
2532
- },
2497
+ loc: getLocation(('name' in node ? node.name : node).loc, 'name' in node ? node.name.value : 'schema'),
2533
2498
  messageId: REQUIRE_DESCRIPTION_ERROR,
2534
2499
  data: {
2535
2500
  nodeType: node.kind,
@@ -2547,7 +2512,7 @@ const rule$i = {
2547
2512
  examples: [
2548
2513
  {
2549
2514
  title: 'Incorrect',
2550
- usage: [{ on: [Kind.OBJECT_TYPE_DEFINITION, Kind.FIELD_DEFINITION] }],
2515
+ usage: [{ on: ['ObjectTypeDefinition', 'FieldDefinition'] }],
2551
2516
  code: /* GraphQL */ `
2552
2517
  type someTypeName {
2553
2518
  name: String
@@ -2556,7 +2521,7 @@ const rule$i = {
2556
2521
  },
2557
2522
  {
2558
2523
  title: 'Correct',
2559
- usage: [{ on: [Kind.OBJECT_TYPE_DEFINITION, Kind.FIELD_DEFINITION] }],
2524
+ usage: [{ on: ['ObjectTypeDefinition', 'FieldDefinition'] }],
2560
2525
  code: /* GraphQL */ `
2561
2526
  """
2562
2527
  Some type description
@@ -2650,18 +2615,22 @@ const rule$j = {
2650
2615
  if (!mutationType || !queryType) {
2651
2616
  return {};
2652
2617
  }
2653
- const selector = `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${Kind.FIELD_DEFINITION}`;
2618
+ const selector = [
2619
+ `:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2620
+ '>',
2621
+ Kind.FIELD_DEFINITION,
2622
+ Kind.NAMED_TYPE,
2623
+ ].join(' ');
2654
2624
  return {
2655
2625
  [selector](node) {
2656
- const rawNode = node.rawNode();
2657
- const typeName = getTypeName(rawNode);
2626
+ const typeName = node.name.value;
2658
2627
  const graphQLType = schema.getType(typeName);
2659
2628
  if (isObjectType$1(graphQLType)) {
2660
2629
  const { fields } = graphQLType.astNode;
2661
2630
  const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
2662
2631
  if (!hasQueryType) {
2663
2632
  context.report({
2664
- node,
2633
+ loc: getLocation(node.loc, typeName),
2665
2634
  message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
2666
2635
  });
2667
2636
  }
@@ -3000,7 +2969,7 @@ const rule$l = {
3000
2969
  getDocument: () => document,
3001
2970
  reportError: (error) => {
3002
2971
  context.report({
3003
- loc: error.locations[0],
2972
+ loc: getLocation({ start: error.locations[0] }),
3004
2973
  message: error.message,
3005
2974
  });
3006
2975
  },
@@ -3139,7 +3108,7 @@ const rule$m = {
3139
3108
  acceptedIdNames: ['id'],
3140
3109
  acceptedIdTypes: ['ID'],
3141
3110
  exceptions: {},
3142
- ...(context.options[0] || {}),
3111
+ ...context.options[0],
3143
3112
  };
3144
3113
  return {
3145
3114
  ObjectTypeDefinition(node) {
@@ -3156,15 +3125,16 @@ const rule$m = {
3156
3125
  }
3157
3126
  return isValidIdName && isValidIdType;
3158
3127
  });
3128
+ const typeName = node.name.value;
3159
3129
  // Usually, there should be only one unique identifier field per type.
3160
3130
  // Some clients allow multiple fields to be used. If more people need this,
3161
3131
  // we can extend this rule later.
3162
3132
  if (validIds.length !== 1) {
3163
3133
  context.report({
3164
- node,
3165
- message: '{{nodeName}} must have exactly one non-nullable unique identifier. Accepted name(s): {{acceptedNamesString}} ; Accepted type(s): {{acceptedTypesString}}',
3134
+ loc: getLocation(node.name.loc, typeName),
3135
+ message: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }} ; Accepted type(s): {{ acceptedTypesString }}`,
3166
3136
  data: {
3167
- nodeName: node.name.value,
3137
+ typeName,
3168
3138
  acceptedNamesString: options.acceptedIdNames.join(','),
3169
3139
  acceptedTypesString: options.acceptedIdTypes.join(','),
3170
3140
  },
@@ -3178,11 +3148,7 @@ const rule$m = {
3178
3148
  const RULE_NAME$3 = 'unique-fragment-name';
3179
3149
  const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
3180
3150
  const checkNode = (context, node, ruleName, messageId) => {
3181
- var _a;
3182
- const documentName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
3183
- if (!documentName) {
3184
- return;
3185
- }
3151
+ const documentName = node.name.value;
3186
3152
  const siblings = requireSiblingsOperations(ruleName, context);
3187
3153
  const siblingDocuments = node.kind === Kind.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName);
3188
3154
  const filepath = context.getFilename();
@@ -3193,7 +3159,6 @@ const checkNode = (context, node, ruleName, messageId) => {
3193
3159
  return isSameName && !isSamePath;
3194
3160
  });
3195
3161
  if (conflictingDocuments.length > 0) {
3196
- const { start, end } = node.name.loc;
3197
3162
  context.report({
3198
3163
  messageId,
3199
3164
  data: {
@@ -3202,16 +3167,7 @@ const checkNode = (context, node, ruleName, messageId) => {
3202
3167
  .map(f => `\t${relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
3203
3168
  .join('\n'),
3204
3169
  },
3205
- loc: {
3206
- start: {
3207
- line: start.line,
3208
- column: start.column - 1,
3209
- },
3210
- end: {
3211
- line: end.line,
3212
- column: end.column - 1,
3213
- },
3214
- },
3170
+ loc: getLocation(node.name.loc, documentName),
3215
3171
  });
3216
3172
  }
3217
3173
  };
@@ -3328,7 +3284,7 @@ const rule$o = {
3328
3284
  },
3329
3285
  create(context) {
3330
3286
  return {
3331
- OperationDefinition(node) {
3287
+ 'OperationDefinition[name!=undefined]'(node) {
3332
3288
  checkNode(context, node, RULE_NAME$4, UNIQUE_OPERATION_NAME);
3333
3289
  },
3334
3290
  };
@@ -3758,22 +3714,110 @@ function parseForESLint(code, options = {}) {
3758
3714
  }
3759
3715
  }
3760
3716
 
3761
- class GraphQLRuleTester extends require('eslint').RuleTester {
3717
+ class GraphQLRuleTester extends RuleTester {
3762
3718
  constructor(parserOptions = {}) {
3763
- super({
3719
+ const config = {
3764
3720
  parser: require.resolve('@graphql-eslint/eslint-plugin'),
3765
3721
  parserOptions: {
3766
3722
  ...parserOptions,
3767
3723
  skipGraphQLConfig: true,
3768
3724
  },
3769
- });
3725
+ };
3726
+ super(config);
3727
+ this.config = config;
3770
3728
  }
3771
3729
  fromMockFile(path) {
3772
3730
  return readFileSync(resolve(__dirname, `../tests/mocks/${path}`), 'utf-8');
3773
3731
  }
3774
3732
  runGraphQLTests(name, rule, tests) {
3775
- super.run(name, rule, tests);
3733
+ const ruleTests = Linter.version.startsWith('8')
3734
+ ? tests
3735
+ : {
3736
+ valid: tests.valid.map(test => {
3737
+ if (typeof test === 'string') {
3738
+ return test;
3739
+ }
3740
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3741
+ const { name, ...testCaseOptions } = test;
3742
+ return testCaseOptions;
3743
+ }),
3744
+ invalid: tests.invalid.map(test => {
3745
+ // ESLint 7 throws an error on CI - Unexpected top-level property "name"
3746
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3747
+ const { name, ...testCaseOptions } = test;
3748
+ return testCaseOptions;
3749
+ }),
3750
+ };
3751
+ super.run(name, rule, ruleTests);
3752
+ // Skip snapshot testing if `expect` variable is not defined
3753
+ if (typeof expect === 'undefined') {
3754
+ return;
3755
+ }
3756
+ const linter = new Linter();
3757
+ linter.defineRule(name, rule);
3758
+ for (const testCase of tests.invalid) {
3759
+ const verifyConfig = getVerifyConfig(name, this.config, testCase);
3760
+ defineParser(linter, verifyConfig.parser);
3761
+ const { code, filename } = testCase;
3762
+ const messages = linter.verify(code, verifyConfig, { filename });
3763
+ for (const message of messages) {
3764
+ if (message.fatal) {
3765
+ throw new Error(message.message);
3766
+ }
3767
+ const messageForSnapshot = visualizeEslintMessage(code, message);
3768
+ // eslint-disable-next-line no-undef
3769
+ expect(messageForSnapshot).toMatchSnapshot();
3770
+ }
3771
+ }
3776
3772
  }
3777
3773
  }
3774
+ function getVerifyConfig(ruleId, testerConfig, testCase) {
3775
+ const { options, parserOptions, parser = testerConfig.parser } = testCase;
3776
+ return {
3777
+ ...testerConfig,
3778
+ parser,
3779
+ parserOptions: {
3780
+ ...testerConfig.parserOptions,
3781
+ ...parserOptions,
3782
+ },
3783
+ rules: {
3784
+ [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
3785
+ },
3786
+ };
3787
+ }
3788
+ const parsers = new WeakMap();
3789
+ function defineParser(linter, parser) {
3790
+ if (!parser) {
3791
+ return;
3792
+ }
3793
+ if (!parsers.has(linter)) {
3794
+ parsers.set(linter, new Set());
3795
+ }
3796
+ const defined = parsers.get(linter);
3797
+ if (!defined.has(parser)) {
3798
+ defined.add(parser);
3799
+ linter.defineParser(parser, require(parser));
3800
+ }
3801
+ }
3802
+ function visualizeEslintMessage(text, result) {
3803
+ const { line, column, endLine, endColumn, message } = result;
3804
+ const location = {
3805
+ start: {
3806
+ line,
3807
+ column,
3808
+ },
3809
+ };
3810
+ if (typeof endLine === 'number' && typeof endColumn === 'number') {
3811
+ location.end = {
3812
+ line: endLine,
3813
+ column: endColumn,
3814
+ };
3815
+ }
3816
+ return codeFrameColumns(text, location, {
3817
+ linesAbove: Number.POSITIVE_INFINITY,
3818
+ linesBelow: Number.POSITIVE_INFINITY,
3819
+ message,
3820
+ });
3821
+ }
3778
3822
 
3779
3823
  export { GraphQLRuleTester, configs, convertDescription, convertLocation, convertRange, convertToESTree, extractCommentsFromAst, getBaseType, isNodeWithDescription, parse, parseForESLint, processors, rules, valueFromNode };