Skip to content

린터 규칙 추가하기

Oxlint에 기여하는 가장 좋고 쉬운 방법은 새로운 린터 규칙을 추가하는 것입니다.

이 가이드에서는 ESLint의 no-debugger 규칙을 예제로 사용하여 이 과정을 단계별로 설명합니다.

TIP

먼저 설치 지침을 읽어보세요.

단계 1: 규칙 선택하기

우리의 린터 제품 계획 및 진행 상황 이슈는 기존 ESLint 플러그인에서 구현하고자 하는 모든 규칙의 상태를 추적합니다. 거기에서 관심 있는 플러그인을 골라, 아직 구현되지 않은 규칙을 찾으세요.

중요: 이제는 ESLint 호환 자바스크립트 플러그인 지원이 가능해졌기 때문에, 새롭게 루스트 기반 플러그인을 추가할 계획이 없습니다. 그러나 기존 플러그인에 규칙을 추가하는 기여는 매우 권장됩니다. 만약 어떤 규칙이나 플러그인이 루스트로 작성되는 것이 더 유리하다고 생각한다면, 먼저 논의를 여신 후에 프로그램 요청을 제출하세요.

대부분의 ESLint 규칙 문서 페이지에는 규칙의 소스 코드로 연결되는 링크가 포함되어 있습니다. 이를 참고하면 구현에 도움이 됩니다.

단계 2: 규칙 생성하기

다음으로, 규칙 생성 스크립트를 실행하여 새 규칙을 위한 기본 코드를 생성하세요.

bash
just new-rule no-debugger

이 명령은 다음과 같은 작업을 수행합니다:

  1. crates/oxc_linter/src/rules/<plugin-name>/<rule-name>.rs에 새 파일을 생성하고, 규칙 구현의 시작 부분과 모든 테스트 케이스를 ESLint에서 가져와 포팅합니다.
  2. rules.rs의 적절한 mod에 규칙을 등록합니다.
  3. oxc_macros::declare_all_lint_rules!에 규칙을 추가합니다.

다른 플러그인에 속한 규칙의 경우, 해당 플러그인의 자체 rulegen 스크립트를 사용해야 합니다.

TIP

모든 사용 가능한 명령을 보려면 just에 인수 없이 실행하세요.

bash
just new-rule [name]            # ESLint 커널 규칙용
just new-jest-rule [name]       # eslint-plugin-jest 용
just new-ts-rule [name]         # @typescript-eslint/eslint-plugin 용
just new-unicorn-rule [name]    # eslint-plugin-unicorn 용
just new-import-rule [name]     # eslint-plugin-import 용
just new-react-rule [name]      # eslint-plugin-react 및 eslint-plugin-react-hooks 용
just new-jsx-a11y-rule [name]   # eslint-plugin-jsx-a11y 용
just new-oxc-rule [name]        # oxc 자체 규칙 용
just new-nextjs-rule [name]     # eslint-plugin-next 용
just new-jsdoc-rule [name]      # eslint-plugin-jsdoc 용
just new-react-perf-rule [name] # eslint-plugin-react-perf 용
just new-n-rule [name]          # eslint-plugin-n 용
just new-promise-rule [name]    # eslint-plugin-promise 용
just new-vitest-rule [name]     # eslint-plugin-vitest 용

생성된 파일은 다음과 비슷하게 보일 것입니다:

펼치기
rust
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{
    context::LintContext,
    fixer::{RuleFix, RuleFixer},
    rule::Rule,
    AstNode,
};

#[derive(Debug, Default, Clone)]
pub struct NoDebugger;

declare_oxc_lint!(
    /// ### 무엇을 합니까?
    ///
    ///
    /// ### 왜 문제가 되나요?
    ///
    ///
    /// ### 예시
    ///
    /// 이 규칙에 대한 **잘못된** 코드 예시:
    /// ```js
    /// FIXME: 예시가 누락되거나 문법적으로 잘못되었으면 테스트가 실패합니다.
    /// ```
    ///
    /// 이 규칙에 대한 **올바른** 코드 예시:
    /// ```js
    /// FIXME: 예시가 누락되거나 문법적으로 잘못되었으면 테스트가 실패합니다.
    /// ```
    NoDebugger,
    nursery, // TODO: 카테고리를 `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, 또는 `style`로 변경하세요.
             // 자세한 내용은 <https://oxc.rs/docs/contribute/linter.html#rule-category>를 참조하세요

    pending  // TODO: 수정 기능을 설명하세요. 수정이 불가능하면 제거하고,
             // 수정이 가능할 수 있지만 방법을 모르겠다면 'pending' 유지하세요.
             // 옵션은 'fix', 'fix_dangerous', 'suggestion', 그리고 'conditional_fix_suggestion'입니다.
);

impl Rule for NoDebugger {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {}
}

#[test]
fn test() {
    use crate::tester::Tester;
    let pass = vec!["var test = { debugger: 1 }; test.debugger;"];
    let fail = vec!["if (foo) debugger"];
    Tester::new(NoDebugger::NAME, pass, fail).test_and_snapshot();
}

