Skip to content

リンタールールの追加

Oxlint へのコントリビューで、最も手堅く簡単なのは新しいリンタールールを追加することです。

ここでは ESLint の no-debugger ルールを例に、手順を説明します。

TIP

先に セットアップ手順 を読んでください。

ステップ 1: ルールを選ぶ

リンターのロードマップと進捗 の Issue で、既存の ESLint プラグインから実装したいルールの状況を追っています。興味のありそうなプラグインを開き、まだ実装されていないルールを探してください。

重要: ESLint 互換の JavaScript プラグインが利用できるようになったため、新しい Rust 製プラグインは追加する予定はありません。ただし、既存プラグインへのルール追加は積極的に歓迎します。Rust で書いた方がよいルールやプラグインがある場合は、Pull Request の前に Discussion を開いて相談してください。

多くの ESLint ルールドキュメントには、ソースコード へのリンクがあります。実装の参考にしてください。

ステップ 2: ルール生成

次に rulegen スクリプトを実行し、ボイラープレートを生成します。

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 core
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 / 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!(
    /// ### What it does
    ///
    ///
    /// ### Why is this bad?
    ///
    ///
    /// ### Examples
    ///
    /// このルールで**適合しない**コード例:
    /// ```js
    /// FIXME: Tests will fail if examples are missing or syntactically incorrect.
    /// ```
    ///
    /// このルールで**適合する**コード例:
    /// ```js
    /// FIXME: Tests will fail if examples are missing or syntactically incorrect.
    /// ```
    NoDebugger,
    nursery, // TODO: change category to `correctness`, `suspicious`, `pedantic`, `perf`, `restriction`, or `style`
             // See <https://oxc.rs/ja/contribute/linter.html#rule-category> for details

    pending  // TODO: describe fix capabilities. Remove if no fix can be done,
             // keep at 'pending' if you think one could be added but don't know how.
             // Options are 'fix', 'fix_dangerous', 'suggestion', and '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: テンプレートを埋める

ドキュメント

各セクションを埋めます。

  • ルールが何をするかを、短くはっきり書く
  • なぜ重要で、どんな望ましくない挙動を防ぐのかを書く
  • 違反例と問題ない例を示す

このドキュメントはサイトの ルール一覧 を生成するのに使われるので、読みやすくて役に立つ内容にしてください。

設定項目のドキュメント

ルールに設定オプションがある場合は、自動生成ドキュメントの仕組みで説明を書きます。その一部は rulegen が既に用意します。

設定はルールの struct にフィールドとして足します。

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

または、別の Config struct にまとめても構いません。

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 {
  /// Whether to check for foo and bar when evaluating baz.
  /// The comment can be as long as you need to fully describe the option.
  option_name: bool,
}

既定値や型は struct 定義から自動抽出されるため、ドキュメントコメント本文には書かないでください。

様々なルールでの設定ドキュメントの書き方は、この Issue に多数あります。

生成結果は次で確認できます。target/rule-docs/<plugin-name>/<rule-name>.md を開いてください。

bash
cargo run -p website -- linter-rules --rule-docs target/rule-docs --git-ref $(git rev-parse HEAD)

ルールカテゴリ

まず ルールカテゴリ のうち、ルールに最も合うものを選びます。correctness はデフォルトで走るため、選ぶときは注意してください。カテゴリは declare_oxc_lint! マクロで指定します。

フィクサーの種類

ルールにフィクサーがある場合は、declare_oxc_lint! 内でどの種類の修正を提供するか登録します。フィクサーの実装に不安がある場合は、プレースホルダとして pending を使っても構いません。後から他のコントリビュータが足しやすくなります。

診断メッセージ

違反時の診断を組み立てる関数を書きます。次を意識してください。

  1. message は「何がまずいか」を命題形で書く(ルールの説明ではない)。
  2. help はユーザーが直し方を実行できるような、指示に近い一文にする。
rust
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("`debugger` statement is not allowed")
        .with_help("Remove this `debugger` statement")
        .with_label(span)
}
rust
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Disallow `debugger` statements")
        .with_help("`debugger` statements are not allowed.")
        .with_label(span)

ステップ 4: ルールの実装

ESLint のソースを読み、挙動を把握してください。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` statement is not allowed")
        .with_label(span)
}

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

declare_oxc_lint!(
    /// ### What it does
    /// Checks for usage of the `debugger` statement
    ///
    /// ### Why is this bad?
    /// `debugger` statements do not affect functionality when a
    /// debugger isn't attached. They're most commonly an
    /// accidental debugging leftover.
    ///
    /// ### Examples
    ///
    /// このルールで**適合しない**コード例:
    /// ```js
    /// async function main() {
    ///     const data = await getData();
    ///     const result = complexCalculation(data);
    ///     debugger;
    /// }
    /// ```
    NoDebugger,
    correctness
);

impl Rule for NoDebugger {
    // Runs on each node in the AST
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        // `debugger` statements have their own AST kind
        if let AstKind::DebuggerStatement(stmt) = node.kind() {
            // Report a violation
            ctx.diagnostic(no_debugger_diagnostic(stmt.span));
        }
    }
}

TIP

セマンティック解析の結果は Semantic に集約されます。AST では AstNodeAstKind が特に重要です。

ステップ 5: テスト

変更のたびにルールだけテストするには:

bash
just watch "cargo 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 は失敗します。テスト出力に diff を出したくない場合は cargo insta test -p oxc_lintercargo insta review で確認するか、cargo insta accept で一括採用できます。

PR 前にローカルで CI に近いチェックをするなら just ready(短縮 just r)。Lint やフォーマットは just fix も利用できます。just ready が通れば PR を出し、メンテナがレビューします。

一般的なアドバイス

エラーのスパンは最短に

ユーザーがメッセージを読み解いて場所を特定するより、問題のコードそのものに目が向くようにします。

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 は毎回ヒープ確保がありますが、短い文字列はインラインで格納できる CompactStr を使うと(64-bit では最大 24 バイトまで)、余計な確保を避けられます。大きすぎる場合は自動でヒープに回ります。String&str を期待する多くの場面で置き換え可能で、メモリと CPU の両方を節約できます。

rust
struct Element {
  name: CompactStr
}

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

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