Skip to content

Commit bbff5e5

Browse files
committed
better types
1 parent bff7d5e commit bbff5e5

File tree

14 files changed

+291
-82
lines changed

14 files changed

+291
-82
lines changed

example/.eslintrc.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
},
66
"plugins": ["@graphql-eslint"],
77
"rules": {
8+
"@graphql-eslint/require-id-when-available": "error",
89
"@graphql-eslint/validate-against-schema": "error",
9-
"@graphql-eslint/no-anonymous-operations": "error",
10+
"@graphql-eslint/no-anonymous-operations": "warn",
1011
"@graphql-eslint/no-operation-name-suffix": "error",
1112
"@graphql-eslint/deprecation-must-have-reason": "error",
1213
"@graphql-eslint/avoid-operation-name-prefix": ["error", {
@@ -15,7 +16,7 @@
1516
"@graphql-eslint/no-case-insensitive-enum-values-duplicates": [
1617
"error"
1718
],
18-
"@graphql-eslint/require-description": ["error", {
19+
"@graphql-eslint/require-description": ["warn", {
1920
"on": ["SchemaDefinition", "FieldDefinition"]
2021
}]
2122
}

example/query.graphql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
query my {
2-
hello
1+
query test {
2+
user {
3+
name
4+
}
35
}

example/schema.graphql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
type Query {
2-
# eslint-disable-next-line @graphql-eslint/require-description
3-
hello: String
2+
user: User!
3+
}
4+
5+
type User {
6+
id: ID!
7+
name: String!
48
}

packages/graphql-estree/src/converter.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
import { extractCommentsFromAst, convertDescription, GraphQLESTreeNode, SafeGraphQLType, convertLocation, convertRange } from "@graphql-eslint/types";
1+
import {
2+
convertDescription,
3+
GraphQLESTreeNode,
4+
SafeGraphQLType,
5+
convertLocation,
6+
convertRange,
7+
} from "@graphql-eslint/types";
28
import {
39
ASTNode,
410
TypeNode,
511
TypeInfo,
612
visit,
713
visitWithTypeInfo,
14+
TokenKind,
815
} from "graphql";
916
import { Comment } from "estree";
1017

1118
export function convertToESTree<T extends ASTNode>(
1219
node: T,
1320
typeInfo?: TypeInfo
14-
): { rootTree: GraphQLESTreeNode<T>, comments: Comment[] } {
15-
const comments: Comment[] = extractCommentsFromAst(node);
16-
console.log(comments);
21+
): { rootTree: GraphQLESTreeNode<T>; comments: Comment[] } {
22+
const comments = extractCommentsFromAst(node);
1723
const visitor = { leave: convertNode(typeInfo) };
18-
24+
1925
return {
20-
rootTree: visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor),
26+
rootTree: visit(
27+
node,
28+
typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor
29+
),
2130
comments,
2231
};
2332
}
@@ -46,7 +55,6 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
4655
}
4756
: {},
4857
leadingComments: convertDescription(node),
49-
trailingComments: [],
5058
loc: convertLocation(node.loc),
5159
range: convertRange(node.loc),
5260
};
@@ -57,26 +65,64 @@ const convertNode = (typeInfo?: TypeInfo) => <T extends ASTNode>(
5765
...rest,
5866
gqlType,
5967
} as SafeGraphQLType<T & { readonly type: TypeNode }>;
60-
const estreeNode: GraphQLESTreeNode<T> = {
68+
const estreeNode: GraphQLESTreeNode<T> = ({
6169
...typeFieldSafe,
6270
...commonFields,
6371
type: node.kind,
6472
rawNode: node,
6573
gqlLocation,
66-
} as any as GraphQLESTreeNode<T>;
74+
} as any) as GraphQLESTreeNode<T>;
6775

6876
return estreeNode;
6977
} else {
7078
const { loc: gqlLocation, ...rest } = node;
71-
const typeFieldSafe: SafeGraphQLType<T> = rest as SafeGraphQLType<T & { readonly type: TypeNode }>;
72-
const estreeNode: GraphQLESTreeNode<T> = {
79+
const typeFieldSafe: SafeGraphQLType<T> = rest as SafeGraphQLType<
80+
T & { readonly type: TypeNode }
81+
>;
82+
const estreeNode: GraphQLESTreeNode<T> = ({
7383
...typeFieldSafe,
7484
...commonFields,
7585
type: node.kind,
7686
rawNode: node,
7787
gqlLocation,
78-
} as any as GraphQLESTreeNode<T>;
88+
} as any) as GraphQLESTreeNode<T>;
7989

8090
return estreeNode;
8191
}
8292
};
93+
94+
export function extractCommentsFromAst(node: ASTNode): Comment[] {
95+
const loc = node.loc;
96+
97+
if (!loc) {
98+
return [];
99+
}
100+
101+
const comments: Comment[] = [];
102+
let token = loc.startToken;
103+
104+
while (token !== null) {
105+
if (token.kind === TokenKind.COMMENT) {
106+
const value = String(token.value);
107+
comments.push({
108+
type: "Block",
109+
value: " " + value + " ",
110+
loc: {
111+
start: {
112+
line: token.line,
113+
column: token.column,
114+
},
115+
end: {
116+
line: token.line,
117+
column: token.column,
118+
},
119+
},
120+
range: [token.start, token.end],
121+
});
122+
}
123+
124+
token = token.next;
125+
}
126+
127+
return comments;
128+
}

