Skip to content

Commit

Permalink
feat(linter): eslint-plugin-unicorn prefer-modern-dom-apis(style) (#1646
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Ken-HH24 committed Dec 10, 2023
1 parent 0c19991 commit b425b73
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ mod unicorn {
pub mod prefer_includes;
pub mod prefer_logical_operator_over_ternary;
pub mod prefer_math_trunc;
pub mod prefer_modern_dom_apis;
pub mod prefer_native_coercion_functions;
pub mod prefer_node_protocol;
pub mod prefer_number_properties;
Expand Down Expand Up @@ -393,6 +394,7 @@ oxc_macros::declare_all_lint_rules! {
unicorn::prefer_includes,
unicorn::prefer_logical_operator_over_ternary,
unicorn::prefer_math_trunc,
unicorn::prefer_modern_dom_apis,
unicorn::prefer_native_coercion_functions,
unicorn::no_useless_spread,
unicorn::prefer_number_properties,
Expand Down
203 changes: 203 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/prefer_modern_dom_apis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use oxc_ast::{
ast::{Argument, Expression, MemberExpression},
AstKind,
};
use oxc_diagnostics::{
miette::{self, Diagnostic},
thiserror::{self, Error},
};
use oxc_macros::declare_oxc_lint;
use oxc_span::{Atom, Span};
use phf::phf_map;

use crate::{ast_util::is_method_call, context::LintContext, rule::Rule, AstNode};

#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `{0}` over `{1}`.")]
#[diagnostic(severity(warning))]
struct PreferModernDomApisDiagnostic(pub &'static str, Atom, #[label] pub Span);

#[derive(Debug, Default, Clone)]
pub struct PreferModernDomApis;

const DISALLOWED_METHODS: phf::Map<&'static str, &'static str> = phf_map!(
"replaceChild" => "replaceWith",
"insertBefore" => "before",
);

const POSITION_REPLACERS: phf::Map<&'static str, &'static str> = phf_map!(
"beforebegin" => "before",
"afterbegin" => "prepend",
"beforeend" => "append",
"afterend" => "after",
);

declare_oxc_lint!(
/// ### What it does
///
/// Enforces the use of:
/// - childNode.replaceWith(newNode) over parentNode.replaceChild(newNode, oldNode)
/// - referenceNode.before(newNode) over parentNode.insertBefore(newNode, referenceNode)
/// - referenceNode.before('text') over referenceNode.insertAdjacentText('beforebegin', 'text')
/// - referenceNode.before(newNode) over referenceNode.insertAdjacentElement('beforebegin', newNode)
///
/// ### Why is this bad?
///
/// There are some advantages of using the newer DOM APIs, like:
/// - Traversing to the parent node is not necessary.
/// - Appending multiple nodes at once.
/// - Both DOMString and DOM node objects can be manipulated.
///
/// ### Example
/// ```javascript
/// // Bad
/// ("oldChildNode.replaceWith(newChildNode);", None),
///
/// // Good
/// ("parentNode.replaceChild(newChildNode, oldChildNode);", None),
/// ```
PreferModernDomApis,
style
);

impl Rule for PreferModernDomApis {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};

let Expression::MemberExpression(oxc_allocator::Box(
MemberExpression::StaticMemberExpression(member_expr),
)) = &call_expr.callee
else {
return;
};

let method = member_expr.property.name.as_str();

if is_method_call(
call_expr,
None,
Some(&["replaceChild", "insertBefore"]),
Some(2),
Some(2),
) && call_expr
.arguments
.iter()
.all(|argument| matches!(argument, Argument::Expression(expr) if !expr.is_undefined()))
&& matches!(member_expr.object, Expression::Identifier(_))
&& !call_expr.optional
{
if let Some(preferred_method) = DISALLOWED_METHODS.get(method) {
ctx.diagnostic(PreferModernDomApisDiagnostic(
preferred_method,
Atom::from(method),
member_expr.property.span,
));

return;
}
}

if is_method_call(
call_expr,
None,
Some(&["insertAdjacentText", "insertAdjacentElement"]),
Some(2),
Some(2),
) {
if let Argument::Expression(Expression::StringLiteral(lit)) = &call_expr.arguments[0] {
for (position, replacer) in &POSITION_REPLACERS {
if lit.value == position {
ctx.diagnostic(PreferModernDomApisDiagnostic(
replacer,
Atom::from(method),
member_expr.property.span,
));
}
}
}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
("oldChildNode.replaceWith(newChildNode);", None),
("referenceNode.before(newNode);", None),
("referenceNode.before(\"text\");", None),
("referenceNode.prepend(newNode);", None),
("referenceNode.prepend(\"text\");", None),
("referenceNode.append(newNode);", None),
("referenceNode.append(\"text\");", None),
("referenceNode.after(newNode);", None),
("referenceNode.after(\"text\");", None),
("oldChildNode.replaceWith(undefined, oldNode);", None),
("oldChildNode.replaceWith(newNode, undefined);", None),
("new parentNode.replaceChild(newNode, oldNode);", None),
("new parentNode.insertBefore(newNode, referenceNode);", None),
("new referenceNode.insertAdjacentText('beforebegin', 'text');", None),
("new referenceNode.insertAdjacentElement('beforebegin', newNode);", None),
("replaceChild(newNode, oldNode);", None),
("insertBefore(newNode, referenceNode);", None),
("insertAdjacentText('beforebegin', 'text');", None),
("insertAdjacentElement('beforebegin', newNode);", None),
("parentNode['replaceChild'](newNode, oldNode);", None),
("parentNode['insertBefore'](newNode, referenceNode);", None),
("referenceNode['insertAdjacentText']('beforebegin', 'text');", None),
("referenceNode['insertAdjacentElement']('beforebegin', newNode);", None),
("parentNode[replaceChild](newNode, oldNode);", None),
("parentNode[insertBefore](newNode, referenceNode);", None),
("referenceNode[insertAdjacentText]('beforebegin', 'text');", None),
("referenceNode[insertAdjacentElement]('beforebegin', newNode);", None),
("parent.foo(a, b);", None),
("parentNode.replaceChild(newNode);", None),
("parentNode.insertBefore(newNode);", None),
("referenceNode.insertAdjacentText('beforebegin');", None),
("referenceNode.insertAdjacentElement('beforebegin');", None),
("parentNode.replaceChild(newNode, oldNode, extra);", None),
("parentNode.insertBefore(newNode, referenceNode, extra);", None),
("referenceNode.insertAdjacentText('beforebegin', 'text', extra);", None),
("referenceNode.insertAdjacentElement('beforebegin', newNode, extra);", None),
("parentNode.replaceChild(...argumentsArray1, ...argumentsArray2);", None),
("parentNode.insertBefore(...argumentsArray1, ...argumentsArray2);", None),
("referenceNode.insertAdjacentText(...argumentsArray1, ...argumentsArray2);", None),
("referenceNode.insertAdjacentElement(...argumentsArray1, ...argumentsArray2);", None),
("referenceNode.insertAdjacentText('foo', 'text');", None),
("referenceNode.insertAdjacentElement('foo', newNode);", None),
];

let fail = vec![
("parentNode.replaceChild(newChildNode, oldChildNode);", None),
("const foo = parentNode.replaceChild(newChildNode, oldChildNode);", None),
("foo = parentNode.replaceChild(newChildNode, oldChildNode);", None),
("parentNode.insertBefore(newNode, referenceNode);", None),
("parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta);", None),
("const foo = parentNode.insertBefore(alfa, beta);", None),
("foo = parentNode.insertBefore(alfa, beta);", None),
("new Dom(parentNode.insertBefore(alfa, beta))", None),
("`${parentNode.insertBefore(alfa, beta)}`", None),
("referenceNode.insertAdjacentText(\"beforebegin\", \"text\");", None),
("referenceNode.insertAdjacentText(\"afterbegin\", \"text\");", None),
("referenceNode.insertAdjacentText(\"beforeend\", \"text\");", None),
("referenceNode.insertAdjacentText(\"afterend\", \"text\");", None),
("const foo = referenceNode.insertAdjacentText(\"beforebegin\", \"text\");", None),
("foo = referenceNode.insertAdjacentText(\"beforebegin\", \"text\");", None),
("referenceNode.insertAdjacentElement(\"beforebegin\", newNode);", None),
("referenceNode.insertAdjacentElement(\"afterbegin\", \"text\");", None),
("referenceNode.insertAdjacentElement(\"beforeend\", \"text\");", None),
("referenceNode.insertAdjacentElement(\"afterend\", newNode);", None),
("const foo = referenceNode.insertAdjacentElement(\"beforebegin\", newNode);", None),
("foo = referenceNode.insertAdjacentElement(\"beforebegin\", newNode);", None),
("const foo = [referenceNode.insertAdjacentElement(\"beforebegin\", newNode)]", None),
("foo(bar = referenceNode.insertAdjacentElement(\"beforebegin\", newNode))", None),
("const foo = () => { return referenceNode.insertAdjacentElement(\"beforebegin\", newNode); }", None),
("if (referenceNode.insertAdjacentElement(\"beforebegin\", newNode)) {}", None),
("const foo = { bar: referenceNode.insertAdjacentElement(\"beforebegin\", newNode) }", None),
];

Tester::new(PreferModernDomApis::NAME, pass, fail).test_and_snapshot();
}
161 changes: 161 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_modern_dom_apis.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
source: crates/oxc_linter/src/tester.rs
expression: prefer_modern_dom_apis
---
eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `replaceWith` over `replaceChild`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1parentNode.replaceChild(newChildNode, oldChildNode);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `replaceWith` over `replaceChild`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = parentNode.replaceChild(newChildNode, oldChildNode);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `replaceWith` over `replaceChild`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1foo = parentNode.replaceChild(newChildNode, oldChildNode);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1parentNode.insertBefore(newNode, referenceNode);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = parentNode.insertBefore(alfa, beta);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1foo = parentNode.insertBefore(alfa, beta);
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1new Dom(parentNode.insertBefore(alfa, beta))
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertBefore`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1`${parentNode.insertBefore(alfa, beta)}`
· ────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentText("beforebegin", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `prepend` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentText("afterbegin", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `append` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentText("beforeend", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `after` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentText("afterend", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = referenceNode.insertAdjacentText("beforebegin", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentText`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1foo = referenceNode.insertAdjacentText("beforebegin", "text");
· ──────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentElement("beforebegin", newNode);
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `prepend` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentElement("afterbegin", "text");
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `append` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentElement("beforeend", "text");
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `after` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1referenceNode.insertAdjacentElement("afterend", newNode);
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = referenceNode.insertAdjacentElement("beforebegin", newNode);
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1foo = referenceNode.insertAdjacentElement("beforebegin", newNode);
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = [referenceNode.insertAdjacentElement("beforebegin", newNode)]
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1foo(bar = referenceNode.insertAdjacentElement("beforebegin", newNode))
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = () => { return referenceNode.insertAdjacentElement("beforebegin", newNode); }
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1if (referenceNode.insertAdjacentElement("beforebegin", newNode)) {}
· ─────────────────────
╰────

eslint-plugin-unicorn(prefer-modern-dom-apis): Prefer using `before` over `insertAdjacentElement`.
╭─[prefer_modern_dom_apis.tsx:1:1]
1const foo = { bar: referenceNode.insertAdjacentElement("beforebegin", newNode) }
· ─────────────────────
╰────


0 comments on commit b425b73

Please sign in to comment.