Claude
Skills
Sign in
Back

eslint-rule-dev

Included with Lifetime
$97 forever

ESLint custom rule development - AST traversal, rule testing, plugins, and flat config When user creates ESLint rules, develops ESLint plugins, works with AST, or mentions RuleTester

General

What this skill does


# ESLint Rule Development Agent

## What's New in ESLint 9+ (2024-2025)

- **Flat config**: `eslint.config.js` replaces `.eslintrc.*`
- **ESM support**: Native ES modules in configs and rules
- **`defineConfig()` helper**: Type-safe configuration with autocomplete
- **Stricter plugin format**: Plugins must use new object structure
- **Removed formatters**: Many built-in formatters moved to packages

## Rule Structure

Every ESLint rule exports an object with `meta` and `create`:

```javascript
export default {
  meta: {
    type: "problem", // "problem" | "suggestion" | "layout"
    docs: {
      description: "Disallow foo assigned to anything other than bar",
      recommended: true,
      url: "https://example.com/rules/no-foo",
    },
    fixable: "code", // "code" | "whitespace" | null
    hasSuggestions: true,
    schema: [], // JSON Schema for rule options
    messages: {
      avoidFoo: "Avoid using 'foo' - use 'bar' instead.",
      suggestBar: "Replace with 'bar'.",
    },
  },

  create(context) {
    return {
      // Visitor methods for AST nodes
      Identifier(node) {
        if (node.name === "foo") {
          context.report({
            node,
            messageId: "avoidFoo",
          });
        }
      },
    };
  },
};
```

## Meta Properties

| Property           | Purpose                                          |
| ------------------ | ------------------------------------------------ |
| `type`             | Rule category: "problem", "suggestion", "layout" |
| `docs.description` | Short description for documentation              |
| `docs.recommended` | Include in recommended config                    |
| `docs.url`         | Link to full documentation                       |
| `fixable`          | Enable auto-fix ("code" or "whitespace")         |
| `hasSuggestions`   | Rule provides suggestions                        |
| `schema`           | JSON Schema for options validation               |
| `messages`         | Message templates with IDs                       |
| `defaultOptions`   | Default values for options                       |
| `deprecated`       | Mark rule as deprecated                          |

## The Context Object

The `context` object passed to `create()` provides:

### Properties

```javascript
create(context) {
  // Rule configuration
  context.id              // Rule ID (e.g., "no-console")
  context.options         // Array of configured options
  context.settings        // Shared settings from config

  // File information
  context.filename        // Current file path
  context.cwd             // Current working directory

  // Source code access
  context.sourceCode      // SourceCode object for analysis

  // Language configuration
  context.languageOptions // Parser options, globals, etc.
}
```

### Methods

```javascript
// Report a problem
context.report({
  node,
  messageId: "myMessage",
  data: { name: "foo" },
  fix: (fixer) => fixer.replaceText(node, "bar"),
});
```

## AST Node Visitors

Rules work by defining visitor functions for AST node types:

```javascript
create(context) {
  return {
    // Called when entering a node
    CallExpression(node) {
      // Analyze call expressions
    },

    // Called when exiting a node (use ":exit" suffix)
    "FunctionDeclaration:exit"(node) {
      // Run after all children processed
    },

    // Selector syntax for complex matching
    "CallExpression[callee.name='require']"(node) {
      // Only matches require() calls
    },
  };
}
```

### Common Node Types

| Node Type                  | Matches                        |
| -------------------------- | ------------------------------ |
| `Identifier`               | Variable names, function names |
| `Literal`                  | Strings, numbers, booleans     |
| `CallExpression`           | Function calls                 |
| `MemberExpression`         | Property access (a.b, a['b'])  |
| `FunctionDeclaration`      | Named function declarations    |
| `ArrowFunctionExpression`  | Arrow functions                |
| `VariableDeclaration`      | let, const, var declarations   |
| `ImportDeclaration`        | import statements              |
| `ExportDefaultDeclaration` | export default                 |

## AST Selectors

ESLint supports CSS-like selectors for targeting nodes:

```javascript
// Basic selectors
"Identifier"; // Any identifier
"CallExpression"; // Any function call

// Attribute selectors
"Identifier[name='foo']"; // Identifier named "foo"
"Literal[value=123]"; // Literal with value 123
"CallExpression[callee.name='require']"; // require() calls

// Descendant selectors
"FunctionDeclaration Identifier"; // Identifiers inside functions

// Child selectors
"CallExpression > MemberExpression"; // Direct child

// Sibling selectors
"VariableDeclaration ~ VariableDeclaration"; // Following sibling

// Pseudo-classes
":first-child"; // First child node
":last-child"; // Last child node
":nth-child(2)"; // Second child
":not(Literal)"; // Not a Literal

// Combinations
"CallExpression[callee.object.name='console'][callee.property.name='log']";
```

## Reporting Problems

### Basic Report

```javascript
context.report({
  node: node,
  messageId: "unexpectedFoo",
  data: { name: node.name },
});
```

### Report with Location

```javascript
context.report({
  loc: {
    start: { line: 1, column: 0 },
    end: { line: 1, column: 5 },
  },
  messageId: "unexpectedFoo",
});
```

### Report with Fix

```javascript
context.report({
  node,
  messageId: "useBar",
  fix(fixer) {
    return fixer.replaceText(node, "bar");
  },
});
```

### Report with Suggestions

```javascript
context.report({
  node,
  messageId: "useBetterName",
  suggest: [
    {
      messageId: "renameToBar",
      fix(fixer) {
        return fixer.replaceText(node, "bar");
      },
    },
    {
      messageId: "renameToQux",
      fix(fixer) {
        return fixer.replaceText(node, "qux");
      },
    },
  ],
});
```

## Fixer Methods

The `fixer` object provides these methods:

```javascript
// Insert text
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");
```

### Multiple Fixes

Return an array or iterable for multiple fixes:

```javascript
fix(fixer) {
  return [
    fixer.insertTextBefore(node, "/* comment */ "),
    fixer.replaceText(node.property, "info"),
  ];
}
```

## Accessing Source Code

```javascript
create(context) {
  const sourceCode = context.sourceCode;

  return {
    CallExpression(node) {
      // Get source text
      const text = sourceCode.getText(node);

      // Get tokens
      const tokens = sourceCode.getTokens(node);
      const firstToken = sourceCode.getFirstToken(node);
      const lastToken = sourceCode.getLastToken(node);

      // Get comments
      const commentsBefore = sourceCode.getCommentsBefore(node);
      const commentsAfter = sourceCode.getCommentsAfter(node);
      const commentsInside = sourceCode.getCommentsInside(node);

      // Get scope information
      const scope = sourceCode.getScope(node);
      const variables = sourceCode.getDeclaredVariables(node);
    }
  };
}
```

## Scope Analysis

Access variable scopes for advanced analysis:

```javascript
create(context) {
  return {
    "Program:exit"(node) {
      const scope = context.sourceCode.getScope(node);

      // All variables in scope
      scope.variables.forEach(variable => {
        console.log(variable.name);
        console.log(variable.references);  // Where it's used
        console.log(variable.defs);        // Where it's defined
      });

      // Unresolved references (global access)
      scope.through.forEach(reference => {
        console.log(reference.identifier.name);
      });

      // Child scopes
      sco

Related in General