添加 Linter 规则
🌐 Adding Linter Rules
为 Oxlint 做出贡献最简单、最有效的方法是添加新的代码检查规则。
🌐 The best and easiest way to contribute to Oxlint is by adding new linter rules.
本指南将引导你完成此过程,以 ESLint 的 no-debugger 规则为例。
🌐 This guide will walk you through this process, using ESLint's no-debugger rule as an example.
TIP
确保你先阅读了安装说明。
步骤 1:选择一条规则
🌐 Step 1: Pick a Rule
我们的Linter 产品计划和进展问题跟踪我们希望从现有 ESLint 插件中实现的所有规则的状态。从中挑选一个你觉得有趣的插件,并找到一个尚未实现的规则。
🌐 Our Linter product plan and progress issue tracks the status of all rules we want to implement from existing ESLint plugins. From there, pick a plugin that looks interesting to you and find a rule that has not been implemented.
重要:由于现在已有支持 ESLint 的 JavaScript 插件,我们不打算添加新的基于 Rust 的插件。不过,非常欢迎为现有插件添加规则的贡献。如果你认为某条规则或插件用 Rust 编写会更好,请先开启讨论,再提交拉取请求。
大多数 ESLint 规则的文档页面都包含指向该规则源代码的链接。将其作为参考将有助于你的实现。
🌐 Most documentation pages for ESLint rules include a link to the rule's source code. Using this as a reference will help you with your implementation.
步骤 2:规则生成
🌐 Step 2: Rule Generation
接下来,运行 rulegen 脚本为你的新规则生成模板代码。
🌐 Next, run the rulegen script to generate boilerplate code for your new rule.
just new-rule no-debugger这将会:
🌐 This will:
- 在
crates/oxc_linter/src/rules/<plugin-name>/<rule-name>.rs中创建一个新文件,包含你的规则实现的开头以及从 ESLint 移植的所有测试用例 - 在
rules.rs中的适当mod中注册规则 - 将规则添加到
oxc_macros::declare_all_lint_rules!
对于属于其他插件的规则,你需要使用该插件自带的规则生成脚本。
🌐 For rules that are part of a different plugin, you'll need to use that plugin's own rulegen script.
TIP
运行 just 不带任何参数以查看所有可用命令。
just new-rule [name] # for eslint core rules
just new-jest-rule [name] # for eslint-plugin-jest
just new-ts-rule [name] # for @typescript-eslint/eslint-plugin
just new-unicorn-rule [name] # for eslint-plugin-unicorn
just new-import-rule [name] # for eslint-plugin-import
just new-react-rule [name] # for eslint-plugin-react and eslint-plugin-react-hooks
just new-jsx-a11y-rule [name] # for eslint-plugin-jsx-a11y
just new-oxc-rule [name] # for oxc's own rules
just new-nextjs-rule [name] # for eslint-plugin-next
just new-jsdoc-rule [name] # for eslint-plugin-jsdoc
just new-react-perf-rule [name] # for eslint-plugin-react-perf
just new-n-rule [name] # for eslint-plugin-n
just new-promise-rule [name] # for eslint-plugin-promise
just new-vitest-rule [name] # for eslint-plugin-vitest生成的文件将类似于这样:
🌐 The generated file will look something like this:
点击展开
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
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// FIXME: Tests will fail if examples are missing or syntactically incorrect.
/// ```
///
/// Examples of **correct** code for this rule:
/// ```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/docs/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 来试一下。测试应该会失败,因为你还没有实现这个规则。
🌐 Your rule should now be ready to run! You can try it out with cargo test -p oxc_linter. The tests should fail, since you haven't implemented the rule yet.
步骤 3:填写模板
🌐 Step 3: Fill Out the Template
文档
🌐 Documentation
填写各个文档部分。
🌐 Fill out the various documentation sections.
- 提供一个清晰简明的规则功能总结。
- 解释该规则为何重要以及它可以防止哪些不良行为。
- 提供违反规则的代码示例以及不违反规则的代码示例。
请记住,我们使用此文档来为本网站生成规则文档页面,因此请确保你的文档清晰且有帮助!
🌐 Remember, we use this documentation to generate the rule documentation pages for this website, so make sure your documentation is clear and helpful!
配置文档
🌐 Configuration Documentation
如果你的规则有配置选项,你需要对其进行文档记录。你应该通过自动生成文档的系统来完成。这部分内容将由 rulegen 脚本自动为你生成。
🌐 If your rule has configuration options, you will need to document them. You should do so via the system for auto-generating documentation. This should be partially generated for you automatically by the rulegen script.
每个配置选项都应通过向规则的结构体添加字段来定义:
🌐 Each configuration option should be defined by adding fields to the rule's struct:
pub struct RuleName {
option_name: bool,
another_option: String,
yet_another_option: Vec<CompactStr>,
}或者,你也可以定义一个单独的“Config”结构体来存储所有配置选项:
🌐 Alternatively, you can instead define a separate Config struct to hold all configuration options:
pub struct RuleName(Box<RuleNameConfig>);
pub struct RuleNameConfig {
option_name: bool,
}配置选项应该为它们派生 JsonSchema,并且还有一个 serde 装饰,如下所示:
🌐 The configuration options should have JsonSchema derived for them and also a serde decoration, like so:
use schemars::JsonSchema;
#[derive(Debug, Default, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct RuleName {
option_name: bool,
}在每个字段后添加文档注释(“///”)来描述该选项,例如:
🌐 Add documentation comments (///) to each field to describe the option, for example:
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,
}每个选项的默认值和类型将自动从结构体定义中提取,不应在文档注释中提及。
🌐 The default value and the type of each option will be automatically extracted from the struct definition, and should not be mentioned in the documentation comments.
请参阅此问题,了解数十个关于如何在各种规则中正确记录配置选项的示例。
🌐 See this issue for dozens of examples of how to properly document configuration options in all kinds of rules.
你可以通过运行 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 来查看生成的文档。
规则类别
🌐 Rule Category
首先,选择一个最适合该规则的规则类别。请记住,correctness 规则默认会运行,因此在选择此类别时要小心。在 declare_oxc_lint! 宏中设置你的类别。
🌐 First, pick a rule category that best fits the rule. Remember that correctness rules will be run by default, so be careful when choosing this category. Set your category within the declare_oxc_lint! macro.
修复工状态
🌐 Fixer Status
如果该规则有修复程序,请在 declare_oxc_lint! 中登记它提供的修复类型。如果你不熟悉实现修复程序,也可以使用 pending 作为占位符。这有助于其他贡献者在之后查找和实现缺失的修复程序。
🌐 If the rule has a fixer, register what kind of fixes it provides within declare_oxc_lint!. If you're not comfortable with implementing a fixer, you can also use pending as a placeholder. This helps other contributors find and implement missing fixers down the line.
诊断
🌐 Diagnostics
创建一个函数来为规则违规生成诊断。遵循以下原则:
🌐 Create a function to create diagnostics for rule violations. Follow these principles:
message应该是一个说明错误的命令性语句,而不是描述规则功能的语句。help消息应该是类似命令的语句,告诉用户如何解决问题。
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("`debugger` statement is not allowed")
.with_help("Remove this `debugger` statement")
.with_label(span)
}fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Disallow `debugger` statements")
.with_help("`debugger` statements are not allowed.")
.with_label(span)步骤 4:规则实现
🌐 Step 4: Rule Implementation
阅读规则的源代码以了解其工作原理。虽然 Oxlint 的工作方式类似于 ESLint,但该规则不太可能直接移植。
🌐 Read the rule's source code to understand how it works. Although Oxlint works similarly to ESLint, it is unlikely that the rule can be ported directly.
ESLint 规则有一个 create 函数,该函数返回一个对象,对象的键是触发规则的 AST 节点,值是对这些节点运行 lint 的函数。Oxlint 规则在几个触发器中的一个上运行,每个触发器都来自 Rule trait:
🌐 ESLint rules have a create function that returns an object whose keys are AST nodes that trigger the rule and values are functions that run lints on those nodes. Oxlint rules run on one of a few triggers, each of which come from the Rule trait:
- 在每个 AST 节点上运行(通过
run) - 在每个符号上运行(通过
run_on_symbol) - 通过
run_once在整个文件上运行一次
在 no-debugger 的情况下,我们正在寻找 DebuggerStatement 节点,因此我们将使用 run。下面是该规则的简化版本:
🌐 In the case of no-debugger, we are looking for DebuggerStatement nodes, so we'll use run. Here's a simplified version of the rule:
点击展开
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.
///
/// ### Example
///
/// Examples of **incorrect** code for this rule:
/// ```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 结构。这里两个最重要的数据结构是 AstNode 和 AstKind。
🌐 You will want to get familiar with the data stored in Semantic, which is where all data extracted during semantic analysis is stored. You will also want to familiarize yourself with the AST structure. The two most important data structures here are AstNode and AstKind
步骤 5:测试
🌐 Step 5: Testing
每次进行更改时,要测试你的规则,请运行:
🌐 To test your rule whenever you make a change, run:
just watch "test -p oxc_linter -- rule-name"或者只测试一次,运行:
🌐 Or to just test it once, run:
cargo test -p oxc_linter -- rule-name
# Or
cargo insta test -p oxc_linter -- rule-nameOxlint 使用 cargo insta 进行快照测试。如果快照发生了更改或刚刚创建,cargo test 会失败。你可以运行 cargo insta test -p oxc_linter 来在测试结果中不显示差异。你可以通过运行 cargo insta review 来查看快照,或者跳过查看,直接使用 cargo insta accept 接受所有更改。
🌐 Oxlint uses cargo insta for snapshot testing. cargo test will fail if snapshots have changed or have just been created. You can run cargo insta test -p oxc_linter to not see diffs in your test results. You can review the snapshots by running cargo insta review, or skip the review and just accept all changes using cargo insta accept.
当你准备提交你的 PR 时,运行 just ready 或 just r 来在本地执行 CI 检查。你也可以运行 just fix 来自动修复任何 lint、格式或拼写问题。一旦 just ready 通过,创建一个 PR,维护者将会审查你的更改。
🌐 When you are ready to submit your PR, run just ready or just r to run CI checks locally. You can also run just fix to auto-fix any lint, format, or typo problems. Once just ready is passing, create a PR and a maintainer will review your changes.
一般建议
🌐 General Advice
将错误信息准确指向最短的代码段
🌐 Pin point the error message to the shortest code span
我们希望用户专注于有问题的代码,而不是解读错误信息来确定代码中哪一部分有误。
🌐 We want the user to focus on the problematic code rather than deciphering the error message to identify which part of the code is erroneous.
使用 let-else 语句
🌐 Use let-else statements
如果你发现自己在深度嵌套 if-let 语句,考虑改用 let-else。
🌐 If you find yourself deeply nesting if-let statements, consider using let-else instead.
TIP
CodeAesthetic 的从不嵌套视频对此概念有更详细的讲解。
// let-else is easier to read
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;
};
// ...
}// deep nesting is hard to read
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
🌐 Use CompactStr where possible
在 oxc 中,尽可能减少分配对性能至关重要。String 类型需要在堆上分配内存,这会消耗内存和 CPU 周期。可以使用 CompactStr 将小字符串内联存储(在 64 位系统上最多 24 字节)在栈上,这意味着我们不需要分配内存。如果字符串太大而无法内联存储,它将分配所需的空间。CompactStr 几乎可以在任何具有 String 或 &str 类型的地方使用,相比 String 类型,可以节省大量内存和 CPU 周期。
🌐 Reducing allocations as much as possible is critical for performance in oxc. The String type requires allocating memory on the heap, which costs memory and CPU cycles. It is possible to store small strings inline (up to 24 bytes on 64-bit systems) on the stack using CompactStr, which means we don't need to allocate memory. If the string is too large to store inline, it will allocate the necessary space. Using CompactStr can be used almost anywhere that has the type String or &str, and can save a significant amount memory and CPU cycles compared to the String type.
struct Element {
name: CompactStr
}
let element = Element {
name: "div".into()
};struct Element {
name: String
}
let element = Element {
name: "div".to_string()
};