From da1617b4574dda0c03fb2a42b9454fa1381427eb Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 02:35:46 +0700 Subject: [PATCH 01/14] aa --- .../src/rules/naming-convention/index.test.ts | 22 ++++++++- .../src/rules/naming-convention/snapshot.md | 8 ++-- .../src/rules/require-description/index.ts | 48 ++++++++----------- website/app/layout.tsx | 2 +- website/content/rules/naming-convention.mdx | 17 ++----- 5 files changed, 50 insertions(+), 47 deletions(-) diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index ba7f5e77cb3..c2227f0d6ef 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -237,6 +237,24 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + name: 'custom case', + options: [ + { FragmentDefinition: { style: 'PascalCase', requiredPattern: /_(?.+?)$/ } }, + ], + code: /* GraphQL */ ` + fragment UserProfileFields_user on User { + id + name + email + } + `, + parserOptions: { + graphQLConfig: { + schema: 'type User', + }, + }, + }, ], invalid: [ { @@ -536,13 +554,13 @@ ruleTester.run('naming-convention', rule, { errors: 2, }, { - name: 'requiredPatterns', + name: 'requiredPattern', code: 'type Test { enabled: Boolean! }', options: [ { 'FieldDefinition[gqlType.gqlType.name.value=Boolean]': { style: 'camelCase', - requiredPatterns: [/^(is|has)/], + requiredPattern: /^(is|has)/, }, }, ], diff --git a/packages/plugin/src/rules/naming-convention/snapshot.md b/packages/plugin/src/rules/naming-convention/snapshot.md index 12750ad44fc..94a4c6cc3eb 100644 --- a/packages/plugin/src/rules/naming-convention/snapshot.md +++ b/packages/plugin/src/rules/naming-convention/snapshot.md @@ -1973,7 +1973,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 13 | fragment Test on Test { id } `; -exports[`naming-convention > invalid > requiredPatterns 1`] = ` +exports[`naming-convention > invalid > requiredPattern 1`] = ` #### ⌨️ Code 1 | type Test { enabled: Boolean! } @@ -1983,16 +1983,14 @@ exports[`naming-convention > invalid > requiredPatterns 1`] = ` { "FieldDefinition[gqlType.gqlType.name.value=Boolean]": { "style": "camelCase", - "requiredPatterns": [ - "/^(is|has)/" - ] + "requiredPattern": "/^(is|has)/" } } #### ❌ Error > 1 | type Test { enabled: Boolean! } - | ^^^^^^^ Field "enabled" should contain the required pattern: ^(is|has) + | ^^^^^^^ Field "enabled" should contain the required pattern: /^(is|has)/ `; exports[`naming-convention > invalid > schema-recommended config 1`] = ` diff --git a/packages/plugin/src/rules/require-description/index.ts b/packages/plugin/src/rules/require-description/index.ts index bdcb4a8d2d6..12bd6cbd28b 100644 --- a/packages/plugin/src/rules/require-description/index.ts +++ b/packages/plugin/src/rules/require-description/index.ts @@ -1,4 +1,5 @@ import { ASTKindToNode, Kind, TokenKind } from 'graphql'; +import { FromSchema } from 'json-schema-to-ts'; import { getRootTypeNames } from '@graphql-tools/utils'; import { GraphQLESTreeNode } from '../../estree-converter/index.js'; import { GraphQLESLintRule, ValueOf } from '../../types.js'; @@ -26,6 +27,24 @@ type AllowedKind = (typeof ALLOWED_KINDS)[number]; type AllowedKindToNode = Pick; type SelectorNode = GraphQLESTreeNode>; +const entries: Record = Object.create(null); + +for (const kind of [...ALLOWED_KINDS].sort()) { + let description = `> [!NOTE] +> +> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`; + if (kind === Kind.OPERATION_DEFINITION) { + description += [ + '', + '', + '> [!WARNING]', + '>', + '> You must use only comment syntax `#` and not description syntax `"""` or `"`.', + ].join('\n'); + } + entries[kind] = { type: 'boolean', description }; +} + const schema = { type: 'array', minItems: 1, @@ -49,37 +68,12 @@ const schema = { ...ARRAY_DEFAULT_OPTIONS, description: ['Ignore specific selectors', eslintSelectorsTip].join('\n'), }, - ...Object.fromEntries( - [...ALLOWED_KINDS].sort().map(kind => { - let description = `> [!NOTE] -> -> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`; - if (kind === Kind.OPERATION_DEFINITION) { - description += [ - '', - '', - '> [!WARNING]', - '>', - '> You must use only comment syntax `#` and not description syntax `"""` or `"`.', - ].join('\n'); - } - return [kind, { type: 'boolean', description }]; - }), - ), + ...entries, }, }, } as const; -// TODO try import { FromSchema } from 'json-schema-to-ts'; -export type RuleOptions = [ - { - [key in AllowedKind]?: boolean; - } & { - types?: true; - rootField?: true; - ignoredSelectors?: string[]; - }, -]; +export type RuleOptions = FromSchema; export const rule: GraphQLESLintRule = { meta: { diff --git a/website/app/layout.tsx b/website/app/layout.tsx index 55b509ad5c4..87ad8a5d7ec 100644 --- a/website/app/layout.tsx +++ b/website/app/layout.tsx @@ -59,7 +59,7 @@ const RootLayout: FC<{ icon: , children: 'GitHub', }, - ] + ], }} > {children} diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index 9011f499497..b18caf82474 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -338,6 +338,8 @@ Properties of the `asObject` object: ### `style` (enum) +One of: `camelCase`, `PascalCase`, `snake_case`, `UPPER_CASE` + This element must be one of the following enum values: - `camelCase` @@ -362,19 +364,10 @@ Additional restrictions: - Minimum items: `1` - Unique items: `true` -### `requiredPatterns` (array) +### `requiredPattern` (object) Should be of instance of `RegEx` -The object is an array with all elements of the type `object`. - -The array object has the following properties: - -Additional restrictions: - -- Minimum items: `1` -- Unique items: `true` - ### `forbiddenPrefixes` (array) > [!WARNING] @@ -408,7 +401,7 @@ Additional restrictions: > [!WARNING] > > This option is deprecated and will be removed in the next major release. Use -> [`requiredPatterns`](#requiredpatterns-array) instead. +> [`requiredPattern`](#requiredpattern-object) instead. The object is an array with all elements of the type `string`. @@ -422,7 +415,7 @@ Additional restrictions: > [!WARNING] > > This option is deprecated and will be removed in the next major release. Use -> [`requiredPatterns`](#requiredpatterns-array) instead. +> [`requiredPattern`](#requiredpattern-object) instead. The object is an array with all elements of the type `string`. From 57fb6b737942e5228bb5478184f1b97d30617245 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 02:53:07 +0700 Subject: [PATCH 02/14] Merge branch 'master' into use-search-params --- packages/rule-tester/src/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/rule-tester/src/index.ts b/packages/rule-tester/src/index.ts index 07e9c72b29b..4e8893d27ef 100644 --- a/packages/rule-tester/src/index.ts +++ b/packages/rule-tester/src/index.ts @@ -26,13 +26,11 @@ function applyFix(code: string, { range, text }: Rule.Fix): string { } // @ts-expect-error -- Extend RegExp with a custom toJSON method -RegExp.prototype.toJSON = function () { - return `/${this.source}/${this.flags}`; -}; +RegExp.prototype.toJSON = RegExp.prototype.toString; export class RuleTester extends ESLintRuleTester { fromMockFile(path: string): string { - return readFileSync(resolve(__dirname, `../../plugin/__tests__/mocks/${path}`), 'utf-8'); + return readFileSync(resolve(__dirname, `../../plugin/__tests__/mocks/${path}`), 'utf8'); } // @ts-expect-error -- fix later From eb51d337f79dad17fdfc2c63ebf81ea9a46f1a6c Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 02:59:14 +0700 Subject: [PATCH 03/14] Merge branch 'master' into use-search-params --- .../rules/match-document-filename/index.ts | 12 +-- .../src/rules/naming-convention/index.test.ts | 13 ++- .../src/rules/naming-convention/index.ts | 79 +++++++++++++------ .../content/rules/match-document-filename.mdx | 2 + 4 files changed, 73 insertions(+), 33 deletions(-) diff --git a/packages/plugin/src/rules/match-document-filename/index.ts b/packages/plugin/src/rules/match-document-filename/index.ts index 282de79f2aa..2b887827349 100644 --- a/packages/plugin/src/rules/match-document-filename/index.ts +++ b/packages/plugin/src/rules/match-document-filename/index.ts @@ -28,18 +28,20 @@ const schemaOption = { oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }], } as const; +const caseSchema = { + enum: CASE_STYLES, + description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`, +} + const schema = { definitions: { - asString: { - enum: CASE_STYLES, - description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`, - }, + asString: caseSchema, asObject: { type: 'object', additionalProperties: false, minProperties: 1, properties: { - style: { enum: CASE_STYLES }, + style: caseSchema, suffix: { type: 'string' }, prefix: { type: 'string' }, }, diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index c2227f0d6ef..0588cfa7761 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -238,12 +238,17 @@ ruleTester.run('naming-convention', rule, { ], }, { - name: 'custom case', + name: 'requiredPattern for typeName', options: [ - { FragmentDefinition: { style: 'PascalCase', requiredPattern: /_(?.+?)$/ } }, + { + FragmentDefinition: { + style: 'PascalCase', + requiredPattern: /^(?.+?)_/, + }, + }, ], code: /* GraphQL */ ` - fragment UserProfileFields_user on User { + fragment myUser_UserProfileFields on MyUser { id name email @@ -251,7 +256,7 @@ ruleTester.run('naming-convention', rule, { `, parserOptions: { graphQLConfig: { - schema: 'type User', + schema: 'type MyUser', }, }, }, diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index 739128800b9..d3831720b1b 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -4,6 +4,7 @@ import { GraphQLESTreeNode } from '../../estree-converter/index.js'; import { GraphQLESLintRule, GraphQLESLintRuleListener, ValueOf } from '../../types.js'; import { ARRAY_DEFAULT_OPTIONS, + CaseStyle, convertCase, displayNodeName, englishJoinWords, @@ -47,22 +48,24 @@ const schemaOption = { oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }], } as const; -const descriptionPrefixesSuffixes = (name: 'forbiddenPatterns' | 'requiredPatterns') => +const descriptionPrefixesSuffixes = (name: 'forbiddenPatterns' | 'requiredPattern', id: string) => `> [!WARNING] > -> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`; +> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${id}) instead.`; + +const caseSchema = { + enum: ALLOWED_STYLES, + description: `One of: ${ALLOWED_STYLES.map(t => `\`${t}\``).join(', ')}`, +}; const schema = { definitions: { - asString: { - enum: ALLOWED_STYLES, - description: `One of: ${ALLOWED_STYLES.map(t => `\`${t}\``).join(', ')}`, - }, + asString: caseSchema, asObject: { type: 'object', additionalProperties: false, properties: { - style: { enum: ALLOWED_STYLES }, + style: caseSchema, prefix: { type: 'string' }, suffix: { type: 'string' }, forbiddenPatterns: { @@ -72,28 +75,25 @@ const schema = { }, description: 'Should be of instance of `RegEx`', }, - requiredPatterns: { - ...ARRAY_DEFAULT_OPTIONS, - items: { - type: 'object', - }, + requiredPattern: { + type: 'object', description: 'Should be of instance of `RegEx`', }, forbiddenPrefixes: { ...ARRAY_DEFAULT_OPTIONS, - description: descriptionPrefixesSuffixes('forbiddenPatterns'), + description: descriptionPrefixesSuffixes('forbiddenPatterns', 'forbiddenpatterns-array'), }, forbiddenSuffixes: { ...ARRAY_DEFAULT_OPTIONS, - description: descriptionPrefixesSuffixes('forbiddenPatterns'), + description: descriptionPrefixesSuffixes('forbiddenPatterns', 'forbiddenpatterns-array'), }, requiredPrefixes: { ...ARRAY_DEFAULT_OPTIONS, - description: descriptionPrefixesSuffixes('requiredPatterns'), + description: descriptionPrefixesSuffixes('requiredPattern', 'requiredpattern-object'), }, requiredSuffixes: { ...ARRAY_DEFAULT_OPTIONS, - description: descriptionPrefixesSuffixes('requiredPatterns'), + description: descriptionPrefixesSuffixes('requiredPattern', 'requiredpattern-object'), }, ignorePattern: { type: 'string', @@ -152,7 +152,7 @@ type PropertySchema = { suffix?: string; prefix?: string; forbiddenPatterns?: RegExp[]; - requiredPatterns?: RegExp[]; + requiredPattern?: RegExp; forbiddenPrefixes?: string[]; forbiddenSuffixes?: string[]; requiredPrefixes?: string[]; @@ -378,7 +378,7 @@ export const rule: GraphQLESLintRule = { requiredPrefixes, requiredSuffixes, forbiddenPatterns, - requiredPatterns, + requiredPattern, } = normalisePropertyOption(selector); const nodeName = node.value; const error = getError(); @@ -401,7 +401,7 @@ export const rule: GraphQLESLintRule = { errorMessage: string; renameToNames: string[]; } | void { - const name = nodeName.replace(/(^_+)|(_+$)/g, ''); + let name = nodeName.replace(/(^_+)|(_+$)/g, ''); if (ignorePattern && new RegExp(ignorePattern, 'u').test(name)) { if ('name' in n) { ignoredNodes.add(n.name); @@ -427,11 +427,42 @@ export const rule: GraphQLESLintRule = { renameToNames: [name.replace(forbidden, '')], }; } - if (requiredPatterns && !requiredPatterns.some(pattern => pattern.test(name))) { - return { - errorMessage: `contain the required pattern: ${englishJoinWords(requiredPatterns.map(re => re.source))}`, - renameToNames: [], - }; + if (requiredPattern) { + if (requiredPattern.source.includes('(?<')) { + try { + name = name.replace(requiredPattern, (originalString, ...args) => { + const groups = args.at(-1); + for (const [key, value] of Object.entries(groups)) { + const isTypeName = /_typeName$/.test(key); + const styleName = isTypeName ? key.replace(/_typeName$/, '') : key; + const caseRegex = StyleToRegex[styleName as AllowedStyle]; + if (!caseRegex) { + throw new Error('Invalid case style in `requiredPatterns` option'); + } + if (isTypeName) { + // @ts-expect-error + if (value === convertCase(styleName as CaseStyle, n.typeInfo().gqlType.name)) { + return ''; + } + } else if (value === convertCase(styleName as CaseStyle, value as string)) { + return ''; + } + throw new Error(`contain the required pattern: ${requiredPattern}`); + } + return originalString; + }); + } catch (error) { + return { + errorMessage: (error as Error).message, + renameToNames: [], + }; + } + } else if (!requiredPattern.test(name)) { + return { + errorMessage: `contain the required pattern: ${requiredPattern}`, + renameToNames: [], + }; + } } const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix)); if (forbiddenPrefix) { diff --git a/website/content/rules/match-document-filename.mdx b/website/content/rules/match-document-filename.mdx index b5d0df00537..576c8401e76 100644 --- a/website/content/rules/match-document-filename.mdx +++ b/website/content/rules/match-document-filename.mdx @@ -167,6 +167,8 @@ Properties of the `asObject` object: ### `style` (enum) +One of: `camelCase`, `PascalCase`, `snake_case`, `UPPER_CASE`, `kebab-case`, `matchDocumentStyle` + This element must be one of the following enum values: - `camelCase` From 9485fae18b171ee5f7ee3ec23560115bde918079 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 02:59:33 +0700 Subject: [PATCH 04/14] Merge branch 'master' into use-search-params --- packages/plugin/src/rules/match-document-filename/index.ts | 2 +- packages/plugin/src/rules/naming-convention/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/src/rules/match-document-filename/index.ts b/packages/plugin/src/rules/match-document-filename/index.ts index 2b887827349..6aa595b0bf9 100644 --- a/packages/plugin/src/rules/match-document-filename/index.ts +++ b/packages/plugin/src/rules/match-document-filename/index.ts @@ -31,7 +31,7 @@ const schemaOption = { const caseSchema = { enum: CASE_STYLES, description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`, -} +}; const schema = { definitions: { diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index d3831720b1b..3589845a3ed 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -440,7 +440,7 @@ export const rule: GraphQLESLintRule = { throw new Error('Invalid case style in `requiredPatterns` option'); } if (isTypeName) { - // @ts-expect-error + // @ts-expect-error if (value === convertCase(styleName as CaseStyle, n.typeInfo().gqlType.name)) { return ''; } From e783ee1d64d4d931797b0e88ec20a5b90790206e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 03:09:49 +0700 Subject: [PATCH 05/14] Merge branch 'master' into use-search-params --- .../src/rules/naming-convention/index.test.ts | 49 ++++++++++++++++++- .../src/rules/naming-convention/index.ts | 3 ++ .../src/rules/naming-convention/snapshot.md | 25 ++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index 0588cfa7761..ebafffe2fcd 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -238,7 +238,7 @@ ruleTester.run('naming-convention', rule, { ], }, { - name: 'requiredPattern for typeName', + name: 'requiredPattern for typeName in prefix', options: [ { FragmentDefinition: { @@ -260,6 +260,29 @@ ruleTester.run('naming-convention', rule, { }, }, }, + { + name: 'requiredPattern for typeName in suffix', + options: [ + { + FragmentDefinition: { + style: 'PascalCase', + requiredPattern: /_(?.+?)$/, + }, + }, + ], + code: /* GraphQL */ ` + fragment UserProfileFields_my_user on MyUser { + id + name + email + } + `, + parserOptions: { + graphQLConfig: { + schema: 'type MyUser', + }, + }, + }, ], invalid: [ { @@ -571,5 +594,29 @@ ruleTester.run('naming-convention', rule, { ], errors: 1, }, + { + name: 'requiredPattern for typeName in suffix', + options: [ + { + FragmentDefinition: { + style: 'PascalCase', + requiredPattern: /_(?.+?)$/, + }, + }, + ], + code: /* GraphQL */ ` + fragment UserProfileFields on MyUser { + id + name + email + } + `, + parserOptions: { + graphQLConfig: { + schema: 'type MyUser', + }, + }, + errors: 1, + }, ], }); diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index 3589845a3ed..e72853411fb 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -451,6 +451,9 @@ export const rule: GraphQLESLintRule = { } return originalString; }); + if (name === nodeName) { + throw new Error(`contain the required pattern: ${requiredPattern}`); + } } catch (error) { return { errorMessage: (error as Error).message, diff --git a/packages/plugin/src/rules/naming-convention/snapshot.md b/packages/plugin/src/rules/naming-convention/snapshot.md index 94a4c6cc3eb..1b4d6ad54e3 100644 --- a/packages/plugin/src/rules/naming-convention/snapshot.md +++ b/packages/plugin/src/rules/naming-convention/snapshot.md @@ -1993,6 +1993,31 @@ exports[`naming-convention > invalid > requiredPattern 1`] = ` | ^^^^^^^ Field "enabled" should contain the required pattern: /^(is|has)/ `; +exports[`naming-convention > invalid > requiredPattern for typeName in suffix 1`] = ` +#### ⌨️ Code + + 1 | fragment UserProfileFields on MyUser { + 2 | id + 3 | name + 4 | email + 5 | } + +#### ⚙️ Options + + { + "FragmentDefinition": { + "style": "PascalCase", + "requiredPattern": "/_(?.+?)$/" + } + } + +#### ❌ Error + + > 1 | fragment UserProfileFields on MyUser { + | ^^^^^^^^^^^^^^^^^ Fragment "UserProfileFields" should contain the required pattern: /_(?.+?)$/ + 2 | id +`; + exports[`naming-convention > invalid > schema-recommended config 1`] = ` #### ⌨️ Code From d477a8e202c0a47f071bbee93ada0f2ad5a0c5f1 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 03:35:36 +0700 Subject: [PATCH 06/14] Merge branch 'master' into use-search-params --- .changeset/polite-impalas-float.md | 7 +++ .../src/rules/naming-convention/index.test.ts | 30 +++++------ .../src/rules/naming-convention/index.ts | 53 ++++++++++++------- .../src/rules/naming-convention/snapshot.md | 14 +++-- 4 files changed, 59 insertions(+), 45 deletions(-) create mode 100644 .changeset/polite-impalas-float.md diff --git a/.changeset/polite-impalas-float.md b/.changeset/polite-impalas-float.md new file mode 100644 index 00000000000..a9c0b4d55be --- /dev/null +++ b/.changeset/polite-impalas-float.md @@ -0,0 +1,7 @@ +--- +'@graphql-eslint/eslint-plugin': patch +--- + +- allow to config `naming-convention` for Relay fragment convention `_` via `requiredPattern` option + +- replace `requiredPatterns: RegEx[]` by `requiredPattern: RegEx` option diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index ebafffe2fcd..aecdf3be8da 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -238,48 +238,44 @@ ruleTester.run('naming-convention', rule, { ], }, { - name: 'requiredPattern for typeName in prefix', + name: 'requiredPattern with case style in prefix', options: [ { FragmentDefinition: { style: 'PascalCase', - requiredPattern: /^(?.+?)_/, + requiredPattern: /^(?.+?)_/, }, }, ], code: /* GraphQL */ ` - fragment myUser_UserProfileFields on MyUser { + fragment myUser_UserProfileFields on User { id - name - email } `, parserOptions: { graphQLConfig: { - schema: 'type MyUser', + schema: 'type User', }, }, }, { - name: 'requiredPattern for typeName in suffix', + name: 'requiredPattern with case style in suffix', options: [ { FragmentDefinition: { style: 'PascalCase', - requiredPattern: /_(?.+?)$/, + requiredPattern: /_(?.+?)$/, }, }, ], code: /* GraphQL */ ` - fragment UserProfileFields_my_user on MyUser { + fragment UserProfileFields_my_user on User { id - name - email } `, parserOptions: { graphQLConfig: { - schema: 'type MyUser', + schema: 'type User', }, }, }, @@ -595,25 +591,23 @@ ruleTester.run('naming-convention', rule, { errors: 1, }, { - name: 'requiredPattern for typeName in suffix', + name: 'requiredPattern with case style in suffix', options: [ { FragmentDefinition: { style: 'PascalCase', - requiredPattern: /_(?.+?)$/, + requiredPattern: /_(?.+?)$/, }, }, ], code: /* GraphQL */ ` - fragment UserProfileFields on MyUser { + fragment UserProfileFields on User { id - name - email } `, parserOptions: { graphQLConfig: { - schema: 'type MyUser', + schema: 'type User', }, }, errors: 1, diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index e72853411fb..e4ed15e0111 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -278,6 +278,27 @@ export const rule: GraphQLESLintRule = { } `, }, + { + title: 'Correct (Relay fragment convention `_`)', + usage: [ + { + FragmentDefinition: { + style: 'PascalCase', + requiredPattern: /_(?.+?)$/, + }, + } + ], + code: /* GraphQL */ ` + # schema + type User { + # ... + } + # operations + fragment UserFields_data on User { + # ... + } + ` + } ], configOptions: { schema: [ @@ -401,7 +422,9 @@ export const rule: GraphQLESLintRule = { errorMessage: string; renameToNames: string[]; } | void { - let name = nodeName.replace(/(^_+)|(_+$)/g, ''); + let name = nodeName; + if (allowLeadingUnderscore) name = name.replace(/^_+/, ''); + if (allowTrailingUnderscore) name = name.replace(/_+$/, ''); if (ignorePattern && new RegExp(ignorePattern, 'u').test(name)) { if ('name' in n) { ignoredNodes.add(n.name); @@ -420,31 +443,16 @@ export const rule: GraphQLESLintRule = { renameToNames: [name + suffix], }; } - const forbidden = forbiddenPatterns?.find(pattern => pattern.test(name)); - if (forbidden) { - return { - errorMessage: `not contain the forbidden pattern "${forbidden}"`, - renameToNames: [name.replace(forbidden, '')], - }; - } if (requiredPattern) { if (requiredPattern.source.includes('(?<')) { try { name = name.replace(requiredPattern, (originalString, ...args) => { const groups = args.at(-1); - for (const [key, value] of Object.entries(groups)) { - const isTypeName = /_typeName$/.test(key); - const styleName = isTypeName ? key.replace(/_typeName$/, '') : key; - const caseRegex = StyleToRegex[styleName as AllowedStyle]; - if (!caseRegex) { + for (const [styleName, value] of Object.entries(groups)) { + if (!(styleName in StyleToRegex)) { throw new Error('Invalid case style in `requiredPatterns` option'); } - if (isTypeName) { - // @ts-expect-error - if (value === convertCase(styleName as CaseStyle, n.typeInfo().gqlType.name)) { - return ''; - } - } else if (value === convertCase(styleName as CaseStyle, value as string)) { + if (value === convertCase(styleName as CaseStyle, value as string)) { return ''; } throw new Error(`contain the required pattern: ${requiredPattern}`); @@ -467,6 +475,13 @@ export const rule: GraphQLESLintRule = { }; } } + const forbidden = forbiddenPatterns?.find(pattern => pattern.test(name)); + if (forbidden) { + return { + errorMessage: `not contain the forbidden pattern "${forbidden}"`, + renameToNames: [name.replace(forbidden, '')], + }; + } const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix)); if (forbiddenPrefix) { return { diff --git a/packages/plugin/src/rules/naming-convention/snapshot.md b/packages/plugin/src/rules/naming-convention/snapshot.md index 1b4d6ad54e3..ee50df91edd 100644 --- a/packages/plugin/src/rules/naming-convention/snapshot.md +++ b/packages/plugin/src/rules/naming-convention/snapshot.md @@ -1993,28 +1993,26 @@ exports[`naming-convention > invalid > requiredPattern 1`] = ` | ^^^^^^^ Field "enabled" should contain the required pattern: /^(is|has)/ `; -exports[`naming-convention > invalid > requiredPattern for typeName in suffix 1`] = ` +exports[`naming-convention > invalid > requiredPattern with case style in suffix 1`] = ` #### ⌨️ Code - 1 | fragment UserProfileFields on MyUser { + 1 | fragment UserProfileFields on User { 2 | id - 3 | name - 4 | email - 5 | } + 3 | } #### ⚙️ Options { "FragmentDefinition": { "style": "PascalCase", - "requiredPattern": "/_(?.+?)$/" + "requiredPattern": "/_(?.+?)$/" } } #### ❌ Error - > 1 | fragment UserProfileFields on MyUser { - | ^^^^^^^^^^^^^^^^^ Fragment "UserProfileFields" should contain the required pattern: /_(?.+?)$/ + > 1 | fragment UserProfileFields on User { + | ^^^^^^^^^^^^^^^^^ Fragment "UserProfileFields" should contain the required pattern: /_(?.+?)$/ 2 | id `; From 546bad59bb2011edcb6aea2b8598f9c75d394fbf Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 03:41:08 +0700 Subject: [PATCH 07/14] Merge branch 'master' into use-search-params --- .changeset/polite-impalas-float.md | 3 ++- .../plugin/src/rules/naming-convention/index.ts | 7 ++++--- website/app/play/page.client.tsx | 2 +- website/content/rules/naming-convention.mdx | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.changeset/polite-impalas-float.md b/.changeset/polite-impalas-float.md index a9c0b4d55be..7a019432b03 100644 --- a/.changeset/polite-impalas-float.md +++ b/.changeset/polite-impalas-float.md @@ -2,6 +2,7 @@ '@graphql-eslint/eslint-plugin': patch --- -- allow to config `naming-convention` for Relay fragment convention `_` via `requiredPattern` option +- allow to config `naming-convention` for Relay fragment convention `_` + via `requiredPattern` option - replace `requiredPatterns: RegEx[]` by `requiredPattern: RegEx` option diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index e4ed15e0111..2091d184112 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -286,7 +286,7 @@ export const rule: GraphQLESLintRule = { style: 'PascalCase', requiredPattern: /_(?.+?)$/, }, - } + }, ], code: /* GraphQL */ ` # schema @@ -297,8 +297,8 @@ export const rule: GraphQLESLintRule = { fragment UserFields_data on User { # ... } - ` - } + `, + }, ], configOptions: { schema: [ @@ -448,6 +448,7 @@ export const rule: GraphQLESLintRule = { try { name = name.replace(requiredPattern, (originalString, ...args) => { const groups = args.at(-1); + // eslint-disable-next-line no-unreachable-loop -- expected for (const [styleName, value] of Object.entries(groups)) { if (!(styleName in StyleToRegex)) { throw new Error('Invalid case style in `requiredPatterns` option'); diff --git a/website/app/play/page.client.tsx b/website/app/play/page.client.tsx index 5a8a814bbc5..69428f51c90 100644 --- a/website/app/play/page.client.tsx +++ b/website/app/play/page.client.tsx @@ -48,7 +48,7 @@ function useSetterSearchParams( const handleChange = useCallback((value: string) => { router.push(pathname + createQueryString(searchParams, paramKey, value)); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount return [searchParams.get(paramKey) ?? defaultValue, handleChange]; } diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index b18caf82474..c0ce5c6e828 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -118,6 +118,21 @@ type Account { } ``` +### Correct (Relay fragment convention `_`) + +```graphql +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', requiredPattern: {} } }] + +# schema +type User { + # ... +} +# operations +fragment UserFields_data on User { + # ... +} +``` + ## Config Schema > It's possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts From 6a4230707a78900a6339b03afe904b9dd6c31561 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 03:44:39 +0700 Subject: [PATCH 08/14] Merge branch 'master' into use-search-params --- scripts/generate-docs.ts | 3 +++ website/content/rules/naming-convention.mdx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 9db7ed728d3..b1f478df7fa 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -28,6 +28,9 @@ type Column = { align: 'center' | 'right'; }; +// @ts-expect-error -- Extend RegExp with a custom toJSON method to print RegEx in examples +RegExp.prototype.toJSON = RegExp.prototype.toString; + function printMarkdownTable(columns: (Column | string)[], dataSource: string[][]): string { const headerRow: string[] = []; const alignRow: ('-:' | '-' | ':-:')[] = []; diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index c0ce5c6e828..292a4aa494b 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -121,7 +121,7 @@ type Account { ### Correct (Relay fragment convention `_`) ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', requiredPattern: {} } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', requiredPattern: '/_(?.+?)$/' } }] # schema type User { From 9bc1e0eb8d1b65232765f38af7390908dc3c0031 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 03:57:13 +0700 Subject: [PATCH 09/14] Merge branch 'master' into use-search-params --- .../src/rules/naming-convention/index.ts | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index 2091d184112..72f9645551b 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -184,7 +184,7 @@ export const rule: GraphQLESLintRule = { }, { title: 'Incorrect', - usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }], + usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: [/(^fragment)|(fragment$)/i] } }], code: /* GraphQL */ ` fragment UserFragment on User { # ... @@ -211,7 +211,7 @@ export const rule: GraphQLESLintRule = { }, { title: 'Correct', - usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }], + usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: [/(^fragment)|(fragment$)/i] } }], code: /* GraphQL */ ` fragment UserFields on User { # ... @@ -314,28 +314,22 @@ export const rule: GraphQLESLintRule = { forbiddenSuffixes: ['Query'], }, 'FieldDefinition[parent.name.value=Mutation]': { - forbiddenPrefixes: ['mutation'], - forbiddenSuffixes: ['Mutation'], + forbiddenPatterns: [/(^mutation)|(mutation$)/i], }, 'FieldDefinition[parent.name.value=Subscription]': { - forbiddenPrefixes: ['subscription'], - forbiddenSuffixes: ['Subscription'], + forbiddenPatterns: [/(^subscription)|(subscription$)/i], }, 'EnumTypeDefinition,EnumTypeExtension': { - forbiddenPrefixes: ['Enum'], - forbiddenSuffixes: ['Enum'], + forbiddenPatterns: [/(^enum)|(enum$)/i], }, 'InterfaceTypeDefinition,InterfaceTypeExtension': { - forbiddenPrefixes: ['Interface'], - forbiddenSuffixes: ['Interface'], + forbiddenPatterns: [/(^interface)|(interface$)/i], }, 'UnionTypeDefinition,UnionTypeExtension': { - forbiddenPrefixes: ['Union'], - forbiddenSuffixes: ['Union'], + forbiddenPatterns: [/(^union)|(union$)/i], }, 'ObjectTypeDefinition,ObjectTypeExtension': { - forbiddenPrefixes: ['Type'], - forbiddenSuffixes: ['Type'], + forbiddenPatterns: [/(^type)|(type$)/i], }, }, ], @@ -349,8 +343,7 @@ export const rule: GraphQLESLintRule = { }, FragmentDefinition: { style: 'PascalCase', - forbiddenPrefixes: ['Fragment'], - forbiddenSuffixes: ['Fragment'], + forbiddenPatterns: [/(^fragment)|(fragment$)/i], }, }, ], From 1d83f924e966a81bc9721124d35c06a6e30ac273 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 04:03:35 +0700 Subject: [PATCH 10/14] Merge branch 'master' into use-search-params --- .../src/rules/naming-convention/index.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index 72f9645551b..ce3b2f69d44 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -184,7 +184,14 @@ export const rule: GraphQLESLintRule = { }, { title: 'Incorrect', - usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: [/(^fragment)|(fragment$)/i] } }], + usage: [ + { + FragmentDefinition: { + style: 'PascalCase', + forbiddenPatterns: [/(^fragment)|(fragment$)/i], + }, + }, + ], code: /* GraphQL */ ` fragment UserFragment on User { # ... @@ -193,7 +200,7 @@ export const rule: GraphQLESLintRule = { }, { title: 'Incorrect', - usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }], + usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPatterns: [/^get/i] } }], code: /* GraphQL */ ` type Query { getUsers: [User!]! @@ -211,7 +218,14 @@ export const rule: GraphQLESLintRule = { }, { title: 'Correct', - usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: [/(^fragment)|(fragment$)/i] } }], + usage: [ + { + FragmentDefinition: { + style: 'PascalCase', + forbiddenPatterns: [/(^fragment)|(fragment$)/i], + }, + }, + ], code: /* GraphQL */ ` fragment UserFields on User { # ... @@ -220,7 +234,7 @@ export const rule: GraphQLESLintRule = { }, { title: 'Correct', - usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }], + usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPatterns: [/^get/i] } }], code: /* GraphQL */ ` type Query { users: [User!]! @@ -244,11 +258,11 @@ export const rule: GraphQLESLintRule = { { 'FieldDefinition[gqlType.name.value=Boolean]': { style: 'camelCase', - requiredPrefixes: ['is', 'has'], + requiredPattern: /^(is|has)/, }, 'FieldDefinition[gqlType.gqlType.name.value=Boolean]': { style: 'camelCase', - requiredPrefixes: ['is', 'has'], + requiredPattern: /^(is|has)/, }, }, ], @@ -266,7 +280,7 @@ export const rule: GraphQLESLintRule = { { 'FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]': { style: 'camelCase', - requiredSuffixes: ['SensitiveSecret'], + requiredPattern: /SensitiveSecret$/, }, }, ], @@ -310,8 +324,7 @@ export const rule: GraphQLESLintRule = { DirectiveDefinition: 'camelCase', EnumValueDefinition: 'UPPER_CASE', 'FieldDefinition[parent.name.value=Query]': { - forbiddenPrefixes: ['query', 'get'], - forbiddenSuffixes: ['Query'], + forbiddenPattern: [/^(query|get)/i, /query$/i], }, 'FieldDefinition[parent.name.value=Mutation]': { forbiddenPatterns: [/(^mutation)|(mutation$)/i], @@ -338,8 +351,10 @@ export const rule: GraphQLESLintRule = { VariableDefinition: 'camelCase', OperationDefinition: { style: 'PascalCase', - forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'], - forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'], + forbiddenPattern: [ + /^(query|mutation|subscription|get)/i, + /(query|mutation|subscription)$/i, + ], }, FragmentDefinition: { style: 'PascalCase', From 420bd18dd1416efb2d5ba2a1bc50b920542c322d Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 04:07:49 +0700 Subject: [PATCH 11/14] Merge branch 'master' into use-search-params --- .../src/rules/naming-convention/index.test.ts | 18 +-- .../src/rules/naming-convention/index.ts | 4 +- .../src/rules/naming-convention/snapshot.md | 119 +++++++----------- website/content/rules/naming-convention.mdx | 12 +- 4 files changed, 61 insertions(+), 92 deletions(-) diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index aecdf3be8da..b5832fcc949 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -488,15 +488,15 @@ ruleTester.run('naming-convention', rule, { `, options: (rule.meta.docs!.configOptions as any).operations, errors: [ - { 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' }, + { message: 'Query "TestQuery" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, + { message: 'Query "QueryTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, + { message: 'Query "GetQuery" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, + { message: 'Mutation "TestMutation" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, + { message: 'Mutation "MutationTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, + { message: 'Subscription "TestSubscription" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, + { message: 'Subscription "SubscriptionTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, + { message: 'Fragment "TestFragment" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"' }, + { message: 'Fragment "FragmentTest" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"' }, ], }, { diff --git a/packages/plugin/src/rules/naming-convention/index.ts b/packages/plugin/src/rules/naming-convention/index.ts index ce3b2f69d44..5f7d2c446d1 100644 --- a/packages/plugin/src/rules/naming-convention/index.ts +++ b/packages/plugin/src/rules/naming-convention/index.ts @@ -324,7 +324,7 @@ export const rule: GraphQLESLintRule = { DirectiveDefinition: 'camelCase', EnumValueDefinition: 'UPPER_CASE', 'FieldDefinition[parent.name.value=Query]': { - forbiddenPattern: [/^(query|get)/i, /query$/i], + forbiddenPatterns: [/^(query|get)/i, /query$/i], }, 'FieldDefinition[parent.name.value=Mutation]': { forbiddenPatterns: [/(^mutation)|(mutation$)/i], @@ -351,7 +351,7 @@ export const rule: GraphQLESLintRule = { VariableDefinition: 'camelCase', OperationDefinition: { style: 'PascalCase', - forbiddenPattern: [ + forbiddenPatterns: [ /^(query|mutation|subscription|get)/i, /(query|mutation|subscription)$/i, ], diff --git a/packages/plugin/src/rules/naming-convention/snapshot.md b/packages/plugin/src/rules/naming-convention/snapshot.md index ee50df91edd..e1a96ab5ed3 100644 --- a/packages/plugin/src/rules/naming-convention/snapshot.md +++ b/packages/plugin/src/rules/naming-convention/snapshot.md @@ -1744,25 +1744,15 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` "VariableDefinition": "camelCase", "OperationDefinition": { "style": "PascalCase", - "forbiddenPrefixes": [ - "Query", - "Mutation", - "Subscription", - "Get" - ], - "forbiddenSuffixes": [ - "Query", - "Mutation", - "Subscription" + "forbiddenPatterns": [ + "/^(query|mutation|subscription|get)/i", + "/(query|mutation|subscription)$/i" ] }, "FragmentDefinition": { "style": "PascalCase", - "forbiddenPrefixes": [ - "Fragment" - ], - "forbiddenSuffixes": [ - "Fragment" + "forbiddenPatterns": [ + "/(^fragment)|(fragment$)/i" ] } } @@ -1770,7 +1760,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` #### ❌ Error 1/9 > 1 | query TestQuery { test } - | ^^^^^^^^^ Query "TestQuery" should not have "Query" suffix + | ^^^^^^^^^ Query "TestQuery" should not contain the forbidden pattern "/(query|mutation|subscription)$/i" 2 | query QueryTest { test } #### 💡 Suggestion: Rename to \`Test\` @@ -1793,7 +1783,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 1 | query TestQuery { test } > 2 | query QueryTest { test } - | ^^^^^^^^^ Query "QueryTest" should not have "Query" prefix + | ^^^^^^^^^ Query "QueryTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i" 3 | query GetQuery { test } #### 💡 Suggestion: Rename to \`Test\` @@ -1816,7 +1806,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 2 | query QueryTest { test } > 3 | query GetQuery { test } - | ^^^^^^^^ Query "GetQuery" should not have "Get" prefix + | ^^^^^^^^ Query "GetQuery" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i" 4 | query Test { test(CONTROLLED_BY_SCHEMA: 0) } #### 💡 Suggestion: Rename to \`Query\` @@ -1839,7 +1829,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 5 | > 6 | mutation TestMutation { test } - | ^^^^^^^^^^^^ Mutation "TestMutation" should not have "Mutation" suffix + | ^^^^^^^^^^^^ Mutation "TestMutation" should not contain the forbidden pattern "/(query|mutation|subscription)$/i" 7 | mutation MutationTest { test } #### 💡 Suggestion: Rename to \`Test\` @@ -1862,7 +1852,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 6 | mutation TestMutation { test } > 7 | mutation MutationTest { test } - | ^^^^^^^^^^^^ Mutation "MutationTest" should not have "Mutation" prefix + | ^^^^^^^^^^^^ Mutation "MutationTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i" 8 | #### 💡 Suggestion: Rename to \`Test\` @@ -1885,7 +1875,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 8 | > 9 | subscription TestSubscription { test } - | ^^^^^^^^^^^^^^^^ Subscription "TestSubscription" should not have "Subscription" suffix + | ^^^^^^^^^^^^^^^^ Subscription "TestSubscription" should not contain the forbidden pattern "/(query|mutation|subscription)$/i" 10 | subscription SubscriptionTest { test } #### 💡 Suggestion: Rename to \`Test\` @@ -1908,7 +1898,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 9 | subscription TestSubscription { test } > 10 | subscription SubscriptionTest { test } - | ^^^^^^^^^^^^^^^^ Subscription "SubscriptionTest" should not have "Subscription" prefix + | ^^^^^^^^^^^^^^^^ Subscription "SubscriptionTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i" 11 | #### 💡 Suggestion: Rename to \`Test\` @@ -1931,7 +1921,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 11 | > 12 | fragment TestFragment on Test { id } - | ^^^^^^^^^^^^ Fragment "TestFragment" should not have "Fragment" suffix + | ^^^^^^^^^^^^ Fragment "TestFragment" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i" 13 | fragment FragmentTest on Test { id } #### 💡 Suggestion: Rename to \`Test\` @@ -1954,7 +1944,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = ` 12 | fragment TestFragment on Test { id } > 13 | fragment FragmentTest on Test { id } - | ^^^^^^^^^^^^ Fragment "FragmentTest" should not have "Fragment" prefix + | ^^^^^^^^^^^^ Fragment "FragmentTest" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i" #### 💡 Suggestion: Rename to \`Test\` @@ -2063,60 +2053,39 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` "DirectiveDefinition": "camelCase", "EnumValueDefinition": "UPPER_CASE", "FieldDefinition[parent.name.value=Query]": { - "forbiddenPrefixes": [ - "query", - "get" - ], - "forbiddenSuffixes": [ - "Query" + "forbiddenPatterns": [ + "/^(query|get)/i", + "/query$/i" ] }, "FieldDefinition[parent.name.value=Mutation]": { - "forbiddenPrefixes": [ - "mutation" - ], - "forbiddenSuffixes": [ - "Mutation" + "forbiddenPatterns": [ + "/(^mutation)|(mutation$)/i" ] }, "FieldDefinition[parent.name.value=Subscription]": { - "forbiddenPrefixes": [ - "subscription" - ], - "forbiddenSuffixes": [ - "Subscription" + "forbiddenPatterns": [ + "/(^subscription)|(subscription$)/i" ] }, "EnumTypeDefinition,EnumTypeExtension": { - "forbiddenPrefixes": [ - "Enum" - ], - "forbiddenSuffixes": [ - "Enum" + "forbiddenPatterns": [ + "/(^enum)|(enum$)/i" ] }, "InterfaceTypeDefinition,InterfaceTypeExtension": { - "forbiddenPrefixes": [ - "Interface" - ], - "forbiddenSuffixes": [ - "Interface" + "forbiddenPatterns": [ + "/(^interface)|(interface$)/i" ] }, "UnionTypeDefinition,UnionTypeExtension": { - "forbiddenPrefixes": [ - "Union" - ], - "forbiddenSuffixes": [ - "Union" + "forbiddenPatterns": [ + "/(^union)|(union$)/i" ] }, "ObjectTypeDefinition,ObjectTypeExtension": { - "forbiddenPrefixes": [ - "Type" - ], - "forbiddenSuffixes": [ - "Type" + "forbiddenPatterns": [ + "/(^type)|(type$)/i" ] } } @@ -2125,7 +2094,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 1 | type Query { > 2 | fieldQuery: ID - | ^^^^^^^^^^ Field "fieldQuery" should not have "Query" suffix + | ^^^^^^^^^^ Field "fieldQuery" should not contain the forbidden pattern "/query$/i" 3 | queryField: ID #### 💡 Suggestion: Rename to \`field\` @@ -2168,7 +2137,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 2 | fieldQuery: ID > 3 | queryField: ID - | ^^^^^^^^^^ Field "queryField" should not have "query" prefix + | ^^^^^^^^^^ Field "queryField" should not contain the forbidden pattern "/^(query|get)/i" 4 | getField: ID #### 💡 Suggestion: Rename to \`Field\` @@ -2211,7 +2180,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 3 | queryField: ID > 4 | getField: ID - | ^^^^^^^^ Field "getField" should not have "get" prefix + | ^^^^^^^^ Field "getField" should not contain the forbidden pattern "/^(query|get)/i" 5 | } #### 💡 Suggestion: Rename to \`Field\` @@ -2254,7 +2223,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 7 | type Mutation { > 8 | fieldMutation: ID - | ^^^^^^^^^^^^^ Field "fieldMutation" should not have "Mutation" suffix + | ^^^^^^^^^^^^^ Field "fieldMutation" should not contain the forbidden pattern "/(^mutation)|(mutation$)/i" 9 | mutationField: ID #### 💡 Suggestion: Rename to \`field\` @@ -2297,7 +2266,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 8 | fieldMutation: ID > 9 | mutationField: ID - | ^^^^^^^^^^^^^ Field "mutationField" should not have "mutation" prefix + | ^^^^^^^^^^^^^ Field "mutationField" should not contain the forbidden pattern "/(^mutation)|(mutation$)/i" 10 | } #### 💡 Suggestion: Rename to \`Field\` @@ -2340,7 +2309,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 12 | type Subscription { > 13 | fieldSubscription: ID - | ^^^^^^^^^^^^^^^^^ Field "fieldSubscription" should not have "Subscription" suffix + | ^^^^^^^^^^^^^^^^^ Field "fieldSubscription" should not contain the forbidden pattern "/(^subscription)|(subscription$)/i" 14 | subscriptionField: ID #### 💡 Suggestion: Rename to \`field\` @@ -2383,7 +2352,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 13 | fieldSubscription: ID > 14 | subscriptionField: ID - | ^^^^^^^^^^^^^^^^^ Field "subscriptionField" should not have "subscription" prefix + | ^^^^^^^^^^^^^^^^^ Field "subscriptionField" should not contain the forbidden pattern "/(^subscription)|(subscription$)/i" 15 | } #### 💡 Suggestion: Rename to \`Field\` @@ -2426,7 +2395,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 16 | > 17 | enum TestEnum - | ^^^^^^^^ Enum "TestEnum" should not have "Enum" suffix + | ^^^^^^^^ Enum "TestEnum" should not contain the forbidden pattern "/(^enum)|(enum$)/i" 18 | extend enum EnumTest { #### 💡 Suggestion: Rename to \`Test\` @@ -2469,7 +2438,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 17 | enum TestEnum > 18 | extend enum EnumTest { - | ^^^^^^^^ Enum "EnumTest" should not have "Enum" prefix + | ^^^^^^^^ Enum "EnumTest" should not contain the forbidden pattern "/(^enum)|(enum$)/i" 19 | A #### 💡 Suggestion: Rename to \`Test\` @@ -2512,7 +2481,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 21 | > 22 | interface TestInterface - | ^^^^^^^^^^^^^ Interface "TestInterface" should not have "Interface" suffix + | ^^^^^^^^^^^^^ Interface "TestInterface" should not contain the forbidden pattern "/(^interface)|(interface$)/i" 23 | extend interface InterfaceTest { #### 💡 Suggestion: Rename to \`Test\` @@ -2555,7 +2524,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 22 | interface TestInterface > 23 | extend interface InterfaceTest { - | ^^^^^^^^^^^^^ Interface "InterfaceTest" should not have "Interface" prefix + | ^^^^^^^^^^^^^ Interface "InterfaceTest" should not contain the forbidden pattern "/(^interface)|(interface$)/i" 24 | id: ID #### 💡 Suggestion: Rename to \`Test\` @@ -2598,7 +2567,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 26 | > 27 | union TestUnion - | ^^^^^^^^^ Union "TestUnion" should not have "Union" suffix + | ^^^^^^^^^ Union "TestUnion" should not contain the forbidden pattern "/(^union)|(union$)/i" 28 | extend union UnionTest = TestInterface #### 💡 Suggestion: Rename to \`Test\` @@ -2641,7 +2610,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 27 | union TestUnion > 28 | extend union UnionTest = TestInterface - | ^^^^^^^^^ Union "UnionTest" should not have "Union" prefix + | ^^^^^^^^^ Union "UnionTest" should not contain the forbidden pattern "/(^union)|(union$)/i" 29 | #### 💡 Suggestion: Rename to \`Test\` @@ -2684,7 +2653,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 29 | > 30 | type TestType - | ^^^^^^^^ Type "TestType" should not have "Type" suffix + | ^^^^^^^^ Type "TestType" should not contain the forbidden pattern "/(^type)|(type$)/i" 31 | extend type TypeTest { #### 💡 Suggestion: Rename to \`Test\` @@ -2727,7 +2696,7 @@ exports[`naming-convention > invalid > schema-recommended config 1`] = ` 30 | type TestType > 31 | extend type TypeTest { - | ^^^^^^^^ Type "TypeTest" should not have "Type" prefix + | ^^^^^^^^ Type "TypeTest" should not contain the forbidden pattern "/(^type)|(type$)/i" 32 | id: ID #### 💡 Suggestion: Rename to \`Test\` diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index 292a4aa494b..c2d0dba90bb 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -35,7 +35,7 @@ type user { ### Incorrect ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] fragment UserFragment on User { # ... @@ -45,7 +45,7 @@ fragment UserFragment on User { ### Incorrect ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[parent.name.value=Query]': { forbiddenPatterns: ['/^get/i'] } }] type Query { getUsers: [User!]! @@ -65,7 +65,7 @@ type User { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] fragment UserFields on User { # ... @@ -75,7 +75,7 @@ fragment UserFields on User { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[parent.name.value=Query]': { forbiddenPatterns: ['/^get/i'] } }] type Query { users: [User!]! @@ -97,7 +97,7 @@ type Product { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[gqlType.name.value=Boolean]': { style: 'camelCase', requiredPrefixes: ['is', 'has'] }, 'FieldDefinition[gqlType.gqlType.name.value=Boolean]': { style: 'camelCase', requiredPrefixes: ['is', 'has'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[gqlType.name.value=Boolean]': { style: 'camelCase', requiredPattern: '/^(is|has)/' }, 'FieldDefinition[gqlType.gqlType.name.value=Boolean]': { style: 'camelCase', requiredPattern: '/^(is|has)/' } }] type Product { isBackordered: Boolean @@ -109,7 +109,7 @@ type Product { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]': { style: 'camelCase', requiredSuffixes: ['SensitiveSecret'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { 'FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]': { style: 'camelCase', requiredPattern: '/SensitiveSecret$/' } }] scalar SensitiveSecret From 740d028c3d43a6dde0eaef21a5805369b2495c6e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 04:08:17 +0700 Subject: [PATCH 12/14] Merge branch 'master' into use-search-params --- .../src/rules/naming-convention/index.test.ts | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/plugin/src/rules/naming-convention/index.test.ts b/packages/plugin/src/rules/naming-convention/index.test.ts index b5832fcc949..455fe627227 100644 --- a/packages/plugin/src/rules/naming-convention/index.test.ts +++ b/packages/plugin/src/rules/naming-convention/index.test.ts @@ -488,15 +488,42 @@ ruleTester.run('naming-convention', rule, { `, options: (rule.meta.docs!.configOptions as any).operations, errors: [ - { message: 'Query "TestQuery" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, - { message: 'Query "QueryTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, - { message: 'Query "GetQuery" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, - { message: 'Mutation "TestMutation" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, - { message: 'Mutation "MutationTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, - { message: 'Subscription "TestSubscription" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"' }, - { message: 'Subscription "SubscriptionTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"' }, - { message: 'Fragment "TestFragment" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"' }, - { message: 'Fragment "FragmentTest" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"' }, + { + message: + 'Query "TestQuery" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"', + }, + { + message: + 'Query "QueryTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"', + }, + { + message: + 'Query "GetQuery" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"', + }, + { + message: + 'Mutation "TestMutation" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"', + }, + { + message: + 'Mutation "MutationTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"', + }, + { + message: + 'Subscription "TestSubscription" should not contain the forbidden pattern "/(query|mutation|subscription)$/i"', + }, + { + message: + 'Subscription "SubscriptionTest" should not contain the forbidden pattern "/^(query|mutation|subscription|get)/i"', + }, + { + message: + 'Fragment "TestFragment" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"', + }, + { + message: + 'Fragment "FragmentTest" should not contain the forbidden pattern "/(^fragment)|(fragment$)/i"', + }, ], }, { From 41eb4549764dc0314b5bd4f257ea6667b178540e Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 04:08:33 +0700 Subject: [PATCH 13/14] Update .changeset/polite-impalas-float.md --- .changeset/polite-impalas-float.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/polite-impalas-float.md b/.changeset/polite-impalas-float.md index 7a019432b03..2d1f25f70ba 100644 --- a/.changeset/polite-impalas-float.md +++ b/.changeset/polite-impalas-float.md @@ -1,5 +1,5 @@ --- -'@graphql-eslint/eslint-plugin': patch +'@graphql-eslint/eslint-plugin': minor --- - allow to config `naming-convention` for Relay fragment convention `_` From bb82835bf9da71bdd9bedaeadb75c8c1625ecd87 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Sun, 8 Dec 2024 04:11:31 +0700 Subject: [PATCH 14/14] Apply suggestions from code review --- website/content/rules/naming-convention.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/rules/naming-convention.mdx b/website/content/rules/naming-convention.mdx index c2d0dba90bb..e144adfea60 100644 --- a/website/content/rules/naming-convention.mdx +++ b/website/content/rules/naming-convention.mdx @@ -35,7 +35,7 @@ type user { ### Incorrect ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/fragment$/i'] } }] fragment UserFragment on User { # ... @@ -65,7 +65,7 @@ type User { ### Correct ```graphql -# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/(^fragment)|(fragment$)/i'] } }] +# eslint @graphql-eslint/naming-convention: ['error', { FragmentDefinition: { style: 'PascalCase', forbiddenPatterns: ['/fragment$/i'] } }] fragment UserFields on User { # ...