---
id: wiki-2026-0508-custom-eslint-rules-dev
title: Custom ESLint Rules Development
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [custom ESLint, AST rule, eslint plugin, ast visitor, autofix, semgrep alternative]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [eslint, ast, static-analysis, code-quality, custom-rule, plugin, autofix, semgrep]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: JavaScript / TypeScript
  framework: ESLint / Semgrep / typescript-eslint
---

# Custom ESLint Rules

## 매 한 줄
> **"매 team-specific rule 의 자동 enforcement"**. 매 AST visitor pattern + 매 fixer API. 매 architectural rule (no domain → infra), 매 anti-pattern (deprecated API), 매 convention (naming). 매 modern alternative: Semgrep (multi-language).

## 매 핵심

### 매 ESLint architecture
1. **Parser** (espree, @typescript-eslint/parser): 매 source → 매 AST.
2. **Rule**: 매 visit AST node + 매 report.
3. **Fixer**: 매 auto-correct.
4. **Config**: 매 enable / disable.

### 매 AST node type
- **Program**: 매 root.
- **VariableDeclaration**, **FunctionDeclaration**.
- **CallExpression**, **MemberExpression**.
- **ArrowFunctionExpression**, **ObjectExpression**, **ArrayExpression**.
- **TSInterfaceDeclaration**, **TSTypeAliasDeclaration** (TS).

### 매 use case
1. **Architectural**: domain → infrastructure 의 forbid.
2. **Convention**: naming, file structure.
3. **Deprecation**: 매 old API 의 detect.
4. **Security**: dangerous pattern (eval, dangerouslySetInnerHTML).
5. **Performance**: anti-pattern (e.g., unnecessary re-render).
6. **Domain-specific**: business rule.

### 매 selector syntax (Esquery)
- `Identifier[name="foo"]`: 매 specific name.
- `CallExpression > MemberExpression`: 매 chain.
- `:not(...)`: 매 exclusion.
- `[callee.name="alert"]`: 매 attribute match.

### 매 modern alternative: Semgrep
- 매 multi-language (Python, Go, Java, Rust, ...).
- 매 pattern-based (more readable).
- 매 ESLint custom 보다 매 fast write.

### 매 LLM-aided rule generation
- 매 plain English → 매 ESLint rule code.
- 매 example-based.

## 💻 패턴

### Basic ESLint rule
```js
// eslint-rule-no-console-log.js
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow console.log calls',
      recommended: true,
    },
    fixable: 'code',
    schema: [],
    messages: {
      noConsoleLog: 'Avoid console.log in production. Use logger.',
    },
  },
  
  create(context) {
    return {
      CallExpression(node) {
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.object.name === 'console' &&
          node.callee.property.name === 'log'
        ) {
          context.report({
            node,
            messageId: 'noConsoleLog',
            fix(fixer) {
              // 매 auto-fix: 매 logger.info 의 replace
              return fixer.replaceText(node.callee, 'logger.info');
            },
          });
        }
      },
    };
  },
};
```

### Plugin structure
```
my-eslint-plugin/
├── package.json
├── lib/
│   ├── index.js           # 매 plugin entry
│   └── rules/
│       ├── no-console-log.js
│       └── enforce-domain-isolation.js
└── tests/
    └── no-console-log.test.js
```

```js
// lib/index.js
module.exports = {
  rules: {
    'no-console-log': require('./rules/no-console-log'),
    'enforce-domain-isolation': require('./rules/enforce-domain-isolation'),
  },
  configs: {
    recommended: {
      plugins: ['@my-org'],
      rules: {
        '@my-org/no-console-log': 'error',
        '@my-org/enforce-domain-isolation': 'error',
      },
    },
  },
};
```

### Architectural rule (Clean Architecture)
```js
// 매 src/domain/* 의 매 src/infrastructure/* 의 import 의 forbid
module.exports = {
  meta: {
    type: 'problem',
    messages: {
      noInfraInDomain: 'domain layer must not import from infrastructure',
    },
  },
  
  create(context) {
    return {
      ImportDeclaration(node) {
        const filename = context.getFilename();
        if (!filename.includes('/domain/')) return;
        
        const importPath = node.source.value;
        if (importPath.includes('/infrastructure/')) {
          context.report({
            node,
            messageId: 'noInfraInDomain',
          });
        }
      },
    };
  },
};
```

### Test (RuleTester)
```js
const { RuleTester } = require('eslint');
const rule = require('./no-console-log');

const tester = new RuleTester();

tester.run('no-console-log', rule, {
  valid: [
    'logger.info("hi");',
    'console.error("err");',
  ],
  invalid: [
    {
      code: 'console.log("hi");',
      output: 'logger.info("hi");',
      errors: [{ messageId: 'noConsoleLog' }],
    },
  ],
});
```

### TypeScript-aware rule
```ts
import { TSESTree, ESLintUtils } from '@typescript-eslint/utils';

const createRule = ESLintUtils.RuleCreator(name => `https://docs.example.com/${name}`);

