Adding Linter Rules
The best and easiest way to contribute to Oxlint is by adding new linter rules.
This guide will walk you through this process, using ESLint's no-debugger rule as an example.
TIP
Make sure you've read the setup instructions first.
Step 1: Pick a Rule
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.
Important: Since ESLint-compatible JavaScript plugin support is now available, we do not plan to add new Rust-based plugins. However, contributions that add rules to existing plugins are highly encouraged. If you think a rule or plugin would benefit from being written in rust, please open a discussion first, before making a pull request.
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.
Step 2: Rule Generation
Next, run the rulegen script to generate boilerplate code for your new rule.
just new-rule no-debugger
``````bash
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
```````rust [rules/eslint/no_debugger.rs]
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/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();
}
``````rust
pub struct RuleName {
option_name: bool,
another_option: String,
yet_another_option: Vec<CompactStr>,
}
``````rust
pub struct RuleName(Box<RuleNameConfig>);
pub struct RuleNameConfig {
option_name: bool,
}
``````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,
}
``````rust [good]
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("`debugger` statement is not allowed")
.with_help("Remove this `debugger` statement")
.with_label(span)
}
``````rust [bad]
fn no_debugger_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Disallow `debugger` statements")
.with_help("`debugger` statements are not allowed.")
.with_label(span)
```````rust [rules/eslint/no_debugger.rs]
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!(
/// ### Qué hace
/// Checks for usage of the `debugger` statement
///
/// ### ¿Por qué es problemático?
/// `debugger` statements do not affect functionality when a
/// debugger isn't attached. They're most commonly an
/// accidental debugging leftover.
///
/// ### Example
///
/// Ejemplos de código **incorrecto** para esta regla:
/// ```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));
}
}
}
``````bash
just watch "cargo test -p oxc_linter -- rule-name"
``````bash
cargo test -p oxc_linter -- rule-name
# Or
cargo insta test -p oxc_linter -- rule-name
``````rust [good]
// 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;
};
// ...
}
``````rust [bad]
// 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() {
// ...
}
}
}
}
``````rust [good]
struct Element {
name: CompactStr
}
let element = Element {
name: "div".into()
};
``````rust [bad]
struct Element {
name: String
}
let element = Element {
name: "div".to_string()
};