@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.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`
|
@@ -236,6 +238,24 @@ const convertCase = (style, str) => {
|
|
236
238
|
return lowerCase(str).replace(/ /g, '-');
|
237
239
|
}
|
238
240
|
};
|
241
|
+
function getLocation(loc, fieldName = '', offset) {
|
242
|
+
const { start } = loc;
|
243
|
+
/*
|
244
|
+
* ESLint has 0-based column number
|
245
|
+
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
246
|
+
*/
|
247
|
+
const { offsetStart = 1, offsetEnd = 1 } = offset !== null && offset !== void 0 ? offset : {};
|
248
|
+
return {
|
249
|
+
start: {
|
250
|
+
line: start.line,
|
251
|
+
column: start.column - offsetStart,
|
252
|
+
},
|
253
|
+
end: {
|
254
|
+
line: start.line,
|
255
|
+
column: start.column - offsetEnd + fieldName.length,
|
256
|
+
},
|
257
|
+
};
|
258
|
+
}
|
239
259
|
|
240
260
|
function extractRuleName(stack) {
|
241
261
|
const match = (stack || '').match(/validation[/\\\\]rules[/\\\\](.*?)\.js:/) || [];
|
@@ -249,7 +269,7 @@ function validateDoc(sourceNode, context, schema, documentNode, rules, ruleName
|
|
249
269
|
for (const error of validationErrors) {
|
250
270
|
const validateRuleName = ruleName || `[${extractRuleName(error.stack)}]`;
|
251
271
|
context.report({
|
252
|
-
loc: error.locations[0],
|
272
|
+
loc: getLocation({ start: error.locations[0] }),
|
253
273
|
message: ruleName ? error.message : `${validateRuleName} ${error.message}`,
|
254
274
|
});
|
255
275
|
}
|
@@ -286,6 +306,7 @@ const validationToRule = (name, ruleName, docs, getDocumentNode) => {
|
|
286
306
|
meta: {
|
287
307
|
docs: {
|
288
308
|
...docs,
|
309
|
+
graphQLJSRuleName: ruleName,
|
289
310
|
category: 'Validation',
|
290
311
|
recommended: true,
|
291
312
|
requiresSchema,
|
@@ -585,7 +606,7 @@ const rule = {
|
|
585
606
|
],
|
586
607
|
},
|
587
608
|
messages: {
|
588
|
-
[ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"
|
609
|
+
[ALPHABETIZE]: '"{{ currName }}" should be before "{{ prevName }}"',
|
589
610
|
},
|
590
611
|
schema: {
|
591
612
|
type: 'array',
|
@@ -642,16 +663,9 @@ const rule = {
|
|
642
663
|
for (const node of nodes) {
|
643
664
|
const currName = node.name.value;
|
644
665
|
if (prevName && prevName > currName) {
|
645
|
-
const { start, end } = node.name.loc;
|
646
666
|
const isVariableNode = node.kind === Kind.VARIABLE;
|
647
667
|
context.report({
|
648
|
-
loc: {
|
649
|
-
start: {
|
650
|
-
line: start.line,
|
651
|
-
column: start.column - (isVariableNode ? 2 : 1),
|
652
|
-
},
|
653
|
-
end,
|
654
|
-
},
|
668
|
+
loc: getLocation(node.loc, node.name.value, { offsetEnd: isVariableNode ? 0 : 1 }),
|
655
669
|
messageId: ALPHABETIZE,
|
656
670
|
data: isVariableNode
|
657
671
|
? {
|
@@ -717,35 +731,22 @@ const rule = {
|
|
717
731
|
};
|
718
732
|
|
719
733
|
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
734
|
const rule$1 = {
|
734
735
|
meta: {
|
735
736
|
type: 'suggestion',
|
736
737
|
docs: {
|
737
|
-
description:
|
738
|
+
description: `Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.`,
|
738
739
|
category: 'Stylistic Issues',
|
739
740
|
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
|
740
741
|
examples: [
|
741
742
|
{
|
742
743
|
title: 'Incorrect',
|
743
744
|
code: /* GraphQL */ `
|
744
|
-
query
|
745
|
+
query {
|
745
746
|
user {
|
746
|
-
name
|
747
|
+
name
|
747
748
|
email
|
748
|
-
name #
|
749
|
+
name # duplicate field
|
749
750
|
}
|
750
751
|
}
|
751
752
|
`,
|
@@ -753,7 +754,7 @@ const rule$1 = {
|
|
753
754
|
{
|
754
755
|
title: 'Incorrect',
|
755
756
|
code: /* GraphQL */ `
|
756
|
-
query
|
757
|
+
query {
|
757
758
|
users(
|
758
759
|
first: 100
|
759
760
|
skip: 50
|
@@ -768,9 +769,11 @@ const rule$1 = {
|
|
768
769
|
{
|
769
770
|
title: 'Incorrect',
|
770
771
|
code: /* GraphQL */ `
|
771
|
-
query
|
772
|
-
|
773
|
-
|
772
|
+
query (
|
773
|
+
$first: Int!
|
774
|
+
$first: Int! # duplicate variable
|
775
|
+
) {
|
776
|
+
users(first: $first, skip: 50) {
|
774
777
|
id
|
775
778
|
}
|
776
779
|
}
|
@@ -779,58 +782,47 @@ const rule$1 = {
|
|
779
782
|
],
|
780
783
|
},
|
781
784
|
messages: {
|
782
|
-
[AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times
|
785
|
+
[AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times`,
|
783
786
|
},
|
784
787
|
schema: [],
|
785
788
|
},
|
786
789
|
create(context) {
|
790
|
+
function checkNode(usedFields, fieldName, type, node) {
|
791
|
+
if (usedFields.has(fieldName)) {
|
792
|
+
context.report({
|
793
|
+
loc: getLocation((node.kind === Kind.FIELD && node.alias ? node.alias : node).loc, fieldName, {
|
794
|
+
offsetEnd: node.kind === Kind.VARIABLE_DEFINITION ? 0 : 1,
|
795
|
+
}),
|
796
|
+
messageId: AVOID_DUPLICATE_FIELDS,
|
797
|
+
data: {
|
798
|
+
type,
|
799
|
+
fieldName,
|
800
|
+
},
|
801
|
+
});
|
802
|
+
}
|
803
|
+
else {
|
804
|
+
usedFields.add(fieldName);
|
805
|
+
}
|
806
|
+
}
|
787
807
|
return {
|
788
808
|
OperationDefinition(node) {
|
789
|
-
const
|
790
|
-
for (const
|
791
|
-
|
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
|
-
});
|
809
|
+
const set = new Set();
|
810
|
+
for (const varDef of node.variableDefinitions) {
|
811
|
+
checkNode(set, varDef.variable.name.value, 'Operation variable', varDef);
|
801
812
|
}
|
802
813
|
},
|
803
814
|
Field(node) {
|
804
|
-
const
|
805
|
-
for (const arg of node.arguments
|
806
|
-
|
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
|
-
});
|
815
|
+
const set = new Set();
|
816
|
+
for (const arg of node.arguments) {
|
817
|
+
checkNode(set, arg.name.value, 'Field argument', arg);
|
816
818
|
}
|
817
819
|
},
|
818
820
|
SelectionSet(node) {
|
819
821
|
var _a;
|
820
|
-
const
|
821
|
-
for (const selection of node.selections
|
822
|
+
const set = new Set();
|
823
|
+
for (const selection of node.selections) {
|
822
824
|
if (selection.kind === Kind.FIELD) {
|
823
|
-
|
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
|
-
});
|
825
|
+
checkNode(set, ((_a = selection.alias) === null || _a === void 0 ? void 0 : _a.value) || selection.name.value, 'Field', selection);
|
834
826
|
}
|
835
827
|
}
|
836
828
|
},
|
@@ -961,16 +953,20 @@ const rule$3 = {
|
|
961
953
|
if (!mutationType) {
|
962
954
|
return {};
|
963
955
|
}
|
964
|
-
const selector =
|
956
|
+
const selector = [
|
957
|
+
`:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
|
958
|
+
'>',
|
959
|
+
Kind.FIELD_DEFINITION,
|
960
|
+
Kind.NAMED_TYPE,
|
961
|
+
].join(' ');
|
965
962
|
return {
|
966
963
|
[selector](node) {
|
967
|
-
const
|
968
|
-
const typeName = getTypeName(rawNode);
|
964
|
+
const typeName = node.name.value;
|
969
965
|
const graphQLType = schema.getType(typeName);
|
970
966
|
if (isScalarType(graphQLType)) {
|
971
967
|
context.report({
|
972
|
-
node,
|
973
|
-
message: `Unexpected scalar result type "${typeName}"
|
968
|
+
loc: getLocation(node.loc, typeName),
|
969
|
+
message: `Unexpected scalar result type "${typeName}"`,
|
974
970
|
});
|
975
971
|
}
|
976
972
|
},
|
@@ -1019,23 +1015,13 @@ const rule$4 = {
|
|
1019
1015
|
for (const field of node.fields) {
|
1020
1016
|
const fieldName = field.name.value;
|
1021
1017
|
if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
|
1022
|
-
const { start } = field.loc;
|
1023
1018
|
context.report({
|
1024
1019
|
data: {
|
1025
1020
|
fieldName,
|
1026
1021
|
typeName,
|
1027
1022
|
},
|
1028
1023
|
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
|
-
},
|
1024
|
+
loc: getLocation(field.loc, lowerTypeName),
|
1039
1025
|
});
|
1040
1026
|
}
|
1041
1027
|
}
|
@@ -1095,7 +1081,7 @@ const rule$5 = {
|
|
1095
1081
|
'[description.type="StringValue"]': node => {
|
1096
1082
|
if (node.description.block !== (style === 'block')) {
|
1097
1083
|
context.report({
|
1098
|
-
|
1084
|
+
loc: getLocation(node.description.loc),
|
1099
1085
|
message: `Unexpected ${wrongDescriptionType} description`,
|
1100
1086
|
});
|
1101
1087
|
}
|
@@ -1182,10 +1168,11 @@ const rule$6 = {
|
|
1182
1168
|
const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
|
1183
1169
|
const listeners = {
|
1184
1170
|
'FieldDefinition > InputValueDefinition': node => {
|
1185
|
-
|
1171
|
+
const name = node.name.value;
|
1172
|
+
if (name !== 'input' && shouldCheckType(node.parent.parent)) {
|
1186
1173
|
context.report({
|
1187
|
-
|
1188
|
-
message: `Input "${
|
1174
|
+
loc: getLocation(node.loc, name),
|
1175
|
+
message: `Input "${name}" should be called "input"`,
|
1189
1176
|
});
|
1190
1177
|
}
|
1191
1178
|
},
|
@@ -1202,11 +1189,12 @@ const rule$6 = {
|
|
1202
1189
|
const inputValueNode = findInputType(node);
|
1203
1190
|
if (shouldCheckType(inputValueNode.parent.parent)) {
|
1204
1191
|
const mutationName = `${inputValueNode.parent.name.value}Input`;
|
1192
|
+
const name = node.name.value;
|
1205
1193
|
if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
|
1206
|
-
|
1194
|
+
name.toLowerCase() !== mutationName.toLowerCase()) {
|
1207
1195
|
context.report({
|
1208
|
-
node,
|
1209
|
-
message: `InputType "${
|
1196
|
+
loc: getLocation(node.loc, name),
|
1197
|
+
message: `InputType "${name}" name should be "${mutationName}"`,
|
1210
1198
|
});
|
1211
1199
|
}
|
1212
1200
|
}
|
@@ -1363,7 +1351,8 @@ const rule$7 = {
|
|
1363
1351
|
var _a;
|
1364
1352
|
if (options.fileExtension && options.fileExtension !== fileExtension) {
|
1365
1353
|
context.report({
|
1366
|
-
|
1354
|
+
// Report on first character
|
1355
|
+
loc: { column: 0, line: 1 },
|
1367
1356
|
messageId: MATCH_EXTENSION,
|
1368
1357
|
data: {
|
1369
1358
|
fileExtension,
|
@@ -1395,7 +1384,8 @@ const rule$7 = {
|
|
1395
1384
|
const filenameWithExtension = filename + expectedExtension;
|
1396
1385
|
if (expectedFilename !== filenameWithExtension) {
|
1397
1386
|
context.report({
|
1398
|
-
|
1387
|
+
// Report on first character
|
1388
|
+
loc: { column: 0, line: 1 },
|
1399
1389
|
messageId: MATCH_STYLE,
|
1400
1390
|
data: {
|
1401
1391
|
expectedFilename,
|
@@ -1593,7 +1583,7 @@ const rule$8 = {
|
|
1593
1583
|
});
|
1594
1584
|
if (result.ok === false) {
|
1595
1585
|
context.report({
|
1596
|
-
node,
|
1586
|
+
loc: getLocation(node.loc, node.value),
|
1597
1587
|
message: result.errorMessage,
|
1598
1588
|
data: {
|
1599
1589
|
prefix,
|
@@ -1620,10 +1610,16 @@ const rule$8 = {
|
|
1620
1610
|
return {
|
1621
1611
|
Name: node => {
|
1622
1612
|
if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
|
1623
|
-
context.report({
|
1613
|
+
context.report({
|
1614
|
+
loc: getLocation(node.loc, node.value),
|
1615
|
+
message: 'Leading underscores are not allowed',
|
1616
|
+
});
|
1624
1617
|
}
|
1625
1618
|
if (node.value.endsWith('_') && options.trailingUnderscore === 'forbid') {
|
1626
|
-
context.report({
|
1619
|
+
context.report({
|
1620
|
+
loc: getLocation(node.loc, node.value),
|
1621
|
+
message: 'Trailing underscores are not allowed',
|
1622
|
+
});
|
1627
1623
|
}
|
1628
1624
|
},
|
1629
1625
|
ObjectTypeDefinition: node => {
|
@@ -1737,28 +1733,14 @@ const rule$9 = {
|
|
1737
1733
|
},
|
1738
1734
|
create(context) {
|
1739
1735
|
return {
|
1740
|
-
OperationDefinition(node) {
|
1741
|
-
|
1742
|
-
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
1746
|
-
|
1747
|
-
|
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
|
-
}
|
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
|
+
});
|
1762
1744
|
},
|
1763
1745
|
};
|
1764
1746
|
},
|
@@ -1865,8 +1847,8 @@ const rule$b = {
|
|
1865
1847
|
mutation {
|
1866
1848
|
changeSomething(
|
1867
1849
|
type: OLD # This is deprecated, so you'll get an error
|
1868
|
-
) {
|
1869
|
-
...
|
1850
|
+
) {
|
1851
|
+
...
|
1870
1852
|
}
|
1871
1853
|
}
|
1872
1854
|
`,
|
@@ -1904,8 +1886,9 @@ const rule$b = {
|
|
1904
1886
|
const typeInfo = node.typeInfo();
|
1905
1887
|
if (typeInfo && typeInfo.enumValue) {
|
1906
1888
|
if (typeInfo.enumValue.deprecationReason) {
|
1889
|
+
const enumValueName = node.value;
|
1907
1890
|
context.report({
|
1908
|
-
loc: node.loc,
|
1891
|
+
loc: getLocation(node.loc, enumValueName),
|
1909
1892
|
messageId: NO_DEPRECATED,
|
1910
1893
|
data: {
|
1911
1894
|
type: 'enum value',
|
@@ -1920,8 +1903,9 @@ const rule$b = {
|
|
1920
1903
|
const typeInfo = node.typeInfo();
|
1921
1904
|
if (typeInfo && typeInfo.fieldDef) {
|
1922
1905
|
if (typeInfo.fieldDef.deprecationReason) {
|
1906
|
+
const fieldName = node.name.value;
|
1923
1907
|
context.report({
|
1924
|
-
loc: node.loc,
|
1908
|
+
loc: getLocation(node.loc, fieldName),
|
1925
1909
|
messageId: NO_DEPRECATED,
|
1926
1910
|
data: {
|
1927
1911
|
type: 'field',
|
@@ -1997,10 +1981,7 @@ const rule$c = {
|
|
1997
1981
|
if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
|
1998
1982
|
context.report({
|
1999
1983
|
messageId: HASHTAG_COMMENT,
|
2000
|
-
loc: {
|
2001
|
-
start: { line, column },
|
2002
|
-
end: { line, column },
|
2003
|
-
},
|
1984
|
+
loc: getLocation({ start: { line, column } }),
|
2004
1985
|
});
|
2005
1986
|
}
|
2006
1987
|
}
|
@@ -2129,7 +2110,7 @@ const rule$e = {
|
|
2129
2110
|
const typeName = node.name.value;
|
2130
2111
|
if (!reachableTypes.has(typeName)) {
|
2131
2112
|
context.report({
|
2132
|
-
node,
|
2113
|
+
loc: getLocation(node.name.loc, typeName, { offsetStart: node.kind === Kind.DIRECTIVE_DEFINITION ? 2 : 1 }),
|
2133
2114
|
messageId: UNREACHABLE_TYPE,
|
2134
2115
|
data: { typeName },
|
2135
2116
|
fix: fixer => fixer.remove(node),
|
@@ -2227,7 +2208,7 @@ const rule$f = {
|
|
2227
2208
|
return;
|
2228
2209
|
}
|
2229
2210
|
context.report({
|
2230
|
-
node,
|
2211
|
+
loc: getLocation(node.loc, fieldName),
|
2231
2212
|
messageId: UNUSED_FIELD,
|
2232
2213
|
data: { fieldName },
|
2233
2214
|
fix(fixer) {
|
@@ -2382,10 +2363,10 @@ const rule$g = {
|
|
2382
2363
|
],
|
2383
2364
|
},
|
2384
2365
|
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
|
2366
|
+
[MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
|
2367
|
+
[MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
|
2368
|
+
[MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
|
2369
|
+
[MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
|
2389
2370
|
},
|
2390
2371
|
schema: [
|
2391
2372
|
{
|
@@ -2406,13 +2387,16 @@ const rule$g = {
|
|
2406
2387
|
const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
|
2407
2388
|
const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
|
2408
2389
|
if (!deletionDateNode) {
|
2409
|
-
context.report({
|
2390
|
+
context.report({
|
2391
|
+
loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
|
2392
|
+
messageId: MESSAGE_REQUIRE_DATE,
|
2393
|
+
});
|
2410
2394
|
return;
|
2411
2395
|
}
|
2412
2396
|
const deletionDate = valueFromNode(deletionDateNode.value);
|
2413
2397
|
const isValidDate = DATE_REGEX.test(deletionDate);
|
2414
2398
|
if (!isValidDate) {
|
2415
|
-
context.report({ node:
|
2399
|
+
context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
|
2416
2400
|
return;
|
2417
2401
|
}
|
2418
2402
|
let [day, month, year] = deletionDate.split('/');
|
@@ -2421,7 +2405,7 @@ const rule$g = {
|
|
2421
2405
|
const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
|
2422
2406
|
if (Number.isNaN(deletionDateInMS)) {
|
2423
2407
|
context.report({
|
2424
|
-
node:
|
2408
|
+
node: deletionDateNode.value,
|
2425
2409
|
messageId: MESSAGE_INVALID_DATE,
|
2426
2410
|
data: {
|
2427
2411
|
deletionDate,
|
@@ -2432,7 +2416,7 @@ const rule$g = {
|
|
2432
2416
|
const canRemove = Date.now() > deletionDateInMS;
|
2433
2417
|
if (canRemove) {
|
2434
2418
|
context.report({
|
2435
|
-
node
|
2419
|
+
node,
|
2436
2420
|
messageId: MESSAGE_CAN_BE_REMOVED,
|
2437
2421
|
data: {
|
2438
2422
|
nodeName: node.parent.name.value,
|
@@ -2483,17 +2467,15 @@ const rule$h = {
|
|
2483
2467
|
},
|
2484
2468
|
create(context) {
|
2485
2469
|
return {
|
2486
|
-
Directive(node) {
|
2487
|
-
|
2488
|
-
|
2489
|
-
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2493
|
-
|
2494
|
-
|
2495
|
-
});
|
2496
|
-
}
|
2470
|
+
'Directive[name.value=deprecated]'(node) {
|
2471
|
+
const args = node.arguments || [];
|
2472
|
+
const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
|
2473
|
+
const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
|
2474
|
+
if (!value) {
|
2475
|
+
context.report({
|
2476
|
+
loc: getLocation(node.loc, node.name.value, { offsetEnd: 0 }),
|
2477
|
+
message: 'Directive "@deprecated" must have a reason!',
|
2478
|
+
});
|
2497
2479
|
}
|
2498
2480
|
},
|
2499
2481
|
};
|
@@ -2516,20 +2498,8 @@ const DESCRIBABLE_NODES = [
|
|
2516
2498
|
function verifyRule(context, node) {
|
2517
2499
|
if (node) {
|
2518
2500
|
if (!node.description || !node.description.value || node.description.value.trim().length === 0) {
|
2519
|
-
const { start, end } = ('name' in node ? node.name : node).loc;
|
2520
2501
|
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
|
-
},
|
2502
|
+
loc: getLocation(('name' in node ? node.name : node).loc, 'name' in node ? node.name.value : 'schema'),
|
2533
2503
|
messageId: REQUIRE_DESCRIPTION_ERROR,
|
2534
2504
|
data: {
|
2535
2505
|
nodeType: node.kind,
|
@@ -2650,18 +2620,22 @@ const rule$j = {
|
|
2650
2620
|
if (!mutationType || !queryType) {
|
2651
2621
|
return {};
|
2652
2622
|
}
|
2653
|
-
const selector =
|
2623
|
+
const selector = [
|
2624
|
+
`:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})[name.value=${mutationType.name}]`,
|
2625
|
+
'>',
|
2626
|
+
Kind.FIELD_DEFINITION,
|
2627
|
+
Kind.NAMED_TYPE,
|
2628
|
+
].join(' ');
|
2654
2629
|
return {
|
2655
2630
|
[selector](node) {
|
2656
|
-
const
|
2657
|
-
const typeName = getTypeName(rawNode);
|
2631
|
+
const typeName = node.name.value;
|
2658
2632
|
const graphQLType = schema.getType(typeName);
|
2659
2633
|
if (isObjectType$1(graphQLType)) {
|
2660
2634
|
const { fields } = graphQLType.astNode;
|
2661
2635
|
const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
|
2662
2636
|
if (!hasQueryType) {
|
2663
2637
|
context.report({
|
2664
|
-
node,
|
2638
|
+
loc: getLocation(node.loc, typeName),
|
2665
2639
|
message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}".`,
|
2666
2640
|
});
|
2667
2641
|
}
|
@@ -3000,7 +2974,7 @@ const rule$l = {
|
|
3000
2974
|
getDocument: () => document,
|
3001
2975
|
reportError: (error) => {
|
3002
2976
|
context.report({
|
3003
|
-
loc: error.locations[0],
|
2977
|
+
loc: getLocation({ start: error.locations[0] }),
|
3004
2978
|
message: error.message,
|
3005
2979
|
});
|
3006
2980
|
},
|
@@ -3156,15 +3130,16 @@ const rule$m = {
|
|
3156
3130
|
}
|
3157
3131
|
return isValidIdName && isValidIdType;
|
3158
3132
|
});
|
3133
|
+
const typeName = node.name.value;
|
3159
3134
|
// Usually, there should be only one unique identifier field per type.
|
3160
3135
|
// Some clients allow multiple fields to be used. If more people need this,
|
3161
3136
|
// we can extend this rule later.
|
3162
3137
|
if (validIds.length !== 1) {
|
3163
3138
|
context.report({
|
3164
|
-
node,
|
3165
|
-
message:
|
3139
|
+
loc: getLocation(node.name.loc, typeName),
|
3140
|
+
message: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }} ; Accepted type(s): {{ acceptedTypesString }}`,
|
3166
3141
|
data: {
|
3167
|
-
|
3142
|
+
typeName,
|
3168
3143
|
acceptedNamesString: options.acceptedIdNames.join(','),
|
3169
3144
|
acceptedTypesString: options.acceptedIdTypes.join(','),
|
3170
3145
|
},
|
@@ -3178,11 +3153,7 @@ const rule$m = {
|
|
3178
3153
|
const RULE_NAME$3 = 'unique-fragment-name';
|
3179
3154
|
const UNIQUE_FRAGMENT_NAME = 'UNIQUE_FRAGMENT_NAME';
|
3180
3155
|
const checkNode = (context, node, ruleName, messageId) => {
|
3181
|
-
|
3182
|
-
const documentName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
|
3183
|
-
if (!documentName) {
|
3184
|
-
return;
|
3185
|
-
}
|
3156
|
+
const documentName = node.name.value;
|
3186
3157
|
const siblings = requireSiblingsOperations(ruleName, context);
|
3187
3158
|
const siblingDocuments = node.kind === Kind.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName);
|
3188
3159
|
const filepath = context.getFilename();
|
@@ -3193,7 +3164,6 @@ const checkNode = (context, node, ruleName, messageId) => {
|
|
3193
3164
|
return isSameName && !isSamePath;
|
3194
3165
|
});
|
3195
3166
|
if (conflictingDocuments.length > 0) {
|
3196
|
-
const { start, end } = node.name.loc;
|
3197
3167
|
context.report({
|
3198
3168
|
messageId,
|
3199
3169
|
data: {
|
@@ -3202,16 +3172,7 @@ const checkNode = (context, node, ruleName, messageId) => {
|
|
3202
3172
|
.map(f => `\t${relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
|
3203
3173
|
.join('\n'),
|
3204
3174
|
},
|
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
|
-
},
|
3175
|
+
loc: getLocation(node.name.loc, documentName),
|
3215
3176
|
});
|
3216
3177
|
}
|
3217
3178
|
};
|
@@ -3328,7 +3289,7 @@ const rule$o = {
|
|
3328
3289
|
},
|
3329
3290
|
create(context) {
|
3330
3291
|
return {
|
3331
|
-
OperationDefinition(node) {
|
3292
|
+
'OperationDefinition[name!=undefined]'(node) {
|
3332
3293
|
checkNode(context, node, RULE_NAME$4, UNIQUE_OPERATION_NAME);
|
3333
3294
|
},
|
3334
3295
|
};
|
@@ -3758,22 +3719,110 @@ function parseForESLint(code, options = {}) {
|
|
3758
3719
|
}
|
3759
3720
|
}
|
3760
3721
|
|
3761
|
-
class GraphQLRuleTester extends
|
3722
|
+
class GraphQLRuleTester extends RuleTester {
|
3762
3723
|
constructor(parserOptions = {}) {
|
3763
|
-
|
3724
|
+
const config = {
|
3764
3725
|
parser: require.resolve('@graphql-eslint/eslint-plugin'),
|
3765
3726
|
parserOptions: {
|
3766
3727
|
...parserOptions,
|
3767
3728
|
skipGraphQLConfig: true,
|
3768
3729
|
},
|
3769
|
-
}
|
3730
|
+
};
|
3731
|
+
super(config);
|
3732
|
+
this.config = config;
|
3770
3733
|
}
|
3771
3734
|
fromMockFile(path) {
|
3772
3735
|
return readFileSync(resolve(__dirname, `../tests/mocks/${path}`), 'utf-8');
|
3773
3736
|
}
|
3774
3737
|
runGraphQLTests(name, rule, tests) {
|
3775
|
-
|
3738
|
+
const ruleTests = Linter.version.startsWith('8')
|
3739
|
+
? tests
|
3740
|
+
: {
|
3741
|
+
valid: tests.valid.map(test => {
|
3742
|
+
if (typeof test === 'string') {
|
3743
|
+
return test;
|
3744
|
+
}
|
3745
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
3746
|
+
const { name, ...testCaseOptions } = test;
|
3747
|
+
return testCaseOptions;
|
3748
|
+
}),
|
3749
|
+
invalid: tests.invalid.map(test => {
|
3750
|
+
// ESLint 7 throws an error on CI - Unexpected top-level property "name"
|
3751
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
3752
|
+
const { name, ...testCaseOptions } = test;
|
3753
|
+
return testCaseOptions;
|
3754
|
+
}),
|
3755
|
+
};
|
3756
|
+
super.run(name, rule, ruleTests);
|
3757
|
+
// Skip snapshot testing if `expect` variable is not defined
|
3758
|
+
if (typeof expect === 'undefined') {
|
3759
|
+
return;
|
3760
|
+
}
|
3761
|
+
const linter = new Linter();
|
3762
|
+
linter.defineRule(name, rule);
|
3763
|
+
for (const testCase of tests.invalid) {
|
3764
|
+
const verifyConfig = getVerifyConfig(name, this.config, testCase);
|
3765
|
+
defineParser(linter, verifyConfig.parser);
|
3766
|
+
const { code, filename } = testCase;
|
3767
|
+
const messages = linter.verify(code, verifyConfig, { filename });
|
3768
|
+
for (const message of messages) {
|
3769
|
+
if (message.fatal) {
|
3770
|
+
throw new Error(message.message);
|
3771
|
+
}
|
3772
|
+
const messageForSnapshot = visualizeEslintMessage(code, message);
|
3773
|
+
// eslint-disable-next-line no-undef
|
3774
|
+
expect(messageForSnapshot).toMatchSnapshot();
|
3775
|
+
}
|
3776
|
+
}
|
3777
|
+
}
|
3778
|
+
}
|
3779
|
+
function getVerifyConfig(ruleId, testerConfig, testCase) {
|
3780
|
+
const { options, parserOptions, parser = testerConfig.parser } = testCase;
|
3781
|
+
return {
|
3782
|
+
...testerConfig,
|
3783
|
+
parser,
|
3784
|
+
parserOptions: {
|
3785
|
+
...testerConfig.parserOptions,
|
3786
|
+
...parserOptions,
|
3787
|
+
},
|
3788
|
+
rules: {
|
3789
|
+
[ruleId]: ['error', ...(Array.isArray(options) ? options : [])],
|
3790
|
+
},
|
3791
|
+
};
|
3792
|
+
}
|
3793
|
+
const parsers = new WeakMap();
|
3794
|
+
function defineParser(linter, parser) {
|
3795
|
+
if (!parser) {
|
3796
|
+
return;
|
3797
|
+
}
|
3798
|
+
if (!parsers.has(linter)) {
|
3799
|
+
parsers.set(linter, new Set());
|
3776
3800
|
}
|
3801
|
+
const defined = parsers.get(linter);
|
3802
|
+
if (!defined.has(parser)) {
|
3803
|
+
defined.add(parser);
|
3804
|
+
linter.defineParser(parser, require(parser));
|
3805
|
+
}
|
3806
|
+
}
|
3807
|
+
function visualizeEslintMessage(text, result) {
|
3808
|
+
const { line, column, endLine, endColumn, message } = result;
|
3809
|
+
const location = {
|
3810
|
+
start: {
|
3811
|
+
line,
|
3812
|
+
column,
|
3813
|
+
},
|
3814
|
+
};
|
3815
|
+
if (typeof endLine === 'number' && typeof endColumn === 'number') {
|
3816
|
+
location.end = {
|
3817
|
+
line: endLine,
|
3818
|
+
column: endColumn,
|
3819
|
+
};
|
3820
|
+
}
|
3821
|
+
return codeFrameColumns(text, location, {
|
3822
|
+
linesAbove: Number.POSITIVE_INFINITY,
|
3823
|
+
linesBelow: Number.POSITIVE_INFINITY,
|
3824
|
+
message,
|
3825
|
+
});
|
3777
3826
|
}
|
3778
3827
|
|
3779
3828
|
export { GraphQLRuleTester, configs, convertDescription, convertLocation, convertRange, convertToESTree, extractCommentsFromAst, getBaseType, isNodeWithDescription, parse, parseForESLint, processors, rules, valueFromNode };
|