home / skills / third774 / dotfiles / eslint-plugins

eslint-plugins skill

/opencode/skills/eslint-plugins

This skill helps you author custom ESLint plugins and rules using test-driven development, ensuring robust, maintainable linting with auto-fixes.

npx playbooks add skill third774/dotfiles --skill eslint-plugins

Review the files below or copy the command above to add this skill to your agents.

Files (5)
SKILL.md
7.5 KB
---
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

Overview

This skill helps you author custom ESLint plugins and rules using test-driven development. It supports both ESLint flat config (v9+) and legacy formats, includes TypeScript-aware rules, and guides creation of problem, suggestion, and layout rules with idempotent auto-fixes.

How this skill works

Follow a TDD workflow: ask edge-case questions, write failing tests with @typescript-eslint/rule-tester, then implement the rule to satisfy tests. The skill provides templates for rule files, tests, type-aware logic, and plugin packaging for flat and legacy configs, plus guidance on fixer APIs and common AST selectors.

When to use it

  • Enforcing project-specific coding standards or patterns
  • Adding auto-fixers or suggestions to automate small refactors
  • Creating TypeScript-aware rules that need type information
  • Migrating or replacing deprecated rules in a codebase
  • Authoring plugin bundles for shared or organization-wide rules

Best practices

  • Always write tests before implementation and confirm failing tests first
  • Ask the edge-case questions up front (file types, exemptions, fix vs suggest)
  • Ensure fixes are idempotent—applying them twice should be a no-op
  • Include coverage for main case, no-op, idempotency, edge cases, and options
  • Detect project config and test runner to pick the correct RuleTester setup

Example use cases

  • Create a rule that replaces a deprecated API call with a new one and auto-fixes call sites
  • Enforce import ordering and provide suggestions for reordering without breaking type-only imports
  • Write a TypeScript rule that flags misuse of enums using parser services and type checks
  • Bundle multiple rules into a plugin with a recommended flat-config profile for ESLint 9+
  • Add a layout rule that normalizes trailing commas with an idempotent fixer

FAQ

Which test runner should I configure for RuleTester?

Detect the project's devDependencies: use bun:test for Bun, vitest for Vitest, or jest for Jest and wire RuleTester.describe/it accordingly.

How do I test rules that need TypeScript type information?

Use @typescript-eslint/parser in RuleTester languageOptions, enable projectService with appropriate patterns, and obtain services via ESLintUtils.getParserServices(context).