Skip to content

Commit 46d759e

Browse files
authored
[naming-convention] Add QueryDefinition option, forbiddenSuffixes and forbiddenPrefixes properties. (#256)
1 parent 91880b0 commit 46d759e

File tree

4 files changed

+133
-15
lines changed

4 files changed

+133
-15
lines changed

.changeset/funny-swans-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
[naming-convention] Add forbiddenPrefixes and forbiddenSuffixes options. Add QueryDefinition which targets queries ( may break existing config if FieldDefinition is used )

docs/rules/naming-convention.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ The object must be one of the following types:
114114
* `asString`
115115
* `asObject`
116116

117+
#### `QueryDefinition`
118+
119+
The object must be one of the following types:
120+
121+
* `asString`
122+
* `asObject`
123+
117124
#### `leadingUnderscore` (string, enum)
118125

119126
This element must be one of the following enum values:
@@ -157,4 +164,20 @@ This element must be one of the following enum values:
157164

158165
### `prefix` (string)
159166

160-
### `suffix` (string)
167+
### `suffix` (string)
168+
169+
### `forbiddenPrefixes` (array)
170+
171+
The object is an array with all elements of the type `string`.
172+
173+
Additional restrictions:
174+
175+
* Minimum items: `1`
176+
177+
### `forbiddenSuffixes` (array)
178+
179+
The object is an array with all elements of the type `string`.
180+
181+
Additional restrictions:
182+
183+
* Minimum items: `1`

packages/plugin/src/rules/naming-convention.ts

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,20 @@ interface CheckNameFormatParams {
2222
trailingUnderscore: 'allow' | 'forbid';
2323
prefix: string;
2424
suffix: string;
25+
forbiddenPrefixes: string[];
26+
forbiddenSuffixes: string[];
2527
}
2628
function checkNameFormat(params: CheckNameFormatParams): { ok: false; errorMessage: string } | { ok: true } {
27-
const { value, style, leadingUnderscore, trailingUnderscore, suffix, prefix } = params;
29+
const {
30+
value,
31+
style,
32+
leadingUnderscore,
33+
trailingUnderscore,
34+
suffix,
35+
prefix,
36+
forbiddenPrefixes,
37+
forbiddenSuffixes,
38+
} = params;
2839
let name = value;
2940
if (leadingUnderscore === 'allow') {
3041
[, name] = name.match(/^_*(.*)$/);
@@ -46,6 +57,22 @@ function checkNameFormat(params: CheckNameFormatParams): { ok: false; errorMessa
4657
)}`,
4758
};
4859
}
60+
if (forbiddenPrefixes.some(forbiddenPrefix => name.startsWith(forbiddenPrefix))) {
61+
return {
62+
ok: false,
63+
errorMessage:
64+
'{{nodeType}} "{{nodeName}}" should not have one of the following prefix(es): {{forbiddenPrefixes}}',
65+
};
66+
}
67+
68+
if (forbiddenSuffixes.some(forbiddenSuffix => name.endsWith(forbiddenSuffix))) {
69+
return {
70+
ok: false,
71+
errorMessage:
72+
'{{nodeType}} "{{nodeName}}" should not have one of the following suffix(es): {{forbiddenSuffixes}}',
73+
};
74+
}
75+
4976
if (!formats[style]) {
5077
return { ok: true };
5178
}
@@ -64,12 +91,15 @@ interface PropertySchema {
6491
style?: ValidNaming;
6592
suffix?: string;
6693
prefix?: string;
94+
forbiddenPrefixes?: string[];
95+
forbiddenSuffixes?: string[];
6796
}
6897

6998
type NamingConventionRuleConfig = [
7099
{
71100
leadingUnderscore?: 'allow' | 'forbid';
72101
trailingUnderscore?: 'allow' | 'forbid';
102+
QueryDefinition?: ValidNaming | PropertySchema;
73103
[Kind.FIELD_DEFINITION]?: ValidNaming | PropertySchema;
74104
[Kind.ENUM_VALUE_DEFINITION]?: ValidNaming | PropertySchema;
75105
[Kind.INPUT_VALUE_DEFINITION]?: ValidNaming | PropertySchema;
@@ -135,6 +165,22 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
135165
suffix: {
136166
type: 'string',
137167
},
168+
forbiddenPrefixes: {
169+
additionalItems: false,
170+
type: 'array',
171+
minItems: 1,
172+
items: {
173+
type: 'string',
174+
},
175+
},
176+
forbiddenSuffixes: {
177+
additionalItems: false,
178+
type: 'array',
179+
minItems: 1,
180+
items: {
181+
type: 'string',
182+
},
183+
},
138184
},
139185
},
140186
},
@@ -154,6 +200,7 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
154200
[Kind.SCALAR_TYPE_DEFINITION as string]: schemaOption,
155201
[Kind.OPERATION_DEFINITION as string]: schemaOption,
156202
[Kind.FRAGMENT_DEFINITION as string]: schemaOption,
203+
QueryDefinition: schemaOption,
157204
leadingUnderscore: {
158205
type: 'string',
159206
enum: ['allow', 'forbid'],
@@ -175,14 +222,17 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
175222
...(context.options[0] || {}),
176223
};
177224

178-
const checkNode = (node, style, nodeType, suffix = '', prefix = '') => {
225+
const checkNode = (node, property: PropertySchema, nodeType: string) => {
226+
const { style, suffix = '', prefix = '', forbiddenPrefixes = [], forbiddenSuffixes = [] } = property;
179227
const result = checkNameFormat({
180228
value: node.value,
181229
style,
182230
leadingUnderscore: options.leadingUnderscore,
183231
trailingUnderscore: options.trailingUnderscore,
184232
prefix: prefix,
185233
suffix: suffix,
234+
forbiddenPrefixes: forbiddenPrefixes,
235+
forbiddenSuffixes: forbiddenSuffixes,
186236
});
187237
if (result.ok === false) {
188238
context.report({
@@ -192,6 +242,8 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
192242
prefix: prefix,
193243
suffix: suffix,
194244
format: style,
245+
forbiddenPrefixes: forbiddenPrefixes.join(', '),
246+
forbiddenSuffixes: forbiddenSuffixes.join(', '),
195247
nodeType,
196248
nodeName: node.value,
197249
},
@@ -210,6 +262,12 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
210262
};
211263
};
212264

265+
const isQueryType = (node): boolean => {
266+
return (
267+
(node.type === 'ObjectTypeDefinition' || node.type === 'ObjectTypeExtension') && node.name.value === 'Query'
268+
);
269+
};
270+
213271
return {
214272
Name: node => {
215273
if (node.value.startsWith('_') && options.leadingUnderscore === 'forbid') {
@@ -225,61 +283,66 @@ const rule: GraphQLESLintRule<NamingConventionRuleConfig> = {
225283
ObjectTypeDefinition: node => {
226284
if (options.ObjectTypeDefinition) {
227285
const property = normalisePropertyOption(options.ObjectTypeDefinition);
228-
checkNode(node.name, property.style, 'Type', property.suffix, property.prefix);
286+
checkNode(node.name, property, 'Type');
229287
}
230288
},
231289
InterfaceTypeDefinition: node => {
232290
if (options.InterfaceTypeDefinition) {
233291
const property = normalisePropertyOption(options.InterfaceTypeDefinition);
234-
checkNode(node.name, property.style, 'Interface', property.suffix, property.prefix);
292+
checkNode(node.name, property, 'Interface');
235293
}
236294
},
237295
EnumTypeDefinition: node => {
238296
if (options.EnumTypeDefinition) {
239297
const property = normalisePropertyOption(options.EnumTypeDefinition);
240-
checkNode(node.name, property.style, 'Enumerator', property.suffix, property.prefix);
298+
checkNode(node.name, property, 'Enumerator');
241299
}
242300
},
243301
InputObjectTypeDefinition: node => {
244302
if (options.InputObjectTypeDefinition) {
245303
const property = normalisePropertyOption(options.InputObjectTypeDefinition);
246-
checkNode(node.name, property.style, 'Input type', property.suffix, property.prefix);
304+
checkNode(node.name, property, 'Input type');
247305
}
248306
},
249-
FieldDefinition: node => {
250-
if (options.FieldDefinition) {
307+
FieldDefinition: (node: any) => {
308+
if (options.QueryDefinition && isQueryType(node.parent)) {
309+
const property = normalisePropertyOption(options.QueryDefinition);
310+
checkNode(node.name, property, 'Query');
311+
}
312+
313+
if (options.FieldDefinition && !isQueryType(node.parent)) {
251314
const property = normalisePropertyOption(options.FieldDefinition);
252-
checkNode(node.name, property.style, 'Field', property.suffix, property.prefix);
315+
checkNode(node.name, property, 'Field');
253316
}
254317
},
255318
EnumValueDefinition: node => {
256319
if (options.EnumValueDefinition) {
257320
const property = normalisePropertyOption(options.EnumValueDefinition);
258-
checkNode(node.name, property.style, 'Enumeration value', property.suffix, property.prefix);
321+
checkNode(node.name, property, 'Enumeration value');
259322
}
260323
},
261324
InputValueDefinition: node => {
262325
if (options.InputValueDefinition) {
263326
const property = normalisePropertyOption(options.InputValueDefinition);
264-
checkNode(node.name, property.style, 'Input property', property.suffix, property.prefix);
327+
checkNode(node.name, property, 'Input property');
265328
}
266329
},
267330
FragmentDefinition: node => {
268331
if (options.FragmentDefinition) {
269332
const property = normalisePropertyOption(options.FragmentDefinition);
270-
checkNode(node.name, property.style, 'Fragment', property.suffix, property.prefix);
333+
checkNode(node.name, property, 'Fragment');
271334
}
272335
},
273336
ScalarTypeDefinition: node => {
274337
if (options.ScalarTypeDefinition) {
275338
const property = normalisePropertyOption(options.ScalarTypeDefinition);
276-
checkNode(node.name, property.style, 'Scalar', property.suffix, property.prefix);
339+
checkNode(node.name, property, 'Scalar');
277340
}
278341
},
279342
UnionTypeDefinition: node => {
280343
if (options.UnionTypeDefinition) {
281344
const property = normalisePropertyOption(options.UnionTypeDefinition);
282-
checkNode(node.name, property.style, 'Scalar', property.suffix, property.prefix);
345+
checkNode(node.name, property, 'Union');
283346
}
284347
},
285348
};

packages/plugin/tests/naming-convention.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ ruleTester.runGraphQLTests('naming-convention', rule, {
103103
},
104104
],
105105
},
106+
{
107+
code: 'type One { fieldA: String } type Query { QUERY_A(id: ID!): String }',
108+
options: [
109+
{
110+
FieldDefinition: { style: 'camelCase', prefix: 'field' },
111+
QueryDefinition: { style: 'UPPER_CASE', prefix: 'QUERY' },
112+
},
113+
],
114+
},
106115
],
107116
invalid: [
108117
{
@@ -199,5 +208,23 @@ ruleTester.runGraphQLTests('naming-convention', rule, {
199208
{ message: 'Enumeration value name "VALUE_TWO" should have "ENUM" prefix' },
200209
],
201210
},
211+
{
212+
code:
213+
'type One { getFoo: String, queryBar: String } type Query { getA(id: ID!): String, queryB: String } extend type Query { getC: String }',
214+
options: [
215+
{
216+
ObjectTypeDefinition: { style: 'PascalCase', forbiddenPrefixes: ['On'] },
217+
FieldDefinition: { style: 'camelCase', forbiddenPrefixes: ['foo', 'bar'], forbiddenSuffixes: ['Foo'] },
218+
QueryDefinition: { style: 'camelCase', forbiddenPrefixes: ['get', 'query'] },
219+
},
220+
],
221+
errors: [
222+
{ message: 'Type "One" should not have one of the following prefix(es): On' },
223+
{ message: 'Field "getFoo" should not have one of the following suffix(es): Foo' },
224+
{ message: 'Query "getA" should not have one of the following prefix(es): get, query' },
225+
{ message: 'Query "queryB" should not have one of the following prefix(es): get, query' },
226+
{ message: 'Query "getC" should not have one of the following prefix(es): get, query' },
227+
],
228+
},
202229
],
203230
});

0 commit comments

Comments
 (0)