--- name: n8n:create-community-node-lint-rule description: >- Create new ESLint rules for the @n8n/eslint-plugin-community-nodes package. Use when adding a lint rule, creating a community node lint, or working on eslint-plugin-community-nodes. Guides rule implementation, tests, docs, and plugin registration. --- # Create Community Node Lint Rule Guide for adding new ESLint rules to `packages/@n8n/eslint-plugin-community-nodes/`. All paths below are relative to `packages/@n8n/eslint-plugin-community-nodes/`. ## Step 1: Understand the Rule Before writing code, clarify: - **What** does the rule detect? (missing property, wrong pattern, bad value) - **Where** does it apply? (`.node.ts` files, credential classes, both) - **Severity**: `error` (must fix) or `warn` (should fix)? - **Fixable?** Can it be auto-fixed safely, or only suggest? - **Scope**: Both `recommended` configs, or exclude from `recommendedWithoutN8nCloudSupport`? ## Step 2: Implement the Rule Create `src/rules/.ts`: ```typescript import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { isNodeTypeClass, // or isCredentialTypeClass findClassProperty, findObjectProperty, createRule, } from '../utils/index.js'; export const YourRuleNameRule = createRule({ name: 'rule-name', meta: { type: 'problem', // or 'suggestion' docs: { description: 'One-line description of what the rule enforces', }, messages: { messageId: 'Human-readable message. Use {{placeholder}} for dynamic data.', }, fixable: 'code', // omit if not auto-fixable hasSuggestions: true, // omit if no suggestions schema: [], // add options schema if configurable }, defaultOptions: [], create(context) { return { ClassDeclaration(node) { if (!isNodeTypeClass(node)) return; const descriptionProperty = findClassProperty(node, 'description'); if (!descriptionProperty) return; const descriptionValue = descriptionProperty.value; if (descriptionValue?.type !== AST_NODE_TYPES.ObjectExpression) return; // Rule logic here — use findObjectProperty(), getLiteralValue(), etc. context.report({ node: targetNode, messageId: 'messageId', data: { /* template vars */ }, fix(fixer) { return fixer.replaceText(targetNode, 'replacement'); }, }); }, }; }, }); ``` **Naming**: Export as `PascalCaseRule` (e.g. `MissingPairedItemRule`). The `name` field is kebab-case. **Available AST helpers** — see [reference.md](reference.md) for the full catalog of `ast-utils` and `file-utils` exports. ## Step 3: Write Tests Create `src/rules/.test.ts`: ```typescript import { RuleTester } from '@typescript-eslint/rule-tester'; import { YourRuleNameRule } from './rule-name.js'; const ruleTester = new RuleTester(); // Helper to generate test code — keeps test cases readable function createNodeCode(/* parameterize the varying parts */): string { return ` import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; export class TestNode implements INodeType { description: INodeTypeDescription = { displayName: 'Test Node', name: 'testNode', group: ['input'], version: 1, description: 'A test node', defaults: { name: 'Test Node' }, inputs: [], outputs: [], properties: [], }; }`; } ruleTester.run('rule-name', YourRuleNameRule, { valid: [ { name: 'class that does not implement INodeType', code: '...' }, { name: 'node with correct pattern', code: createNodeCode(/* correct */) }, ], invalid: [ { name: 'descriptive case name', code: createNodeCode(/* incorrect */), errors: [{ messageId: 'messageId', data: { /* expected template vars */ } }], output: createNodeCode(/* expected after fix */), // or `output: null` if no fix }, ], }); ``` **Test guidelines:** - Always test that non-INodeType classes are skipped (valid case) - Test both the error message and the fixed output for fixable rules - For rules with options, test each option combination - For rules using filesystem, mock with `vi.mock('../utils/file-utils.js')` - For suggestion-only rules, use `errors: [{ messageId, suggestions: [...] }]` ## Step 4: Register the Rule ### 4a. Add to `src/rules/index.ts` ```typescript import { YourRuleNameRule } from './rule-name.js'; // Add to the rules object: export const rules = { // ... existing rules 'rule-name': YourRuleNameRule, } satisfies Record; ``` ### 4b. Add to `src/plugin.ts` configs Add to **both** config objects (unless the rule depends on n8n cloud features): ```typescript '@n8n/community-nodes/rule-name': 'error', // or 'warn' ``` - Use `error` for rules that catch bugs or required patterns - Use `warn` for style/convention rules (like `options-sorted-alphabetically`) - If the rule uses `no-restricted-globals` or `no-restricted-imports` patterns, only add to `recommended` (not `recommendedWithoutN8nCloudSupport`) ## Step 5: Write Documentation Create `docs/rules/.md`: ```markdown # Description of what the rule does (`@n8n/community-nodes/rule-name`) ## Rule Details Explain why this rule exists and what problem it prevents. ## Examples ### Incorrect \`\`\`typescript // code that triggers the rule \`\`\` ### Correct \`\`\`typescript // code that passes the rule \`\`\` ``` The header above `` will be regenerated by `pnpm build:docs`. Write a reasonable first version — it gets overwritten. ## Step 6: Verify Run from `packages/@n8n/eslint-plugin-community-nodes/`: ```bash pushd packages/@n8n/eslint-plugin-community-nodes pnpm test .test.ts # tests pass pnpm typecheck # types are clean pnpm build # compiles pnpm build:docs # regenerates doc headers and README table pnpm lint:docs # docs match schema popd ``` ## Checklist - [ ] Rule file: `src/rules/.ts` - [ ] Test file: `src/rules/.test.ts` - [ ] Registered in `src/rules/index.ts` - [ ] Added to configs in `src/plugin.ts` - [ ] Doc file: `docs/rules/.md` - [ ] README table updated via `pnpm build:docs` - [ ] All verification commands pass