Добавление правил линтера
Проще всего помочь Oxlint — добавить новое правило.
Ниже описан процесс на примере правила ESLint no-debugger.
TIP
Сначала прочитайте инструкции по окружению.
Шаг 1: выбор правила
В задаче Linter product plan and progress отслеживается перенос правил из плагинов ESLint. Выберите плагин и правило, которое ещё не реализовано.
Важно: поддержка JS-плагинов совместимых с ESLint уже есть; новые плагины целиком на Rust мы не планируем. Дополнения к существующим rust-плагинам приветствуются. Если хотите целый плагин на Rust — сначала обсудите в Discussion, без этого не начинайте PR.
На страницах правил ESLint обычно есть ссылка на исходный код — это хорошая опора для реализации.
Шаг 2: генерация правила
Запустите rulegen:
just new-rule no-debuggerСкрипт:
- Создаст файл
crates/oxc_linter/src/rules/<plugin-name>/<rule-name>.rsс заготовкой и тестами из ESLint - Зарегистрирует правило в нужном
modвrules.rs - Добавит правило в
oxc_macros::declare_all_lint_rules!
Для других плагинов — своя команда rulegen.
TIP
just без аргументов покажет все команды.
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Сгенерированный файл будет похож на следующий:
Развернуть
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 правила:
pub struct RuleName {
option_name: bool,
another_option: String,
yet_another_option: Vec<CompactStr>,
}Либо отдельный Config:
pub struct RuleName(Box<RuleNameConfig>);
pub struct RuleNameConfig {
option_name: bool,
}Нужны JsonSchema и serde-атрибуты:
use schemars::JsonSchema;
#[derive(Debug, Default, Clone, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct RuleName {
option_name: bool,
}Комментарии /// к полям описывают опцию:
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 — так другим проще найти незакрытые задачи.
Диагностики
Функция диагностики нарушений:
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: реализация правила
Изучите исходник правила в ESLint. Oxlint похож по идее, но один в один перенести редко получается.
В ESLint create возвращает объект слушателей узлов. В Oxlint триггеры задаются трейтом Rule:
- каждый узел AST —
run - каждый символ —
run_on_symbol - один раз на файл —
run_once
Для no-debugger нужны узлы DebuggerStatement, значит используем run. Упрощённый пример:
Развернуть
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: тесты
При каждом изменении:
just watch "cargo test -p oxc_linter -- rule-name"Разовый запуск:
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
// 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 вместо String, где возможно
Меньше аллокаций — быстрее oxc. Мелкие строки до 24 байт на 64-bit можно держать на стеке — см. /ru/learn/performance#string-inlining.
struct Element {
name: CompactStr
}
let element = Element {
name: "div".into()
};struct Element {
name: String
}
let element = Element {
name: "div".to_string()
};