Skip to content

Commit

Permalink
feat(linter): eslint-plugin-unicorn prefer-dom-node-text-content(styl…
Browse files Browse the repository at this point in the history
  • Loading branch information
Ken-HH24 authored Dec 13, 2023
1 parent 0080638 commit 282771a
Show file tree
Hide file tree
Showing 3 changed files with 242 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 @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
130 changes: 130 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/prefer_dom_node_text_content.rs
Original file line number Diff line number Diff line change
@@ -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();
}
110 changes: 110 additions & 0 deletions crates/oxc_linter/src/snapshots/prefer_dom_node_text_content.snap
Original file line number Diff line number Diff line change
@@ -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]
1node.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]
1node?.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]
1node.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]
1innerText.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]
1const {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]
1const {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]
1const {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]
1const {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]
1const {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]
1function 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]
1for (const [{innerText}] of elements);
· ─────────
╰────
help: Replace `.innerText` with `.textContent`.


0 comments on commit 282771a

Please sign in to comment.