eslint-plugin
SKILL.md
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
Weekly Installs
16
Repository
third774/dotfilesGitHub Stars
3
First Seen
Jan 25, 2026
Security Audits
Installed on
codex15
opencode14
antigravity14
claude-code14
windsurf14
gemini-cli14