Skip to content
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