From 282771afc458a1122647a06f8528bb1fccc0c66b Mon Sep 17 00:00:00 2001 From: Ken-HH24 <62000888+Ken-HH24@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:19:00 +0800 Subject: [PATCH] feat(linter): eslint-plugin-unicorn prefer-dom-node-text-content(style) (#1658) Implement [prefer-dom-node-text-content](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-text-content.md) for #684 . --- crates/oxc_linter/src/rules.rs | 2 + .../unicorn/prefer_dom_node_text_content.rs | 130 ++++++++++++++++++ .../prefer_dom_node_text_content.snap | 110 +++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 crates/oxc_linter/src/rules/unicorn/prefer_dom_node_text_content.rs create mode 100644 crates/oxc_linter/src/snapshots/prefer_dom_node_text_content.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 78f867560..606a26359 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -196,6 +196,7 @@ mod unicorn { pub mod prefer_dom_node_append; pub mod prefer_dom_node_dataset; pub mod prefer_dom_node_remove; + pub mod prefer_dom_node_text_content; pub mod prefer_event_target; pub mod prefer_includes; pub mod prefer_logical_operator_over_ternary; @@ -393,6 +394,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::prefer_dom_node_append, unicorn::prefer_dom_node_dataset, unicorn::prefer_dom_node_remove, + unicorn::prefer_dom_node_text_content, unicorn::prefer_event_target, unicorn::prefer_includes, unicorn::prefer_logical_operator_over_ternary, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_dom_node_text_content.rs b/crates/oxc_linter/src/rules/unicorn/prefer_dom_node_text_content.rs new file mode 100644 index 000000000..247e40bed --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_dom_node_text_content.rs @@ -0,0 +1,130 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode, Fix}; + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`." +)] +#[diagnostic(severity(warning), help("Replace `.innerText` with `.textContent`."))] +struct PreferDomNodeTextContentDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferDomNodeTextContent; + +declare_oxc_lint!( + /// ### What it does + /// + /// Enforces the use of `.textContent` over `.innerText` for DOM nodes. + /// + /// ### Why is this bad? + /// + /// There are some disadvantages of using .innerText. + /// - `.innerText` is much more performance-heavy as it requires layout information to return the result. + /// - `.innerText` is defined only for HTMLElement objects, while `.textContent` is defined for all Node objects. + /// - `.innerText` is not standard, for example, it is not present in Firefox. + /// + /// ### Example + /// ```javascript + /// // Bad + /// const text = foo.innerText; + /// + /// // Good + /// const text = foo.textContent; + /// ``` + PreferDomNodeTextContent, + style +); + +impl Rule for PreferDomNodeTextContent { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::MemberExpression(member_expr) = node.kind() { + if let Some((span, name)) = member_expr.static_property_info() { + if name == "innerText" && !member_expr.is_computed() { + ctx.diagnostic_with_fix(PreferDomNodeTextContentDiagnostic(span), || { + Fix::new("textContent", span) + }); + } + } + } + + let Some(parent_node) = ctx.nodes().parent_node(node.id()) else { + return; + }; + + let Some(grand_parent_node) = ctx.nodes().parent_node(parent_node.id()) else { + return; + }; + + let parent_node_kind = parent_node.kind(); + let grand_parent_node_kind = grand_parent_node.kind(); + + // `const {innerText} = node` or `({innerText: text} = node)` + if let AstKind::IdentifierName(identifier) = node.kind() { + if identifier.name == "innerText" + && matches!(parent_node_kind, AstKind::PropertyKey(_)) + && (matches!(grand_parent_node_kind, AstKind::ObjectPattern(_)) + || matches!(grand_parent_node_kind, AstKind::AssignmentTarget(_))) + { + ctx.diagnostic(PreferDomNodeTextContentDiagnostic(identifier.span)); + return; + } + } + + // `({innerText} = node)` + if let AstKind::IdentifierReference(identifier_ref) = node.kind() { + if identifier_ref.name == "innerText" + && matches!(parent_node_kind, AstKind::AssignmentTarget(_)) + && matches!(grand_parent_node_kind, AstKind::AssignmentExpression(_)) + { + ctx.diagnostic(PreferDomNodeTextContentDiagnostic(identifier_ref.span)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("innerText;", None), + ("node.textContent;", None), + ("node[innerText];", None), + ("innerText = true;", None), + ("node['innerText'];", None), + ("innerText.textContent", None), + ("const [innerText] = node;", None), + ("[innerText] = node;", None), + ("const {[innerText]: text} = node;", None), + ("({[innerText]: text} = node);", None), + ("const foo = {innerText}", None), + ("const foo = {innerText: text}", None), + ]; + + let fail = vec![ + ("node.innerText;", None), + ("node?.innerText;", None), + ("node.innerText = 'foo';", None), + ("innerText.innerText;", None), + ("const {innerText} = node;", None), + ("const {innerText,} = node;", None), + ("const {innerText: text} = node;", None), + ("const {innerText = \"default text\"} = node;", None), + ("const {innerText: text = \"default text\"} = node;", None), + ("({innerText} = node);", None), + ("({innerText: text} = node);", None), + ("({innerText = \"default text\"} = node);", None), + ("({innerText: text = \"default text\"} = node);", None), + ("function foo({innerText}) {return innerText}", None), + ("for (const [{innerText}] of elements);", None), + ]; + + Tester::new(PreferDomNodeTextContent::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_dom_node_text_content.snap b/crates/oxc_linter/src/snapshots/prefer_dom_node_text_content.snap new file mode 100644 index 000000000..f619e7d56 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_dom_node_text_content.snap @@ -0,0 +1,110 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: prefer_dom_node_text_content +--- + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ node.innerText; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ node?.innerText; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ node.innerText = 'foo'; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ innerText.innerText; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ const {innerText} = node; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ const {innerText,} = node; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ const {innerText: text} = node; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ const {innerText = "default text"} = node; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ const {innerText: text = "default text"} = node; + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ ({innerText} = node); + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ ({innerText: text} = node); + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ ({innerText = "default text"} = node); + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ ({innerText: text = "default text"} = node); + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ function foo({innerText}) {return innerText} + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + + ⚠ eslint-plugin-unicorn(prefer-dom-node-text-content): Prefer `.textContent` over `.innerText`. + ╭─[prefer_dom_node_text_content.tsx:1:1] + 1 │ for (const [{innerText}] of elements); + · ───────── + ╰──── + help: Replace `.innerText` with `.textContent`. + +