이제 규칙이 실행 준비가 되었습니다! cargo test -p oxc_linter로 시험해볼 수 있습니다. 아직 규칙을 구현하지 않았기 때문에 테스트는 실패할 것입니다.

단계 3: 템플릿 채우기

문서화

각 문서 섹션을 채우세요.

  • 규칙이 어떤 일을 하는지 명확하고 간결하게 요약하세요.
  • 왜 이 규칙이 중요한지, 어떤 부정적인 행동을 방지하는지 설명하세요.
  • 규칙을 위반하는 코드와 위반하지 않는 코드의 예시를 제공하세요.

참고로, 우리는 이 문서를 이 웹사이트의 규칙 문서 페이지를 자동 생성하기 위해 사용하므로, 문서가 명확하고 도움이 되도록 해야 합니다!

구성 문서화

규칙이 구성 옵션을 가진다면, 이를 문서화해야 합니다. 이는 자동 문서 생성 시스템을 통해 이루어져야 하며, 규칙 생성 스크립트가 일부 자동으로 생성해 줄 것입니다.

각 구성 옵션은 규칙의 구조체에 필드를 추가하여 정의해야 합니다:

rust
pub struct RuleName {
  option_name: bool,
  another_option: String,
  yet_another_option: Vec<CompactStr>,
}

또는 별도의 Config 구조체를 만들어 모든 구성 옵션을 저장할 수도 있습니다:

rust
pub struct RuleName(Box<RuleNameConfig>);

pub struct RuleNameConfig {
  option_name: bool,
}

구성 옵션은 JsonSchema를 유도하고, 다음과 같이 serde 장식을 포함해야 합니다:

rust
use schemars::JsonSchema;

#[derive(Debug, Default, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct RuleName {
  option_name: bool,
}

각 필드에 설명을 달아 옵션을 설명하세요. 예를 들어:

rust
use schemars::JsonSchema;

#[derive(Debug, Default, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct RuleName {
  /// baz를 평가할 때 foo와 bar를 확인할지 여부.
  /// 설명은 필요에 따라 얼마든지 길게 작성할 수 있습니다.
  option_name: bool,
}

각 옵션의 기본값과 타입은 구조체 정의에서 자동으로 추출되며, 문서화 주석에 언급해서는 안 됩니다.

구성 옵션을 올바르게 문서화하는 다양한 예시는 이 이슈를 참조하세요.

cargo run -p website -- linter-rules --rule-docs target/rule-docs --git-ref $(git rev-parse HEAD)를 실행하고 target/rule-docs/<plugin-name>/<rule-name>.md를 열어 생성된 문서를 확인할 수 있습니다.

규칙 카테고리

먼저, 규칙에 가장 적합한 규칙 카테고리를 선택하세요. 기억하세요, correctness 규칙은 기본적으로 실행되기 때문에, 이 카테고리를 선택할 때 신중해야 합니다. declare_oxc_lint! 마크로 내에서 카테고리를 설정하세요.

수정자 상태

규칙이 수정자가 있다면, declare_oxc_lint! 내에서 제공하는 수정 종류를 등록하세요. 수정자를 구현하는 데 익숙하지 않다면, 임시로 pending을 사용할 수도 있습니다. 이는 나중에 다른 기여자가 누락된 수정자를 찾아 구현하는 데 도움이 됩니다.

진단 정보

규칙 위반 사항에 대한 진단 정보를 생성하는 함수를 만드세요. 다음 원칙을 따르세요:

  1. message는 규칙이 하는 일에 대한 설명이 아니라, 무엇이 잘못되었는지를 강령적으로 표현해야 합니다.
  2. help 메시지는 사용자가 문제를 해결하는 방법을 알려주는 명령형 문장이어야 합니다.
rust
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("`debugger` 문은 허용되지 않습니다")
        .with_help("이 `debugger` 문을 제거하세요")
        .with_label(span)
}
rust
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("`debugger` 문을 허용하지 않습니다")
        .with_help("`debugger` 문은 허용되지 않습니다.")
        .with_label(span)

단계 4: 규칙 구현하기

규칙의 소스 코드를 읽어 어떻게 작동하는지 이해하세요. 비록 Oxlint가 ESLint와 유사하게 동작하지만, 규칙을 직접 포팅하는 것은 거의 불가능할 것입니다.

ESLint 규칙은 create 함수를 가지고 있으며, 이 함수는 규칙을 트리거하는 AST 노드의 키와 해당 노드에서 린트를 실행하는 함수의 값으로 구성된 객체를 반환합니다. 반면, Oxlint 규칙은 몇 가지 트리거 중 하나에서 작동하며, 각각은 Rule 트레이트에서 제공됩니다:

  1. 각 AST 노드에서 실행 (이름: run)
  2. 각 심볼에서 실행 (이름: run_on_symbol)
  3. 전체 파일에 대해 한 번만 실행 (이름: run_once)

