Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ntroduce forbiddenPattern and requiredPattern options for naming-convention rule and deprecate forbiddenPrefixes, forbiddenSuffixes and requiredPrefixes and requiredSuffixes #2780

Merged
merged 14 commits into from
Nov 28, 2024
6 changes: 6 additions & 0 deletions .changeset/happy-bottles-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-eslint/eslint-plugin': minor
---

introduce `forbiddenPattern` and `requiredPattern` options for `naming-convention` rule and
deprecate `forbiddenPrefixes`, `forbiddenSuffixes` and `requiredPrefixes` and `requiredSuffixes`
16 changes: 8 additions & 8 deletions packages/plugin/__tests__/__snapshots__/examples.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`Examples > should work in monorepo 1`] = `
endColumn: 15,
endLine: 1,
line: 1,
message: Operation "getUsers" should be in PascalCase format,
message: Query "getUsers" should be in PascalCase format,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -198,7 +198,7 @@ exports[`Examples > should work in monorepo 2`] = `
endColumn: 15,
endLine: 1,
line: 1,
message: Operation "getUsers" should be in PascalCase format,
message: Query "getUsers" should be in PascalCase format,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -583,7 +583,7 @@ exports[`Examples > should work in svelte 1`] = `
{
column: 0,
line: 1,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand All @@ -610,7 +610,7 @@ exports[`Examples > should work in svelte 2`] = `
{
column: 0,
line: 1,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -675,7 +675,7 @@ exports[`Examples > should work in vue 1`] = `
endColumn: 19,
endLine: 16,
line: 16,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -714,7 +714,7 @@ exports[`Examples > should work in vue 2`] = `
{
column: 0,
line: 1,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -822,7 +822,7 @@ exports[`Examples > should work on \`.js\` files 1`] = `
endColumn: 18,
endLine: 12,
line: 12,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down Expand Up @@ -920,7 +920,7 @@ exports[`Examples > should work on \`.js\` files 2`] = `
endColumn: 18,
endLine: 12,
line: 12,
message: Operation "UserQuery" should not have "Query" suffix,
message: Query "UserQuery" should not have "Query" suffix,
nodeType: Name,
ruleId: @graphql-eslint/naming-convention,
severity: 2,
Expand Down
112 changes: 63 additions & 49 deletions packages/plugin/src/rules/naming-convention/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,49 +259,44 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
],
errors: [
{
message:
'Input type "_idOperatorsFilterFindManyUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterFindManyUserInput" should be in PascalCase format',
},
{
message: 'Input type "_idOperatorsFilterFindOneUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterFindOneUserInput" should be in PascalCase format',
},
{
message:
'Input type "_idOperatorsFilterRemoveManyUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterRemoveManyUserInput" should be in PascalCase format',
},
{
message:
'Input type "_idOperatorsFilterRemoveOneUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterRemoveOneUserInput" should be in PascalCase format',
},
{
message:
'Input type "_idOperatorsFilterUpdateManyUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterUpdateManyUserInput" should be in PascalCase format',
},
{
message:
'Input type "_idOperatorsFilterUpdateOneUserInput" should be in PascalCase format',
message: 'Input "_idOperatorsFilterUpdateOneUserInput" should be in PascalCase format',
},
{ message: 'Input type "_idOperatorsFilterUserInput" should be in PascalCase format' },
{ message: 'Enumeration value "male" should be in UPPER_CASE format' },
{ message: 'Enumeration value "female" should be in UPPER_CASE format' },
{ message: 'Enumeration value "ladyboy" should be in UPPER_CASE format' },
{ message: 'Enumeration value "basic" should be in UPPER_CASE format' },
{ message: 'Enumeration value "fluent" should be in UPPER_CASE format' },
{ message: 'Enumeration value "native" should be in UPPER_CASE format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input property "OR" should be in camelCase format' },
{ message: 'Input property "AND" should be in camelCase format' },
{ message: 'Input "_idOperatorsFilterUserInput" should be in PascalCase format' },
{ message: 'Enum value "male" should be in UPPER_CASE format' },
{ message: 'Enum value "female" should be in UPPER_CASE format' },
{ message: 'Enum value "ladyboy" should be in UPPER_CASE format' },
{ message: 'Enum value "basic" should be in UPPER_CASE format' },
{ message: 'Enum value "fluent" should be in UPPER_CASE format' },
{ message: 'Enum value "native" should be in UPPER_CASE format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
{ message: 'Input value "OR" should be in camelCase format' },
{ message: 'Input value "AND" should be in camelCase format' },
],
},
{
Expand All @@ -313,16 +308,16 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
},
],
errors: [
{ message: 'Enumerator "B" should be in camelCase format' },
{ message: 'Enumeration value "test" should be in UPPER_CASE format' },
{ message: 'Enum "B" should be in camelCase format' },
{ message: 'Enum value "test" should be in UPPER_CASE format' },
],
},
{
code: 'input test { _Value: String }',
options: [{ types: 'PascalCase', InputValueDefinition: 'snake_case' }],
errors: [
{ message: 'Input type "test" should be in PascalCase format' },
{ message: 'Input property "_Value" should be in snake_case format' },
{ message: 'Input "test" should be in PascalCase format' },
{ message: 'Input value "_Value" should be in snake_case format' },
{ message: 'Leading underscores are not allowed' },
],
},
Expand All @@ -338,8 +333,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
errors: [
{ message: 'Type "TypeOne" should be in camelCase format' },
{ message: 'Field "aField" should have "AAA" suffix' },
{ message: 'Enumeration value "VALUE_ONE" should have "ENUM" suffix' },
{ message: 'Enumeration value "VALUE_TWO" should have "ENUM" suffix' },
{ message: 'Enum value "VALUE_ONE" should have "ENUM" suffix' },
{ message: 'Enum value "VALUE_TWO" should have "ENUM" suffix' },
],
},
{
Expand All @@ -353,8 +348,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
],
errors: [
{ message: 'Field "aField" should have "Field" prefix' },
{ message: 'Enumeration value "A_ENUM_VALUE_ONE" should have "ENUM" prefix' },
{ message: 'Enumeration value "VALUE_TWO" should have "ENUM" prefix' },
{ message: 'Enum value "A_ENUM_VALUE_ONE" should have "ENUM" prefix' },
{ message: 'Enum value "VALUE_TWO" should have "ENUM" prefix' },
],
},
{
Expand Down Expand Up @@ -385,8 +380,8 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
code: 'query Foo { foo } query getBar { bar }',
options: [{ OperationDefinition: { style: 'camelCase', forbiddenPrefixes: ['get'] } }],
errors: [
{ message: 'Operation "Foo" should be in camelCase format' },
{ message: 'Operation "getBar" should not have "get" prefix' },
{ message: 'Query "Foo" should be in camelCase format' },
{ message: 'Query "getBar" should not have "get" prefix' },
],
},
{
Expand Down Expand Up @@ -448,13 +443,13 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
`,
options: (rule.meta.docs!.configOptions as any).operations,
errors: [
{ message: 'Operation "TestQuery" should not have "Query" suffix' },
{ message: 'Operation "QueryTest" should not have "Query" prefix' },
{ message: 'Operation "GetQuery" should not have "Get" prefix' },
{ message: 'Operation "TestMutation" should not have "Mutation" suffix' },
{ message: 'Operation "MutationTest" should not have "Mutation" prefix' },
{ message: 'Operation "TestSubscription" should not have "Subscription" suffix' },
{ message: 'Operation "SubscriptionTest" should not have "Subscription" prefix' },
{ message: 'Query "TestQuery" should not have "Query" suffix' },
{ message: 'Query "QueryTest" should not have "Query" prefix' },
{ message: 'Query "GetQuery" should not have "Get" prefix' },
{ message: 'Mutation "TestMutation" should not have "Mutation" suffix' },
{ message: 'Mutation "MutationTest" should not have "Mutation" prefix' },
{ message: 'Subscription "TestSubscription" should not have "Subscription" suffix' },
{ message: 'Subscription "SubscriptionTest" should not have "Subscription" prefix' },
{ message: 'Fragment "TestFragment" should not have "Fragment" suffix' },
{ message: 'Fragment "FragmentTest" should not have "Fragment" prefix' },
],
Expand Down Expand Up @@ -531,5 +526,24 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
],
errors: 2,
},
{
name: 'forbiddenPattern',
code: 'query queryFoo { foo } query getBar { bar }',
options: [{ OperationDefinition: { forbiddenPattern: [/^(get|query)/] } }],
errors: 2,
},
{
name: 'requiredPattern',
code: 'type Test { enabled: Boolean! }',
options: [
{
'FieldDefinition[gqlType.gqlType.name.value=Boolean]': {
style: 'camelCase',
requiredPattern: [/^(is|has)/],
},
},
],
errors: 1,
},
],
});
65 changes: 59 additions & 6 deletions packages/plugin/src/rules/naming-convention/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GraphQLESLintRule, GraphQLESLintRuleListener, ValueOf } from '../../typ
import {
ARRAY_DEFAULT_OPTIONS,
convertCase,
displayNodeName,
englishJoinWords,
truthy,
TYPES_KINDS,
Expand Down Expand Up @@ -47,6 +48,11 @@ const schemaOption = {
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
} as const;

const descriptionPrefixesSuffixes = (name: 'forbiddenPattern' | 'requiredPattern') =>
`> [!WARNING]
>
> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`;

const schema = {
definitions: {
asString: {
Expand All @@ -60,10 +66,36 @@ const schema = {
style: { enum: ALLOWED_STYLES },
prefix: { type: 'string' },
suffix: { type: 'string' },
forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
requiredPrefixes: ARRAY_DEFAULT_OPTIONS,
requiredSuffixes: ARRAY_DEFAULT_OPTIONS,
forbiddenPattern: {
...ARRAY_DEFAULT_OPTIONS,
items: {
type: 'object',
},
description: 'Should be of instance of `RegEx`',
},
requiredPattern: {
...ARRAY_DEFAULT_OPTIONS,
items: {
type: 'object',
},
description: 'Should be of instance of `RegEx`',
},
forbiddenPrefixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('forbiddenPattern'),
},
forbiddenSuffixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('forbiddenPattern'),
},
requiredPrefixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('requiredPattern'),
},
requiredSuffixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('requiredPattern'),
},
ignorePattern: {
type: 'string',
description: 'Option to skip validation of some words, e.g. acronyms',
Expand Down Expand Up @@ -118,6 +150,8 @@ type PropertySchema = {
style?: AllowedStyle;
suffix?: string;
prefix?: string;
forbiddenPattern?: RegExp[];
requiredPattern?: RegExp[];
forbiddenPrefixes?: string[];
forbiddenSuffixes?: string[];
requiredPrefixes?: string[];
Expand Down Expand Up @@ -341,8 +375,9 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
ignorePattern,
requiredPrefixes,
requiredSuffixes,
forbiddenPattern,
requiredPattern,
} = normalisePropertyOption(selector);
const nodeType = KindToDisplayName[n.kind] || n.kind;
const nodeName = node.value;
const error = getError();
if (error) {
Expand All @@ -352,7 +387,12 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
const suggestedNames = renameToNames.map(
renameToName => leadingUnderscores + renameToName + trailingUnderscores,
);
report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedNames);
const name = displayNodeName(n);
report(
node,
`${name[0].toUpperCase()}${name.slice(1)} should ${errorMessage}`,
suggestedNames,
);
}

function getError(): {
Expand All @@ -375,6 +415,19 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
renameToNames: [name + suffix],
};
}
const forbidden = forbiddenPattern?.find(pattern => pattern.test(name));
if (forbidden) {
return {
errorMessage: `not contain the forbidden pattern "${forbidden}"`,
renameToNames: [name.replace(forbidden, '')],
};
}
if (requiredPattern && !requiredPattern.some(pattern => pattern.test(name))) {
return {
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPattern.map(re => re.source))}`,
renameToNames: [],
};
}
const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix));
if (forbiddenPrefix) {
return {
Expand Down
Loading
Loading