diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b795e250a..69470e820 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -131,6 +131,7 @@ mod react { pub mod jsx_no_useless_fragment; pub mod no_children_prop; pub mod no_dangerously_set_inner_html; + pub mod no_find_dom_node; } mod unicorn { @@ -260,6 +261,7 @@ oxc_macros::declare_all_lint_rules! { react::jsx_no_useless_fragment, react::no_children_prop, react::no_dangerously_set_inner_html, + react::no_find_dom_node, import::named, import::no_cycle, import::no_self_import, diff --git a/crates/oxc_linter/src/rules/react/no_find_dom_node.rs b/crates/oxc_linter/src/rules/react/no_find_dom_node.rs new file mode 100644 index 000000000..02de64c45 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_find_dom_node.rs @@ -0,0 +1,194 @@ +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}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`.")] +#[diagnostic(severity(warning), help("Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives."))] +struct NoFindDomNodeDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoFindDomNode; + +declare_oxc_lint!( + /// ### What it does + /// This rule disallows the use of `findDOMNode`. + /// + /// ### Why is this bad? + /// `findDOMNode` is an escape hatch used to access the underlying DOM node. + /// In most cases, use of this escape hatch is discouraged because it pierces the component abstraction. + /// [It has been deprecated in `StrictMode`.](https://legacy.reactjs.org/docs/strict-mode.html#warning-about-deprecated-finddomnode-usage) + /// + /// ### Example + /// ```javascript + /// class MyComponent extends Component { + /// componentDidMount() { + /// findDOMNode(this).scrollIntoView(); + /// } + /// render() { + /// return
; + /// } + /// } + /// ``` + NoFindDomNode, + correctness +); + +impl Rule for NoFindDomNode { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { return }; + + if let Some(ident) = call_expr.callee.get_identifier_reference() { + if ident.name == "findDOMNode" { + ctx.diagnostic(NoFindDomNodeDiagnostic(ident.span)); + } + return; + } + + let Some(member_expr) = call_expr.callee.get_member_expr() else { return }; + let member = member_expr.object(); + if !member.is_specific_id("React") + && !member.is_specific_id("ReactDOM") + && !member.is_specific_id("ReactDom") + { + return; + } + let Some((span, "findDOMNode")) = member_expr.static_property_info() else { return }; + ctx.diagnostic(NoFindDomNodeDiagnostic(span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("var Hello = function() {};", None), + ( + r#" + var Hello = createReactClass({ + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + someNonMemberFunction(arg); + this.someFunc = React.findDOMNode; + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + React.someFunc(this); + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + SomeModule.findDOMNode(this).scrollIntoView(); + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ]; + + let fail = vec![ + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + React.findDOMNode(this).scrollIntoView(); + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + ReactDOM.findDOMNode(this).scrollIntoView(); + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + var Hello = createReactClass({ + componentDidMount: function() { + ReactDom.findDOMNode(this).scrollIntoView(); + }, + render: function() { + return
Hello
; + } + }); + "#, + None, + ), + ( + r#" + class Hello extends Component { + componentDidMount() { + findDOMNode(this).scrollIntoView(); + } + render() { + return
Hello
; + } + } + "#, + None, + ), + ( + r#" + class Hello extends Component { + componentDidMount() { + this.node = findDOMNode(this); + } + render() { + return
Hello
; + } + } + "#, + None, + ), + ]; + + Tester::new(NoFindDomNode::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_find_dom_node.snap b/crates/oxc_linter/src/snapshots/no_find_dom_node.snap new file mode 100644 index 000000000..acd94d40d --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_find_dom_node.snap @@ -0,0 +1,51 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 105 +expression: no_find_dom_node +--- + ⚠ eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`. + ╭─[no_find_dom_node.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ React.findDOMNode(this).scrollIntoView(); + · ─────────── + 5 │ }, + ╰──── + help: Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives. + + ⚠ eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`. + ╭─[no_find_dom_node.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ ReactDOM.findDOMNode(this).scrollIntoView(); + · ─────────── + 5 │ }, + ╰──── + help: Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives. + + ⚠ eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`. + ╭─[no_find_dom_node.tsx:3:1] + 3 │ componentDidMount: function() { + 4 │ ReactDom.findDOMNode(this).scrollIntoView(); + · ─────────── + 5 │ }, + ╰──── + help: Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives. + + ⚠ eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`. + ╭─[no_find_dom_node.tsx:3:1] + 3 │ componentDidMount() { + 4 │ findDOMNode(this).scrollIntoView(); + · ─────────── + 5 │ } + ╰──── + help: Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives. + + ⚠ eslint-plugin-react(no-find-dom-node): Unexpected call to `findDOMNode`. + ╭─[no_find_dom_node.tsx:3:1] + 3 │ componentDidMount() { + 4 │ this.node = findDOMNode(this); + · ─────────── + 5 │ } + ╰──── + help: Replace `findDOMNode` with one of the alternatives documented at https://react.dev/reference/react-dom/findDOMNode#alternatives. + +