Skip to content

Добавление правил линтера

Проще всего помочь Oxlint — добавить новое правило.

Ниже описан процесс на примере правила ESLint no-debugger.

TIP

Сначала прочитайте инструкции по окружению.

Шаг 1: выбор правила

В задаче Linter product plan and progress отслеживается перенос правил из плагинов ESLint. Выберите плагин и правило, которое ещё не реализовано.

Важно: поддержка JS-плагинов совместимых с ESLint уже есть; новые плагины целиком на Rust мы не планируем. Дополнения к существующим rust-плагинам приветствуются. Если хотите целый плагин на Rust — сначала обсудите в Discussion, без этого не начинайте PR.

На страницах правил ESLint обычно есть ссылка на исходный код — это хорошая опора для реализации.

Шаг 2: генерация правила

Запустите rulegen:

bash
just new-rule no-debugger

Скрипт:

  1. Создаст файл crates/oxc_linter/src/rules/<plugin-name>/<rule-name>.rs с заготовкой и тестами из ESLint
  2. Зарегистрирует правило в нужном mod в rules.rs
  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!(
    /// ### 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();
}

Правило уже можно запускать: cargo test -p oxc_linter. Тесты должны падать, пока логика не реализована.

Шаг 3: заполнить шаблон

Документация

Заполните блоки документации:

  • кратко, что делает правило;
  • зачем оно нужно и какой вред предотвращает;
  • примеры неверного и верного кода.

Эти комментарии попадают на страницы правил сайта — пишите ясно.

Опции конфигурации

Если у правила есть опции, документируйте их через систему автогенерации (rulegen частично создаёт заготовку).

Поля в struct правила:

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

Тип и значение по умолчанию берутся из struct и не дублируются в комментарии.

Примеры оформления опций: issue.

Сгенерированную документацию можно посмотреть так:

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` 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)

Шаг 4: реализация правила

Изучите исходник правила в ESLint. Oxlint похож по идее, но один в один перенести редко получается.

В ESLint create возвращает объект слушателей узлов. В 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` 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.

Шаг 5: тесты

При каждом изменении:

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

Разовый запуск:

bash
cargo test -p oxc_linter -- rule-name
# или
cargo insta test -p oxc_linter -- rule-name

Снапшоты — cargo insta. После изменений смотрите diff через cargo insta review или примите всё: cargo insta accept.

Перед PR выполните just ready или just r; при необходимости just fix. Когда локально всё зелёное — открывайте PR.

Общие советы

Указывайте минимальный span

Пользователь должен видеть проблемный фрагмент кода, а не гадать по длинному сообщению.

Используйте let-else

Глубокая вложенность if let замените на let-else.

TIP

Идею объясняет видео CodeAesthetic про never-nesting: https://www.youtube.com/watch?v=CFRhGnuXG-4

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

CompactStr вместо String, где возможно

Меньше аллокаций — быстрее oxc. Мелкие строки до 24 байт на 64-bit можно держать на стеке — см. /ru/learn/performance#string-inlining.

rust
struct Element {
  name: CompactStr
}

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

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