@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.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`
@@ -33,7 +35,34 @@ const recommendedConfig = {
33
35
  '@graphql-eslint/known-type-names': 'error',
34
36
  '@graphql-eslint/lone-anonymous-operation': 'error',
35
37
  '@graphql-eslint/lone-schema-definition': 'error',
36
- '@graphql-eslint/naming-convention': 'error',
38
+ '@graphql-eslint/naming-convention': [
39
+ 'error',
40
+ {
41
+ types: 'PascalCase',
42
+ fields: 'camelCase',
43
+ overrides: {
44
+ EnumValueDefinition: 'UPPER_CASE',
45
+ OperationDefinition: {
46
+ style: 'PascalCase',
47
+ forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
48
+ forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
49
+ },
50
+ FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] },
51
+ 'FieldDefinition[parent.name.value=Query]': {
52
+ forbiddenPrefixes: ['query', 'get'],
53
+ forbiddenSuffixes: ['Query'],
54
+ },
55
+ 'FieldDefinition[parent.name.value=Mutation]': {
56
+ forbiddenPrefixes: ['mutation'],
57
+ forbiddenSuffixes: ['Mutation'],
58
+ },
59
+ 'FieldDefinition[parent.name.value=Subscription]': {
60
+ forbiddenPrefixes: ['subscription'],
61
+ forbiddenSuffixes: ['Subscription'],
62
+ },
63
+ },
64
+ },
65
+ ],
37
66
  '@graphql-eslint/no-anonymous-operations': 'error',
38
67
  '@graphql-eslint/no-case-insensitive-enum-values-duplicates': 'error',
39
68
  '@graphql-eslint/no-fragment-cycles': 'error',
@@ -209,9 +238,14 @@ const loaderCache = new Proxy(Object.create(null), {
209
238
  return true;
210
239
  },
211
240
  });
212
- const isObjectType = (node) => [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.OBJECT_TYPE_EXTENSION].includes(node.type);
213
- const isQueryType = (node) => isObjectType(node) && node.name.value === 'Query';
214
- const isMutationType = (node) => isObjectType(node) && node.name.value === 'Mutation';
241
+ const TYPES_KINDS = [
242
+ graphql.Kind.OBJECT_TYPE_DEFINITION,
243
+ graphql.Kind.INTERFACE_TYPE_DEFINITION,
244
+ graphql.Kind.ENUM_TYPE_DEFINITION,
245
+ graphql.Kind.SCALAR_TYPE_DEFINITION,
246
+ graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION,
247
+ graphql.Kind.UNION_TYPE_DEFINITION,
248
+ ];
215
249
  var CaseStyle;
216
250
  (function (CaseStyle) {
217
251
  CaseStyle["camelCase"] = "camelCase";
@@ -242,9 +276,27 @@ const convertCase = (style, str) => {
242
276
  return lowerCase(str).replace(/ /g, '-');
243
277
  }
244
278
  };
279
+ function getLocation(loc, fieldName = '', offset) {
280
+ const { start } = loc;
281
+ /*
282
+ * ESLint has 0-based column number
283
+ * https://eslint.org/docs/developer-guide/working-with-rules#contextreport
284
+ */
285
+ const { offsetStart = 1, offsetEnd = 1 } = offset !== null && offset !== void 0 ? offset : {};
286
+ return {
287
+ start: {
288
+ line: start.line,
289
+ column: start.column - offsetStart,
290
+ },
291
+ end: {
292
+ line: start.line,
293
+ column: start.column - offsetEnd + fieldName.length,
294
+ },
295
+ };
296
+ }
245
297
 
246
298
  function extractRuleName(stack) {
247
- const match = (stack || '').match(/validation[/\\\\]rules[/\\\\](.*?)\.js:/) || [];
299
+ const match = (stack || '').match(/validation[/\\]rules[/\\](.*?)\.js:/) || [];
248
300
  return match[1] || null;
249
301
  }
250
302
  function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName = null) {
@@ -255,7 +307,7 @@ function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName
255
307
  for (const error of validationErrors) {
256
308
  const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
257
309
  context.report({
258
- loc: error.locations[0],
310
+ loc: getLocation({ start: error.locations[0] }),
259
311
  message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
260
312
  });
261
313
  }
@@ -292,6 +344,7 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
292
344
  meta: {
293
345
  docs: {
294
346
  ...docs,
347
+ graphQLJSRuleName: ruleName,
295
348
  category: 'Validation',
296
349
  recommended: true,
297
350
  requiresSchema,
@@ -591,7 +644,7 @@ const rule = {
591
644
  ],
592
645
  },
593
646
  messages: {
594
- [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}".',
647
+ [ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
595
648
  },
596
649
  schema: {
597
650
  type: 'array',
@@ -604,38 +657,48 @@ const rule = {
604
657
  properties: {
605
658
  fields: {
606
659
  type: 'array',
607
- contains: {
660
+ uniqueItems: true,
661
+ minItems: 1,
662
+ items: {
608
663
  enum: fieldsEnum,
609
664
  },
610
- description: 'Fields of `type`, `interface`, and `input`.',
665
+ description: 'Fields of `type`, `interface`, and `input`',
611
666
  },
612
667
  values: {
613
668
  type: 'array',
614
- contains: {
669
+ uniqueItems: true,
670
+ minItems: 1,
671
+ items: {
615
672
  enum: valuesEnum,
616
673
  },
617
- description: 'Values of `enum`.',
674
+ description: 'Values of `enum`',
618
675
  },
619
676
  selections: {
620
677
  type: 'array',
621
- contains: {
678
+ uniqueItems: true,
679
+ minItems: 1,
680
+ items: {
622
681
  enum: selectionsEnum,
623
682
  },
624
- description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`.',
683
+ description: 'Selections of operations (`query`, `mutation` and `subscription`) and `fragment`',
625
684
  },
626
685
  variables: {
627
686
  type: 'array',
628
- contains: {
687
+ uniqueItems: true,
688
+ minItems: 1,
689
+ items: {
629
690
  enum: variablesEnum,
630
691
  },
631
- description: 'Variables of operations (`query`, `mutation` and `subscription`).',
692
+ description: 'Variables of operations (`query`, `mutation` and `subscription`)',
632
693
  },
633
694
  arguments: {
634
695
  type: 'array',
635
- contains: {
696
+ uniqueItems: true,
697
+ minItems: 1,
698
+ items: {
636
699
  enum: argumentsEnum,
637
700
  },
638
- description: 'Arguments of fields and directives.',
701
+ description: 'Arguments of fields and directives',
639
702
  },
640
703
  },
641
704
  },
