eslint-plugin
ESLint Plugin Author
Write custom ESLint rules using TDD. This skill covers rule creation, testing, and plugin packaging.
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
- Should the rule apply to all file types or specific extensions?
- Should it be auto-fixable, provide suggestions, or just report?
- 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:testorbun - Vitest:
vitest - Jest:
jest
Rule Template
// 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
// 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.
Type-Aware Rules
For rules needing TypeScript type information:
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:
import parser from "@typescript-eslint/parser";
const ruleTester = new RuleTester({
languageOptions: {
parser,
parserOptions: {
projectService: { allowDefaultProject: ["*.ts*"] },
tsconfigRootDir: import.meta.dirname,
},
},
});
Plugin Structure (Flat Config)
// 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.
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
fixer.replaceText(node, "new")
fixer.insertTextBefore(node, "prefix")
fixer.insertTextAfter(node, "suffix")
fixer.remove(node)
fixer.replaceTextRange([start, end], "new")
Common Selectors
"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 - AST selectors, fixer API, reporting, context API
- Test Patterns - RuleTester setup for Bun/Vitest/Jest, test cases
- Plugin Templates - Flat config, legacy, dual-format structures
- Troubleshooting - 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
More from third774/dotfiles
opensrc
Fetch source code for npm, PyPI, or crates.io packages and GitHub/GitLab repos to provide AI agents with implementation context beyond types and docs. Use when needing to understand how a library works internally, debug dependency issues, or explore package implementations.
90natural-writing
Write like a human, not a language model. Avoid AI-tell vocabulary, formulaic structures, and hollow emphasis. Apply to ALL written output including prose, documentation, comments, and communication. Use when drafting prose, documentation, comments, or any written output that should sound human.
66agent-skills
Author and improve Agent Skills following the agentskills.io specification. Use when creating new SKILL.md files, modifying existing skills, reviewing skill quality, or organizing skill directories with proper naming, descriptions, and progressive disclosure.
31documenting-code-comments
Standards for writing self-documenting code and best practices for when to write (and avoid) code comments. Use when auditing, cleaning up, or improving inline code documentation.
28customizing-opencode
Configure OpenCode via opencode.json, agents, commands, MCP servers, custom tools, plugins, themes, keybinds, and permissions. Use when setting up or modifying OpenCode configuration.
23adversarial-code-review
Review code through hostile perspectives to find bugs, security issues, and unintended consequences the author missed. Use when reviewing PRs, auditing codebases, or before critical deployments.
21