From 9a563ebeb8df1a605701e252232d1b071beb74fc Mon Sep 17 00:00:00 2001 From: Mikhail Baev Date: Tue, 13 Jan 2026 15:37:16 +0500 Subject: [PATCH 1/2] feat(linter): add suggestion for `unicorn/prefer-modern-dom-apis` rule --- .../rules/unicorn/prefer_modern_dom_apis.rs | 53 +++++++++++++++---- .../unicorn_prefer_modern_dom_apis.snap | 12 +++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs b/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs index cb1de98e1cc35..76dc969f8d1c8 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs @@ -4,7 +4,7 @@ use oxc_ast::{ }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_span::Span; +use oxc_span::{GetSpan, Span}; use crate::{AstNode, ast_util::is_method_call, context::LintContext, rule::Rule}; @@ -38,6 +38,11 @@ fn get_replacement_for_position(position: &str) -> Option<&'static str> { } } +fn is_value_not_usable(node: &AstNode, ctx: &LintContext) -> bool { + let parent = ctx.nodes().parent_node(node.id()); + matches!(parent.kind(), AstKind::ExpressionStatement(_)) +} + declare_oxc_lint!( /// ### What it does /// @@ -68,7 +73,7 @@ declare_oxc_lint!( PreferModernDomApis, unicorn, style, - pending + suggestion ); impl Rule for PreferModernDomApis { @@ -96,11 +101,24 @@ impl Rule for PreferModernDomApis { && !call_expr.optional && let Some(preferred_method) = get_replacement_for_disallowed_method(method) { - ctx.diagnostic(prefer_modern_dom_apis_diagnostic( + let diagnostic = prefer_modern_dom_apis_diagnostic( preferred_method, method, member_expr.property.span, - )); + ); + + if is_value_not_usable(node, ctx) { + ctx.diagnostic_with_suggestion(diagnostic, |fixer| { + let new_node = ctx.source_range(call_expr.arguments[0].span()); + let old_node = ctx.source_range(call_expr.arguments[1].span()); + + let replacement = format!("{old_node}.{preferred_method}({new_node})"); + + fixer.replace(call_expr.span, replacement) + }); + } else { + ctx.diagnostic(diagnostic); + } return; } @@ -112,13 +130,28 @@ impl Rule for PreferModernDomApis { Some(2), Some(2), ) && let Argument::StringLiteral(lit) = &call_expr.arguments[0] - && let Some(replacer) = get_replacement_for_position(lit.value.as_str()) + && let Some(preferred_method) = get_replacement_for_position(lit.value.as_str()) { - ctx.diagnostic(prefer_modern_dom_apis_diagnostic( - replacer, + let diagnostic = prefer_modern_dom_apis_diagnostic( + preferred_method, method, member_expr.property.span, - )); + ); + + let can_fix = method == "insertAdjacentText" || is_value_not_usable(node, ctx); + + if can_fix { + ctx.diagnostic_with_suggestion(diagnostic, |fixer| { + let content = ctx.source_range(call_expr.arguments[1].span()); + let reference = ctx.source_range(member_expr.object.span()); + + let replacement = format!("{reference}.{preferred_method}({content})"); + + fixer.replace(call_expr.span, replacement) + }); + } else { + ctx.diagnostic(diagnostic); + } } } } @@ -207,8 +240,7 @@ fn test() { ), ]; - // TODO: Implement autofix and use these tests. - let _fix = vec![ + let fix = vec![ ( "parentNode.replaceChild(newChildNode, oldChildNode);", "oldChildNode.replaceWith(newChildNode);", @@ -285,5 +317,6 @@ fn test() { ]; Tester::new(PreferModernDomApis::NAME, PreferModernDomApis::PLUGIN, pass, fail) + .expect_fix(fix) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/unicorn_prefer_modern_dom_apis.snap b/crates/oxc_linter/src/snapshots/unicorn_prefer_modern_dom_apis.snap index 494cccab9ab4a..eb53ac93d7f3a 100644 --- a/crates/oxc_linter/src/snapshots/unicorn_prefer_modern_dom_apis.snap +++ b/crates/oxc_linter/src/snapshots/unicorn_prefer_modern_dom_apis.snap @@ -6,6 +6,7 @@ source: crates/oxc_linter/src/tester.rs 1 │ parentNode.replaceChild(newChildNode, oldChildNode); · ──────────── ╰──── + help: Replace `parentNode.replaceChild(newChildNode, oldChildNode)` with `oldChildNode.replaceWith(newChildNode)`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `replaceWith` over `replaceChild`. ╭─[prefer_modern_dom_apis.tsx:1:24] @@ -24,6 +25,7 @@ source: crates/oxc_linter/src/tester.rs 1 │ parentNode.insertBefore(newNode, referenceNode); · ──────────── ╰──── + help: Replace `parentNode.insertBefore(newNode, referenceNode)` with `referenceNode.before(newNode)`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`. ╭─[prefer_modern_dom_apis.tsx:1:12] @@ -60,60 +62,70 @@ source: crates/oxc_linter/src/tester.rs 1 │ referenceNode.insertAdjacentText("beforebegin", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("beforebegin", "text")` with `referenceNode.before("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `prepend` over `insertAdjacentText`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentText("afterbegin", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("afterbegin", "text")` with `referenceNode.prepend("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `append` over `insertAdjacentText`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentText("beforeend", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("beforeend", "text")` with `referenceNode.append("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `after` over `insertAdjacentText`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentText("afterend", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("afterend", "text")` with `referenceNode.after("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentText`. ╭─[prefer_modern_dom_apis.tsx:1:27] 1 │ const foo = referenceNode.insertAdjacentText("beforebegin", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("beforebegin", "text")` with `referenceNode.before("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentText`. ╭─[prefer_modern_dom_apis.tsx:1:21] 1 │ foo = referenceNode.insertAdjacentText("beforebegin", "text"); · ────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentText("beforebegin", "text")` with `referenceNode.before("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentElement("beforebegin", newNode); · ───────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentElement("beforebegin", newNode)` with `referenceNode.before(newNode)`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `prepend` over `insertAdjacentElement`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentElement("afterbegin", "text"); · ───────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentElement("afterbegin", "text")` with `referenceNode.prepend("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `append` over `insertAdjacentElement`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentElement("beforeend", "text"); · ───────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentElement("beforeend", "text")` with `referenceNode.append("text")`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `after` over `insertAdjacentElement`. ╭─[prefer_modern_dom_apis.tsx:1:15] 1 │ referenceNode.insertAdjacentElement("afterend", newNode); · ───────────────────── ╰──── + help: Replace `referenceNode.insertAdjacentElement("afterend", newNode)` with `referenceNode.after(newNode)`. ⚠ eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`. ╭─[prefer_modern_dom_apis.tsx:1:27] From 9890976f9f535f352ea690b14ea7bec17dea5a51 Mon Sep 17 00:00:00 2001 From: Mikhail Baev Date: Tue, 13 Jan 2026 16:05:19 +0500 Subject: [PATCH 2/2] update is_value_not_usable method --- .../src/rules/unicorn/prefer_modern_dom_apis.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs b/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs index 76dc969f8d1c8..ed395e96ab9ac 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs @@ -39,8 +39,13 @@ fn get_replacement_for_position(position: &str) -> Option<&'static str> { } fn is_value_not_usable(node: &AstNode, ctx: &LintContext) -> bool { - let parent = ctx.nodes().parent_node(node.id()); - matches!(parent.kind(), AstKind::ExpressionStatement(_)) + let parent_node = ctx.nodes().parent_node(node.id()); + let grandparent_node = ctx.nodes().parent_node(parent_node.id()); + matches!( + (parent_node.kind(), grandparent_node.kind()), + (AstKind::ExpressionStatement(_), _) + | (AstKind::ChainExpression(_), AstKind::ExpressionStatement(_)) + ) } declare_oxc_lint!(