export default createRule({
  name: 'no-any-type',
  meta: {
    type: 'suggestion',
    docs: { description: 'Forbid `any` type', recommended: true },
    schema: [],
    messages: { noAny: 'Avoid `any` — use `unknown` or specific type.' },
  },
  defaultOptions: [],
  create(context) {
    return {
      TSAnyKeyword(node) {
        context.report({ node, messageId: 'noAny' });
      },
    };
  },
});
```

### Selector-based (esquery)
```js
// 매 매 React.useState() 의 매 first arg 의 type check
module.exports = {
  create(context) {
    return {
      'CallExpression[callee.object.name="React"][callee.property.name="useState"]'(node) {
        const arg = node.arguments[0];
        if (!arg) {
          context.report({ node, message: 'useState requires initial value' });
        }
      },
    };
  },
};
```

### Semgrep alternative
```yaml
# .semgrep/no-console-log.yml
rules:
  - id: no-console-log
    pattern: console.log(...)
    message: 'Avoid console.log in production'
    severity: WARNING
    languages: [javascript, typescript]
    fix: logger.info(...)

  - id: domain-no-infra
    patterns:
      - pattern: import $X from "$Y"
      - metavariable-pattern:
          metavariable: $Y
          pattern: '*infrastructure*'
    paths:
      include:
        - 'src/domain/**'
    message: 'domain must not import infrastructure'
    severity: ERROR
    languages: [typescript]
```

### LLM-generated rule
```python
def generate_eslint_rule(description, examples):
    prompt = f"""Generate an ESLint custom rule.

Description: {description}

Bad examples:
{format_examples(examples['bad'])}

Good examples (allowed):
{format_examples(examples['good'])}

Output: complete eslint rule .js file with autofix where possible.
Use selector syntax. Include test cases."""
    return llm.generate(prompt)
```

### Apply via flat config (ESLint 9+)
```js
// eslint.config.js
import myPlugin from '@my-org/eslint-plugin';

export default [
  {
    plugins: { '@my-org': myPlugin },
    rules: {
      '@my-org/no-console-log': 'error',
      '@my-org/enforce-domain-isolation': 'error',
    },
  },
];
```

### Performance: visitor reuse
```js
// 매 ❌ Bad — 매 매 node 마다 fileName check
create(context) {
  return {
    'ImportDeclaration'(node) {
      const fn = context.getFilename();
      if (!fn.includes('/domain/')) return;
      // ...
    },
  };
}

// 매 ✅ Better — 매 file-level cache
create(context) {
  const filename = context.getFilename();
  if (!filename.includes('/domain/')) return {};  // 매 visitor 의 skip entire
  
  return {
    'ImportDeclaration'(node) { /* ... */ },
  };
}
```

## 🤔 결정 기준
| 상황 | Tool |
|---|---|
| Single language (JS/TS) | ESLint custom |
| Multi-language | Semgrep |
| Cross-cutting (security) | CodeQL / Semgrep |
| Architectural | dependency-cruiser |
| Quick + readable pattern | Semgrep |
| AST manipulation needed | ESLint with autofix |
| Type-aware | typescript-eslint |

**기본값**: 매 single-lang JS/TS = ESLint. 매 multi-lang = Semgrep.

## 🔗 Graph
- 부모: [[ESLint-Static-Analysis|Static-Analysis-Linting]] · [[Code-Quality]]
- 변형: [[ESLint]] · [[Semgrep]] · [[CodeQL]] · [[dependency-cruiser]]
- 응용: [[AST]] · [[Code_Smells]] · [[CI_CD 파이프라인 및 IDE 통합 보안]] · [[Architecture-Anti-Patterns]]
- Adjacent: [[Abstract_Syntax_Tree]] · [[Clean-Code-Principles]] · [[Quality_Code_Review_Modern]]

## 🤖 LLM 활용
**언제**: 매 team-wide convention enforcement. 매 architectural rule. 매 AI-generated code 의 normalize.
**언제 X**: 매 single-time check (just grep). 매 dynamic / runtime issue.

## ❌ 안티패턴
- **No test (RuleTester)**: 매 rule 의 buggy.
- **No fixer**: 매 manual burden.
- **Slow rule**: 매 매 file performance ↓.
- **Over-broad selector**: 매 false positive.
- **No documentation URL**: 매 dev 의 confusion.
- **Custom rule for what built-in exists**: 매 wheel reinvent.

## 🧪 검증 / 중복
- Verified (ESLint docs, typescript-eslint, Semgrep docs).
- 신뢰도 A.
- Related: [[ESLint-Static-Analysis|Static-Analysis-Linting]] · [[Code_Smells]] · [[Architecture-Anti-Patterns]] · [[CI_CD 파이프라인 및 IDE 통합 보안]] · [[Abstract_Syntax_Tree]].

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — AST + 매 basic / architectural / TS / Semgrep / LLM-gen code |
