Skip to content

Commit 36150de

Browse files
committed
added more rules
1 parent 97455cc commit 36150de

12 files changed

+267
-25
lines changed

example/.eslintrc.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
"@graphql-eslint/validate-against-schema": "error",
99
"@graphql-eslint/no-anonymous-operations": "error",
1010
"@graphql-eslint/no-operation-name-suffix": "error",
11-
"@graphql-eslint/deprecation-must-have-reason": "error"
11+
"@graphql-eslint/deprecation-must-have-reason": "error",
12+
"@graphql-eslint/avoid-operation-name-prefix": ["error", {
13+
"keywords": ["get"]
14+
}],
15+
"@graphql-eslint/no-case-insensitive-enum-values-duplicates": [
16+
"error"
17+
],
18+
"@graphql-eslint/require-description": ["error", {
19+
"on": ["SchemaDefinition", "FieldDefinition"]
20+
}]
1221
}
1322
}

example/query.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
query myQuery {
1+
query my {
22
hello
33
}

example/schema.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
type Query {
2-
hello: String! @deprecated(test: "", reason: "!")
2+
hello: String
33
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
GraphQLESLintRule,
3+
GraphQLESlintRuleContext,
4+
GraphQLESTreeNode,
5+
} from "@graphql-eslint/types";
6+
import { OperationDefinitionNode, FragmentDefinitionNode } from "graphql";
7+
8+
type AvoidOperationNamePrefixConfig = {
9+
keywords: string[];
10+
caseSensitive?: boolean;
11+
};
12+
13+
const AVOID_OPERATION_NAME_PREFIX = "AVOID_OPERATION_NAME_PREFIX";
14+
15+
function verifyRule(
16+
context: GraphQLESlintRuleContext<AvoidOperationNamePrefixConfig>,
17+
node:
18+
| GraphQLESTreeNode<OperationDefinitionNode>
19+
| GraphQLESTreeNode<FragmentDefinitionNode>
20+
) {
21+
const caseSensitive = context.options[0].caseSensitive;
22+
23+
if (node && node.name && node.name.value !== "") {
24+
for (const keyword of context.options[0].keywords) {
25+
const testKeyword = caseSensitive ? keyword : keyword.toLowerCase();
26+
const testName = caseSensitive
27+
? node.name.value
28+
: node.name.value.toLowerCase();
29+
30+
if (testName.startsWith(testKeyword)) {
31+
context.report({
32+
loc: {
33+
start: {
34+
line: node.name.loc.start.line,
35+
column: node.name.loc.start.column - 1,
36+
},
37+
end: {
38+
line: node.name.loc.start.line,
39+
column: node.name.loc.start.column + testKeyword.length - 1,
40+
},
41+
},
42+
data: {
43+
invalidPrefix: keyword,
44+
},
45+
messageId: AVOID_OPERATION_NAME_PREFIX,
46+
});
47+
}
48+
}
49+
}
50+
}
51+
52+
const rule: GraphQLESLintRule<AvoidOperationNamePrefixConfig> = {
53+
meta: {
54+
messages: {
55+
[AVOID_OPERATION_NAME_PREFIX]: `Forbidden operation name prefix: "{{ invalidPrefix }}"`,
56+
},
57+
schema: {
58+
type: "array",
59+
minItems: 1,
60+
maxItems: 1,
61+
additionalItems: false,
62+
items: {
63+
additionalProperties: false,
64+
type: "object",
65+
required: ["keywords"],
66+
properties: {
67+
caseSensitive: {
68+
default: false,
69+
type: "boolean",
70+
},
71+
keywords: {
72+
additionalItems: false,
73+
type: "array",
74+
minItems: 1,
75+
items: {
76+
type: "string",
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
create(context) {
84+
return {
85+
OperationDefinition(node) {
86+
verifyRule(context, node);
87+
},
88+
FragmentDefinition(node) {
89+
verifyRule(context, node);
90+
},
91+
};
92+
},
93+
};
94+
95+
export default rule;

packages/plugin/src/rules/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import validate from "./validate-against-schema";
22
import noAnonymousOperations from "./no-anonymous-operations";
33
import noOperationNameSuffix from "./no-operation-name-suffix";
44
import deprecationMustHaveReason from "./deprecation-must-have-reason";
5+
import avoidOperationNamePrefix from "./avoid-operation-name-prefix";
6+
import noCaseInsensitiveEnumValuesDuplicates from "./no-case-insensitive-enum-values-duplicates";
7+
import requireDescription from "./require-description";
58

69
export const rules = {
710
'validate-against-schema': validate,
811
'no-anonymous-operations': noAnonymousOperations,
912
'no-operation-name-suffix': noOperationNameSuffix,
10-
'deprecation-must-have-reason': deprecationMustHaveReason
13+
'deprecation-must-have-reason': deprecationMustHaveReason,
14+
'avoid-operation-name-prefix': avoidOperationNamePrefix,
15+
'no-case-insensitive-enum-values-duplicates': noCaseInsensitiveEnumValuesDuplicates,
16+
'require-description': requireDescription,
1117
};
1218

packages/plugin/src/rules/no-anonymous-operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const NO_ANONYMOUS_OPERATIONS = 'NO_ANONYMOUS_OPERATIONS';
55
const rule: GraphQLESLintRule = {
66
meta: {
77
messages: {
8-
NO_ANONYMOUS_OPERATIONS: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`
8+
[NO_ANONYMOUS_OPERATIONS]: `Anonymous GraphQL operations are forbidden. Please make sure to name your {{ operation }}!`
99
}
1010
},
1111
create(context) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { GraphQLESLintRule } from "@graphql-eslint/types";
2+
import { OperationDefinitionNode, FragmentDefinitionNode, isTypeNode } from "graphql";
3+
4+
const ERROR_MESSAGE_ID = "NO_CASE_INSENSITIVE_ENUM_VALUES_DUPLICATES";
5+
6+
const rule: GraphQLESLintRule = {
7+
meta: {
8+
fixable: "code",
9+
messages: {
10+
[ERROR_MESSAGE_ID]: `Case-insensitive enum values duplicates are not allowed! Found: "{{ found }}"`,
11+
},
12+
},
13+
create(context) {
14+
return {
15+
EnumTypeDefinition(node) {
16+
const foundDuplicates = node.values.filter(
17+
(item, index) =>
18+
node.values.findIndex((v) => v.name.value.toLowerCase() === item.name.value.toLowerCase()) !== index
19+
);
20+
21+
for (const dup of foundDuplicates) {
22+
context.report({
23+
node: dup.name,
24+
data: {
25+
found: dup.name.value,
26+
},
27+
messageId: ERROR_MESSAGE_ID
28+
})
29+
}
30+
},
31+
};
32+
},
33+
};
34+
35+
export default rule;

packages/plugin/src/rules/no-operation-name-suffix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const rule: GraphQLESLintRule = {
2424
meta: {
2525
fixable: "code",
2626
messages: {
27-
NO_OPERATION_NAME_SUFFIX: `Unnecessary "{{ invalidSuffix }}" suffix in your operation name!`
27+
[NO_OPERATION_NAME_SUFFIX]: `Unnecessary "{{ invalidSuffix }}" suffix in your operation name!`
2828
}
2929
},
3030
create(context) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
GraphQLESLintRule,
3+
GraphQLESlintRuleContext,
4+
GraphQLESTreeNode,
5+
} from "@graphql-eslint/types";
6+
import { ASTNode, Kind, StringValueNode } from "graphql";
7+
8+
const REQUIRE_DESCRIPTION_ERROR = "REQUIRE_DESCRIPTION_ERROR";
9+
const DESCRIBABLE_NODES = [
10+
Kind.SCHEMA_DEFINITION,
11+
Kind.OBJECT_TYPE_DEFINITION,
12+
Kind.FIELD_DEFINITION,
13+
Kind.INPUT_VALUE_DEFINITION,
14+
Kind.INTERFACE_TYPE_DEFINITION,
15+
Kind.UNION_TYPE_DEFINITION,
16+
Kind.ENUM_TYPE_DEFINITION,
17+
Kind.ENUM_VALUE_DEFINITION,
18+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
19+
Kind.DIRECTIVE_DEFINITION,
20+
];
21+
type RequireDescriptionRuleConfig = { on: typeof DESCRIBABLE_NODES };
22+
23+
function verifyRule(
24+
context: GraphQLESlintRuleContext<RequireDescriptionRuleConfig>,
25+
node: {
26+
kind: string;
27+
readonly description?: GraphQLESTreeNode<StringValueNode>;
28+
}
29+
) {
30+
if (node) {
31+
if (
32+
!node.description ||
33+
!node.description.value ||
34+
node.description.value.trim().length === 0
35+
) {
36+
context.report({
37+
node: node as GraphQLESTreeNode<ASTNode>,
38+
messageId: REQUIRE_DESCRIPTION_ERROR,
39+
data: {
40+
nodeType: node.kind,
41+
},
42+
});
43+
}
44+
}
45+
}
46+
47+
const rule: GraphQLESLintRule<RequireDescriptionRuleConfig> = {
48+
meta: {
49+
messages: {
50+
[REQUIRE_DESCRIPTION_ERROR]: `Description is required for nodes of type {{ nodeType }}"`,
51+
},
52+
schema: {
53+
type: "array",
54+
additionalItems: false,
55+
minItems: 1,
56+
maxItems: 1,
57+
items: {
58+
type: "object",
59+
require: ["on"],
60+
properties: {
61+
on: {
62+
type: "array",
63+
minItems: 1,
64+
additionalItems: false,
65+
items: {
66+
type: "string",
67+
enum: DESCRIBABLE_NODES,
68+
},
69+
},
70+
},
71+
},
72+
},
73+
},
74+
create(context) {
75+
return context.options[0].on.reduce((prev, optionKey) => {
76+
return {
77+
...prev,
78+
[optionKey]: (node) => verifyRule(context, node),
79+
};
80+
}, {});
81+
},
82+
};
83+
84+
export default rule;

packages/types/src/estree-ast.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@ export type SafeGraphQLType<T extends ASTNode> = Omit<
88
"loc"
99
>;
1010

11-
export type SingleESTreeNode<T extends any> = T extends ASTNode ? SafeGraphQLType<T> &
12-
Pick<BaseNode, "leadingComments" | "trailingComments" | "loc" | "range"> & {
13-
type: T["kind"];
14-
gqlLocation: Location;
15-
} : T;
11+
export type SingleESTreeNode<T extends any> = T extends ASTNode
12+
? SafeGraphQLType<T> &
13+
Pick<
14+
BaseNode,
15+
"leadingComments" | "trailingComments" | "loc" | "range"
16+
> & {
17+
type: T["kind"];
18+
gqlLocation: Location;
19+
}
20+
: T;
1621

17-
export type GraphQLESTreeNode<T extends any> = {rawNode: T; } & {
18-
[K in keyof SingleESTreeNode<T>]: SingleESTreeNode<T>[K] extends ASTNode ? GraphQLESTreeNode<SingleESTreeNode<T>[K]> : SingleESTreeNode<T>[K];
22+
export type GraphQLESTreeNode<T extends any> = { rawNode: T } & {
23+
[K in keyof SingleESTreeNode<T>]: SingleESTreeNode<T>[K] extends ASTNode
24+
? GraphQLESTreeNode<SingleESTreeNode<T>[K]>
25+
: SingleESTreeNode<T>[K] extends ReadonlyArray<infer Nested>
26+
? Nested extends ASTNode
27+
? ReadonlyArray<GraphQLESTreeNode<Nested>>
28+
: SingleESTreeNode<T>[K]
29+
: SingleESTreeNode<T>[K];
1930
};

0 commit comments

Comments
 (0)