packages/parser/src/parser.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { convertToESTree } from "@graphql-eslint/graphql-estree";
22
import { parseGraphQLSDL } from "@graphql-tools/utils";
3-
import {
4-
GraphQLError,
5-
GraphQLSchema,
6-
TypeInfo,
7-
} from "graphql";
3+
import { GraphQLError, GraphQLSchema, TypeInfo } from "graphql";
84
import { loadConfigSync, GraphQLProjectConfig } from "graphql-config";
95
import { loadSchemaSync } from "@graphql-tools/load";
106
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
@@ -70,12 +66,15 @@ export function parseForESLint(
7066
schema,
7167
};
7268

73-
const graphqlAst = parseGraphQLSDL(config.filePath || '', code, {
69+
const graphqlAst = parseGraphQLSDL(config.filePath || "", code, {
7470
...config,
7571
noLocation: false,
7672
});
7773

78-
const { rootTree, comments } = convertToESTree(graphqlAst.document, schema ? new TypeInfo(schema) : null);
74+
const { rootTree, comments } = convertToESTree(
75+
graphqlAst.document,
76+
schema ? new TypeInfo(schema) : null
77+
);
7978

8079
return {
8180
services: parserServices,
@@ -86,7 +85,7 @@ export function parseForESLint(
8685
sourceType: "script",
8786
comments,
8887
loc: rootTree.loc,
89-
range: rootTree.range,
88+
range: rootTree.range as [number, number],
9089
tokens: [],
9190
},
9291
};
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { GraphQLESLintRule, valueFromNode } from '@graphql-eslint/types';
1+
import { GraphQLESLintRule, valueFromNode } from "@graphql-eslint/types";
22

33
const rule: GraphQLESLintRule = {
44
meta: {},
55
create(context) {
66
return {
77
Directive(node) {
8-
if (node && node.name && node.name.value === 'deprecated') {
8+
if (node && node.name && node.name.value === "deprecated") {
99
const args = node.arguments || [];
10-
const reasonArg = args.find(arg => arg.name && arg.name.value === 'reason');
11-
const value = reasonArg ? String(valueFromNode(reasonArg.value, {}) || '').trim() : null;
10+
const reasonArg = args.find(
11+
(arg) => arg.name && arg.name.value === "reason"
12+
);
13+
const value = reasonArg
14+
? String(valueFromNode(reasonArg.value, {}) || "").trim()
15+
: null;
1216

1317
if (!value) {
1418
context.report({
@@ -18,8 +22,8 @@ const rule: GraphQLESLintRule = {
1822
}
1923
}
2024
},
21-
}
22-
}
23-
}
25+
};
26+
},
27+
};
2428

2529
export default rule;

packages/plugin/src/rules/index.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import validate from "./validate-against-schema";
2-
import noAnonymousOperations from "./no-anonymous-operations";
2+
import noAnonymousOperations from "./no-anonymous-operations";
33
import noOperationNameSuffix from "./no-operation-name-suffix";
44
import deprecationMustHaveReason from "./deprecation-must-have-reason";
55
import avoidOperationNamePrefix from "./avoid-operation-name-prefix";
66
import noCaseInsensitiveEnumValuesDuplicates from "./no-case-insensitive-enum-values-duplicates";
77
import requireDescription from "./require-description";
8+
import requireIdWhenAvailable from "./require-id-when-available";
89

910
export const rules = {
10-
'validate-against-schema': validate,
11-
'no-anonymous-operations': noAnonymousOperations,
12-
'no-operation-name-suffix': noOperationNameSuffix,
13-
'deprecation-must-have-reason': deprecationMustHaveReason,
14-
'avoid-operation-name-prefix': avoidOperationNamePrefix,
15-
'no-case-insensitive-enum-values-duplicates': noCaseInsensitiveEnumValuesDuplicates,
16-
'require-description': requireDescription,
11+
"validate-against-schema": validate,
12+
"no-anonymous-operations": noAnonymousOperations,
13+
"no-operation-name-suffix": noOperationNameSuffix,
14+
"deprecation-must-have-reason": deprecationMustHaveReason,
15+
"avoid-operation-name-prefix": avoidOperationNamePrefix,
16+
"no-case-insensitive-enum-values-duplicates": noCaseInsensitiveEnumValuesDuplicates,
17+
"require-description": requireDescription,
18+
"require-id-when-available": requireIdWhenAvailable,
1719
};
18-

packages/plugin/src/rules/require-description.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function verifyRule(
4141
end: {
4242
line: node.loc.end.line,
4343
column: node.loc.end.column - 1,
44-
}
44+
},
4545
},
4646
messageId: REQUIRE_DESCRIPTION_ERROR,
4747
data: {
@@ -55,7 +55,7 @@ function verifyRule(
5555
const rule: GraphQLESLintRule<RequireDescriptionRuleConfig> = {
5656
meta: {
5757
messages: {
58-
[REQUIRE_DESCRIPTION_ERROR]: `Description is required for nodes of type {{ nodeType }}"`,
58+
[REQUIRE_DESCRIPTION_ERROR]: `Description is required for nodes of type "{{ nodeType }}"`,
5959
},
6060
schema: {
6161
type: "array",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { GraphQLESLintRule } from "@graphql-eslint/types";
2+
import { GraphQLInterfaceType, GraphQLObjectType } from "graphql";
3+
4+
const REQUIRE_ID_WHEN_AVAILABLE = "REQUIRE_ID_WHEN_AVAILABLE";
5+
const ID_FIELD_NAME = "id";
6+
7+
const rule: GraphQLESLintRule<any, true> = {
8+
meta: {
9+
messages: {
10+
[REQUIRE_ID_WHEN_AVAILABLE]: `Field "id" must be selected when it's available on a type. Please make sure to include it in your selection set!`,
11+
},
12+
},
13+
create(context) {
14+
return {
15+
SelectionSet(node) {
16+
if (!node.selections || node.selections.length > 0) {
17+
return;
18+
}
19+
20+
if (node.typeInfo && node.typeInfo.gqlType) {
21+
if (
22+
node.typeInfo.gqlType instanceof GraphQLObjectType ||
23+
node.typeInfo.gqlType instanceof GraphQLInterfaceType
24+
) {
25+
const fields = node.typeInfo.gqlType.getFields();
26+
const hasIdFieldInType = !!fields[ID_FIELD_NAME];
27+
const hasIdFieldInSelectionSet = !!node.selections.find(
28+
(s) => s.kind === "Field" && s.name.value === ID_FIELD_NAME
29+
);
30+
31+
if (hasIdFieldInType && !hasIdFieldInSelectionSet) {
32+
context.report({
33+
loc: {
34+
start: {
35+
line: node.loc.start.line,
36+
column: node.loc.start.column - 1,
37+
},
38+
end: {
39+
line: node.loc.end.line,
40+
column: node.loc.end.column - 1,
41+
},
42+
},
43+
messageId: REQUIRE_ID_WHEN_AVAILABLE,
44+
data: {
45+
nodeType: node.kind,
46+
},
47+
});
48+
}
49+
}
50+
}
51+
},
52+
};
53+
},
54+
};
55+
56+
export default rule;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// import { GraphQLRuleTester } from "@graphql-eslint/types";
2+
// import rule from "../src/rules/require-id-when-available";
3+
4+
// const ruleTester = new GraphQLRuleTester();
5+
6+
// ruleTester.runGraphQLTests("require-id-when-available", rule, {
7+
// valid: [
8+
// {
9+
// code: `query test { username }`
10+
// }
11+
// ],
12+
// invalid: [
13+
// // {
14+
// // code: "foo",
15+
// // errors: [
16+
// // {
17+
// // messageId: "avoidName",
18+
// // },
19+
// // ],
20+
// // },
21+
// ],
22+
// });

0 commit comments

Comments
 (0)