no-debugger의 경우, 우리는 DebuggerStatement 노드를 찾고 있으므로 run을 사용할 것입니다. 아래는 규칙의 단순화된 버전입니다:

펼치기
rust
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{context::LintContext, rule::Rule, AstNode};

fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("`debugger` 문은 허용되지 않습니다")
        .with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoDebugger;

declare_oxc_lint!(
    /// ### 무엇을 합니까?
    /// `debugger` 문 사용 여부를 확인합니다.
    ///
    /// ### 왜 문제가 되나요?
    /// 디버거가 연결되지 않은 상태에서 `debugger` 문은 기능에 영향을 미치지 않습니다. 일반적으로 실수로 남겨진 디버깅 코드입니다.
    ///
    /// ### 예시
    ///
    /// 이 규칙에 대한 **잘못된** 코드 예시:
    /// ```js
    /// async function main() {
    ///     const data = await getData();
    ///     const result = complexCalculation(data);
    ///     debugger;
    /// }
    /// ```
    NoDebugger,
    correctness
);

impl Rule for NoDebugger {
    // AST의 각 노드에서 실행
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        // `debugger` 문은 별도의 AST 종류를 가집니다
        if let AstKind::DebuggerStatement(stmt) = node.kind() {
            // 위반 사항 보고
            ctx.diagnostic(no_debugger_diagnostic(stmt.span));
        }
    }
}

TIP

Semantic에 저장된 데이터를 익숙해지는 것이 중요합니다. 여기에는 의미 분석 중에 추출된 모든 데이터가 저장됩니다. 또한, AST 구조에 익숙해지는 것도 중요합니다. 여기서 가장 중요한 두 가지 데이터 구조는 AstNodeAstKind 입니다.

단계 5: 테스트

변경 사항을 적용할 때마다 규칙을 테스트하려면 다음 명령을 실행하세요:

bash
just watch "test -p oxc_linter -- rule-name"

또는 단 한번만 테스트하려면 다음 명령을 실행하세요:

bash
cargo test -p oxc_linter -- rule-name
# 또는
cargo insta test -p oxc_linter -- rule-name

Oxlint는 cargo insta를 사용하여 스냅샷 테스트를 수행합니다. cargo test는 스냅샷이 변경되었거나 새로 생성되었을 경우 실패합니다. cargo insta test -p oxc_linter를 실행하면 테스트 결과의 차이를 보지 않을 수 있습니다. 스냅샷을 검토하려면 cargo insta review를 실행하거나, cargo insta accept로 모든 변경 사항을 수락하고 스킵할 수 있습니다.

프로젝트 제출 준비가 되었을 때는 just ready 또는 just r을 실행하여 로컬에서 CI 검사를 수행하세요. just fix를 실행하면 린트, 형식, 오타 문제를 자동으로 수정할 수 있습니다. just ready가 통과되면 프로그램 요청을 만들고, 관리자가 변경 사항을 검토할 것입니다.

일반적인 조언

오류 메시지를 가장 짧은 코드 범위에 정확히 표시하세요

사용자가 문제 있는 코드에 집중하도록 하고, 오류 메시지를 해독하여 코드의 어느 부분이 잘못되었는지 파악하는 것을 피하고 싶습니다.

let-else 문 사용하기

깊이 중첩된 if-let 문을 사용하고 있다면, 대신 let-else를 고려하세요.

TIP

CodeAesthetic의 항상 중첩하지 않는 비디오는 이 개념을 더 자세히 설명합니다.

rust
// `let-else`는 읽기 쉽습니다
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
    let AstKind::JSXOpeningElement(jsx_opening_elem) = node.kind() else {
        return;
    };
    let Some(expr) = container.expression.as_expression() else {
        return;
    };
    let Expression::BooleanLiteral(expr) = expr.without_parenthesized() else {
        return;
    };
    // ...
}
rust
// 깊이 중첩은 읽기 어렵습니다
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
    if let AstKind::JSXOpeningElement(jsx_opening_elem) = node.kind() {
        if let Some(expr) = container.expression.as_expression() {
            if let Expression::BooleanLiteral(expr) = expr.without_parenthesized() {
                // ...
            }
        }
    }
}

가능한 곳에서 CompactStr 사용하기

oxc에서 성능을 위해 할당량을 최소화하는 것이 매우 중요합니다. String 타입은 힙에 메모리 할당을 필요로 하며, 이는 메모리와 CPU 사이클을 소모합니다. CompactStr을 사용하면 작은 문자열을 스택에 인라인으로 저장할 수 있습니다 (64비트 시스템에서는 최대 24바이트까지). 이렇게 하면 메모리 할당이 필요하지 않습니다. 문자열이 너무 크면 필요한 공간을 할당합니다. String 또는 &str 타입이 사용되는 거의 모든 곳에서 CompactStr을 사용할 수 있으며, String 타입보다 메모리와 CPU 사이클을 크게 절약할 수 있습니다.

rust
struct Element {
  name: CompactStr
}

let element = Element {
  name: "div".into()
};
rust
struct Element {
  name: String
}

let element = Element {
  name: "div".to_string()
};