Skip to content

添加 Linter 规则

向 Oxlint 贡献的最佳且最简单的方式是添加新的 linter 规则。

本指南将带你完成此过程,以 ESLint 的 no-debugger 规则为例。

TIP

请先阅读 设置说明

第一步:选择规则

我们 Linter 产品计划与进度的问题跟踪了所有我们希望从现有 ESLint 插件中实现的规则的状态。从中挑选一个对你有吸引力的插件,并找到尚未实现的规则。

重要:由于现在已支持与 ESLint 兼容的 JavaScript 插件,我们不再计划添加新的基于 Rust 的插件。然而,向现有插件添加规则的贡献是 非常鼓励的。如果你认为某个规则或插件用 Rust 编写会更有益,请在提交拉取请求前先开启讨论。

大多数 ESLint 规则的文档页面都包含指向该规则 源代码 的链接。使用它作为参考将有助于你的实现。

第二步:规则生成

接下来,运行 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 核心规则
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 来尝试运行。由于你尚未实现规则,测试应会失败。

第三步:填写模板

文档

填写各个文档部分。

  • 提供规则作用的清晰简洁摘要。
  • 解释规则为何重要,以及它防止了哪些不良行为。
  • 提供违反规则和未违反规则的代码示例。

记住,我们使用这些文档来生成此网站上的 规则文档页面,因此请确保文档清晰且有帮助!

配置文档

如果规则具有配置选项,则需要对其进行文档化。你应该通过自动生成文档的系统来完成。rulegen 脚本会为你自动部分生成这些内容。

每个配置选项应通过在规则结构体中添加字段来定义:

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)

第四步:规则实现

阅读规则的源代码以理解其工作原理。尽管 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

第五步:测试

每次修改规则后,可以通过以下方式测试:

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 readyjust 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 字节)在栈上,这意味着我们无需分配内存。如果字符串太大无法内联存储,则会分配必要的空间。CompactStr 几乎可以在任何 String&str 类型的地方使用,相比 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()
};