Skip to content

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.

bash
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()
};