From 8b718d2aae5145e22790290eed0e9cc9b309bc5d Mon Sep 17 00:00:00 2001 From: Garrit Franke <32395585+garritfra@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:57:54 +0200 Subject: [PATCH] feat: Add cred-class-name-field-conventions ESLint rule for community nodes (no-changelog) (#31472) Co-authored-by: Claude Opus 4.8 (1M context) --- .../eslint-plugin-community-nodes/README.md | 74 +++++------ .../cred-class-name-field-conventions.md | 38 ++++++ .../docs/rules/no-builder-hint-leakage.md | 38 ++++++ .../package.json | 3 +- .../src/plugin.ts | 2 + .../cred-class-name-field-conventions.test.ts | 121 ++++++++++++++++++ .../cred-class-name-field-conventions.ts | 102 +++++++++++++++ .../src/rules/credential-password-field.ts | 2 +- .../src/rules/index.ts | 2 + .../src/rules/no-credential-reuse.ts | 2 +- .../src/rules/no-restricted-globals.ts | 2 +- .../src/rules/node-usable-as-tool.ts | 2 +- .../src/rules/require-node-api-error.ts | 7 +- .../src/rules/valid-credential-references.ts | 2 +- pnpm-lock.yaml | 3 + 15 files changed, 355 insertions(+), 45 deletions(-) create mode 100644 packages/@n8n/eslint-plugin-community-nodes/docs/rules/cred-class-name-field-conventions.md create mode 100644 packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-builder-hint-leakage.md create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.test.ts create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.ts diff --git a/packages/@n8n/eslint-plugin-community-nodes/README.md b/packages/@n8n/eslint-plugin-community-nodes/README.md index 2ce5ae04c74..c3a0da178be 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/README.md +++ b/packages/@n8n/eslint-plugin-community-nodes/README.md @@ -44,41 +44,43 @@ export default [ πŸ’‘ Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).\ ❌ Deprecated. -| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | πŸ’‘ | ❌ | -| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :--- | :--- | :- | :- | :- | -| [ai-node-package-json](docs/rules/ai-node-package-json.md) | Enforce consistency between n8n.aiNodeSdkVersion and ai-node-sdk peer dependency in community node packages | βœ… β˜‘οΈ | | | | | -| [cred-class-field-icon-missing](docs/rules/cred-class-field-icon-missing.md) | Credential class must have an `icon` property defined | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [cred-class-name-suffix](docs/rules/cred-class-name-suffix.md) | Credential class names must be suffixed with `Api` | βœ… β˜‘οΈ | | πŸ”§ | | | -| [cred-class-oauth2-naming](docs/rules/cred-class-oauth2-naming.md) | OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` | βœ… β˜‘οΈ | | πŸ”§ | | | -| [credential-documentation-url](docs/rules/credential-documentation-url.md) | Enforce valid credential documentationUrl format (URL or lowercase alphanumeric slug) | βœ… β˜‘οΈ | | πŸ”§ | | | -| [credential-password-field](docs/rules/credential-password-field.md) | Ensure credential fields with sensitive names have typeOptions.password = true | βœ… β˜‘οΈ | | πŸ”§ | | | -| [credential-test-required](docs/rules/credential-test-required.md) | Ensure credentials have a credential test | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [icon-validation](docs/rules/icon-validation.md) | Validate node and credential icon files exist, are SVG format, and light/dark icons are different | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [missing-paired-item](docs/rules/missing-paired-item.md) | Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking. | βœ… β˜‘οΈ | | | | | -| [n8n-object-validation](docs/rules/n8n-object-validation.md) | Validate the structure of the "n8n" object in community node package.json (required keys, types, and dist/ paths) | βœ… β˜‘οΈ | | | | | -| [no-credential-reuse](docs/rules/no-credential-reuse.md) | Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [no-deprecated-workflow-functions](docs/rules/no-deprecated-workflow-functions.md) | Disallow usage of deprecated functions and types from n8n-workflow package | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [no-forbidden-lifecycle-scripts](docs/rules/no-forbidden-lifecycle-scripts.md) | Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages | βœ… β˜‘οΈ | | | | | -| [no-http-request-with-manual-auth](docs/rules/no-http-request-with-manual-auth.md) | Disallow this.helpers.httpRequest() in functions that call this.getCredentials(). Use this.helpers.httpRequestWithAuthentication() instead. | βœ… β˜‘οΈ | | | | | -| [no-overrides-field](docs/rules/no-overrides-field.md) | Ban the "overrides" field in community node package.json | βœ… β˜‘οΈ | | | | | -| [no-restricted-globals](docs/rules/no-restricted-globals.md) | Disallow usage of restricted global variables in community nodes. | βœ… | | | | | -| [no-restricted-imports](docs/rules/no-restricted-imports.md) | Disallow usage of restricted imports in community nodes. | βœ… | | | | | -| [no-runtime-dependencies](docs/rules/no-runtime-dependencies.md) | Disallow non-empty "dependencies" in community node package.json | βœ… β˜‘οΈ | | | | | -| [no-template-placeholders](docs/rules/no-template-placeholders.md) | Disallow unresolved template placeholders in package.json | βœ… β˜‘οΈ | | | | | -| [node-class-description-icon-missing](docs/rules/node-class-description-icon-missing.md) | Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead. | | | | πŸ’‘ | ❌ | -| [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` β€” use `NodeConnectionTypes` enum instead | βœ… β˜‘οΈ | | πŸ”§ | | | -| [node-operation-error-itemindex](docs/rules/node-operation-error-itemindex.md) | Require { itemIndex } in NodeOperationError / NodeApiError options inside item loops | βœ… β˜‘οΈ | | | | | -| [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | βœ… β˜‘οΈ | | πŸ”§ | | | -| [options-sorted-alphabetically](docs/rules/options-sorted-alphabetically.md) | Enforce alphabetical ordering of options arrays in n8n node properties | | βœ… β˜‘οΈ | | | | -| [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [require-community-node-keyword](docs/rules/require-community-node-keyword.md) | Require the "n8n-community-node-package" keyword in community node package.json | | βœ… β˜‘οΈ | πŸ”§ | | | -| [require-continue-on-fail](docs/rules/require-continue-on-fail.md) | Require continueOnFail() handling in execute() methods of node classes | βœ… β˜‘οΈ | | | | | -| [require-node-api-error](docs/rules/require-node-api-error.md) | Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI. | βœ… β˜‘οΈ | | | | | -| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | βœ… β˜‘οΈ | | | | | -| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | βœ… β˜‘οΈ | | | | -| [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | βœ… β˜‘οΈ | | | πŸ’‘ | | -| [valid-description](docs/rules/valid-description.md) | Require a non-empty "description" field in community node package.json | βœ… β˜‘οΈ | | | | | -| [valid-peer-dependencies](docs/rules/valid-peer-dependencies.md) | Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") | βœ… β˜‘οΈ | | πŸ”§ | | | -| [webhook-lifecycle-complete](docs/rules/webhook-lifecycle-complete.md) | Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) | βœ… β˜‘οΈ | | | | | +| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β  | Description | πŸ’Ό | ⚠️ | πŸ”§ | πŸ’‘ | ❌ | +| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- | :--- | :- | :- | :- | +| [ai-node-package-json](docs/rules/ai-node-package-json.md) | Enforce consistency between n8n.aiNodeSdkVersion and ai-node-sdk peer dependency in community node packages | βœ… β˜‘οΈ | | | | | +| [cred-class-field-icon-missing](docs/rules/cred-class-field-icon-missing.md) | Credential class must have an `icon` property defined | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [cred-class-name-field-conventions](docs/rules/cred-class-name-field-conventions.md) | Credential `name` field must end with `Api` and start with a lowercase letter | βœ… β˜‘οΈ | | πŸ”§ | | | +| [cred-class-name-suffix](docs/rules/cred-class-name-suffix.md) | Credential class names must be suffixed with `Api` | βœ… β˜‘οΈ | | πŸ”§ | | | +| [cred-class-oauth2-naming](docs/rules/cred-class-oauth2-naming.md) | OAuth2 credentials must include `OAuth2` in the class name, `name`, and `displayName` | βœ… β˜‘οΈ | | πŸ”§ | | | +| [credential-documentation-url](docs/rules/credential-documentation-url.md) | Enforce valid credential documentationUrl format (URL or lowercase alphanumeric slug) | βœ… β˜‘οΈ | | πŸ”§ | | | +| [credential-password-field](docs/rules/credential-password-field.md) | Ensure credential fields with sensitive names have typeOptions.password = true | βœ… β˜‘οΈ | | πŸ”§ | | | +| [credential-test-required](docs/rules/credential-test-required.md) | Ensure credentials have a credential test | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [icon-validation](docs/rules/icon-validation.md) | Validate node and credential icon files exist, are SVG format, and light/dark icons are different | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [missing-paired-item](docs/rules/missing-paired-item.md) | Require pairedItem on INodeExecutionData objects in execute() methods to preserve item linking. | βœ… β˜‘οΈ | | | | | +| [n8n-object-validation](docs/rules/n8n-object-validation.md) | Validate the structure of the "n8n" object in community node package.json (required keys, types, and dist/ paths) | βœ… β˜‘οΈ | | | | | +| [no-builder-hint-leakage](docs/rules/no-builder-hint-leakage.md) | Disallow wire-format expression syntax (={{...}}) and NodeConnectionType string literals in builderHint texts and AI-builder prompts. Use expr() and SDK-canonical references instead. | βœ… β˜‘οΈ | | | | | +| [no-credential-reuse](docs/rules/no-credential-reuse.md) | Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [no-deprecated-workflow-functions](docs/rules/no-deprecated-workflow-functions.md) | Disallow usage of deprecated functions and types from n8n-workflow package | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [no-forbidden-lifecycle-scripts](docs/rules/no-forbidden-lifecycle-scripts.md) | Ban lifecycle scripts (prepare, preinstall, postinstall, etc.) in community node packages | βœ… β˜‘οΈ | | | | | +| [no-http-request-with-manual-auth](docs/rules/no-http-request-with-manual-auth.md) | Disallow this.helpers.httpRequest() in functions that call this.getCredentials(). Use this.helpers.httpRequestWithAuthentication() instead. | βœ… β˜‘οΈ | | | | | +| [no-overrides-field](docs/rules/no-overrides-field.md) | Ban the "overrides" field in community node package.json | βœ… β˜‘οΈ | | | | | +| [no-restricted-globals](docs/rules/no-restricted-globals.md) | Disallow usage of restricted global variables in community nodes. | βœ… | | | | | +| [no-restricted-imports](docs/rules/no-restricted-imports.md) | Disallow usage of restricted imports in community nodes. | βœ… | | | | | +| [no-runtime-dependencies](docs/rules/no-runtime-dependencies.md) | Disallow non-empty "dependencies" in community node package.json | βœ… β˜‘οΈ | | | | | +| [no-template-placeholders](docs/rules/no-template-placeholders.md) | Disallow unresolved template placeholders in package.json | βœ… β˜‘οΈ | | | | | +| [node-class-description-icon-missing](docs/rules/node-class-description-icon-missing.md) | Node class description must have an `icon` property defined. Deprecated: use `require-node-description-fields` instead. | | | | πŸ’‘ | ❌ | +| [node-connection-type-literal](docs/rules/node-connection-type-literal.md) | Disallow string literals in node description `inputs`/`outputs` β€” use `NodeConnectionTypes` enum instead | βœ… β˜‘οΈ | | πŸ”§ | | | +| [node-operation-error-itemindex](docs/rules/node-operation-error-itemindex.md) | Require { itemIndex } in NodeOperationError / NodeApiError options inside item loops | βœ… β˜‘οΈ | | | | | +| [node-usable-as-tool](docs/rules/node-usable-as-tool.md) | Ensure node classes have usableAsTool property | βœ… β˜‘οΈ | | πŸ”§ | | | +| [options-sorted-alphabetically](docs/rules/options-sorted-alphabetically.md) | Enforce alphabetical ordering of options arrays in n8n node properties | | βœ… β˜‘οΈ | | | | +| [package-name-convention](docs/rules/package-name-convention.md) | Enforce correct package naming convention for n8n community nodes | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [require-community-node-keyword](docs/rules/require-community-node-keyword.md) | Require the "n8n-community-node-package" keyword in community node package.json | | βœ… β˜‘οΈ | πŸ”§ | | | +| [require-continue-on-fail](docs/rules/require-continue-on-fail.md) | Require continueOnFail() handling in execute() methods of node classes | βœ… β˜‘οΈ | | | | | +| [require-node-api-error](docs/rules/require-node-api-error.md) | Require NodeApiError or NodeOperationError for error wrapping in catch blocks. Raw errors lose HTTP context in the n8n UI. | βœ… β˜‘οΈ | | | | | +| [require-node-description-fields](docs/rules/require-node-description-fields.md) | Node class description must define all required fields: icon, subtitle | βœ… β˜‘οΈ | | | | | +| [resource-operation-pattern](docs/rules/resource-operation-pattern.md) | Enforce proper resource/operation pattern for better UX in n8n nodes | | βœ… β˜‘οΈ | | | | +| [valid-credential-references](docs/rules/valid-credential-references.md) | Ensure credentials referenced in node descriptions exist as credential classes in the package | βœ… β˜‘οΈ | | | πŸ’‘ | | +| [valid-description](docs/rules/valid-description.md) | Require a non-empty "description" field in community node package.json | βœ… β˜‘οΈ | | | | | +| [valid-peer-dependencies](docs/rules/valid-peer-dependencies.md) | Require community node package.json peerDependencies to contain only "n8n-workflow": "*" (and optionally "ai-node-sdk") | βœ… β˜‘οΈ | | πŸ”§ | | | +| [webhook-lifecycle-complete](docs/rules/webhook-lifecycle-complete.md) | Require webhook trigger nodes to implement the complete webhookMethods lifecycle (checkExists, create, delete) | βœ… β˜‘οΈ | | | | | diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/cred-class-name-field-conventions.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/cred-class-name-field-conventions.md new file mode 100644 index 00000000000..26509876d15 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/cred-class-name-field-conventions.md @@ -0,0 +1,38 @@ +# Credential `name` field must end with `Api` and start with a lowercase letter (`@n8n/community-nodes/cred-class-name-field-conventions`) + +πŸ’Ό This rule is enabled in the following configs: βœ… `recommended`, β˜‘οΈ `recommendedWithoutN8nCloudSupport`. + +πŸ”§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +## Rule Details + +The `name` field of a credential class (those implementing `ICredentialType` in `*.credentials.ts` files) is the internal identifier referenced by nodes and stored in the credential registry. n8n convention requires this identifier to: + +- End with `Api` (e.g. `githubApi`), so credentials are easily recognisable across the codebase. +- Start with a lowercase letter (camelCase), since it is an identifier value rather than a class name. + +Both checks are automatically fixable. + +## Examples + +### ❌ Incorrect + +```typescript +export class GithubApi implements ICredentialType { + name = 'Github'; + displayName = 'GitHub API'; + properties: INodeProperties[] = []; +} +``` + +### βœ… Correct + +```typescript +export class GithubApi implements ICredentialType { + name = 'githubApi'; + displayName = 'GitHub API'; + properties: INodeProperties[] = []; +} +``` diff --git a/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-builder-hint-leakage.md b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-builder-hint-leakage.md new file mode 100644 index 00000000000..e4826ba1c28 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/docs/rules/no-builder-hint-leakage.md @@ -0,0 +1,38 @@ +# Disallow wire-format expression syntax (={{...}}) and NodeConnectionType string literals in builderHint texts and AI-builder prompts. Use expr() and SDK-canonical references instead (`@n8n/community-nodes/no-builder-hint-leakage`) + +πŸ’Ό This rule is enabled in the following configs: βœ… `recommended`, β˜‘οΈ `recommendedWithoutN8nCloudSupport`. + + + +## Rule Details + +`builderHint` texts and AI-builder prompts are authored as human-facing guidance, but they sometimes leak n8n's internal wire format: + +- Raw expression syntax such as `={{ $json.foo }}`, which only makes sense inside the execution engine. +- `NodeConnectionType` string literals such as `ai_languageModel` or `ai_tool`, which are structured connection identifiers rather than prose. + +When these leak into hints and prompts, they confuse the builder experience and the AI assistant. Use the `expr()` SDK helper for expressions and the SDK-canonical reference helpers (e.g. `languageModel()`, `tool()`, `memory()`) instead. + +## Options + +This rule accepts an options object: + +- `scope` (`'builderHint' | 'all'`, default `'builderHint'`) β€” `builderHint` only scans string values inside `builderHint` property values. `all` scans every string in the file, intended for AI-builder prompt files. + +## Examples + +### ❌ Incorrect + +```typescript +const node = { + builderHint: 'Set the model with ={{ $json.model }} and connect an ai_languageModel.', +}; +``` + +### βœ… Correct + +```typescript +const node = { + builderHint: 'Set the model with expr($json.model) and connect a languageModel().', +}; +``` diff --git a/packages/@n8n/eslint-plugin-community-nodes/package.json b/packages/@n8n/eslint-plugin-community-nodes/package.json index dbd527d2ac3..c49bcb896c8 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/package.json +++ b/packages/@n8n/eslint-plugin-community-nodes/package.json @@ -17,7 +17,7 @@ "dev": "pnpm watch", "format": "biome format --write .", "format:check": "biome ci .", - "lint": "eslint src", + "lint": "eslint src && pnpm lint:docs", "lint:fix": "eslint src --fix", "lint:docs": "pnpm build && eslint-doc-generator --check", "test": "vitest run", @@ -27,6 +27,7 @@ "watch": "tsc --watch --project tsconfig.build.json" }, "dependencies": { + "@typescript-eslint/typescript-estree": "^8.35.0", "@typescript-eslint/utils": "^8.35.0", "fastest-levenshtein": "catalog:" }, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index c4359cf31ef..88dde6751cc 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -40,6 +40,7 @@ const configs = { '@n8n/community-nodes/resource-operation-pattern': 'warn', '@n8n/community-nodes/credential-documentation-url': 'error', '@n8n/community-nodes/cred-class-field-icon-missing': 'error', + '@n8n/community-nodes/cred-class-name-field-conventions': 'error', '@n8n/community-nodes/cred-class-name-suffix': 'error', '@n8n/community-nodes/cred-class-oauth2-naming': 'error', '@n8n/community-nodes/node-connection-type-literal': 'error', @@ -80,6 +81,7 @@ const configs = { '@n8n/community-nodes/credential-documentation-url': 'error', '@n8n/community-nodes/resource-operation-pattern': 'warn', '@n8n/community-nodes/cred-class-field-icon-missing': 'error', + '@n8n/community-nodes/cred-class-name-field-conventions': 'error', '@n8n/community-nodes/cred-class-name-suffix': 'error', '@n8n/community-nodes/cred-class-oauth2-naming': 'error', '@n8n/community-nodes/node-connection-type-literal': 'error', diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.test.ts new file mode 100644 index 00000000000..0c0fb96a65c --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.test.ts @@ -0,0 +1,121 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import { CredClassNameFieldConventionsRule } from './cred-class-name-field-conventions.js'; + +const ruleTester = new RuleTester(); + +const credFilePath = '/tmp/TestCredential.credentials.ts'; +const nonCredFilePath = '/tmp/SomeHelper.ts'; + +function createCredentialCode(name: string): string { + return ` +import type { ICredentialType, INodeProperties } from 'n8n-workflow'; + +export class TestApi implements ICredentialType { + name = '${name}'; + displayName = 'Test API'; + properties: INodeProperties[] = []; +}`; +} + +function createRegularClass(name: string): string { + return ` +export class SomeHelper { + name = '${name}'; +}`; +} + +// Embeds the raw literal text (including its quotes) verbatim, so tests can +// exercise names containing quote characters that need escaping. +function createCredentialCodeWithLiteral(literal: string): string { + return ` +import type { ICredentialType, INodeProperties } from 'n8n-workflow'; + +export class TestApi implements ICredentialType { + name = ${literal}; + displayName = 'Test API'; + properties: INodeProperties[] = []; +}`; +} + +ruleTester.run('cred-class-name-field-conventions', CredClassNameFieldConventionsRule, { + valid: [ + { + name: 'name field with Api suffix and lowercase first char', + filename: credFilePath, + code: createCredentialCode('githubApi'), + }, + { + name: 'OAuth2 name field is also valid', + filename: credFilePath, + code: createCredentialCode('githubOAuth2Api'), + }, + { + name: 'class not implementing ICredentialType is ignored', + filename: credFilePath, + code: createRegularClass('Github'), + }, + { + name: 'non-.credentials.ts file is ignored', + filename: nonCredFilePath, + code: createCredentialCode('Github'), + }, + ], + invalid: [ + { + name: 'name field missing Api suffix', + filename: credFilePath, + code: createCredentialCode('github'), + errors: [{ messageId: 'missingSuffix', data: { value: 'github' } }], + output: createCredentialCode('githubApi'), + }, + { + name: 'name field with uppercase first char', + filename: credFilePath, + code: createCredentialCode('GithubApi'), + errors: [{ messageId: 'uppercaseFirstChar', data: { value: 'GithubApi' } }], + output: createCredentialCode('githubApi'), + }, + { + name: 'name field with both uppercase first char and missing suffix', + filename: credFilePath, + code: createCredentialCode('Github'), + errors: [ + { messageId: 'uppercaseFirstChar', data: { value: 'Github' } }, + { messageId: 'missingSuffix', data: { value: 'Github' } }, + ], + output: createCredentialCode('githubApi'), + }, + { + name: 'name field ending in Ap', + filename: credFilePath, + code: createCredentialCode('githubAp'), + errors: [{ messageId: 'missingSuffix', data: { value: 'githubAp' } }], + output: createCredentialCode('githubApi'), + }, + { + name: 'name field ending in A', + filename: credFilePath, + code: createCredentialCode('githubA'), + errors: [{ messageId: 'missingSuffix', data: { value: 'githubA' } }], + output: createCredentialCode('githubApi'), + }, + { + name: 'autofix escapes single quotes in the name value', + filename: credFilePath, + code: createCredentialCodeWithLiteral("'git\\'hub'"), + errors: [{ messageId: 'missingSuffix', data: { value: "git'hub" } }], + output: createCredentialCodeWithLiteral("'git\\'hubApi'"), + }, + { + name: 'autofix preserves double quotes and escapes them in the name value', + filename: credFilePath, + code: createCredentialCodeWithLiteral('"Git\\"hub"'), + errors: [ + { messageId: 'uppercaseFirstChar', data: { value: 'Git"hub' } }, + { messageId: 'missingSuffix', data: { value: 'Git"hub' } }, + ], + output: createCredentialCodeWithLiteral('"git\\"hubApi"'), + }, + ], +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.ts new file mode 100644 index 00000000000..657281ad824 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/cred-class-name-field-conventions.ts @@ -0,0 +1,102 @@ +import { + createRule, + findClassProperty, + getStringLiteralValue, + isCredentialTypeClass, + isFileType, +} from '../utils/index.js'; + +function lowercaseFirstChar(name: string): string { + return name.charAt(0).toLowerCase() + name.slice(1); +} + +function addApiSuffix(name: string): string { + if (name.endsWith('Api')) return name; + if (name.endsWith('Ap')) return `${name}i`; + if (name.endsWith('A')) return `${name}pi`; + return `${name}Api`; +} + +// Serialize a value as a string literal using the original quote character, +// escaping any characters that would otherwise break the literal so the +// autofix never emits invalid code (e.g. names containing quotes). +function toStringLiteral(value: string, quote: string): string { + const escaped = value + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .split(quote) + .join(`\\${quote}`); + return `${quote}${escaped}${quote}`; +} + +export const CredClassNameFieldConventionsRule = createRule({ + name: 'cred-class-name-field-conventions', + meta: { + type: 'problem', + docs: { + description: 'Credential `name` field must end with `Api` and start with a lowercase letter', + }, + messages: { + missingSuffix: "Credential `name` field '{{value}}' must end with 'Api'", + uppercaseFirstChar: "Credential `name` field '{{value}}' must start with a lowercase letter", + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + if (!isFileType(context.filename, '.credentials.ts')) { + return {}; + } + + return { + ClassDeclaration(node) { + if (!isCredentialTypeClass(node)) { + return; + } + + const nameProperty = findClassProperty(node, 'name'); + if (!nameProperty?.value) { + return; + } + + const nameValue = getStringLiteralValue(nameProperty.value); + if (nameValue === null) { + return; + } + + const startsLowercase = !/^[A-Z]/.test(nameValue); + const endsWithApi = nameValue.endsWith('Api'); + if (startsLowercase && endsWithApi) { + return; + } + + // Compute the fully-corrected value once so each fix yields the + // same result in a single pass, regardless of which one applies. + const fixedValue = addApiSuffix(lowercaseFirstChar(nameValue)); + const valueNode = nameProperty.value; + const quote = context.sourceCode.getText(valueNode).charAt(0); + const replacement = toStringLiteral(fixedValue, quote); + + if (!startsLowercase) { + context.report({ + node: valueNode, + messageId: 'uppercaseFirstChar', + data: { value: nameValue }, + fix: (fixer) => fixer.replaceText(valueNode, replacement), + }); + } + + if (!endsWithApi) { + context.report({ + node: valueNode, + messageId: 'missingSuffix', + data: { value: nameValue }, + fix: (fixer) => fixer.replaceText(valueNode, replacement), + }); + } + }, + }; + }, +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/credential-password-field.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/credential-password-field.ts index 5e11296945f..ad4aefe4f0c 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/credential-password-field.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/credential-password-field.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/types'; +import { TSESTree } from '@typescript-eslint/utils'; import type { ReportFixFunction } from '@typescript-eslint/utils/ts-eslint'; import { diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts index 935b137173a..ea9069e64a7 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts @@ -2,6 +2,7 @@ import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint'; import { AiNodePackageJsonRule } from './ai-node-package-json.js'; import { CredClassFieldIconMissingRule } from './cred-class-field-icon-missing.js'; +import { CredClassNameFieldConventionsRule } from './cred-class-name-field-conventions.js'; import { CredClassNameSuffixRule } from './cred-class-name-suffix.js'; import { CredClassOAuth2NamingRule } from './cred-class-oauth2-naming.js'; import { CredentialDocumentationUrlRule } from './credential-documentation-url.js'; @@ -57,6 +58,7 @@ export const rules = { 'credential-documentation-url': CredentialDocumentationUrlRule, 'node-class-description-icon-missing': NodeClassDescriptionIconMissingRule, 'cred-class-field-icon-missing': CredClassFieldIconMissingRule, + 'cred-class-name-field-conventions': CredClassNameFieldConventionsRule, 'cred-class-name-suffix': CredClassNameSuffixRule, 'cred-class-oauth2-naming': CredClassOAuth2NamingRule, 'node-connection-type-literal': NodeConnectionTypeLiteralRule, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-credential-reuse.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-credential-reuse.ts index c836bbe5252..943e94f2f12 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-credential-reuse.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-credential-reuse.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/types'; +import { TSESTree } from '@typescript-eslint/utils'; import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint'; import { diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-restricted-globals.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-restricted-globals.ts index c94e73e5b9b..c9f2f3f4e29 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-restricted-globals.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-restricted-globals.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/types'; +import { TSESTree } from '@typescript-eslint/utils'; import type { TSESLint } from '@typescript-eslint/utils'; import { createRule } from '../utils/index.js'; diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/node-usable-as-tool.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/node-usable-as-tool.ts index d16fb2e33b1..5e5b0ce8ab3 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/node-usable-as-tool.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/node-usable-as-tool.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/types'; +import { TSESTree } from '@typescript-eslint/utils'; import { isNodeTypeClass, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts index 91e1c038a66..cda04530488 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/require-node-api-error.ts @@ -1,5 +1,4 @@ -import { DefinitionType } from '@typescript-eslint/scope-manager'; -import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, TSESLint, type TSESTree } from '@typescript-eslint/utils'; import { isFileType } from '../utils/index.js'; import { createRule } from '../utils/rule-creator.js'; @@ -68,7 +67,9 @@ export const RequireNodeApiErrorRule = createRule({ const scope = context.sourceCode.getScope(node); const ref = scope.references.find((r) => r.identifier === argument); const isCatchParam = - ref?.resolved?.defs.some((def) => def.type === DefinitionType.CatchClause) ?? false; + ref?.resolved?.defs.some( + (def) => def.type === TSESLint.Scope.DefinitionType.CatchClause, + ) ?? false; if (isCatchParam) { context.report({ node, messageId: 'useNodeApiError' }); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-credential-references.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-credential-references.ts index f3f601c89c0..74e5b0e1b46 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-credential-references.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/valid-credential-references.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/types'; +import { TSESTree } from '@typescript-eslint/utils'; import type { ReportSuggestionArray } from '@typescript-eslint/utils/ts-eslint'; import { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50a6569b1f0..5634fc7bfa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1643,6 +1643,9 @@ importers: packages/@n8n/eslint-plugin-community-nodes: dependencies: + '@typescript-eslint/typescript-estree': + specifier: ^8.35.0 + version: 8.35.0(typescript@6.0.2) '@typescript-eslint/utils': specifier: ^8.35.0 version: 8.35.0(eslint@9.29.0(jiti@2.6.1))(typescript@6.0.2)