f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
356 lines
9.3 KiB
Markdown
356 lines
9.3 KiB
Markdown
---
|
|
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 |
|