9.3 KiB
9.3 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-custom-eslint-rules-dev | Custom ESLint Rules Development | 10_Wiki/Topics | verified | self |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
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
- Parser (espree, @typescript-eslint/parser): 매 source → 매 AST.
- Rule: 매 visit AST node + 매 report.
- Fixer: 매 auto-correct.
- Config: 매 enable / disable.
매 AST node type
- Program: 매 root.
- VariableDeclaration, FunctionDeclaration.
- CallExpression, MemberExpression.
- ArrowFunctionExpression, ObjectExpression, ArrayExpression.
- TSInterfaceDeclaration, TSTypeAliasDeclaration (TS).
매 use case
- Architectural: domain → infrastructure 의 forbid.
- Convention: naming, file structure.
- Deprecation: 매 old API 의 detect.
- Security: dangerous pattern (eval, dangerouslySetInnerHTML).
- Performance: anti-pattern (e.g., unnecessary re-render).
- 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
// 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
// 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)
// 매 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)
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
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)
// 매 매 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
# .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
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+)
// 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
// 매 ❌ 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
- 부모: 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: 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 |