@@ -648,16 +711,9 @@ const rule = {
648
711
  for (const node of nodes) {
649
712
  const currName = node.name.value;
650
713
  if (prevName && prevName > currName) {
651
- const { start, end } = node.name.loc;
652
714
  const isVariableNode = node.kind === graphql.Kind.VARIABLE;
653
715
  context.report({
654
- loc: {
655
- start: {
656
- line: start.line,
657
- column: start.column - (isVariableNode ? 2 : 1),
658
- },
659
- end,
660
- },
716
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
661
717
  messageId: ALPHABETIZE,
662
718
  data: isVariableNode
663
719
  ? {
@@ -673,7 +729,7 @@ const rule = {
673
729
  const opts = context.options[0];
674
730
  const fields = new Set((_a = opts.fields) !== null && _a !== void 0 ? _a : []);
675
731
  const listeners = {};
676
- const fieldsSelector = [
732
+ const kinds = [
677
733
  fields.has(graphql.Kind.OBJECT_TYPE_DEFINITION) && [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.OBJECT_TYPE_EXTENSION],
678
734
  fields.has(graphql.Kind.INTERFACE_TYPE_DEFINITION) && [graphql.Kind.INTERFACE_TYPE_DEFINITION, graphql.Kind.INTERFACE_TYPE_EXTENSION],
679
735
  fields.has(graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION) && [
@@ -681,8 +737,9 @@ const rule = {
681
737
  graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION,
682
738
  ],
683
739
  ]
684
- .flat()
685
- .join(',');
740
+ .filter(Boolean)
741
+ .flat();
742
+ const fieldsSelector = kinds.join(',');
686
743
  const hasEnumValues = ((_b = opts.values) === null || _b === void 0 ? void 0 : _b[0]) === graphql.Kind.ENUM_TYPE_DEFINITION;
687
744
  const selectionsSelector = (_c = opts.selections) === null || _c === void 0 ? void 0 : _c.join(',');
688
745
  const hasVariables = ((_d = opts.variables) === null || _d === void 0 ? void 0 : _d[0]) === graphql.Kind.OPERATION_DEFINITION;
@@ -723,35 +780,22 @@ const rule = {
723
780
  };
724
781
 
725
782
  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
783
  const rule$1 = {
740
784
  meta: {
741
785
  type: 'suggestion',
742
786
  docs: {
743
- description: 'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
787
+ description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
744
788
  category: 'Stylistic Issues',
745
789
  url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
746
790
  examples: [
747
791
  {
748
792
  title: 'Incorrect',
749
793
  code: /* GraphQL */ `
750
- query getUserDetails {
794
+ query {
751
795
  user {
752
- name # first
796
+ name
753
797
  email
754
- name # second
798
+ name # duplicate field
755
799
  }
756
800
  }
757
801
  `,
@@ -759,7 +803,7 @@ const rule$1 = {
759
803
  {
760
804
  title: 'Incorrect',
761
805
  code: /* GraphQL */ `
762
- query getUsers {
806
+ query {
763
807
  users(
764
808
  first: 100
765
809
  skip: 50
@@ -774,9 +818,11 @@ const rule$1 = {
774
818
  {
775
819
  title: 'Incorrect',
776
820
  code: /* GraphQL */ `
777
- query getUsers($first: Int!, $first: Int!) {
778
- # Duplicate variable
779
- users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
821
+ query (
822
+ $first: Int!
823
+ $first: Int! # duplicate variable
824
+ ) {
825
+ users(first: $first, skip: 50) {
780
826
  id
781
827
  }
782
828
  }
@@ -785,58 +831,47 @@ const rule$1 = {
785
831
  ],
786
832
  },
787
833
  messages: {
788
- [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
834
+ [AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
789
835
  },
790
836
  schema: [],
791
837
  },
792
838
  create(context) {
839
+ function checkNode(usedFields, fieldName, type, node) {
840
+ if (usedFields.has(fieldName)) {
841
+ context.report({
842
+ loc: getLocation((node.kind === graphql.Kind.FIELD && node.alias ? node.alias : node).loc, fieldName, {
843
+ offsetEnd: node.kind === graphql.Kind.VARIABLE_DEFINITION ? 0 : 1,
844
+ }),
845
+ messageId: AVOID_DUPLICATE_FIELDS,
846
+ data: {
847
+ type,
848
+ fieldName,
849
+ },
850
+ });
851
+ }
852
+ else {
853
+ usedFields.add(fieldName);
854
+ }
855
+ }
793
856
  return {
794
857
  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
- });
858
+ const set = new Set();
859
+ for (const varDef of node.variableDefinitions) {
860
+ checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
807
861
  }
808
862
  },
809
863
  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
- });
864
+ const set = new Set();
865
+ for (const arg of node.arguments) {
866
+ checkNode(set, arg.name.value, 'Field argument', arg);
822
867
  }
823
868
  },
824
869
  SelectionSet(node) {
825
870
  var _a;
826
- const uniqueCheck = ensureUnique();
827
- for (const selection of node.selections || []) {
871
+ const set = new Set();
872
+ for (const selection of node.selections) {
828
873
  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
- });
874
+ checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
840
875
  }
841
876
  }
842
877
  },
@@ -967,16 +1002,20 @@ const rule$3 = {
967
1002
  if (!mutationType) {
968
1003
  return {};
969
1004
  }
970
- const selector = `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${graphql.Kind.FIELD_DEFINITION}`;
1005
+ const selector = [
1006
+ `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
1007
+ '>',
1008
+ graphql.Kind.FIELD_DEFINITION,
1009
+ graphql.Kind.NAMED_TYPE,
1010
+ ].join(' ');
971
1011
  return {
972
1012
  [selector](node) {
973
- const rawNode = node.rawNode();
974
- const typeName = getTypeName(rawNode);
1013
+ const typeName = node.name.value;
975
1014
  const graphQLType = schema.getType(typeName);
976
1015
  if (graphql.isScalarType(graphQLType)) {
977
1016
  context.report({
978
- node,
979
- message: `Unexpected scalar result type "${typeName}".`,
1017
+ loc: getLocation(node.loc, typeName),
1018
+ message: `Unexpected scalar result type "${typeName}"`,
980
1019
  });
981
1020
  }
982
1021
  },
@@ -1025,23 +1064,13 @@ const rule$4 = {
1025
1064
  for (const field of node.fields) {
1026
1065
  const fieldName = field.name.value;
1027
1066
  if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
1028
- const { start } = field.loc;
1029
1067
  context.report({
1030
1068
  data: {
1031
1069
  fieldName,
1032
1070
  typeName,
1033
1071
  },
1034
1072
  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
- },
1073
+ loc: getLocation(field.loc, lowerTypeName),
1045
1074
  });
1046
1075
  }
1047
1076
  }
@@ -1101,7 +1130,7 @@ const rule$5 = {
1101
1130
  '[description.type="StringValue"]': node => {
1102
1131
  if (node.description.block !== (style === 'block')) {
1103
1132
  context.report({
1104
- node: node.description,
1133
+ loc: getLocation(node.description.loc),
1105
1134
  message: `Unexpected ${wrongDescriptionType} description`,
1106
1135
  });
1107
1136
  }
@@ -1110,6 +1139,9 @@ const rule$5 = {
1110
1139
  },
1111
1140
  };
1112
1141
 
1142
+ const isObjectType = (node) => [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.OBJECT_TYPE_EXTENSION].includes(node.type);
1143
+ const isQueryType = (node) => isObjectType(node) && node.name.value === 'Query';
1144
+ const isMutationType = (node) => isObjectType(node) && node.name.value === 'Mutation';
1113
1145
  const rule$6 = {
1114
1146
  meta: {
1115
1147
  type: 'suggestion',
@@ -1150,6 +1182,7 @@ const rule$6 = {
1150
1182
  schema: [
1151
1183
  {
1152
1184
  type: 'object',
1185
+ additionalProperties: false,
1153
1186
  properties: {
1154
1187
  checkInputType: {
1155
1188
  type: 'boolean',
@@ -1172,35 +1205,34 @@ const rule$6 = {
1172
1205
  description: 'Apply the rule to Mutations',
1173
1206
  },
1174
1207
  },
1175
- additionalProperties: false,
1176
1208
  },
1177
1209
  ],
1178
1210
  },
1179
1211
  create(context) {
1180
- var _a;
1181
1212
  const options = {
1182
- caseSensitiveInputType: true,
1183
1213
  checkInputType: false,
1184
- checkMutations: true,
1214
+ caseSensitiveInputType: true,
1185
1215
  checkQueries: false,
1186
- ...(_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a[0],
1216
+ checkMutations: true,
1217
+ ...context.options[0],
1187
1218
  };
1188
1219
  const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
1189
1220
  const listeners = {
1190
- 'FieldDefinition > InputValueDefinition': node => {
1191
- if (node.name.value !== 'input' && shouldCheckType(node.parent.parent)) {
1221
+ 'FieldDefinition > InputValueDefinition[name.value!=input]'(node) {
1222
+ if (shouldCheckType(node.parent.parent)) {
1223
+ const name = node.name.value;
1192
1224
  context.report({
1193
- node: node.name,
1194
- message: `Input "${node.name.value}" should be called "input"`,
1225
+ loc: getLocation(node.loc, name),
1226
+ message: `Input "${name}" should be called "input"`,
1195
1227
  });
1196
1228
  }
1197
1229
  },
1198
1230
  };
1199
- if (options === null || options === void 0 ? void 0 : options.checkInputType) {
1200
- listeners['FieldDefinition > InputValueDefinition NamedType'] = node => {
1231
+ if (options.checkInputType) {
1232
+ listeners['FieldDefinition > InputValueDefinition NamedType'] = (node) => {
1201
1233
  const findInputType = item => {
1202
1234
  let currentNode = item;
1203
- while (currentNode.type !== 'InputValueDefinition') {
1235
+ while (currentNode.type !== graphql.Kind.INPUT_VALUE_DEFINITION) {
1204
1236
  currentNode = currentNode.parent;
1205
1237
  }
1206
1238
  return currentNode;
@@ -1208,11 +1240,12 @@ const rule$6 = {
1208
1240
  const inputValueNode = findInputType(node);
1209
1241
  if (shouldCheckType(inputValueNode.parent.parent)) {
1210
1242
  const mutationName = `${inputValueNode.parent.name.value}Input`;
1243
+ const name = node.name.value;
1211
1244
  if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
1212
- node.name.value.toLowerCase() !== mutationName.toLowerCase()) {
1245
+ name.toLowerCase() !== mutationName.toLowerCase()) {
1213
1246
  context.report({
1214
- node,
1215
- message: `InputType "${node.name.value}" name should be "${mutationName}"`,
1247
+ loc: getLocation(node.loc, name),
1248
+ message: `InputType "${name}" name should be "${mutationName}"`,
1216
1249
  });
1217
1250
  }
1218
1251
  }
@@ -1369,7 +1402,8 @@ const rule$7 = {
1369
1402
  var _a;
1370
1403
  if (options.fileExtension && options.fileExtension !== fileExtension) {
1371
1404
  context.report({
1372
- node: documentNode,
1405
+ // Report on first character
1406
+ loc: { column: 0, line: 1 },
1373
1407
  messageId: MATCH_EXTENSION,
1374
1408
  data: {
1375
1409
  fileExtension,
@@ -1401,7 +1435,8 @@ const rule$7 = {
1401
1435
  const filenameWithExtension = filename + expectedExtension;
1402
1436
  if (expectedFilename !== filenameWithExtension) {
1403
1437
  context.report({
1404
- node: documentNode,
1438
+ // Report on first character
1439
+ loc: { column: 0, line: 1 },
1405
1440
  messageId: MATCH_STYLE,
1406
1441
  data: {
1407
1442
  expectedFilename,
@@ -1414,69 +1449,40 @@ const rule$7 = {
1414
1449
  },
1415
1450
  };
1416
1451
 
1417
- const formats = {
1418
- camelCase: /^[a-z][^_]*$/g,
1419
- PascalCase: /^[A-Z][^_]*$/g,
1420
- snake_case: /^[a-z_][a-z0-9_]*$/g,
1421
- UPPER_CASE: /^[A-Z_][A-Z0-9_]*$/g,
1422
- };
1423
- const acceptedStyles = [
1424
- 'camelCase',
1425
- 'PascalCase',
1426
- 'snake_case',
1427
- 'UPPER_CASE',
1452
+ const FIELDS_KINDS = [
1453
+ graphql.Kind.FIELD_DEFINITION,
1454
+ graphql.Kind.INPUT_VALUE_DEFINITION,
1455
+ graphql.Kind.VARIABLE_DEFINITION,
1456
+ graphql.Kind.ARGUMENT,
1457
+ graphql.Kind.DIRECTIVE_DEFINITION,
1428
1458
  ];
1429
- function checkNameFormat(params) {
1430
- const { value, style, leadingUnderscore, trailingUnderscore, suffix, prefix, forbiddenPrefixes, forbiddenSuffixes, } = params;
1431
- let name = value;
1432
- if (leadingUnderscore === 'allow') {
1433
- [, name] = name.match(/^_*(.*)$/);
1434
- }
1435
- if (trailingUnderscore === 'allow') {
1436
- name = name.replace(/_*$/, '');
1437
- }
1438
- if (prefix && !name.startsWith(prefix)) {
1439
- return {
1440
- ok: false,
1441
- errorMessage: '{{nodeType}} name "{{nodeName}}" should have "{{prefix}}" prefix',
1442
- };
1443
- }
1444
- if (suffix && !name.endsWith(suffix)) {
1445
- return {
1446
- ok: false,
1447
- errorMessage: '{{nodeType}} name "{{nodeName}}" should have "{{suffix}}" suffix',
1448
- };
1449
- }
1450
- if (style && !acceptedStyles.includes(style)) {
1451
- return {
1452
- ok: false,
1453
- errorMessage: `{{nodeType}} name "{{nodeName}}" should be in one of the following options: ${acceptedStyles.join(',')}`,
1454
- };
1455
- }
1456
- if (forbiddenPrefixes.some(forbiddenPrefix => name.startsWith(forbiddenPrefix))) {
1457
- return {
1458
- ok: false,
1459
- errorMessage: '{{nodeType}} "{{nodeName}}" should not have one of the following prefix(es): {{forbiddenPrefixes}}',
1460
- };
1461
- }
1462
- if (forbiddenSuffixes.some(forbiddenSuffix => name.endsWith(forbiddenSuffix))) {
1463
- return {
1464
- ok: false,
1465
- errorMessage: '{{nodeType}} "{{nodeName}}" should not have one of the following suffix(es): {{forbiddenSuffixes}}',
1466
- };
1467
- }
1468
- if (!formats[style]) {
1469
- return { ok: true };
1470
- }
1471
- const ok = new RegExp(formats[style]).test(name);
1472
- if (ok) {
1473
- return { ok: true };
1474
- }
1475
- return {
1476
- ok: false,
1477
- errorMessage: '{{nodeType}} name "{{nodeName}}" should be in {{format}} format',
1478
- };
1479
- }
1459
+ const KindToDisplayName = {
1460
+ // types
1461
+ [graphql.Kind.OBJECT_TYPE_DEFINITION]: 'Type',
1462
+ [graphql.Kind.INTERFACE_TYPE_DEFINITION]: 'Interface',
1463
+ [graphql.Kind.ENUM_TYPE_DEFINITION]: 'Enumerator',
1464
+ [graphql.Kind.SCALAR_TYPE_DEFINITION]: 'Scalar',
1465
+ [graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'Input type',
1466
+ [graphql.Kind.UNION_TYPE_DEFINITION]: 'Union',
1467
+ // fields
1468
+ [graphql.Kind.FIELD_DEFINITION]: 'Field',
1469
+ [graphql.Kind.INPUT_VALUE_DEFINITION]: 'Input property',
1470
+ [graphql.Kind.VARIABLE_DEFINITION]: 'Variable',
1471
+ [graphql.Kind.ARGUMENT]: 'Argument',
1472
+ [graphql.Kind.DIRECTIVE_DEFINITION]: 'Directive',
1473
+ // rest
1474
+ [graphql.Kind.ENUM_VALUE_DEFINITION]: 'Enumeration value',
1475
+ [graphql.Kind.OPERATION_DEFINITION]: 'Operation',
1476
+ [graphql.Kind.FRAGMENT_DEFINITION]: 'Fragment',
1477
+ };
1478
+ const StyleToRegex = {
1479
+ camelCase: /^[a-z][\dA-Za-z]*$/,
1480
+ PascalCase: /^[A-Z][\dA-Za-z]*$/,
1481
+ snake_case: /^[a-z][\d_a-z]*[\da-z]$/,
1482
+ UPPER_CASE: /^[A-Z][\dA-Z_]*[\dA-Z]$/,
1483
+ };
1484
+ const ALLOWED_KINDS = Object.keys(KindToDisplayName).sort();
1485
+ const ALLOWED_STYLES = Object.keys(StyleToRegex);
1480
1486
  const schemaOption$1 = {
1481
1487
  oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
1482
1488
  };
@@ -1491,89 +1497,120 @@ const rule$8 = {
1491
1497
  examples: [
1492
1498
  {
1493
1499
  title: 'Incorrect',
1494
- usage: [{ ObjectTypeDefinition: 'PascalCase' }],
1500
+ usage: [{ types: 'PascalCase', fields: 'camelCase' }],
1495
1501
  code: /* GraphQL */ `
1496
- type someTypeName {
1497
- f: String!
1502
+ type user {
1503
+ first_name: String!
1498
1504
  }
1499
1505
  `,
1500
1506
  },
1501
1507
  {
1502
1508
  title: 'Correct',
1503
- usage: [{ FieldDefinition: 'camelCase', ObjectTypeDefinition: 'PascalCase' }],
1509
+ usage: [{ types: 'PascalCase', fields: 'camelCase' }],
1504
1510
  code: /* GraphQL */ `
1505
- type SomeTypeName {
1506
- someFieldName: String
1511
+ type User {
1512
+ firstName: String
1507
1513
  }
1508
1514
  `,
1509
1515
  },
1510
1516
  ],
1517
+ optionsForConfig: [
1518
+ {
1519
+ types: 'PascalCase',
1520
+ fields: 'camelCase',
1521
+ overrides: {
1522
+ EnumValueDefinition: 'UPPER_CASE',
1523
+ OperationDefinition: {
1524
+ style: 'PascalCase',
1525
+ forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
1526
+ forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
1527
+ },
1528
+ FragmentDefinition: {
1529
+ style: 'PascalCase',
1530
+ forbiddenPrefixes: ['Fragment'],
1531
+ forbiddenSuffixes: ['Fragment'],
1532
+ },
1533
+ 'FieldDefinition[parent.name.value=Query]': {
1534
+ forbiddenPrefixes: ['query', 'get'],
1535
+ forbiddenSuffixes: ['Query'],
1536
+ },
1537
+ 'FieldDefinition[parent.name.value=Mutation]': {
1538
+ forbiddenPrefixes: ['mutation'],
1539
+ forbiddenSuffixes: ['Mutation'],
1540
+ },
1541
+ 'FieldDefinition[parent.name.value=Subscription]': {
1542
+ forbiddenPrefixes: ['subscription'],
1543
+ forbiddenSuffixes: ['Subscription'],
1544
+ },
1545
+ },
1546
+ },
1547
+ ],
1511
1548
  },
1512
1549
  schema: {
1513
1550
  definitions: {
1514
1551
  asString: {
1515
- type: 'string',
1516
- description: `One of: ${acceptedStyles.map(t => `\`${t}\``).join(', ')}`,
1517
- enum: acceptedStyles,
1552
+ enum: ALLOWED_STYLES,
1553
+ description: `One of: ${ALLOWED_STYLES.map(t => `\`${t}\``).join(', ')}`,
1518
1554
  },
1519
1555
  asObject: {
1520
1556
  type: 'object',
1557
+ additionalProperties: false,
1521
1558
  properties: {
1522
- style: {
1523
- type: 'string',
1524
- enum: acceptedStyles,
1525
- },
1526
- prefix: {
1527
- type: 'string',
1528
- },
1529
- suffix: {
1530
- type: 'string',
1531
- },
1559
+ style: { enum: ALLOWED_STYLES },
1560
+ prefix: { type: 'string' },
1561
+ suffix: { type: 'string' },
1532
1562
  forbiddenPrefixes: {
1533
- additionalItems: false,
1534
1563
  type: 'array',
1564
+ uniqueItems: true,
1535
1565
  minItems: 1,
1536
- items: {
1537
- type: 'string',
1538
- },
1566
+ items: { type: 'string' },
1539
1567
  },
1540
1568
  forbiddenSuffixes: {
1541
- additionalItems: false,
1542
1569
  type: 'array',
1570
+ uniqueItems: true,
1543
1571
  minItems: 1,
1544
- items: {
1545
- type: 'string',
1546
- },
1572
+ items: { type: 'string' },
1547
1573
  },
1548
1574
  },
1549
1575
  },
1550
1576
  },
1551
- $schema: 'http://json-schema.org/draft-04/schema#',
1552
1577
  type: 'array',
1578
+ maxItems: 1,
1553
1579
  items: {
1554
1580
  type: 'object',
1581
+ additionalProperties: false,
1555
1582
  properties: {
1556
- [graphql.Kind.FIELD_DEFINITION]: schemaOption$1,
1557
- [graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION]: schemaOption$1,
1558
- [graphql.Kind.ENUM_VALUE_DEFINITION]: schemaOption$1,
1559
- [graphql.Kind.INPUT_VALUE_DEFINITION]: schemaOption$1,
1560
- [graphql.Kind.OBJECT_TYPE_DEFINITION]: schemaOption$1,
1561
- [graphql.Kind.INTERFACE_TYPE_DEFINITION]: schemaOption$1,
1562
- [graphql.Kind.ENUM_TYPE_DEFINITION]: schemaOption$1,
1563
- [graphql.Kind.UNION_TYPE_DEFINITION]: schemaOption$1,
1564
- [graphql.Kind.SCALAR_TYPE_DEFINITION]: schemaOption$1,
1565
- [graphql.Kind.OPERATION_DEFINITION]: schemaOption$1,
1566
- [graphql.Kind.FRAGMENT_DEFINITION]: schemaOption$1,
1567
- QueryDefinition: schemaOption$1,
1568
- leadingUnderscore: {
1569
- type: 'string',
1570
- enum: ['allow', 'forbid'],
1571
- default: 'forbid',
1583
+ types: {
1584
+ ...schemaOption$1,
1585
+ description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
1572
1586
  },
1573
- trailingUnderscore: {
1574
- type: 'string',
1575
- enum: ['allow', 'forbid'],
1576
- default: 'forbid',
1587
+ fields: {
1588
+ ...schemaOption$1,
1589
+ description: `Includes:\n\n${FIELDS_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
1590
+ },
1591
+ allowLeadingUnderscore: {
1592
+ type: 'boolean',
1593
+ default: false,
1594
+ },
1595
+ allowTrailingUnderscore: {
1596
+ type: 'boolean',
1597
+ default: false,
1598
+ },
1599
+ overrides: {
1600
+ type: 'object',
1601
+ additionalProperties: false,
1602
+ description: [
1603
+ 'May contain the following `ASTNode` names:',
1604
+ '',
1605
+ ...ALLOWED_KINDS.map(kind => `- \`${kind}\``),
1606
+ '',
1607
+ "> It's also possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts with `ASTNode` name",
1608
+ '>',
1609
+ '> Example: pattern property `FieldDefinition[parent.name.value=Query]` will match only fields for type `Query`',
1610
+ ].join('\n'),
1611
+ patternProperties: {
1612
+ [`^(${ALLOWED_KINDS.join('|')})(.+)?$`]: schemaOption$1,
1613
+ },
1577
1614
  },
1578
1615
  },
1579
1616
  },
@@ -1581,130 +1618,83 @@ const rule$8 = {
1581
1618
  },
1582
1619
  create(context) {
1583
1620
  const options = {
1584
- leadingUnderscore: 'forbid',
1585
- trailingUnderscore: 'forbid',
1586
- ...(context.options[0] || {}),
1621
+ overrides: {},
1622
+ ...context.options[0],
1587
1623
  };
1588
- const checkNode = (node, property, nodeType) => {
1589
- const { style, suffix = '', prefix = '', forbiddenPrefixes = [], forbiddenSuffixes = [] } = property;
1590
- const result = checkNameFormat({
1591
- value: node.value,
1592
- style,
1593
- leadingUnderscore: options.leadingUnderscore,
1594
- trailingUnderscore: options.trailingUnderscore,
1595
- prefix,
1596
- suffix,
1597
- forbiddenPrefixes,
1598
- forbiddenSuffixes,
1599
- });
1600
- if (result.ok === false) {
1624
+ function normalisePropertyOption(kind) {
1625
+ let style = options.overrides[kind];
1626
+ if (!style) {
1627
+ style = TYPES_KINDS.includes(kind) ? options.types : options.fields;
1628
+ }
1629
+ return typeof style === 'object' ? style : { style };
1630
+ }
1631
+ const checkNode = (selector) => (node) => {
1632
+ const { name } = node.kind === graphql.Kind.VARIABLE_DEFINITION ? node.variable : node;
1633
+ if (!name) {
1634
+ return;
1635
+ }
1636
+ const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector);
1637
+ const nodeType = KindToDisplayName[node.kind] || node.kind;
1638
+ const nodeName = name.value;
1639
+ const errorMessage = getErrorMessage();
1640
+ if (errorMessage) {
1601
1641
  context.report({
1602
- node,
1603
- message: result.errorMessage,
1604
- data: {
1605
- prefix,
1606
- suffix,
1607
- format: style,
1608
- forbiddenPrefixes: forbiddenPrefixes.join(', '),
1609
- forbiddenSuffixes: forbiddenSuffixes.join(', '),
1610
- nodeType,
1611
- nodeName: node.value,
1612
- },
1642
+ loc: getLocation(name.loc, name.value),
1643
+ message: `${nodeType} "${nodeName}" should ${errorMessage}`,
1613
1644
  });
1614
1645
  }
1615
- };
1616
- const normalisePropertyOption = (value) => {
1617
- if (typeof value === 'object') {
1618
- return value;
1619
- }
1620
- return {
1621
- style: value,
1622
- prefix: '',
1623
- suffix: '',
1624
- };
1625
- };
1626
- return {
1627
- Name: node => {
1628
- if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
1629
- context.report({ node, message: 'Leading underscores are not allowed' });
1646
+ function getErrorMessage() {
1647
+ let name = nodeName;
1648
+ if (options.allowLeadingUnderscore) {
1649
+ name = name.replace(/^_*/, '');
1630
1650
  }
1631
- if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
1632
- context.report({ node, message: 'Trailing underscores are not allowed' });
1651
+ if (options.allowTrailingUnderscore) {
1652
+ name = name.replace(/_*$/, '');
1633
1653
  }
1634
- },
1635
- ObjectTypeDefinition: node => {
1636
- if (options.ObjectTypeDefinition) {
1637
- const property = normalisePropertyOption(options.ObjectTypeDefinition);
1638
- checkNode(node.name, property, 'Type');
1654
+ if (prefix && !name.startsWith(prefix)) {
1655
+ return `have "${prefix}" prefix`;
1639
1656
  }
1640
- },
1641
- InterfaceTypeDefinition: node => {
1642
- if (options.InterfaceTypeDefinition) {
1643
- const property = normalisePropertyOption(options.InterfaceTypeDefinition);
1644
- checkNode(node.name, property, 'Interface');
1657
+ if (suffix && !name.endsWith(suffix)) {
1658
+ return `have "${suffix}" suffix`;
1645
1659
  }
1646
- },
1647
- EnumTypeDefinition: node => {
1648
- if (options.EnumTypeDefinition) {
1649
- const property = normalisePropertyOption(options.EnumTypeDefinition);
1650
- checkNode(node.name, property, 'Enumerator');
1660
+ const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
1661
+ if (forbiddenPrefix) {
1662
+ return `not have "${forbiddenPrefix}" prefix`;
1651
1663
  }
1652
- },
1653
- InputObjectTypeDefinition: node => {
1654
- if (options.InputObjectTypeDefinition) {
1655
- const property = normalisePropertyOption(options.InputObjectTypeDefinition);
1656
- checkNode(node.name, property, 'Input type');
1664
+ const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
1665
+ if (forbiddenSuffix) {
1666
+ return `not have "${forbiddenSuffix}" suffix`;
1657
1667
  }
1658
- },
1659
- FieldDefinition: (node) => {
1660
- if (options.QueryDefinition && isQueryType(node.parent)) {
1661
- const property = normalisePropertyOption(options.QueryDefinition);
1662
- checkNode(node.name, property, 'Query');
1668
+ if (style && !ALLOWED_STYLES.includes(style)) {
1669
+ return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`;
1663
1670
  }
1664
- if (options.FieldDefinition && !isQueryType(node.parent)) {
1665
- const property = normalisePropertyOption(options.FieldDefinition);
1666
- checkNode(node.name, property, 'Field');
1667
- }
1668
- },
1669
- EnumValueDefinition: node => {
1670
- if (options.EnumValueDefinition) {
1671
- const property = normalisePropertyOption(options.EnumValueDefinition);
1672
- checkNode(node.name, property, 'Enumeration value');
1673
- }
1674
- },
1675
- InputValueDefinition: node => {
1676
- if (options.InputValueDefinition) {
1677
- const property = normalisePropertyOption(options.InputValueDefinition);
1678
- checkNode(node.name, property, 'Input property');
1671
+ const caseRegex = StyleToRegex[style];
1672
+ if (caseRegex && !caseRegex.test(name)) {
1673
+ return `be in ${style} format`;
1679
1674
  }
1680
- },
1681
- OperationDefinition: node => {
1682
- if (options.OperationDefinition) {
1683
- const property = normalisePropertyOption(options.OperationDefinition);
1684
- if (node.name) {
1685
- checkNode(node.name, property, 'Operation');
1686
- }
1687
- }
1688
- },
1689
- FragmentDefinition: node => {
1690
- if (options.FragmentDefinition) {
1691
- const property = normalisePropertyOption(options.FragmentDefinition);
1692
- checkNode(node.name, property, 'Fragment');
1693
- }
1694
- },
1695
- ScalarTypeDefinition: node => {
1696
- if (options.ScalarTypeDefinition) {
1697
- const property = normalisePropertyOption(options.ScalarTypeDefinition);
1698
- checkNode(node.name, property, 'Scalar');
1699
- }
1700
- },
1701
- UnionTypeDefinition: node => {
1702
- if (options.UnionTypeDefinition) {
1703
- const property = normalisePropertyOption(options.UnionTypeDefinition);
1704
- checkNode(node.name, property, 'Union');
1705
- }
1706
- },
1675
+ }
1676
+ };
1677
+ const checkUnderscore = (node) => {
1678
+ const name = node.value;
1679
+ context.report({
1680
+ loc: getLocation(node.loc, name),
1681
+ message: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`,
1682
+ });
1707
1683
  };
1684
+ const listeners = {};
1685
+ if (!options.allowLeadingUnderscore) {
1686
+ listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1687
+ }
1688
+ if (!options.allowTrailingUnderscore) {
1689
+ listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore;
1690
+ }
1691
+ const selectors = new Set([options.types && TYPES_KINDS, options.fields && FIELDS_KINDS, Object.keys(options.overrides)]
1692
+ .flat()
1693
+ .filter(Boolean));
1694
+ for (const selector of selectors) {
1695
+ listeners[selector] = checkNode(selector);
1696
+ }
1697
+ return listeners;
1708
1698
  },
1709
1699
  };
1710
1700
 
@@ -1743,28 +1733,14 @@ const rule$9 = {
1743
1733
  },
1744
1734
  create(context) {
1745
1735
  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
- }
1736
+ 'OperationDefinition[name=undefined]'(node) {
1737
+ context.report({
1738
+ loc: getLocation(node.loc, node.operation),
1739
+ data: {
1740
+ operation: node.operation,
1741
+ },
1742
+ messageId: NO_ANONYMOUS_OPERATIONS,
1743
+ });
1768
1744
  },
1769
1745
  };
1770
1746
  },
@@ -1871,8 +1847,8 @@ const rule$b = {
1871
1847
  mutation {
1872
1848
  changeSomething(
1873
1849
  type: OLD # This is deprecated, so you'll get an error
1874
- ) {
1875
- ...
1850
+ ) {
1851
+ ...
1876
1852
  }
1877
1853
  }
1878
1854
  `,
@@ -1909,9 +1885,10 @@ const rule$b = {
1909
1885
  requireGraphQLSchemaFromContext('no-deprecated', context);
1910
1886
  const typeInfo = node.typeInfo();
1911
1887
  if (typeInfo && typeInfo.enumValue) {
1912
- if (typeInfo.enumValue.deprecationReason) {
1888
+ if (typeInfo.enumValue.isDeprecated) {
1889
+ const enumValueName = node.value;
1913
1890
  context.report({
1914
- loc: node.loc,
1891
+ loc: getLocation(node.loc, enumValueName),
1915
1892
  messageId: NO_DEPRECATED,
1916
1893
  data: {
1917
1894
  type: 'enum value',
@@ -1925,9 +1902,10 @@ const rule$b = {
1925
1902
  requireGraphQLSchemaFromContext('no-deprecated', context);
1926
1903
  const typeInfo = node.typeInfo();
1927
1904
  if (typeInfo && typeInfo.fieldDef) {
1928
- if (typeInfo.fieldDef.deprecationReason) {
1905
+ if (typeInfo.fieldDef.isDeprecated) {
1906
+ const fieldName = node.name.value;
1929
1907
  context.report({
1930
- loc: node.loc,
1908
+ loc: getLocation(node.loc, fieldName),
1931
1909
  messageId: NO_DEPRECATED,
1932
1910
  data: {
1933
1911
  type: 'field',
@@ -2003,10 +1981,7 @@ const rule$c = {
2003
1981
  if (!isEslintComment && line !== prev.line && next.kind === graphql.TokenKind.NAME && linesAfter < 2) {
2004
1982
  context.report({
2005
1983
  messageId: HASHTAG_COMMENT,
2006
- loc: {
2007
- start: { line, column },
2008
- end: { line, column },
2009
- },
1984
+ loc: getLocation({ start: { line, column } }),
2010
1985
  });
2011
1986
  }
2012
1987
  }
@@ -2135,7 +2110,7 @@ const rule$e = {
2135
2110
  const typeName = node.name.value;
2136
2111
  if (!reachableTypes.has(typeName)) {
2137
2112
  context.report({
2138
- node,
2113
+ loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === graphql.Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
2139
2114
  messageId: UNREACHABLE_TYPE,
2140
2115
  data: { typeName },
2141
2116
  fix: fixer => fixer.remove(node),
@@ -2233,7 +2208,7 @@ const rule$f = {
2233
2208
  return;
2234
2209
  }
2235
2210
  context.report({
2236
- node,
2211
+ loc: getLocation(node.loc, fieldName),
2237
2212
  messageId: UNUSED_FIELD,
2238
2213
  data: { fieldName },
2239
2214
  fix(fixer) {
@@ -2345,6 +2320,7 @@ function convertDescription(node) {
2345
2320
  return [];
2346
2321
  }
2347
2322
 
2323
+ // eslint-disable-next-line unicorn/better-regex
2348
2324
  const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
2349
2325
  const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
2350
2326
  const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
@@ -2388,10 +2364,10 @@ const rule$g = {
2388
2364
  ],
2389
2365
  },
2390
2366
  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.',
2367
+ [MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
2368
+ [MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
2369
+ [MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
2370
+ [MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
2395
2371
  },
2396
2372
  schema: [
2397
2373
  {
@@ -2412,13 +2388,16 @@ const rule$g = {
2412
2388
  const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
2413
2389
  const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
2414
2390
  if (!deletionDateNode) {
2415
- context.report({ node: node.name, messageId: MESSAGE_REQUIRE_DATE });
2391
+ context.report({
2392
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2393
+ messageId: MESSAGE_REQUIRE_DATE,
2394
+ });
2416
2395
  return;
2417
2396
  }
2418
2397
  const deletionDate = valueFromNode(deletionDateNode.value);
2419
2398
  const isValidDate = DATE_REGEX.test(deletionDate);
2420
2399
  if (!isValidDate) {
2421
- context.report({ node: node.name, messageId: MESSAGE_INVALID_FORMAT });
2400
+ context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
2422
2401
  return;
2423
2402
  }
2424
2403
  let [day, month, year] = deletionDate.split('/');
@@ -2427,7 +2406,7 @@ const rule$g = {
2427
2406
  const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
2428
2407
  if (Number.isNaN(deletionDateInMS)) {
2429
2408
  context.report({
2430
- node: node.name,
2409
+ node: deletionDateNode.value,
2431
2410
  messageId: MESSAGE_INVALID_DATE,
2432
2411
  data: {
2433
2412
  deletionDate,
@@ -2438,7 +2417,7 @@ const rule$g = {
2438
2417
  const canRemove = Date.now() > deletionDateInMS;
2439
2418
  if (canRemove) {
2440
2419
  context.report({
2441
- node: node.name,
2420
+ node,
2442
2421
  messageId: MESSAGE_CAN_BE_REMOVED,
2443
2422
  data: {
2444
2423
  nodeName: node.parent.name.value,
@@ -2489,17 +2468,15 @@ const rule$h = {
2489
2468
  },
2490
2469
  create(context) {
2491
2470
  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
- }
2471
+ 'Directive[name.value=deprecated]'(node) {
2472
+ const args = node.arguments || [];
2473
+ const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
2474
+ const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
2475
+ if (!value) {
2476
+ context.report({
2477
+ loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
2478
+ message: 'Directive "@deprecated" must have a reason!',
2479
+ });
2503
2480
  }
2504
2481
  },
2505
2482
  };
@@ -2522,20 +2499,8 @@ const DESCRIBABLE_NODES = [
2522
2499
  function verifyRule(context, node) {
2523
2500
  if (node) {
2524
2501
  if (!node.description || !node.description.value || node.description.value.trim().length === 0) {
2525
- const { start, end } = ('name' in node ? node.name : node).loc;
2526
2502
  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
- },
2503
+ loc: getLocation(('name' in node ? node.name : node).loc, 'name' in node ? node.name.value : 'schema'),
2539
2504
  messageId: REQUIRE_DESCRIPTION_ERROR,
2540
2505
  data: {
2541
2506
  nodeType: node.kind,
@@ -2553,7 +2518,7 @@ const rule$i = {
2553
2518
  examples: [
2554
2519
  {
2555
2520
  title: 'Incorrect',
2556
- usage: [{ on: [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.FIELD_DEFINITION] }],
2521
+ usage: [{ on: ['ObjectTypeDefinition', 'FieldDefinition'] }],
2557
2522
  code: /* GraphQL */ `
2558
2523
  type someTypeName {
2559
2524
  name: String
@@ -2562,7 +2527,7 @@ const rule$i = {
2562
2527
  },
2563
2528
  {
2564
2529
  title: 'Correct',
2565
- usage: [{ on: [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.FIELD_DEFINITION] }],
2530
+ usage: [{ on: ['ObjectTypeDefinition', 'FieldDefinition'] }],
2566
2531
  code: /* GraphQL */ `
2567
2532
  """
2568
2533
  Some type description
@@ -2656,18 +2621,22 @@ const rule$j = {
2656
2621
  if (!mutationType || !queryType) {
2657
2622
  return {};
2658
2623
  }
2659
- const selector = `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}] > ${graphql.Kind.FIELD_DEFINITION}`;
2624
+ const selector = [
2625
+ `:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
2626
+ '>',
2627
+ graphql.Kind.FIELD_DEFINITION,
2628
+ graphql.Kind.NAMED_TYPE,
2629
+ ].join(' ');
2660
2630
  return {
2661
2631
  [selector](node) {
2662
- const rawNode = node.rawNode();
2663
- const typeName = getTypeName(rawNode);
2632
+ const typeName = node.name.value;
2664
2633
  const graphQLType = schema.getType(typeName);
2665
2634
  if (graphql.isObjectType(graphQLType)) {
2666
2635
  const { fields } = graphQLType.astNode;
2667
2636
  const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
2668
2637
  if (!hasQueryType) {
2669
2638
  context.report({
2670
- node,
2639
+ loc: getLocation(node.loc, typeName),
2671
2640
  message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
2672
2641
  });
2673
2642
  }
@@ -3006,7 +2975,7 @@ const rule$l = {
3006
2975
  getDocument: () => document,
3007
2976
  reportError: (error) => {
3008
2977
  context.report({
3009
- loc: error.locations[0],
2978
+ loc: getLocation({ start: error.locations[0] }),
3010
2979
  message: error.message,
3011
2980
  });
3012
2981
  },
@@ -3145,7 +3114,7 @@ const rule$m = {
3145
3114
  acceptedIdNames: ['id'],
3146
3115
  acceptedIdTypes: ['ID'],
3147
3116
  exceptions: {},
3148
- ...(context.options[0] || {}),
3117
+ ...context.options[0],
3149
3118
  };
3150
3119
  return {
3151
3120
  ObjectTypeDefinition(node) {
@@ -3162,15 +3131,16 @@ const rule$m = {
3162
3131
  }
3163
3132
  return isValidIdName && isValidIdType;
3164
3133
  });
3134
+ const typeName = node.name.value;
3165
3135
  // Usually, there should be only one unique identifier field per type.
3166
3136
  // Some clients allow multiple fields to be used. If more people need this,
3167
3137
  // we can extend this rule later.
3168
3138
  if (validIds.length !== 1) {
3169
3139
  context.report({
3170
- node,
3171
- message: '{{nodeName}} must have exactly one non-nullable unique identifier. Accepted name(s): {{acceptedNamesString}} ; Accepted type(s): {{acceptedTypesString}}',
3140
+ loc: getLocation(node.name.loc, typeName),
3141
+ message: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }} ; Accepted type(s): {{ acceptedTypesString }}`,
3172
3142
  data: {
3173
- nodeName: node.name.value,
3143
+ typeName,
3174
3144
  acceptedNamesString: options.acceptedIdNames.join(','),
3175
3145
  acceptedTypesString: options.acceptedIdTypes.join(','),
3176
3146
  },
@@ -3184,11 +3154,7 @@ const rule$m = {
3184
3154
  const RULE_NAME$3 = 'unique-fragment-name';
3185
3155
  const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
3186
3156
  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
- }
3157
+ const documentName = node.name.value;
3192
3158
  const siblings = requireSiblingsOperations(ruleName, context);
3193
3159
  const siblingDocuments = node.kind === graphql.Kind.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName);
3194
3160
  const filepath = context.getFilename();
@@ -3199,7 +3165,6 @@ const checkNode = (context, node, ruleName, messageId) => {
3199
3165
  return isSameName && !isSamePath;
3200
3166
  });
3201
3167
  if (conflictingDocuments.length > 0) {
3202
- const { start, end } = node.name.loc;
3203
3168
  context.report({
3204
3169
  messageId,
3205
3170
  data: {
@@ -3208,16 +3173,7 @@ const checkNode = (context, node, ruleName, messageId) => {
3208
3173
  .map(f => `\t${path.relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
3209
3174
  .join('\n'),
3210
3175
  },
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
- },
3176
+ loc: getLocation(node.name.loc, documentName),
3221
3177
  });
3222
3178
  }
3223
3179
  };
@@ -3334,7 +3290,7 @@ const rule$o = {
3334
3290
  },
3335
3291
  create(context) {
3336
3292
  return {
3337
- OperationDefinition(node) {
3293
+ 'OperationDefinition[name!=undefined]'(node) {
3338
3294
  checkNode(context, node, RULE_NAME$4, UNIQUE_OPERATION_NAME);
3339
3295
  },
3340
3296
  };
@@ -3764,23 +3720,111 @@ function parseForESLint(code, options = {}) {
3764
3720
  }
3765
3721
  }
3766
3722
 
3767
- class GraphQLRuleTester extends require('eslint').RuleTester {
3723
+ class GraphQLRuleTester extends eslint.RuleTester {
3768
3724
  constructor(parserOptions = {}) {
3769
- super({
3725
+ const config = {
3770
3726
  parser: require.resolve('@graphql-eslint/eslint-plugin'),
3771
3727
  parserOptions: {
3772
3728
  ...parserOptions,
3773
3729
  skipGraphQLConfig: true,
3774
3730
  },
3775
- });
3731
+ };
3732
+ super(config);
3733
+ this.config = config;
3776
3734
  }
3777
3735
  fromMockFile(path$1) {
3778
3736
  return fs.readFileSync(path.resolve(__dirname, `../tests/mocks/${path$1}`), 'utf-8');
3779
3737
  }
3780
3738
  runGraphQLTests(name, rule, tests) {
3781
- super.run(name, rule, tests);
3739
+ const ruleTests = eslint.Linter.version.startsWith('8')
3740
+ ? tests
3741
+ : {
3742
+ valid: tests.valid.map(test => {
3743
+ if (typeof test === 'string') {
3744
+ return test;
3745
+ }
3746
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3747
+ const { name, ...testCaseOptions } = test;
3748
+ return testCaseOptions;
3749
+ }),
3750
+ invalid: tests.invalid.map(test => {
3751
+ // ESLint 7 throws an error on CI - Unexpected top-level property "name"
3752
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3753
+ const { name, ...testCaseOptions } = test;
3754
+ return testCaseOptions;
3755
+ }),
3756
+ };
3757
+ super.run(name, rule, ruleTests);
3758
+ // Skip snapshot testing if `expect` variable is not defined
3759
+ if (typeof expect === 'undefined') {
3760
+ return;
3761
+ }
3762
+ const linter = new eslint.Linter();
3763
+ linter.defineRule(name, rule);
3764
+ for (const testCase of tests.invalid) {
3765
+ const verifyConfig = getVerifyConfig(name, this.config, testCase);
3766
+ defineParser(linter, verifyConfig.parser);
3767
+ const { code, filename } = testCase;
3768
+ const messages = linter.verify(code, verifyConfig, { filename });
3769
+ for (const message of messages) {
3770
+ if (message.fatal) {
3771
+ throw new Error(message.message);
3772
+ }
3773
+ const messageForSnapshot = visualizeEslintMessage(code, message);
3774
+ // eslint-disable-next-line no-undef
3775
+ expect(messageForSnapshot).toMatchSnapshot();
3776
+ }
3777
+ }
3782
3778
  }
3783
3779
  }
3780
+ function getVerifyConfig(ruleId, testerConfig, testCase) {
3781
+ const { options, parserOptions, parser = testerConfig.parser } = testCase;
3782
+ return {
3783
+ ...testerConfig,
3784
+ parser,
3785
+ parserOptions: {
3786
+ ...testerConfig.parserOptions,
3787
+ ...parserOptions,
3788
+ },
3789
+ rules: {
3790
+ [ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
3791
+ },
3792
+ };
3793
+ }
3794
+ const parsers = new WeakMap();
3795
+ function defineParser(linter, parser) {
3796
+ if (!parser) {
3797
+ return;
3798
+ }
3799
+ if (!parsers.has(linter)) {
3800
+ parsers.set(linter, new Set());
3801
+ }
3802
+ const defined = parsers.get(linter);
3803
+ if (!defined.has(parser)) {
3804
+ defined.add(parser);
3805
+ linter.defineParser(parser, require(parser));
3806
+ }
3807
+ }
3808
+ function visualizeEslintMessage(text, result) {
3809
+ const { line, column, endLine, endColumn, message } = result;
3810
+ const location = {
3811
+ start: {
3812
+ line,
3813
+ column,
3814
+ },
3815
+ };
3816
+ if (typeof endLine === 'number' && typeof endColumn === 'number') {
3817
+ location.end = {
3818
+ line: endLine,
3819
+ column: endColumn,
3820
+ };
3821
+ }
3822
+ return codeFrame.codeFrameColumns(text, location, {
3823
+ linesAbove: Number.POSITIVE_INFINITY,
3824
+ linesBelow: Number.POSITIVE_INFINITY,
3825
+ message,
3826
+ });
3827
+ }
3784
3828
 
3785
3829
  exports.GraphQLRuleTester = GraphQLRuleTester;
3786
3830
  exports.configs = configs;