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