eslint-plugin
Author custom ESLint plugins and rules with test-driven development. Supports flat config (ESLint 9+) and legacy formats. Uses @typescript-eslint/rule-tester for testing. Covers problem, suggestion, and layout rules including auto-fixers and type-aware rules. Use when creating or modifying ESLint rules, plugins, custom linting logic, or authoring auto-fixers.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install third774-dotfiles-eslint-plugins
Repository
Skill path: opencode/skills/eslint-plugins
Author custom ESLint plugins and rules with test-driven development. Supports flat config (ESLint 9+) and legacy formats. Uses @typescript-eslint/rule-tester for testing. Covers problem, suggestion, and layout rules including auto-fixers and type-aware rules. Use when creating or modifying ESLint rules, plugins, custom linting logic, or authoring auto-fixers.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Testing, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: third774.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install eslint-plugin into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/third774/dotfiles before adding eslint-plugin to shared team environments
- Use eslint-plugin for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: eslint-plugin
description: Author custom ESLint plugins and rules with test-driven development. Supports flat config (ESLint 9+) and legacy formats. Uses @typescript-eslint/rule-tester for testing. Covers problem, suggestion, and layout rules including auto-fixers and type-aware rules. Use when creating or modifying ESLint rules, plugins, custom linting logic, or authoring auto-fixers.
---
# ESLint Plugin Author
Write custom ESLint rules using TDD. This skill covers rule creation, testing, and plugin packaging.
<IMPORTANT>
1. Write tests BEFORE implementing rules (TDD)
2. ASK about edge cases before writing any code
3. Fixes MUST be idempotent (running twice = running once)
</IMPORTANT>
## When to Use
- Enforcing project-specific coding standards
- Creating rules with auto-fix or suggestions
- Building TypeScript-aware rules using type information
- Migrating from deprecated rules
## Workflow
Copy and track:
```
ESLint Rule Progress:
- [ ] Clarify transformation (before/after examples)
- [ ] Ask edge case questions (see below)
- [ ] Detect project setup (config format, test runner)
- [ ] Write failing tests first
- [ ] Implement rule to pass tests
- [ ] Add edge case tests
- [ ] Document the rule
```
## Edge Case Discovery
**CRITICAL: Ask these BEFORE writing code.**
### Always Ask
1. Should the rule apply to all file types or specific extensions?
2. Should it be auto-fixable, provide suggestions, or just report?
3. Are any patterns exempt (test files, generated code)?
### By Rule Type
| Type | Key Questions |
|------|---------------|
| **Identifiers** | Variables, functions, classes, or all? Destructured? Renamed imports? |
| **Imports** | Re-exports? Dynamic imports? Type-only? Side-effect imports? |
| **Functions** | Arrow vs declaration? Methods vs standalone? Async? Generators? |
| **JSX** | JSX and createElement? Fragments? Self-closing? Spread props? |
| **TypeScript** | Require type info? Handle `any`? Generics? Type assertions? |
## Project Setup Detection
### Config Format
| Files Present | Format |
|---------------|--------|
| `eslint.config.js/mjs/cjs/ts` | Flat config (ESLint 9+) |
| `.eslintrc.*` or `eslintConfig` in package.json | Legacy |
### Test Runner
Check `package.json` devDependencies:
- **Bun**: `bun:test` or `bun`
- **Vitest**: `vitest`
- **Jest**: `jest`
## Rule Template
```typescript
// src/rules/rule-name.ts
import { ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator(
(name) => `https://example.com/rules/${name}`
);
type Options = [{ optionName?: boolean }];
type MessageIds = "errorId" | "suggestionId";
export default createRule<Options, MessageIds>({
name: "rule-name",
meta: {
type: "problem", // "problem" | "suggestion" | "layout"
docs: { description: "What this rule does" },
fixable: "code", // Only if auto-fixable
hasSuggestions: true, // Only if has suggestions
messages: {
errorId: "Error: {{ placeholder }}",
suggestionId: "Try this instead",
},
schema: [{
type: "object",
properties: { optionName: { type: "boolean" } },
additionalProperties: false,
}],
},
defaultOptions: [{ optionName: false }],
create(context, [options]) {
return {
// Use AST selectors - see references/code-patterns.md
"CallExpression[callee.name='forbidden']"(node) {
context.report({
node,
messageId: "errorId",
fix(fixer) {
return fixer.replaceText(node, "replacement");
},
});
},
};
},
});
```
## Test Template
```typescript
// src/rules/__tests__/rule-name.test.ts
import { afterAll, describe, it } from "bun:test"; // or vitest
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule from "../rule-name";
// Configure BEFORE creating instance
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
RuleTester.itOnly = it.only;
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
});
ruleTester.run("rule-name", rule, {
valid: [
`const allowed = 1;`,
{
code: `const exempt = 1;`,
name: "ignores exempt pattern",
},
],
invalid: [
{
code: `const bad = 1;`,
output: `const good = 1;`,
errors: [{ messageId: "errorId" }],
name: "fixes main case",
},
],
});
```
For other test runners and patterns, see [references/test-patterns.md](references/test-patterns.md).
## Type-Aware Rules
For rules needing TypeScript type information:
```typescript
import { ESLintUtils } from "@typescript-eslint/utils";
create(context) {
const services = ESLintUtils.getParserServices(context);
return {
CallExpression(node) {
// v6+ simplified API - direct call
const type = services.getTypeAtLocation(node);
if (type.symbol?.flags & ts.SymbolFlags.Enum) {
context.report({ node, messageId: "enumError" });
}
},
};
}
```
**Test config for type-aware rules:**
```typescript
import parser from "@typescript-eslint/parser";
const ruleTester = new RuleTester({
languageOptions: {
parser,
parserOptions: {
projectService: { allowDefaultProject: ["*.ts*"] },
tsconfigRootDir: import.meta.dirname,
},
},
});
```
## Plugin Structure (Flat Config)
```typescript
// src/index.ts
import { defineConfig } from "eslint/config";
import rule1 from "./rules/rule1";
const plugin = {
meta: { name: "eslint-plugin-my-plugin", version: "1.0.0" },
configs: {} as Record<string, unknown>,
rules: { "rule1": rule1 },
};
Object.assign(plugin.configs, {
recommended: defineConfig([{
plugins: { "my-plugin": plugin },
rules: { "my-plugin/rule1": "error" },
}]),
});
export default plugin;
```
For legacy and dual-format plugins, see [references/plugin-templates.md](references/plugin-templates.md).
## Required Test Coverage
| Category | Purpose |
|----------|---------|
| Main case | Core transformation |
| No-op | Unrelated code unchanged |
| Idempotency | Already-fixed code stays fixed |
| Edge cases | Variations from spec |
| Options | Different configurations |
## Quick Reference
### Rule Types
| Type | Use Case |
|------|----------|
| `problem` | Code that causes errors |
| `suggestion` | Style improvements |
| `layout` | Whitespace/formatting |
### Fixer Methods
```typescript
fixer.replaceText(node, "new")
fixer.insertTextBefore(node, "prefix")
fixer.insertTextAfter(node, "suffix")
fixer.remove(node)
fixer.replaceTextRange([start, end], "new")
```
### Common Selectors
```typescript
"CallExpression[callee.name='target']" // Function call by name
"MemberExpression[property.name='prop']" // Property access
"ImportDeclaration[source.value='pkg']" // Import from package
"Identifier[name='forbidden']" // Identifier by name
":not(CallExpression)" // Negation
"FunctionDeclaration:exit" // Exit visitor
```
## References
- [Code Patterns](references/code-patterns.md) - AST selectors, fixer API, reporting, context API
- [Test Patterns](references/test-patterns.md) - RuleTester setup for Bun/Vitest/Jest, test cases
- [Plugin Templates](references/plugin-templates.md) - Flat config, legacy, dual-format structures
- [Troubleshooting](references/troubleshooting.md) - Common issues, debugging techniques
## External Tools
- **AST Explorer**: https://astexplorer.net (select @typescript-eslint/parser)
- **ast-grep**: `sg --lang ts -p 'pattern'` for structural searches
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/test-patterns.md
```markdown
# ESLint Rule Test Patterns
Reference for RuleTester configuration and test case patterns.
## RuleTester Setup by Framework
### Bun
```typescript
// src/rules/__tests__/rule-name.test.ts
import { afterAll, describe, it } from "bun:test";
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule from "../rule-name";
// Configure BEFORE creating instance
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
RuleTester.itOnly = it.only;
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: { jsx: true },
},
},
});
ruleTester.run("rule-name", rule, {
valid: [],
invalid: [],
});
```
**Optional preload setup** (bunfig.toml):
```toml
[test]
preload = ["./test-setup.ts"]
```
```typescript
// test-setup.ts
import { afterAll, describe, it } from "bun:test";
import { RuleTester } from "@typescript-eslint/rule-tester";
RuleTester.afterAll = afterAll;
RuleTester.describe = describe;
RuleTester.it = it;
RuleTester.itOnly = it.only;
```
### Vitest
```typescript
// src/rules/__tests__/rule-name.test.ts
import * as vitest from "vitest";
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule from "../rule-name";
// Configure BEFORE creating instance
RuleTester.afterAll = vitest.afterAll;
RuleTester.it = vitest.it;
RuleTester.itOnly = vitest.it.only;
RuleTester.describe = vitest.describe;
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
});
ruleTester.run("rule-name", rule, {
valid: [],
invalid: [],
});
```
**If using `globals: true`** in vitest.config.ts, you only need:
```typescript
RuleTester.afterAll = afterAll;
```
### Jest
```typescript
// src/rules/__tests__/rule-name.test.ts
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule from "../rule-name";
// Jest globals work automatically - no setup needed
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
},
});
ruleTester.run("rule-name", rule, {
valid: [],
invalid: [],
});
```
## Type-Aware Rule Testing
For rules that use type information:
```typescript
import parser from "@typescript-eslint/parser";
const ruleTester = new RuleTester({
languageOptions: {
parser,
parserOptions: {
projectService: {
allowDefaultProject: ["*.ts*"],
},
tsconfigRootDir: import.meta.dirname,
},
},
});
```
**Note:** Use `import.meta.dirname` (ESM) instead of `__dirname` (CJS).
## Valid Test Cases
```typescript
valid: [
// Simple string
`const x = 1;`,
// Object with name (recommended for clarity)
{
code: `const x = 1;`,
name: "allows basic assignment",
},
// With options
{
code: `const x = 1;`,
options: [{ allowX: true }],
name: "allows X when option enabled",
},
// With specific filename
{
code: `console.log('test');`,
filename: "test.spec.ts",
name: "allows in test files",
},
// With language options override
{
code: `<Component />`,
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
name: "handles JSX",
},
],
```
## Invalid Test Cases
### Basic Error
```typescript
{
code: `const bad = 1;`,
errors: [{ messageId: "forbidden" }],
name: "detects forbidden pattern",
},
```
### With Fix Output
```typescript
{
code: `const bad = 1;`,
output: `const good = 1;`,
errors: [{ messageId: "forbidden" }],
name: "fixes forbidden pattern",
},
```
### With Specific Location
```typescript
{
code: `const bad = 1;`,
errors: [
{
messageId: "forbidden",
line: 1,
column: 7,
endLine: 1,
endColumn: 10,
},
],
name: "reports correct location",
},
```
### With Data Validation
```typescript
{
code: `const bad = 1;`,
errors: [
{
messageId: "forbidden",
data: { name: "bad" },
},
],
name: "includes correct data in message",
},
```
### Multiple Errors
```typescript
{
code: `const bad1 = 1; const bad2 = 2;`,
errors: [
{ messageId: "forbidden" },
{ messageId: "forbidden" },
],
name: "detects multiple violations",
},
```
### Assert No Fix
```typescript
{
code: `const complexBad = 1;`,
output: null, // Explicitly assert no fix
errors: [{ messageId: "tooComplex" }],
name: "does not fix complex cases",
},
```
## Testing Suggestions
### With Suggestions
```typescript
{
code: `const problematic = 1;`,
errors: [
{
messageId: "hasOptions",
suggestions: [
{
messageId: "option1",
output: `const fixed1 = 1;`,
},
{
messageId: "option2",
data: { newName: "fixed2" },
output: `const fixed2 = 1;`,
},
],
},
],
name: "provides multiple suggestions",
},
```
### Assert No Suggestions
```typescript
{
code: `const edge = 1;`,
errors: [
{
messageId: "noSuggestions",
suggestions: null,
},
],
name: "does not suggest for edge case",
},
```
## Required Test Categories
Every rule SHOULD have tests covering:
| Category | Purpose | Example |
|----------|---------|---------|
| Main case | Core transformation | Primary before/after |
| No-op | Unrelated code passes | Files without pattern |
| Idempotency | Fix is stable | Already-fixed code |
| Edge cases | Variations from spec | Destructuring, aliases |
| Options | Different configs | With/without flags |
## Test Organization
```typescript
ruleTester.run("rule-name", rule, {
valid: [
// --- Basic valid cases ---
`const x = 1;`,
// --- Already correct ---
{
code: `const correct = 1;`,
name: "ignores already-correct code",
},
// --- Exempt patterns ---
{
code: `console.log('debug');`,
filename: "test.spec.ts",
name: "allows in test files",
},
],
invalid: [
// --- Main transformation ---
{
code: `const bad = 1;`,
output: `const good = 1;`,
errors: [{ messageId: "errorId" }],
name: "main case",
},
// --- Edge cases ---
{
code: `const { bad } = obj;`,
errors: [{ messageId: "errorId" }],
name: "handles destructuring",
},
// --- With options ---
{
code: `const bad = 1;`,
options: [{ strict: true }],
errors: [{ messageId: "strictError" }],
name: "strict mode error",
},
],
});
```
## Running Tests
```bash
# Bun
bun test src/rules/__tests__/rule-name.test.ts
bun test --watch src/rules/__tests__/rule-name.test.ts
# Vitest
npx vitest run src/rules/__tests__/rule-name.test.ts
npx vitest src/rules/__tests__/rule-name.test.ts # watch mode
# Jest
npx jest src/rules/__tests__/rule-name.test.ts
npx jest --watch src/rules/__tests__/rule-name.test.ts
```
```
### references/plugin-templates.md
```markdown
# ESLint Plugin Templates
Reference for plugin structure and configuration patterns.
## Directory Structure
```
eslint-plugin-my-plugin/
├── src/
│ ├── index.ts # Plugin entry point
│ └── rules/
│ ├── rule-name.ts # Rule implementation
│ └── __tests__/
│ └── rule-name.test.ts
├── package.json
├── tsconfig.json
└── bunfig.toml # If using Bun
```
## Rule Implementation Template
```typescript
// src/rules/rule-name.ts
import { ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator(
(name) => `https://example.com/rules/${name}`
);
type Options = [
{
optionName?: boolean;
}
];
type MessageIds = "errorId" | "suggestionId";
export default createRule<Options, MessageIds>({
name: "rule-name",
meta: {
type: "problem",
docs: {
description: "Description of what the rule does",
},
fixable: "code",
hasSuggestions: true,
messages: {
errorId: "Error message with {{ placeholder }}",
suggestionId: "Suggestion message",
},
schema: [
{
type: "object",
properties: {
optionName: { type: "boolean" },
},
additionalProperties: false,
},
],
},
defaultOptions: [{ optionName: false }],
create(context, [options]) {
return {
Identifier(node) {
// Rule logic
},
};
},
});
```
## Flat Config Plugin (ESLint 9+)
```typescript
// src/index.ts
import { defineConfig } from "eslint/config";
import rule1 from "./rules/rule1";
import rule2 from "./rules/rule2";
const plugin = {
meta: {
name: "eslint-plugin-my-plugin",
version: "1.0.0",
},
configs: {} as Record<string, unknown>,
rules: {
"rule1": rule1,
"rule2": rule2,
},
};
// Self-referential configs (must be after plugin definition)
Object.assign(plugin.configs, {
recommended: defineConfig([
{
plugins: {
"my-plugin": plugin,
},
rules: {
"my-plugin/rule1": "error",
"my-plugin/rule2": "warn",
},
},
]),
});
export default plugin;
```
**Usage:**
```javascript
// eslint.config.js
import { defineConfig } from "eslint/config";
import myPlugin from "eslint-plugin-my-plugin";
export default defineConfig([
// Use preset
myPlugin.configs.recommended,
// Or configure individually
{
plugins: {
"my-plugin": myPlugin,
},
rules: {
"my-plugin/rule1": "error",
},
},
]);
```
## Legacy Config Plugin (.eslintrc.*)
```typescript
// src/index.ts
import rule1 from "./rules/rule1";
import rule2 from "./rules/rule2";
module.exports = {
rules: {
"rule1": rule1,
"rule2": rule2,
},
configs: {
recommended: {
plugins: ["my-plugin"],
rules: {
"my-plugin/rule1": "error",
"my-plugin/rule2": "warn",
},
},
},
};
```
**Usage:**
```json
// .eslintrc.json
{
"plugins": ["my-plugin"],
"extends": ["plugin:my-plugin/recommended"]
}
```
## Dual-Format Plugin (Both Configs)
```typescript
// src/index.ts
import { defineConfig } from "eslint/config";
import rule1 from "./rules/rule1";
const rules = {
"rule1": rule1,
};
// Flat config plugin object
const plugin = {
meta: {
name: "eslint-plugin-my-plugin",
version: "1.0.0",
},
configs: {} as Record<string, unknown>,
rules,
};
// Flat config presets
Object.assign(plugin.configs, {
recommended: defineConfig([
{
plugins: { "my-plugin": plugin },
rules: { "my-plugin/rule1": "error" },
},
]),
});
// Legacy config presets
const legacyConfigs = {
recommended: {
plugins: ["my-plugin"],
rules: { "my-plugin/rule1": "error" },
},
};
// Export for both systems
export default plugin;
export { rules, legacyConfigs as configs };
```
## package.json
```json
{
"name": "eslint-plugin-my-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "tsc",
"test": "bun test",
"test:watch": "bun test --watch",
"lint": "eslint src"
},
"peerDependencies": {
"eslint": ">=9.0.0",
"@typescript-eslint/parser": ">=8.0.0"
},
"dependencies": {
"@typescript-eslint/utils": "^8.0.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^8.0.0",
"@typescript-eslint/rule-tester": "^8.0.0",
"bun-types": "latest",
"eslint": "^9.0.0",
"typescript": "^5.0.0"
}
}
```
## tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
```
## Local Plugin (No Package)
For rules specific to a single project:
```javascript
// eslint.config.js
import { defineConfig } from "eslint/config";
import myRule from "./eslint-rules/my-rule.js";
export default defineConfig([
{
plugins: {
local: {
rules: {
"my-rule": myRule,
},
},
},
rules: {
"local/my-rule": "error",
},
},
]);
```
## Type-Aware Plugin Config
For plugins with rules using TypeScript type information:
```javascript
// eslint.config.js
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import myPlugin from "eslint-plugin-my-plugin";
export default defineConfig([
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
myPlugin.configs.recommended,
]);
```
```
### references/code-patterns.md
```markdown
# ESLint Rule Code Patterns
Reference for AST traversal, reporting, and fixing patterns.
## Rule Meta Object
```typescript
meta: {
type: "problem", // "problem" | "suggestion" | "layout"
docs: {
description: "What the rule does",
},
fixable: "code", // Include only if auto-fixable
hasSuggestions: true, // Include only if has suggestions
messages: {
errorId: "Error with {{ placeholder }}",
suggestId: "Suggestion message",
},
schema: [
{
type: "object",
properties: {
optionName: { type: "boolean" },
},
additionalProperties: false,
},
],
}
```
| Type | Use Case |
|------|----------|
| `problem` | Code likely to cause errors |
| `suggestion` | Style improvement, not an error |
| `layout` | Whitespace, formatting |
## Context API
```typescript
create(context) {
const options = context.options[0] ?? {};
const sourceCode = context.sourceCode;
// Get text
const text = sourceCode.getText(node);
// Get tokens
const before = sourceCode.getTokenBefore(node);
const after = sourceCode.getTokenAfter(node);
// Get comments
const comments = sourceCode.getCommentsBefore(node);
// Get scope
const scope = sourceCode.getScope(node);
// Get ancestors
const ancestors = sourceCode.getAncestors(node);
return { /* visitors */ };
}
```
## AST Selectors
```typescript
create(context) {
return {
// Simple node type
Identifier(node) { },
// Exit visitor (after children processed)
"FunctionDeclaration:exit"(node) { },
// Attribute condition
"CallExpression[callee.name='forbidden']"(node) { },
// Multiple types
"FunctionDeclaration, ArrowFunctionExpression"(node) { },
// Child selector
"CallExpression > Identifier[name='console']"(node) { },
// Descendant selector
"CallExpression Identifier[name='log']"(node) { },
// Has selector (parent has matching child)
"ImportDeclaration:has(ImportSpecifier[imported.name='deprecated'])"(node) { },
// Literal value
"Literal[value=true]"(node) { },
// Not selector
"CallExpression:not([callee.name='allowed'])"(node) { },
};
}
```
## Reporting
### Basic Report
```typescript
context.report({
node,
messageId: "errorId",
});
```
### With Data Placeholders
```typescript
context.report({
node,
messageId: "errorWithData",
data: {
name: node.name,
expected: "something",
},
});
```
### With Specific Location
```typescript
context.report({
node,
loc: node.loc.start, // or { line, column }
messageId: "errorId",
});
```
### With Fix
```typescript
context.report({
node,
messageId: "fixable",
fix(fixer) {
return fixer.replaceText(node, "replacement");
},
});
```
### With Multiple Fixes
```typescript
context.report({
node,
messageId: "multipleChanges",
fix(fixer) {
return [
fixer.insertTextBefore(node, "prefix"),
fixer.insertTextAfter(node, "suffix"),
];
},
});
```
### With Suggestions
```typescript
context.report({
node,
messageId: "hasSuggestions",
suggest: [
{
messageId: "suggestion1",
fix(fixer) {
return fixer.replaceText(node, "option1");
},
},
{
messageId: "suggestion2",
data: { replacement: "option2" },
fix(fixer) {
return fixer.replaceText(node, "option2");
},
},
],
});
```
## Fixer API
```typescript
fix(fixer) {
// Insert
fixer.insertTextBefore(node, "text");
fixer.insertTextAfter(node, "text");
fixer.insertTextBeforeRange([start, end], "text");
fixer.insertTextAfterRange([start, end], "text");
// Remove
fixer.remove(node);
fixer.removeRange([start, end]);
// Replace
fixer.replaceText(node, "newText");
fixer.replaceTextRange([start, end], "newText");
}
```
**Fix Constraints:**
- Fixes MUST be idempotent (running twice = running once)
- Multiple fixes MUST NOT have overlapping ranges
- Fixes MUST NOT change code behavior
## Type-Aware Rules (typescript-eslint v6+)
```typescript
import { ESLintUtils } from "@typescript-eslint/utils";
import * as ts from "typescript";
create(context) {
const services = ESLintUtils.getParserServices(context);
return {
CallExpression(node) {
// Get type directly (v6+ simplified API)
const type = services.getTypeAtLocation(node);
// Check type string
if (services.program.getTypeChecker().typeToString(type) === "Promise<void>") {
// Handle Promise
}
// Check symbol flags
if (type.symbol?.flags & ts.SymbolFlags.Enum) {
// Handle enum
}
// Get call signatures
const signatures = type.getCallSignatures();
if (signatures.length > 0) {
const returnType = signatures[0].getReturnType();
}
},
};
}
```
**Note:** The v6+ API (`services.getTypeAtLocation(node)`) replaces the verbose v5 pattern:
```typescript
// OLD (v5) - don't use
const checker = services.program.getTypeChecker();
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(tsNode);
```
## Common Node Types
| Node Type | Represents | Example |
|-----------|------------|---------|
| `Identifier` | Names | `foo`, `myVar` |
| `CallExpression` | Function calls | `foo()`, `obj.method()` |
| `MemberExpression` | Property access | `obj.prop`, `arr[0]` |
| `ImportDeclaration` | Import statements | `import { x } from 'y'` |
| `ImportSpecifier` | Named imports | `{ x }` in import |
| `ImportDefaultSpecifier` | Default imports | `x` in `import x from` |
| `VariableDeclaration` | Variable declarations | `const x = 1` |
| `VariableDeclarator` | Individual variable | `x = 1` part |
| `FunctionDeclaration` | Named functions | `function foo() {}` |
| `ArrowFunctionExpression` | Arrow functions | `() => {}` |
| `ObjectExpression` | Object literals | `{ a: 1 }` |
| `ArrayExpression` | Array literals | `[1, 2]` |
| `Literal` | Primitives | `'str'`, `42`, `true` |
| `TemplateLiteral` | Template strings | `` `hello ${name}` `` |
| `JSXElement` | JSX tags | `<div>` |
| `TSTypeAnnotation` | Type annotations | `: string` |
```
### references/troubleshooting.md
```markdown
# ESLint Rule Troubleshooting
Common issues and debugging techniques.
## Common Issues
| Issue | Cause | Solution |
|-------|-------|----------|
| "afterAll is not defined" | RuleTester not configured for test runner | Set `RuleTester.afterAll = afterAll` BEFORE creating instance |
| Fix not applied | `meta.fixable` not set | Add `fixable: "code"` to meta object |
| Suggestions not showing | `meta.hasSuggestions` missing | Add `hasSuggestions: true` to meta |
| Type info unavailable | Missing parser options | Configure `parserOptions.projectService` |
| Test timeout | Type-aware rule slow | Use `projectService.allowDefaultProject` |
| "Unknown rule" | Plugin not registered | Check plugin is in config's `plugins` object |
| Fix creates syntax error | Invalid range or text | Use AST explorer to verify node boundaries |
| `__dirname` undefined | ESM module context | Use `import.meta.dirname` instead |
| Parser not found | Using `require()` in ESM | Use `import parser from '@typescript-eslint/parser'` |
## Debugging Techniques
### Log AST Node Structure
```typescript
create(context) {
return {
CallExpression(node) {
console.log(JSON.stringify(node, null, 2));
},
};
}
```
### Check Text Being Replaced
```typescript
create(context) {
return {
Identifier(node) {
console.log("Text:", context.sourceCode.getText(node));
console.log("Range:", node.range);
},
};
}
```
### Verify Token Boundaries
```typescript
create(context) {
const sourceCode = context.sourceCode;
return {
CallExpression(node) {
const before = sourceCode.getTokenBefore(node);
const after = sourceCode.getTokenAfter(node);
console.log({ before, node, after });
},
};
}
```
### Check Scope Information
```typescript
create(context) {
return {
Identifier(node) {
const scope = context.sourceCode.getScope(node);
console.log("Scope type:", scope.type);
console.log("Variables:", scope.variables.map(v => v.name));
},
};
}
```
## AST Exploration Tools
### AST Explorer (Web)
https://astexplorer.net
1. Select parser: `@typescript-eslint/parser`
2. Paste code sample
3. Click nodes to see structure
4. Use for understanding node types and properties
### ast-grep (CLI)
```bash
# Find pattern in codebase
sg --lang ts -p 'console.log($MSG)'
# Interactive mode
sg --lang ts -p 'console.$METHOD($ARGS)' --interactive
# Show AST for file
sg --lang ts --debug-query 'console.log' file.ts
```
### ESLint Debug Mode
```bash
# See which rules are running
DEBUG=eslint:* npx eslint file.ts
# See rule timing
TIMING=1 npx eslint file.ts
```
## Fix Debugging
### Verify Fix Range
```typescript
fix(fixer) {
console.log("Node range:", node.range);
console.log("Node text:", context.sourceCode.getText(node));
return fixer.replaceText(node, "replacement");
}
```
### Multiple Fix Conflict
If multiple fixes conflict (overlapping ranges), only one applies. Debug by:
```typescript
fix(fixer) {
const fixes = [
fixer.insertTextBefore(node, "a"),
fixer.insertTextAfter(node, "b"),
];
console.log("Fixes:", fixes.map(f => ({ range: f.range, text: f.text })));
return fixes;
}
```
### Fix Not Applying
1. Check `meta.fixable: "code"` is set
2. Check fix function returns a value (not undefined)
3. Check ranges don't overlap with other fixes
4. Run with `--fix-dry-run` to see what would change
```bash
npx eslint --fix-dry-run --format json file.ts
```
## Type-Aware Rule Issues
### "You have used a rule which requires parserServices"
The rule uses type information but the config doesn't provide it.
**Fix:** Add to eslint.config.js:
```javascript
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
```
### Type Information is Wrong
1. Check tsconfig.json includes the file
2. Check `tsconfigRootDir` points to correct location
3. Restart ESLint (type info is cached)
### Slow Type-Aware Rules
Type-aware rules are slower because they load the TypeScript program.
**Optimizations:**
- Use `projectService.allowDefaultProject` in tests
- Limit type-aware rules to specific file patterns
- Consider if type info is actually needed
## Test Debugging
### See Full Error Output
```typescript
// In test file
ruleTester.run("rule-name", rule, {
invalid: [
{
code: `bad code`,
errors: [
{
messageId: "errorId",
// Add all properties to see what's wrong
line: 1,
column: 1,
endLine: 1,
endColumn: 10,
data: { name: "expected" },
},
],
},
],
});
```
### Test Expected Output Mismatch
If `output` doesn't match, the test shows a diff. Common causes:
1. **Whitespace:** Output must match exactly, including newlines
2. **Quotes:** Single vs double quotes matter
3. **Semicolons:** Check if rule adds/removes them
### Isolate Failing Test
```typescript
// Bun
it.only("test name", ...)
// Vitest
it.only("test name", ...)
// Jest
it.only("test name", ...)
```
Or use `RuleTester.itOnly`:
```typescript
RuleTester.itOnly = it.only;
```
## Performance Issues
### Rule is Slow
1. **Exit early:** Check preconditions before expensive operations
2. **Cache lookups:** Store repeated `getScope()` or type lookups
3. **Use selectors:** CSS selectors are optimized; prefer over manual filtering
4. **Avoid reparse:** Don't call `getText()` on entire source repeatedly
```typescript
// BAD: Checks all identifiers
Identifier(node) {
if (node.name === "target") { /* ... */ }
}
// GOOD: Selector filters first
"Identifier[name='target']"(node) {
/* ... */
}
```
### Tests are Slow
1. Use `projectService.allowDefaultProject` for type-aware tests
2. Run specific test file, not entire suite
3. Consider if type-awareness is needed for all tests
```