Skip to content

Adicionar regras ao linter

A forma mais simples de contribuir com o Oxlint costuma ser adicionando novas regras.

Este guia percorre o processo usando a regra no-debugger do ESLint como exemplo.

Passo 1: Escolher uma regra

Nossa issue do plano e progresso do produto Linter acompanha o status de todas as regras que queremos trazer de plugins ESLint existentes. Escolha um plugin que te interesse e uma regra ainda não implementada.

Importante: como já há suporte a plugins JavaScript compatíveis com ESLint, não planejamos adicionar novos plugins escritos em Rust. Mesmo assim, contribuições que acrescentem regras a plugins já existentes são muito bem-vindas. Se achar que uma regra ou plugin ganharia em ser Rust nativo, abra uma discussion antes de abrir o PR.

Na maioria das páginas de documentação das regras ESLint há link para o código-fonte da regra. Use isso como referência na implementação.

Passo 2: Geração da regra

Em seguida, rode o script rulegen para gerar o esqueleto da nova regra.

bash
just new-rule no-debugger

Isso irá:

  1. Criar um arquivo em crates/oxc_linter/src/rules/<plugin-name>/<rule-name>.rs com o começo da implementação e casos de teste portados do ESLint
  2. Registrar a regra no mod correto em rules.rs
  3. Incluir a regra em oxc_macros::declare_all_lint_rules!

Para regras de outro plugin, use o script rulegen daquele plugin.

TIP

Rode just sem argumentos para ver todos os comandos disponíveis.

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

O arquivo gerado será algo neste estilo:

Clique para expandir
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
    ///
    /// 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();
}

Sua regra já deve rodar! Teste com cargo test -p oxc_linter. Os testes devem falhar, porque a regra ainda não foi implementada.

Passo 3: Preencher o template

Documentação

Preencha as seções de documentação.

  • Resumo claro do que a regra faz.
  • Por que importa e que comportamento indesejável evita.
  • Exemplos de código que violam e de código correto.

Usamos essa documentação para gerar as páginas de regras deste site — capricha para ficar clara e útil!

Documentação de configuração

Se a regra tiver opções de configuração, documente-as pelo sistema de geração automática de docs. O rulegen costuma gerar parte disso.

Cada opção deve aparecer como campo na struct da regra:

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

Alternativamente, use uma struct Config separada:

rust
pub struct RuleName(Box<RuleNameConfig>);

pub struct RuleNameConfig {
  option_name: bool,
}

As opções precisam ter JsonSchema derivado e anotação serde, por exemplo:

rust
use schemars::JsonSchema;

#[derive(Debug, Default, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct RuleName {
  option_name: bool,
}

Documente cada campo com comentários ///, por exemplo:

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,
}

Valor padrão e tipo são extraídos automaticamente da struct e não devem ser repetidos nos comentários.

Veja esta issue com dezenas de exemplos de como documentar opções em vários tipos de regra.

Para ver a documentação gerada: cargo run -p website -- linter-rules --rule-docs target/rule-docs --git-ref $(git rev-parse HEAD) e abra target/rule-docs/<plugin-name>/<rule-name>.md.

Categoria da regra

Escolha uma categoria de regra que combine com a regra. Lembre-se de que regras correctness rodam por padrão — escolha com cuidado. Defina a categoria na macro declare_oxc_lint!.

Status do fixer

Se houver fixer, registre em declare_oxc_lint! que tipo de correção oferece. Se não quiser implementar o fixer agora, pode usar pending como marcador — ajuda outros contribuidores a acharem fixers faltantes depois.

Diagnósticos

Crie uma função que produza diagnósticos para violações. Princípios:

  1. A message deve ser uma frase no imperativo sobre o que está errado, não uma descrição da regra.
  2. A mensagem help deve soar como instrução direta de como corrigir.
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)

Passo 4: Implementação da regra

Leia o código-fonte da regra no ESLint para entender o comportamento. Embora o Oxlint se pareça com o ESLint, raramente dá para portar a regra direto.

Regras ESLint têm uma função create que devolve um objeto: chaves são tipos de nó AST que disparam a regra e valores são funções que rodam o lint. Regras do Oxlint disparam de poucos pontos definidos pelo trait Rule:

  1. Em cada nó da AST (run)
  2. Em cada símbolo (run_on_symbol)
  3. Uma vez por arquivo (run_once)

No caso de no-debugger, queremos nós DebuggerStatement, então usamos run. Versão simplificada da regra:

Clique para expandir
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.
    ///
    /// ### 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

Vale estudar os dados em Semantic, onde fica tudo que a análise semântica extrai. Também familiarize-se com a estrutura da AST. As duas estruturas mais importantes são AstNode e AstKind

Passo 5: Testes

Para testar a cada mudança:

bash
just watch "cargo test -p oxc_linter -- rule-name"

Ou só uma vez:

bash
cargo test -p oxc_linter -- rule-name
# Or
cargo insta test -p oxc_linter -- rule-name

O Oxlint usa cargo insta para snapshot testing. cargo test falha se snapshots mudarem ou forem criados. Você pode rodar cargo insta test -p oxc_linter para não ver diffs nos resultados. Revise com cargo insta review, ou aceite tudo com cargo insta accept.

Antes de abrir o PR, rode just ready ou just r para checagens locais de CI. just fix corrige automaticamente lint, formatação e typos. Com just ready verde, abra o PR para revisão.

Conselhos gerais

Mensagem de erro no menor span possível

Queremos que o usuário olhe para o código problemático, não que decifre o texto do erro para achar o trecho certo.

Use let-else

Se estiver aninhando muito if-let, considere let-else.

TIP

O vídeo never-nesting do CodeAesthetic explica melhor.

rust
// 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
// 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() {
                // ...
            }
        }
    }
}

Use CompactStr quando der

Menos alocações é crítico para desempenho no oxc. String exige heap; dá para guardar strings curtas inline (até 24 bytes em sistemas 64-bit) com CompactStr, sem alocar. Se passar desse tamanho, aloca o necessário. CompactStr substitui bem String ou &str na maioria dos casos e economiza memória e CPU frente a String.

rust
struct Element {
  name: CompactStr
}

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

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