@graphql-eslint/eslint-plugin 3.7.0 → 3.7.1-alpha-d9a3c78.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/README.md +1 -1
- package/docs/parser.md +1 -1
- package/docs/rules/alphabetize.md +2 -0
- package/docs/rules/match-document-filename.md +2 -1
- package/docs/rules/require-description.md +18 -1
- package/estree-parser/estree-ast.d.ts +1 -2
- package/estree-parser/utils.d.ts +10 -5
- package/index.js +298 -193
- package/index.mjs +297 -192
- package/package.json +1 -1
- package/rules/alphabetize.d.ts +1 -1
- package/rules/index.d.ts +2 -21
- package/rules/match-document-filename.d.ts +2 -1
- package/rules/require-description.d.ts +2 -2
- package/testkit.d.ts +0 -1
- package/utils.d.ts +4 -8
package/index.mjs
CHANGED
@@ -202,43 +202,6 @@ function requireUsedFieldsFromContext(ruleName, context) {
|
|
202
202
|
const siblings = requireSiblingsOperations(ruleName, context);
|
203
203
|
return context.parserServices.usedFields(schema, siblings);
|
204
204
|
}
|
205
|
-
function getLexer(source) {
|
206
|
-
// GraphQL v14
|
207
|
-
const gqlLanguage = require('graphql/language');
|
208
|
-
if (gqlLanguage && gqlLanguage.createLexer) {
|
209
|
-
return gqlLanguage.createLexer(source, {});
|
210
|
-
}
|
211
|
-
// GraphQL v15
|
212
|
-
const { Lexer: LexerCls } = require('graphql');
|
213
|
-
if (LexerCls && typeof LexerCls === 'function') {
|
214
|
-
return new LexerCls(source);
|
215
|
-
}
|
216
|
-
throw new Error(`Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!`);
|
217
|
-
}
|
218
|
-
function extractTokens(source) {
|
219
|
-
const lexer = getLexer(source);
|
220
|
-
const tokens = [];
|
221
|
-
let token = lexer.advance();
|
222
|
-
while (token && token.kind !== '<EOF>') {
|
223
|
-
tokens.push({
|
224
|
-
type: token.kind,
|
225
|
-
loc: {
|
226
|
-
start: {
|
227
|
-
line: token.line,
|
228
|
-
column: token.column,
|
229
|
-
},
|
230
|
-
end: {
|
231
|
-
line: token.line,
|
232
|
-
column: token.column,
|
233
|
-
},
|
234
|
-
},
|
235
|
-
value: token.value,
|
236
|
-
range: [token.start, token.end],
|
237
|
-
});
|
238
|
-
token = lexer.advance();
|
239
|
-
}
|
240
|
-
return tokens;
|
241
|
-
}
|
242
205
|
const normalizePath = (path) => (path || '').replace(/\\/g, '/');
|
243
206
|
/**
|
244
207
|
* https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
|
@@ -308,21 +271,16 @@ const convertCase = (style, str) => {
|
|
308
271
|
return lowerCase(str).replace(/ /g, '-');
|
309
272
|
}
|
310
273
|
};
|
311
|
-
function getLocation(loc, fieldName = ''
|
312
|
-
const {
|
313
|
-
/*
|
314
|
-
* ESLint has 0-based column number
|
315
|
-
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
316
|
-
*/
|
317
|
-
const { offsetStart = 1, offsetEnd = 1 } = offset !== null && offset !== void 0 ? offset : {};
|
274
|
+
function getLocation(loc, fieldName = '') {
|
275
|
+
const { line, column } = loc.start;
|
318
276
|
return {
|
319
277
|
start: {
|
320
|
-
line
|
321
|
-
column
|
278
|
+
line,
|
279
|
+
column,
|
322
280
|
},
|
323
281
|
end: {
|
324
|
-
line
|
325
|
-
column:
|
282
|
+
line,
|
283
|
+
column: column + fieldName.length,
|
326
284
|
},
|
327
285
|
};
|
328
286
|
}
|
@@ -336,23 +294,16 @@ function validateDocument(context, schema = null, documentNode, rule) {
|
|
336
294
|
? validate(schema, documentNode, [rule])
|
337
295
|
: validateSDL(documentNode, null, [rule]);
|
338
296
|
for (const error of validationErrors) {
|
339
|
-
/*
|
340
|
-
* TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
|
341
|
-
* Example: loc.end always equal loc.start
|
342
|
-
* {
|
343
|
-
* token: {
|
344
|
-
* type: 'Name',
|
345
|
-
* loc: { start: { line: 4, column: 13 }, end: { line: 4, column: 13 } },
|
346
|
-
* value: 'veryBad',
|
347
|
-
* range: [ 40, 47 ]
|
348
|
-
* }
|
349
|
-
* }
|
350
|
-
*/
|
351
297
|
const { line, column } = error.locations[0];
|
352
298
|
const ancestors = context.getAncestors();
|
353
|
-
const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column);
|
299
|
+
const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
|
354
300
|
context.report({
|
355
|
-
loc:
|
301
|
+
loc: token
|
302
|
+
? token.loc
|
303
|
+
: {
|
304
|
+
line,
|
305
|
+
column: column - 1,
|
306
|
+
},
|
356
307
|
message: error.message,
|
357
308
|
});
|
358
309
|
}
|
@@ -682,9 +633,10 @@ const argumentsEnum = [
|
|
682
633
|
const rule = {
|
683
634
|
meta: {
|
684
635
|
type: 'suggestion',
|
636
|
+
fixable: 'code',
|
685
637
|
docs: {
|
686
638
|
category: ['Schema', 'Operations'],
|
687
|
-
description:
|
639
|
+
description: `Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.`,
|
688
640
|
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/alphabetize.md',
|
689
641
|
examples: [
|
690
642
|
{
|
@@ -842,24 +794,56 @@ const rule = {
|
|
842
794
|
},
|
843
795
|
create(context) {
|
844
796
|
var _a, _b, _c, _d, _e;
|
797
|
+
const sourceCode = context.getSourceCode();
|
798
|
+
function isNodeAndCommentOnSameLine(node, comment) {
|
799
|
+
return node.loc.end.line === comment.loc.start.line;
|
800
|
+
}
|
801
|
+
function getBeforeComments(node) {
|
802
|
+
const commentsBefore = sourceCode.getCommentsBefore(node);
|
803
|
+
if (commentsBefore.length === 0) {
|
804
|
+
return [];
|
805
|
+
}
|
806
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
807
|
+
if (tokenBefore) {
|
808
|
+
return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
|
809
|
+
}
|
810
|
+
return commentsBefore;
|
811
|
+
}
|
812
|
+
function getRangeWithComments(node) {
|
813
|
+
const [firstBeforeComment] = getBeforeComments(node);
|
814
|
+
const [firstAfterComment] = sourceCode.getCommentsAfter(node);
|
815
|
+
const from = firstBeforeComment || node;
|
816
|
+
const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node;
|
817
|
+
return [from.range[0], to.range[1]];
|
818
|
+
}
|
845
819
|
function checkNodes(nodes) {
|
846
|
-
|
847
|
-
for (
|
848
|
-
const
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
? {
|
856
|
-
currName: `$${currName}`,
|
857
|
-
prevName: `$${prevName}`,
|
858
|
-
}
|
859
|
-
: { currName, prevName },
|
860
|
-
});
|
820
|
+
// Starts from 1, ignore nodes.length <= 1
|
821
|
+
for (let i = 1; i < nodes.length; i += 1) {
|
822
|
+
const prevNode = nodes[i - 1];
|
823
|
+
const currNode = nodes[i];
|
824
|
+
const prevName = prevNode.name.value;
|
825
|
+
const currName = currNode.name.value;
|
826
|
+
// Compare with lexicographic order
|
827
|
+
if (prevName.localeCompare(currName) !== 1) {
|
828
|
+
continue;
|
861
829
|
}
|
862
|
-
|
830
|
+
const isVariableNode = currNode.kind === Kind.VARIABLE;
|
831
|
+
context.report({
|
832
|
+
node: currNode.name,
|
833
|
+
messageId: ALPHABETIZE,
|
834
|
+
data: isVariableNode
|
835
|
+
? {
|
836
|
+
currName: `$${currName}`,
|
837
|
+
prevName: `$${prevName}`,
|
838
|
+
}
|
839
|
+
: { currName, prevName },
|
840
|
+
*fix(fixer) {
|
841
|
+
const prevRange = getRangeWithComments(prevNode);
|
842
|
+
const currRange = getRangeWithComments(currNode);
|
843
|
+
yield fixer.replaceTextRange(prevRange, sourceCode.getText({ range: currRange }));
|
844
|
+
yield fixer.replaceTextRange(currRange, sourceCode.getText({ range: prevRange }));
|
845
|
+
},
|
846
|
+
});
|
863
847
|
}
|
864
848
|
}
|
865
849
|
const opts = context.options[0];
|
@@ -1056,7 +1040,7 @@ const rule$2 = {
|
|
1056
1040
|
if (shouldCheckType(node.parent.parent)) {
|
1057
1041
|
const name = node.name.value;
|
1058
1042
|
context.report({
|
1059
|
-
|
1043
|
+
node: node.name,
|
1060
1044
|
message: `Input "${name}" should be called "input"`,
|
1061
1045
|
});
|
1062
1046
|
}
|
@@ -1078,7 +1062,7 @@ const rule$2 = {
|
|
1078
1062
|
if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
|
1079
1063
|
name.toLowerCase() !== mutationName.toLowerCase()) {
|
1080
1064
|
context.report({
|
1081
|
-
|
1065
|
+
node: node.name,
|
1082
1066
|
message: `InputType "${name}" name should be "${mutationName}"`,
|
1083
1067
|
});
|
1084
1068
|
}
|
@@ -1092,7 +1076,7 @@ const rule$2 = {
|
|
1092
1076
|
const MATCH_EXTENSION = 'MATCH_EXTENSION';
|
1093
1077
|
const MATCH_STYLE = 'MATCH_STYLE';
|
1094
1078
|
const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
|
1095
|
-
const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case'];
|
1079
|
+
const CASE_STYLES = ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE', 'kebab-case', 'matchDocumentStyle'];
|
1096
1080
|
const schemaOption = {
|
1097
1081
|
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1098
1082
|
};
|
@@ -1266,7 +1250,14 @@ const rule$3 = {
|
|
1266
1250
|
option = { style: option };
|
1267
1251
|
}
|
1268
1252
|
const expectedExtension = options.fileExtension || fileExtension;
|
1269
|
-
|
1253
|
+
let expectedFilename;
|
1254
|
+
if (option.style) {
|
1255
|
+
expectedFilename = option.style === 'matchDocumentStyle' ? docName : convertCase(option.style, docName);
|
1256
|
+
}
|
1257
|
+
else {
|
1258
|
+
expectedFilename = filename;
|
1259
|
+
}
|
1260
|
+
expectedFilename += (option.suffix || '') + expectedExtension;
|
1270
1261
|
const filenameWithExtension = filename + expectedExtension;
|
1271
1262
|
if (expectedFilename !== filenameWithExtension) {
|
1272
1263
|
context.report({
|
@@ -1523,7 +1514,7 @@ const rule$4 = {
|
|
1523
1514
|
const [trailingUnderscores] = nodeName.match(/_*$/);
|
1524
1515
|
const suggestedName = leadingUnderscores + renameToName + trailingUnderscores;
|
1525
1516
|
context.report({
|
1526
|
-
|
1517
|
+
node,
|
1527
1518
|
message: `${nodeType} "${nodeName}" should ${errorMessage}`,
|
1528
1519
|
suggest: [
|
1529
1520
|
{
|
@@ -1581,7 +1572,7 @@ const rule$4 = {
|
|
1581
1572
|
const name = node.value;
|
1582
1573
|
const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), '');
|
1583
1574
|
context.report({
|
1584
|
-
|
1575
|
+
node,
|
1585
1576
|
message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
|
1586
1577
|
suggest: [
|
1587
1578
|
{
|
@@ -1697,7 +1688,7 @@ const rule$6 = {
|
|
1697
1688
|
for (const duplicate of duplicates) {
|
1698
1689
|
const enumName = duplicate.name.value;
|
1699
1690
|
context.report({
|
1700
|
-
|
1691
|
+
node: duplicate.name,
|
1701
1692
|
message: `Case-insensitive enum values duplicates are not allowed! Found: "${enumName}"`,
|
1702
1693
|
});
|
1703
1694
|
}
|
@@ -1791,9 +1782,8 @@ const rule$7 = {
|
|
1791
1782
|
const typeInfo = node.typeInfo();
|
1792
1783
|
if (typeInfo && typeInfo.enumValue) {
|
1793
1784
|
if (typeInfo.enumValue.deprecationReason) {
|
1794
|
-
const enumValueName = node.value;
|
1795
1785
|
context.report({
|
1796
|
-
|
1786
|
+
node,
|
1797
1787
|
messageId: NO_DEPRECATED,
|
1798
1788
|
data: {
|
1799
1789
|
type: 'enum value',
|
@@ -1808,9 +1798,8 @@ const rule$7 = {
|
|
1808
1798
|
const typeInfo = node.typeInfo();
|
1809
1799
|
if (typeInfo && typeInfo.fieldDef) {
|
1810
1800
|
if (typeInfo.fieldDef.deprecationReason) {
|
1811
|
-
const fieldName = node.name.value;
|
1812
1801
|
context.report({
|
1813
|
-
|
1802
|
+
node: node.name,
|
1814
1803
|
messageId: NO_DEPRECATED,
|
1815
1804
|
data: {
|
1816
1805
|
type: 'field',
|
@@ -1882,12 +1871,11 @@ const rule$8 = {
|
|
1882
1871
|
schema: [],
|
1883
1872
|
},
|
1884
1873
|
create(context) {
|
1885
|
-
function checkNode(usedFields,
|
1874
|
+
function checkNode(usedFields, type, node) {
|
1875
|
+
const fieldName = node.value;
|
1886
1876
|
if (usedFields.has(fieldName)) {
|
1887
1877
|
context.report({
|
1888
|
-
|
1889
|
-
offsetEnd: node.kind === Kind.VARIABLE_DEFINITION ? 0 : 1,
|
1890
|
-
}),
|
1878
|
+
node,
|
1891
1879
|
messageId: NO_DUPLICATE_FIELDS,
|
1892
1880
|
data: {
|
1893
1881
|
type,
|
@@ -1903,21 +1891,20 @@ const rule$8 = {
|
|
1903
1891
|
OperationDefinition(node) {
|
1904
1892
|
const set = new Set();
|
1905
1893
|
for (const varDef of node.variableDefinitions) {
|
1906
|
-
checkNode(set,
|
1894
|
+
checkNode(set, 'Operation variable', varDef.variable.name);
|
1907
1895
|
}
|
1908
1896
|
},
|
1909
1897
|
Field(node) {
|
1910
1898
|
const set = new Set();
|
1911
1899
|
for (const arg of node.arguments) {
|
1912
|
-
checkNode(set,
|
1900
|
+
checkNode(set, 'Field argument', arg.name);
|
1913
1901
|
}
|
1914
1902
|
},
|
1915
1903
|
SelectionSet(node) {
|
1916
|
-
var _a;
|
1917
1904
|
const set = new Set();
|
1918
1905
|
for (const selection of node.selections) {
|
1919
1906
|
if (selection.kind === Kind.FIELD) {
|
1920
|
-
checkNode(set,
|
1907
|
+
checkNode(set, 'Field', selection.alias || selection.name);
|
1921
1908
|
}
|
1922
1909
|
}
|
1923
1910
|
},
|
@@ -1976,7 +1963,7 @@ const rule$9 = {
|
|
1976
1963
|
schema: [],
|
1977
1964
|
},
|
1978
1965
|
create(context) {
|
1979
|
-
const selector =
|
1966
|
+
const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
|
1980
1967
|
return {
|
1981
1968
|
[selector](node) {
|
1982
1969
|
const rawNode = node.rawNode();
|
@@ -1989,7 +1976,10 @@ const rule$9 = {
|
|
1989
1976
|
if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
|
1990
1977
|
context.report({
|
1991
1978
|
messageId: HASHTAG_COMMENT,
|
1992
|
-
loc:
|
1979
|
+
loc: {
|
1980
|
+
line,
|
1981
|
+
column: column - 1,
|
1982
|
+
},
|
1993
1983
|
});
|
1994
1984
|
}
|
1995
1985
|
}
|
@@ -2072,7 +2062,7 @@ const rule$a = {
|
|
2072
2062
|
[selector](node) {
|
2073
2063
|
const typeName = node.value;
|
2074
2064
|
context.report({
|
2075
|
-
|
2065
|
+
node,
|
2076
2066
|
message: `Root type "${typeName}" is forbidden`,
|
2077
2067
|
});
|
2078
2068
|
},
|
@@ -2125,7 +2115,7 @@ const rule$b = {
|
|
2125
2115
|
const graphQLType = schema.getType(typeName);
|
2126
2116
|
if (isScalarType(graphQLType)) {
|
2127
2117
|
context.report({
|
2128
|
-
|
2118
|
+
node,
|
2129
2119
|
message: `Unexpected scalar result type "${typeName}"`,
|
2130
2120
|
});
|
2131
2121
|
}
|
@@ -2181,7 +2171,7 @@ const rule$c = {
|
|
2181
2171
|
typeName,
|
2182
2172
|
},
|
2183
2173
|
messageId: NO_TYPENAME_PREFIX,
|
2184
|
-
|
2174
|
+
node: field.name,
|
2185
2175
|
});
|
2186
2176
|
}
|
2187
2177
|
}
|
@@ -2259,7 +2249,7 @@ const rule$d = {
|
|
2259
2249
|
const typeName = node.name.value;
|
2260
2250
|
if (!reachableTypes.has(typeName)) {
|
2261
2251
|
context.report({
|
2262
|
-
|
2252
|
+
node: node.name,
|
2263
2253
|
messageId: UNREACHABLE_TYPE,
|
2264
2254
|
data: { typeName },
|
2265
2255
|
suggest: [
|
@@ -2348,7 +2338,7 @@ const rule$e = {
|
|
2348
2338
|
return;
|
2349
2339
|
}
|
2350
2340
|
context.report({
|
2351
|
-
|
2341
|
+
node: node.name,
|
2352
2342
|
messageId: UNUSED_FIELD,
|
2353
2343
|
data: { fieldName },
|
2354
2344
|
suggest: [
|
@@ -2401,8 +2391,51 @@ function getBaseType(type) {
|
|
2401
2391
|
}
|
2402
2392
|
return type;
|
2403
2393
|
}
|
2404
|
-
function
|
2405
|
-
|
2394
|
+
function convertToken(token, type) {
|
2395
|
+
const { line, column, end, start, value } = token;
|
2396
|
+
return {
|
2397
|
+
type,
|
2398
|
+
value,
|
2399
|
+
/*
|
2400
|
+
* ESLint has 0-based column number
|
2401
|
+
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
2402
|
+
*/
|
2403
|
+
loc: {
|
2404
|
+
start: {
|
2405
|
+
line,
|
2406
|
+
column: column - 1,
|
2407
|
+
},
|
2408
|
+
end: {
|
2409
|
+
line,
|
2410
|
+
column: column - 1 + (end - start),
|
2411
|
+
},
|
2412
|
+
},
|
2413
|
+
range: [start, end],
|
2414
|
+
};
|
2415
|
+
}
|
2416
|
+
function getLexer(source) {
|
2417
|
+
// GraphQL v14
|
2418
|
+
const gqlLanguage = require('graphql/language');
|
2419
|
+
if (gqlLanguage && gqlLanguage.createLexer) {
|
2420
|
+
return gqlLanguage.createLexer(source, {});
|
2421
|
+
}
|
2422
|
+
// GraphQL v15
|
2423
|
+
const { Lexer: LexerCls } = require('graphql');
|
2424
|
+
if (LexerCls && typeof LexerCls === 'function') {
|
2425
|
+
return new LexerCls(source);
|
2426
|
+
}
|
2427
|
+
throw new Error('Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!');
|
2428
|
+
}
|
2429
|
+
function extractTokens(source) {
|
2430
|
+
const lexer = getLexer(source);
|
2431
|
+
const tokens = [];
|
2432
|
+
let token = lexer.advance();
|
2433
|
+
while (token && token.kind !== TokenKind.EOF) {
|
2434
|
+
const result = convertToken(token, token.kind);
|
2435
|
+
tokens.push(result);
|
2436
|
+
token = lexer.advance();
|
2437
|
+
}
|
2438
|
+
return tokens;
|
2406
2439
|
}
|
2407
2440
|
function extractCommentsFromAst(loc) {
|
2408
2441
|
if (!loc) {
|
@@ -2411,35 +2444,16 @@ function extractCommentsFromAst(loc) {
|
|
2411
2444
|
const comments = [];
|
2412
2445
|
let token = loc.startToken;
|
2413
2446
|
while (token !== null) {
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2418
|
-
|
2419
|
-
loc: {
|
2420
|
-
start: { line, column },
|
2421
|
-
end: { line, column },
|
2422
|
-
},
|
2423
|
-
range: [start, end],
|
2424
|
-
});
|
2447
|
+
if (token.kind === TokenKind.COMMENT) {
|
2448
|
+
const comment = convertToken(token,
|
2449
|
+
// `eslint-disable` directive works only with `Block` type comment
|
2450
|
+
token.value.trimStart().startsWith('eslint') ? 'Block' : 'Line');
|
2451
|
+
comments.push(comment);
|
2425
2452
|
}
|
2426
|
-
token = next;
|
2453
|
+
token = token.next;
|
2427
2454
|
}
|
2428
2455
|
return comments;
|
2429
2456
|
}
|
2430
|
-
function convertLocation(gqlLocation) {
|
2431
|
-
return {
|
2432
|
-
start: {
|
2433
|
-
column: gqlLocation.startToken.column,
|
2434
|
-
line: gqlLocation.startToken.line,
|
2435
|
-
},
|
2436
|
-
end: {
|
2437
|
-
column: gqlLocation.endToken.column,
|
2438
|
-
line: gqlLocation.endToken.line,
|
2439
|
-
},
|
2440
|
-
source: gqlLocation.source.body,
|
2441
|
-
};
|
2442
|
-
}
|
2443
2457
|
function isNodeWithDescription(obj) {
|
2444
2458
|
var _a;
|
2445
2459
|
return (_a = obj) === null || _a === void 0 ? void 0 : _a.description;
|
@@ -2525,7 +2539,7 @@ const rule$f = {
|
|
2525
2539
|
const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
|
2526
2540
|
if (!deletionDateNode) {
|
2527
2541
|
context.report({
|
2528
|
-
|
2542
|
+
node: node.name,
|
2529
2543
|
messageId: MESSAGE_REQUIRE_DATE,
|
2530
2544
|
});
|
2531
2545
|
return;
|
@@ -2553,7 +2567,7 @@ const rule$f = {
|
|
2553
2567
|
const canRemove = Date.now() > deletionDateInMS;
|
2554
2568
|
if (canRemove) {
|
2555
2569
|
context.report({
|
2556
|
-
node,
|
2570
|
+
node: node.parent.name,
|
2557
2571
|
messageId: MESSAGE_CAN_BE_REMOVED,
|
2558
2572
|
data: {
|
2559
2573
|
nodeName: node.parent.name.value,
|
@@ -2610,7 +2624,7 @@ const rule$g = {
|
|
2610
2624
|
const value = reasonArg ? String(valueFromNode(reasonArg.value) || '').trim() : null;
|
2611
2625
|
if (!value) {
|
2612
2626
|
context.report({
|
2613
|
-
|
2627
|
+
node: node.name,
|
2614
2628
|
message: 'Directive "@deprecated" must have a reason!',
|
2615
2629
|
});
|
2616
2630
|
}
|
@@ -2619,20 +2633,49 @@ const rule$g = {
|
|
2619
2633
|
},
|
2620
2634
|
};
|
2621
2635
|
|
2622
|
-
const
|
2636
|
+
const RULE_ID$2 = 'require-description';
|
2623
2637
|
const ALLOWED_KINDS$1 = [
|
2624
2638
|
...TYPES_KINDS,
|
2639
|
+
Kind.DIRECTIVE_DEFINITION,
|
2625
2640
|
Kind.FIELD_DEFINITION,
|
2626
2641
|
Kind.INPUT_VALUE_DEFINITION,
|
2627
2642
|
Kind.ENUM_VALUE_DEFINITION,
|
2628
|
-
Kind.
|
2643
|
+
Kind.OPERATION_DEFINITION,
|
2629
2644
|
];
|
2645
|
+
function getNodeName(node) {
|
2646
|
+
const DisplayNodeNameMap = {
|
2647
|
+
[Kind.OBJECT_TYPE_DEFINITION]: 'type',
|
2648
|
+
[Kind.INTERFACE_TYPE_DEFINITION]: 'interface',
|
2649
|
+
[Kind.ENUM_TYPE_DEFINITION]: 'enum',
|
2650
|
+
[Kind.SCALAR_TYPE_DEFINITION]: 'scalar',
|
2651
|
+
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
|
2652
|
+
[Kind.UNION_TYPE_DEFINITION]: 'union',
|
2653
|
+
[Kind.DIRECTIVE_DEFINITION]: 'directive',
|
2654
|
+
};
|
2655
|
+
switch (node.kind) {
|
2656
|
+
case Kind.OBJECT_TYPE_DEFINITION:
|
2657
|
+
case Kind.INTERFACE_TYPE_DEFINITION:
|
2658
|
+
case Kind.ENUM_TYPE_DEFINITION:
|
2659
|
+
case Kind.SCALAR_TYPE_DEFINITION:
|
2660
|
+
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
|
2661
|
+
case Kind.UNION_TYPE_DEFINITION:
|
2662
|
+
return `${DisplayNodeNameMap[node.kind]} ${node.name.value}`;
|
2663
|
+
case Kind.DIRECTIVE_DEFINITION:
|
2664
|
+
return `${DisplayNodeNameMap[node.kind]} @${node.name.value}`;
|
2665
|
+
case Kind.FIELD_DEFINITION:
|
2666
|
+
case Kind.INPUT_VALUE_DEFINITION:
|
2667
|
+
case Kind.ENUM_VALUE_DEFINITION:
|
2668
|
+
return `${node.parent.name.value}.${node.name.value}`;
|
2669
|
+
case Kind.OPERATION_DEFINITION:
|
2670
|
+
return node.name ? `${node.operation} ${node.name.value}` : node.operation;
|
2671
|
+
}
|
2672
|
+
}
|
2630
2673
|
const rule$h = {
|
2631
2674
|
meta: {
|
2632
2675
|
docs: {
|
2633
2676
|
category: 'Schema',
|
2634
|
-
description: 'Enforce descriptions in
|
2635
|
-
url:
|
2677
|
+
description: 'Enforce descriptions in type definitions and operations.',
|
2678
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
|
2636
2679
|
examples: [
|
2637
2680
|
{
|
2638
2681
|
title: 'Incorrect',
|
@@ -2656,6 +2699,16 @@ const rule$h = {
|
|
2656
2699
|
"""
|
2657
2700
|
name: String
|
2658
2701
|
}
|
2702
|
+
`,
|
2703
|
+
},
|
2704
|
+
{
|
2705
|
+
title: 'Correct',
|
2706
|
+
usage: [{ OperationDefinition: true }],
|
2707
|
+
code: /* GraphQL */ `
|
2708
|
+
# Create a new user
|
2709
|
+
mutation createUser {
|
2710
|
+
# ...
|
2711
|
+
}
|
2659
2712
|
`,
|
2660
2713
|
},
|
2661
2714
|
],
|
@@ -2669,7 +2722,7 @@ const rule$h = {
|
|
2669
2722
|
},
|
2670
2723
|
type: 'suggestion',
|
2671
2724
|
messages: {
|
2672
|
-
[
|
2725
|
+
[RULE_ID$2]: 'Description is required for `{{ nodeName }}`.',
|
2673
2726
|
},
|
2674
2727
|
schema: {
|
2675
2728
|
type: 'array',
|
@@ -2684,19 +2737,19 @@ const rule$h = {
|
|
2684
2737
|
type: 'boolean',
|
2685
2738
|
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
|
2686
2739
|
},
|
2687
|
-
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind =>
|
2688
|
-
kind
|
2689
|
-
{
|
2690
|
-
|
2691
|
-
|
2692
|
-
}
|
2693
|
-
|
2740
|
+
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind => {
|
2741
|
+
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
|
2742
|
+
if (kind === Kind.OPERATION_DEFINITION) {
|
2743
|
+
description += '\n\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
|
2744
|
+
}
|
2745
|
+
return [kind, { type: 'boolean', description }];
|
2746
|
+
})),
|
2694
2747
|
},
|
2695
2748
|
},
|
2696
2749
|
},
|
2697
2750
|
},
|
2698
2751
|
create(context) {
|
2699
|
-
const { types, ...restOptions } = context.options[0];
|
2752
|
+
const { types, ...restOptions } = context.options[0] || {};
|
2700
2753
|
const kinds = new Set(types ? TYPES_KINDS : []);
|
2701
2754
|
for (const [kind, isEnabled] of Object.entries(restOptions)) {
|
2702
2755
|
if (isEnabled) {
|
@@ -2710,13 +2763,28 @@ const rule$h = {
|
|
2710
2763
|
return {
|
2711
2764
|
[selector](node) {
|
2712
2765
|
var _a;
|
2713
|
-
|
2714
|
-
|
2766
|
+
let description = '';
|
2767
|
+
const isOperation = node.kind === Kind.OPERATION_DEFINITION;
|
2768
|
+
if (isOperation) {
|
2769
|
+
const rawNode = node.rawNode();
|
2770
|
+
const { prev, line } = rawNode.loc.startToken;
|
2771
|
+
if (prev.kind === TokenKind.COMMENT) {
|
2772
|
+
const value = prev.value.trim();
|
2773
|
+
const linesBefore = line - prev.line;
|
2774
|
+
if (!value.startsWith('eslint') && linesBefore === 1) {
|
2775
|
+
description = value;
|
2776
|
+
}
|
2777
|
+
}
|
2778
|
+
}
|
2779
|
+
else {
|
2780
|
+
description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
|
2781
|
+
}
|
2782
|
+
if (description.length === 0) {
|
2715
2783
|
context.report({
|
2716
|
-
loc: getLocation(node.
|
2717
|
-
messageId:
|
2784
|
+
loc: isOperation ? getLocation(node.loc, node.operation) : node.name.loc,
|
2785
|
+
messageId: RULE_ID$2,
|
2718
2786
|
data: {
|
2719
|
-
|
2787
|
+
nodeName: getNodeName(node),
|
2720
2788
|
},
|
2721
2789
|
});
|
2722
2790
|
}
|
@@ -2786,7 +2854,7 @@ const rule$i = {
|
|
2786
2854
|
const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
|
2787
2855
|
if (!hasQueryType) {
|
2788
2856
|
context.report({
|
2789
|
-
|
2857
|
+
node,
|
2790
2858
|
message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}"`,
|
2791
2859
|
});
|
2792
2860
|
}
|
@@ -2806,16 +2874,30 @@ function convertToESTree(node, typeInfo) {
|
|
2806
2874
|
function hasTypeField(obj) {
|
2807
2875
|
return obj && !!obj.type;
|
2808
2876
|
}
|
2809
|
-
|
2810
|
-
|
2811
|
-
|
2812
|
-
|
2813
|
-
|
2814
|
-
|
2815
|
-
|
2816
|
-
|
2817
|
-
|
2877
|
+
function convertLocation(location) {
|
2878
|
+
const { startToken, endToken, source, start, end } = location;
|
2879
|
+
/*
|
2880
|
+
* ESLint has 0-based column number
|
2881
|
+
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
2882
|
+
*/
|
2883
|
+
const loc = {
|
2884
|
+
start: {
|
2885
|
+
/*
|
2886
|
+
* Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
|
2887
|
+
*/
|
2888
|
+
line: startToken.line === 0 ? 1 : startToken.line,
|
2889
|
+
column: startToken.column === 0 ? 0 : startToken.column - 1,
|
2890
|
+
},
|
2891
|
+
end: {
|
2892
|
+
line: endToken.line,
|
2893
|
+
column: endToken.column - 1,
|
2894
|
+
},
|
2895
|
+
source: source.body,
|
2818
2896
|
};
|
2897
|
+
if (loc.start.column === loc.end.column) {
|
2898
|
+
loc.end.column += end - start;
|
2899
|
+
}
|
2900
|
+
return loc;
|
2819
2901
|
}
|
2820
2902
|
const convertNode = (typeInfo) => (node, key, parent) => {
|
2821
2903
|
const calculatedTypeInfo = typeInfo
|
@@ -2835,7 +2917,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2835
2917
|
typeInfo: () => calculatedTypeInfo,
|
2836
2918
|
leadingComments: convertDescription(node),
|
2837
2919
|
loc: convertLocation(node.loc),
|
2838
|
-
range:
|
2920
|
+
range: [node.loc.start, node.loc.end],
|
2839
2921
|
};
|
2840
2922
|
if (hasTypeField(node)) {
|
2841
2923
|
const { type: gqlType, loc: gqlLocation, ...rest } = node;
|
@@ -2860,7 +2942,6 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2860
2942
|
}
|
2861
2943
|
return parent[key];
|
2862
2944
|
},
|
2863
|
-
gqlLocation: stripTokens(gqlLocation),
|
2864
2945
|
};
|
2865
2946
|
return estreeNode;
|
2866
2947
|
}
|
@@ -2884,13 +2965,12 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2884
2965
|
}
|
2885
2966
|
return parent[key];
|
2886
2967
|
},
|
2887
|
-
gqlLocation: stripTokens(gqlLocation),
|
2888
2968
|
};
|
2889
2969
|
return estreeNode;
|
2890
2970
|
}
|
2891
2971
|
};
|
2892
2972
|
|
2893
|
-
const RULE_ID$
|
2973
|
+
const RULE_ID$3 = 'require-id-when-available';
|
2894
2974
|
const MESSAGE_ID = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2895
2975
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2896
2976
|
const rule$j = {
|
@@ -2899,7 +2979,7 @@ const rule$j = {
|
|
2899
2979
|
docs: {
|
2900
2980
|
category: 'Operations',
|
2901
2981
|
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
2902
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
2982
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
|
2903
2983
|
requiresSchema: true,
|
2904
2984
|
requiresSiblings: true,
|
2905
2985
|
examples: [
|
@@ -2973,8 +3053,8 @@ const rule$j = {
|
|
2973
3053
|
},
|
2974
3054
|
},
|
2975
3055
|
create(context) {
|
2976
|
-
requireGraphQLSchemaFromContext(RULE_ID$
|
2977
|
-
const siblings = requireSiblingsOperations(RULE_ID$
|
3056
|
+
requireGraphQLSchemaFromContext(RULE_ID$3, context);
|
3057
|
+
const siblings = requireSiblingsOperations(RULE_ID$3, context);
|
2978
3058
|
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
2979
3059
|
const idNames = asArray(fieldName);
|
2980
3060
|
const isFound = (s) => s.kind === Kind.FIELD && idNames.includes(s.name.value);
|
@@ -3036,13 +3116,13 @@ const rule$j = {
|
|
3036
3116
|
},
|
3037
3117
|
};
|
3038
3118
|
|
3039
|
-
const RULE_ID$
|
3119
|
+
const RULE_ID$4 = 'selection-set-depth';
|
3040
3120
|
const rule$k = {
|
3041
3121
|
meta: {
|
3042
3122
|
docs: {
|
3043
3123
|
category: 'Operations',
|
3044
3124
|
description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
|
3045
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3125
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
|
3046
3126
|
requiresSiblings: true,
|
3047
3127
|
examples: [
|
3048
3128
|
{
|
@@ -3116,10 +3196,10 @@ const rule$k = {
|
|
3116
3196
|
create(context) {
|
3117
3197
|
let siblings = null;
|
3118
3198
|
try {
|
3119
|
-
siblings = requireSiblingsOperations(RULE_ID$
|
3199
|
+
siblings = requireSiblingsOperations(RULE_ID$4, context);
|
3120
3200
|
}
|
3121
3201
|
catch (e) {
|
3122
|
-
logger.warn(`Rule "${RULE_ID$
|
3202
|
+
logger.warn(`Rule "${RULE_ID$4}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
|
3123
3203
|
}
|
3124
3204
|
const { maxDepth } = context.options[0];
|
3125
3205
|
const ignore = context.options[0].ignore || [];
|
@@ -3135,23 +3215,27 @@ const rule$k = {
|
|
3135
3215
|
};
|
3136
3216
|
checkFn({
|
3137
3217
|
getDocument: () => document,
|
3138
|
-
reportError
|
3218
|
+
reportError(error) {
|
3219
|
+
const { line, column } = error.locations[0];
|
3139
3220
|
context.report({
|
3140
|
-
loc:
|
3221
|
+
loc: {
|
3222
|
+
line,
|
3223
|
+
column: column - 1,
|
3224
|
+
},
|
3141
3225
|
message: error.message,
|
3142
3226
|
});
|
3143
3227
|
},
|
3144
3228
|
});
|
3145
3229
|
}
|
3146
3230
|
catch (e) {
|
3147
|
-
logger.warn(`Rule "${RULE_ID$
|
3231
|
+
logger.warn(`Rule "${RULE_ID$4}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
|
3148
3232
|
}
|
3149
3233
|
},
|
3150
3234
|
};
|
3151
3235
|
},
|
3152
3236
|
};
|
3153
3237
|
|
3154
|
-
const RULE_ID$
|
3238
|
+
const RULE_ID$5 = 'strict-id-in-types';
|
3155
3239
|
const rule$l = {
|
3156
3240
|
meta: {
|
3157
3241
|
type: 'suggestion',
|
@@ -3159,7 +3243,7 @@ const rule$l = {
|
|
3159
3243
|
description: `Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.`,
|
3160
3244
|
category: 'Schema',
|
3161
3245
|
recommended: true,
|
3162
|
-
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$
|
3246
|
+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
|
3163
3247
|
requiresSchema: true,
|
3164
3248
|
examples: [
|
3165
3249
|
{
|
@@ -3273,7 +3357,7 @@ const rule$l = {
|
|
3273
3357
|
},
|
3274
3358
|
},
|
3275
3359
|
messages: {
|
3276
|
-
[RULE_ID$
|
3360
|
+
[RULE_ID$5]: `{{ typeName }} must have exactly one non-nullable unique identifier. Accepted name(s): {{ acceptedNamesString }}; Accepted type(s): {{ acceptedTypesString }}.`,
|
3277
3361
|
},
|
3278
3362
|
},
|
3279
3363
|
create(context) {
|
@@ -3283,7 +3367,7 @@ const rule$l = {
|
|
3283
3367
|
exceptions: {},
|
3284
3368
|
...context.options[0],
|
3285
3369
|
};
|
3286
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$
|
3370
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
|
3287
3371
|
const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
|
3288
3372
|
.filter(Boolean)
|
3289
3373
|
.map(type => type.name);
|
@@ -3312,8 +3396,8 @@ const rule$l = {
|
|
3312
3396
|
// we can extend this rule later.
|
3313
3397
|
if (validIds.length !== 1) {
|
3314
3398
|
context.report({
|
3315
|
-
|
3316
|
-
messageId: RULE_ID$
|
3399
|
+
node: node.name,
|
3400
|
+
messageId: RULE_ID$5,
|
3317
3401
|
data: {
|
3318
3402
|
typeName,
|
3319
3403
|
acceptedNamesString: options.acceptedIdNames.join(', '),
|
@@ -3348,7 +3432,7 @@ const checkNode = (context, node, ruleName, messageId) => {
|
|
3348
3432
|
.map(f => `\t${relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
|
3349
3433
|
.join('\n'),
|
3350
3434
|
},
|
3351
|
-
|
3435
|
+
node: node.name,
|
3352
3436
|
});
|
3353
3437
|
}
|
3354
3438
|
};
|
@@ -3793,7 +3877,8 @@ function getReachableTypes(schema) {
|
|
3793
3877
|
visit(astNode, visitor);
|
3794
3878
|
}
|
3795
3879
|
}
|
3796
|
-
else {
|
3880
|
+
else if (type.astNode) {
|
3881
|
+
// astNode can be undefined for ID, String, Boolean
|
3797
3882
|
visit(type.astNode, visitor);
|
3798
3883
|
}
|
3799
3884
|
};
|
@@ -3812,7 +3897,8 @@ function getReachableTypes(schema) {
|
|
3812
3897
|
schema.getMutationType(),
|
3813
3898
|
schema.getSubscriptionType(),
|
3814
3899
|
]) {
|
3815
|
-
if
|
3900
|
+
// if schema don't have Query type, schema.astNode will be undefined
|
3901
|
+
if (type === null || type === void 0 ? void 0 : type.astNode) {
|
3816
3902
|
visit(type.astNode, visitor);
|
3817
3903
|
}
|
3818
3904
|
}
|
@@ -3902,6 +3988,15 @@ function parseForESLint(code, options = {}) {
|
|
3902
3988
|
}
|
3903
3989
|
}
|
3904
3990
|
|
3991
|
+
function indentCode(code, indent = 4) {
|
3992
|
+
return code.replace(/^/gm, ' '.repeat(indent));
|
3993
|
+
}
|
3994
|
+
function printCode(code) {
|
3995
|
+
return codeFrameColumns(code, { start: { line: 0, column: 0 } }, {
|
3996
|
+
linesAbove: Number.POSITIVE_INFINITY,
|
3997
|
+
linesBelow: Number.POSITIVE_INFINITY,
|
3998
|
+
});
|
3999
|
+
}
|
3905
4000
|
class GraphQLRuleTester extends RuleTester {
|
3906
4001
|
constructor(parserOptions = {}) {
|
3907
4002
|
const config = {
|
@@ -3943,19 +4038,29 @@ class GraphQLRuleTester extends RuleTester {
|
|
3943
4038
|
}
|
3944
4039
|
const linter = new Linter();
|
3945
4040
|
linter.defineRule(name, rule);
|
4041
|
+
const hasOnlyTest = tests.invalid.some(t => t.only);
|
3946
4042
|
for (const testCase of tests.invalid) {
|
4043
|
+
const { only, code, filename } = testCase;
|
4044
|
+
if (hasOnlyTest && !only) {
|
4045
|
+
continue;
|
4046
|
+
}
|
3947
4047
|
const verifyConfig = getVerifyConfig(name, this.config, testCase);
|
3948
4048
|
defineParser(linter, verifyConfig.parser);
|
3949
|
-
const { code, filename } = testCase;
|
3950
4049
|
const messages = linter.verify(code, verifyConfig, { filename });
|
3951
|
-
|
4050
|
+
const messageForSnapshot = [];
|
4051
|
+
for (const [index, message] of messages.entries()) {
|
3952
4052
|
if (message.fatal) {
|
3953
4053
|
throw new Error(message.message);
|
3954
4054
|
}
|
3955
|
-
|
3956
|
-
|
3957
|
-
|
4055
|
+
messageForSnapshot.push(`❌ Error ${index + 1}/${messages.length}`, visualizeEslintMessage(code, message));
|
4056
|
+
}
|
4057
|
+
if (rule.meta.fixable) {
|
4058
|
+
const { fixed, output } = linter.verifyAndFix(code, verifyConfig, { filename });
|
4059
|
+
if (fixed) {
|
4060
|
+
messageForSnapshot.push('🔧 Autofix output', indentCode(printCode(output), 2));
|
4061
|
+
}
|
3958
4062
|
}
|
4063
|
+
expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
|
3959
4064
|
}
|
3960
4065
|
}
|
3961
4066
|
}
|
@@ -4008,4 +4113,4 @@ function visualizeEslintMessage(text, result) {
|
|
4008
4113
|
});
|
4009
4114
|
}
|
4010
4115
|
|
4011
|
-
export { GraphQLRuleTester, configs, convertDescription,
|
4116
|
+
export { GraphQLRuleTester, configs, convertDescription, convertToESTree, convertToken, extractCommentsFromAst, extractTokens, getBaseType, isNodeWithDescription, parse, parseForESLint, processors, rules, valueFromNode };
|