Files
2nd/10_Wiki/Topics/AI_and_ML/Custom-ESLint-Rules-Development.md
T
2026-05-10 22:08:15 +09:00

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
custom ESLint
AST rule
eslint plugin
ast visitor
autofix
semgrep alternative
none A 0.9 applied
eslint
ast
static-analysis
code-quality
custom-rule
plugin
autofix
semgrep
2026-05-10 pending
language framework
JavaScript / TypeScript 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

// 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

🤖 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.

🧪 검증 / 중복

🕓 Changelog

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