Skip to content

Commit 98e0b56

Browse files
authored
ntroduce forbiddenPattern and requiredPattern options for naming-convention rule and deprecate forbiddenPrefixes, forbiddenSuffixes and requiredPrefixes and requiredSuffixes (#2780)
* introduce `forbiddenPattern` and `requiredPattern` options for `naming-convention` rule and deprecate `forbiddenPrefixes`, `forbiddenSuffixes` and `requiredPrefixes` and `requiredSuffixes` * aa * aa * aa * aa * aa * aa * aa * aa * aa * aa * upd snapshots * missing dots * yoyo
1 parent 251d765 commit 98e0b56

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+362
-181
lines changed

.changeset/happy-bottles-warn.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
introduce `forbiddenPattern` and `requiredPattern` options for `naming-convention` rule and
6+
deprecate `forbiddenPrefixes`, `forbiddenSuffixes` and `requiredPrefixes` and `requiredSuffixes`

packages/plugin/__tests__/__snapshots__/examples.spec.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exports[`Examples > should work in monorepo 1`] = `
1010
endColumn: 15,
1111
endLine: 1,
1212
line: 1,
13-
message: Operation "getUsers" should be in PascalCase format,
13+
message: Query "getUsers" should be in PascalCase format,
1414
nodeType: Name,
1515
ruleId: @graphql-eslint/naming-convention,
1616
severity: 2,
@@ -198,7 +198,7 @@ exports[`Examples > should work in monorepo 2`] = `
198198
endColumn: 15,
199199
endLine: 1,
200200
line: 1,
201-
message: Operation "getUsers" should be in PascalCase format,
201+
message: Query "getUsers" should be in PascalCase format,
202202
nodeType: Name,
203203
ruleId: @graphql-eslint/naming-convention,
204204
severity: 2,
@@ -583,7 +583,7 @@ exports[`Examples > should work in svelte 1`] = `
583583
{
584584
column: 0,
585585
line: 1,
586-
message: Operation "UserQuery" should not have "Query" suffix,
586+
message: Query "UserQuery" should not have "Query" suffix,
587587
nodeType: Name,
588588
ruleId: @graphql-eslint/naming-convention,
589589
severity: 2,
@@ -610,7 +610,7 @@ exports[`Examples > should work in svelte 2`] = `
610610
{
611611
column: 0,
612612
line: 1,
613-
message: Operation "UserQuery" should not have "Query" suffix,
613+
message: Query "UserQuery" should not have "Query" suffix,
614614
nodeType: Name,
615615
ruleId: @graphql-eslint/naming-convention,
616616
severity: 2,
@@ -675,7 +675,7 @@ exports[`Examples > should work in vue 1`] = `
675675
endColumn: 19,
676676
endLine: 16,
677677
line: 16,
678-
message: Operation "UserQuery" should not have "Query" suffix,
678+
message: Query "UserQuery" should not have "Query" suffix,
679679
nodeType: Name,
680680
ruleId: @graphql-eslint/naming-convention,
681681
severity: 2,
@@ -714,7 +714,7 @@ exports[`Examples > should work in vue 2`] = `
714714
{
715715
column: 0,
716716
line: 1,
717-
message: Operation "UserQuery" should not have "Query" suffix,
717+
message: Query "UserQuery" should not have "Query" suffix,
718718
nodeType: Name,
719719
ruleId: @graphql-eslint/naming-convention,
720720
severity: 2,
@@ -822,7 +822,7 @@ exports[`Examples > should work on \`.js\` files 1`] = `
822822
endColumn: 18,
823823
endLine: 12,
824824
line: 12,
825-
message: Operation "UserQuery" should not have "Query" suffix,
825+
message: Query "UserQuery" should not have "Query" suffix,
826826
nodeType: Name,
827827
ruleId: @graphql-eslint/naming-convention,
828828
severity: 2,
@@ -920,7 +920,7 @@ exports[`Examples > should work on \`.js\` files 2`] = `
920920
endColumn: 18,
921921
endLine: 12,
922922
line: 12,
923-
message: Operation "UserQuery" should not have "Query" suffix,
923+
message: Query "UserQuery" should not have "Query" suffix,
924924
nodeType: Name,
925925
ruleId: @graphql-eslint/naming-convention,
926926
severity: 2,

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

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -259,49 +259,44 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
259259
],
260260
errors: [
261261
{
262-
message:
263-
'Input type "_idOperatorsFilterFindManyUserInput" should be in PascalCase format',
262+
message: 'Input "_idOperatorsFilterFindManyUserInput" should be in PascalCase format',
264263
},
265264
{
266-
message: 'Input type "_idOperatorsFilterFindOneUserInput" should be in PascalCase format',
265+
message: 'Input "_idOperatorsFilterFindOneUserInput" should be in PascalCase format',
267266
},
268267
{
269-
message:
270-
'Input type "_idOperatorsFilterRemoveManyUserInput" should be in PascalCase format',
268+
message: 'Input "_idOperatorsFilterRemoveManyUserInput" should be in PascalCase format',
271269
},
272270
{
273-
message:
274-
'Input type "_idOperatorsFilterRemoveOneUserInput" should be in PascalCase format',
271+
message: 'Input "_idOperatorsFilterRemoveOneUserInput" should be in PascalCase format',
275272
},
276273
{
277-
message:
278-
'Input type "_idOperatorsFilterUpdateManyUserInput" should be in PascalCase format',
274+
message: 'Input "_idOperatorsFilterUpdateManyUserInput" should be in PascalCase format',
279275
},
280276
{
281-
message:
282-
'Input type "_idOperatorsFilterUpdateOneUserInput" should be in PascalCase format',
277+
message: 'Input "_idOperatorsFilterUpdateOneUserInput" should be in PascalCase format',
283278
},
284-
{ message: 'Input type "_idOperatorsFilterUserInput" should be in PascalCase format' },
285-
{ message: 'Enumeration value "male" should be in UPPER_CASE format' },
286-
{ message: 'Enumeration value "female" should be in UPPER_CASE format' },
287-
{ message: 'Enumeration value "ladyboy" should be in UPPER_CASE format' },
288-
{ message: 'Enumeration value "basic" should be in UPPER_CASE format' },
289-
{ message: 'Enumeration value "fluent" should be in UPPER_CASE format' },
290-
{ message: 'Enumeration value "native" should be in UPPER_CASE format' },
291-
{ message: 'Input property "OR" should be in camelCase format' },
292-
{ message: 'Input property "AND" should be in camelCase format' },
293-
{ message: 'Input property "OR" should be in camelCase format' },
294-
{ message: 'Input property "AND" should be in camelCase format' },
295-
{ message: 'Input property "OR" should be in camelCase format' },
296-
{ message: 'Input property "AND" should be in camelCase format' },
297-
{ message: 'Input property "OR" should be in camelCase format' },
298-
{ message: 'Input property "AND" should be in camelCase format' },
299-
{ message: 'Input property "OR" should be in camelCase format' },
300-
{ message: 'Input property "AND" should be in camelCase format' },
301-
{ message: 'Input property "OR" should be in camelCase format' },
302-
{ message: 'Input property "AND" should be in camelCase format' },
303-
{ message: 'Input property "OR" should be in camelCase format' },
304-
{ message: 'Input property "AND" should be in camelCase format' },
279+
{ message: 'Input "_idOperatorsFilterUserInput" should be in PascalCase format' },
280+
{ message: 'Enum value "male" should be in UPPER_CASE format' },
281+
{ message: 'Enum value "female" should be in UPPER_CASE format' },
282+
{ message: 'Enum value "ladyboy" should be in UPPER_CASE format' },
283+
{ message: 'Enum value "basic" should be in UPPER_CASE format' },
284+
{ message: 'Enum value "fluent" should be in UPPER_CASE format' },
285+
{ message: 'Enum value "native" should be in UPPER_CASE format' },
286+
{ message: 'Input value "OR" should be in camelCase format' },
287+
{ message: 'Input value "AND" should be in camelCase format' },
288+
{ message: 'Input value "OR" should be in camelCase format' },
289+
{ message: 'Input value "AND" should be in camelCase format' },
290+
{ message: 'Input value "OR" should be in camelCase format' },
291+
{ message: 'Input value "AND" should be in camelCase format' },
292+
{ message: 'Input value "OR" should be in camelCase format' },
293+
{ message: 'Input value "AND" should be in camelCase format' },
294+
{ message: 'Input value "OR" should be in camelCase format' },
295+
{ message: 'Input value "AND" should be in camelCase format' },
296+
{ message: 'Input value "OR" should be in camelCase format' },
297+
{ message: 'Input value "AND" should be in camelCase format' },
298+
{ message: 'Input value "OR" should be in camelCase format' },
299+
{ message: 'Input value "AND" should be in camelCase format' },
305300
],
306301
},
307302
{
@@ -313,16 +308,16 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
313308
},
314309
],
315310
errors: [
316-
{ message: 'Enumerator "B" should be in camelCase format' },
317-
{ message: 'Enumeration value "test" should be in UPPER_CASE format' },
311+
{ message: 'Enum "B" should be in camelCase format' },
312+
{ message: 'Enum value "test" should be in UPPER_CASE format' },
318313
],
319314
},
320315
{
321316
code: 'input test { _Value: String }',
322317
options: [{ types: 'PascalCase', InputValueDefinition: 'snake_case' }],
323318
errors: [
324-
{ message: 'Input type "test" should be in PascalCase format' },
325-
{ message: 'Input property "_Value" should be in snake_case format' },
319+
{ message: 'Input "test" should be in PascalCase format' },
320+
{ message: 'Input value "_Value" should be in snake_case format' },
326321
{ message: 'Leading underscores are not allowed' },
327322
],
328323
},
@@ -338,8 +333,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
338333
errors: [
339334
{ message: 'Type "TypeOne" should be in camelCase format' },
340335
{ message: 'Field "aField" should have "AAA" suffix' },
341-
{ message: 'Enumeration value "VALUE_ONE" should have "ENUM" suffix' },
342-
{ message: 'Enumeration value "VALUE_TWO" should have "ENUM" suffix' },
336+
{ message: 'Enum value "VALUE_ONE" should have "ENUM" suffix' },
337+
{ message: 'Enum value "VALUE_TWO" should have "ENUM" suffix' },
343338
],
344339
},
345340
{
@@ -353,8 +348,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
353348
],
354349
errors: [
355350
{ message: 'Field "aField" should have "Field" prefix' },
356-
{ message: 'Enumeration value "A_ENUM_VALUE_ONE" should have "ENUM" prefix' },
357-
{ message: 'Enumeration value "VALUE_TWO" should have "ENUM" prefix' },
351+
{ message: 'Enum value "A_ENUM_VALUE_ONE" should have "ENUM" prefix' },
352+
{ message: 'Enum value "VALUE_TWO" should have "ENUM" prefix' },
358353
],
359354
},
360355
{
@@ -385,8 +380,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
385380
code: 'query Foo { foo } query getBar { bar }',
386381
options: [{ OperationDefinition: { style: 'camelCase', forbiddenPrefixes: ['get'] } }],
387382
errors: [
388-
{ message: 'Operation "Foo" should be in camelCase format' },
389-
{ message: 'Operation "getBar" should not have "get" prefix' },
383+
{ message: 'Query "Foo" should be in camelCase format' },
384+
{ message: 'Query "getBar" should not have "get" prefix' },
390385
],
391386
},
392387
{
@@ -448,13 +443,13 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
448443
`,
449444
options: (rule.meta.docs!.configOptions as any).operations,
450445
errors: [
451-
{ message: 'Operation "TestQuery" should not have "Query" suffix' },
452-
{ message: 'Operation "QueryTest" should not have "Query" prefix' },
453-
{ message: 'Operation "GetQuery" should not have "Get" prefix' },
454-
{ message: 'Operation "TestMutation" should not have "Mutation" suffix' },
455-
{ message: 'Operation "MutationTest" should not have "Mutation" prefix' },
456-
{ message: 'Operation "TestSubscription" should not have "Subscription" suffix' },
457-
{ message: 'Operation "SubscriptionTest" should not have "Subscription" prefix' },
446+
{ message: 'Query "TestQuery" should not have "Query" suffix' },
447+
{ message: 'Query "QueryTest" should not have "Query" prefix' },
448+
{ message: 'Query "GetQuery" should not have "Get" prefix' },
449+
{ message: 'Mutation "TestMutation" should not have "Mutation" suffix' },
450+
{ message: 'Mutation "MutationTest" should not have "Mutation" prefix' },
451+
{ message: 'Subscription "TestSubscription" should not have "Subscription" suffix' },
452+
{ message: 'Subscription "SubscriptionTest" should not have "Subscription" prefix' },
458453
{ message: 'Fragment "TestFragment" should not have "Fragment" suffix' },
459454
{ message: 'Fragment "FragmentTest" should not have "Fragment" prefix' },
460455
],
@@ -531,5 +526,24 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
531526
],
532527
errors: 2,
533528
},
529+
{
530+
name: 'forbiddenPattern',
531+
code: 'query queryFoo { foo } query getBar { bar }',
532+
options: [{ OperationDefinition: { forbiddenPattern: [/^(get|query)/] } }],
533+
errors: 2,
534+
},
535+
{
536+
name: 'requiredPattern',
537+
code: 'type Test { enabled: Boolean! }',
538+
options: [
539+
{
540+
'FieldDefinition[gqlType.gqlType.name.value=Boolean]': {
541+
style: 'camelCase',
542+
requiredPattern: [/^(is|has)/],
543+
},
544+
},
545+
],
546+
errors: 1,
547+
},
534548
],
535549
});

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GraphQLESLintRule, GraphQLESLintRuleListener, ValueOf } from '../../typ
55
import {
66
ARRAY_DEFAULT_OPTIONS,
77
convertCase,
8+
displayNodeName,
89
englishJoinWords,
910
truthy,
1011
TYPES_KINDS,
@@ -47,6 +48,11 @@ const schemaOption = {
4748
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
4849
} as const;
4950

51+
const descriptionPrefixesSuffixes = (name: 'forbiddenPattern' | 'requiredPattern') =>
52+
`> [!WARNING]
53+
>
54+
> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`;
55+
5056
const schema = {
5157
definitions: {
5258
asString: {
@@ -60,10 +66,36 @@ const schema = {
6066
style: { enum: ALLOWED_STYLES },
6167
prefix: { type: 'string' },
6268
suffix: { type: 'string' },
63-
forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
64-
forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
65-
requiredPrefixes: ARRAY_DEFAULT_OPTIONS,
66-
requiredSuffixes: ARRAY_DEFAULT_OPTIONS,
69+
forbiddenPattern: {
70+
...ARRAY_DEFAULT_OPTIONS,
71+
items: {
72+
type: 'object',
73+
},
74+
description: 'Should be of instance of `RegEx`',
75+
},
76+
requiredPattern: {
77+
...ARRAY_DEFAULT_OPTIONS,
78+
items: {
79+
type: 'object',
80+
},
81+
description: 'Should be of instance of `RegEx`',
82+
},
83+
forbiddenPrefixes: {
84+
...ARRAY_DEFAULT_OPTIONS,
85+
description: descriptionPrefixesSuffixes('forbiddenPattern'),
86+
},
87+
forbiddenSuffixes: {
88+
...ARRAY_DEFAULT_OPTIONS,
89+
description: descriptionPrefixesSuffixes('forbiddenPattern'),
90+
},
91+
requiredPrefixes: {
92+
...ARRAY_DEFAULT_OPTIONS,
93+
description: descriptionPrefixesSuffixes('requiredPattern'),
94+
},
95+
requiredSuffixes: {
96+
...ARRAY_DEFAULT_OPTIONS,
97+
description: descriptionPrefixesSuffixes('requiredPattern'),
98+
},
6799
ignorePattern: {
68100
type: 'string',
69101
description: 'Option to skip validation of some words, e.g. acronyms',
@@ -118,6 +150,8 @@ type PropertySchema = {
118150
style?: AllowedStyle;
119151
suffix?: string;
120152
prefix?: string;
153+
forbiddenPattern?: RegExp[];
154+
requiredPattern?: RegExp[];
121155
forbiddenPrefixes?: string[];
122156
forbiddenSuffixes?: string[];
123157
requiredPrefixes?: string[];
@@ -341,8 +375,9 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
341375
ignorePattern,
342376
requiredPrefixes,
343377
requiredSuffixes,
378+
forbiddenPattern,
379+
requiredPattern,
344380
} = normalisePropertyOption(selector);
345-
const nodeType = KindToDisplayName[n.kind] || n.kind;
346381
const nodeName = node.value;
347382
const error = getError();
348383
if (error) {
@@ -352,7 +387,12 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
352387
const suggestedNames = renameToNames.map(
353388
renameToName => leadingUnderscores + renameToName + trailingUnderscores,
354389
);
355-
report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedNames);
390+
const name = displayNodeName(n);
391+
report(
392+
node,
393+
`${name[0].toUpperCase()}${name.slice(1)} should ${errorMessage}`,
394+
suggestedNames,
395+
);
356396
}
357397

358398
function getError(): {
@@ -375,6 +415,19 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
375415
renameToNames: [name + suffix],
376416
};
377417
}
418+
const forbidden = forbiddenPattern?.find(pattern => pattern.test(name));
419+
if (forbidden) {
420+
return {
421+
errorMessage: `not contain the forbidden pattern "${forbidden}"`,
422+
renameToNames: [name.replace(forbidden, '')],
423+
};
424+
}
425+
if (requiredPattern && !requiredPattern.some(pattern => pattern.test(name))) {
426+
return {
427+
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPattern.map(re => re.source))}`,
428+
renameToNames: [],
429+
};
430+
}
378431
const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix));
379432
if (forbiddenPrefix) {
380433
return {

0 commit comments

Comments
 (0)