From d8f07ca71dad8b85981594a667dd039bc7c1dda1 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Tue, 24 Oct 2023 04:34:20 -0500 Subject: [PATCH] feat(linter): support react/no-render-return-value (#1042) refer to https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-render-return-value.md --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/react/no_render_return_value.rs | 135 ++++++++++++++++++ .../src/snapshots/no_render_return_value.snap | 51 +++++++ crates/oxc_span/src/span.rs | 5 + 4 files changed, 193 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/no_render_return_value.rs create mode 100644 crates/oxc_linter/src/snapshots/no_render_return_value.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 548e71c54..0d464e11e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -132,6 +132,7 @@ mod react { pub mod no_children_prop; pub mod no_dangerously_set_inner_html; pub mod no_find_dom_node; + pub mod no_render_return_value; pub mod no_unescaped_entities; } @@ -264,6 +265,7 @@ oxc_macros::declare_all_lint_rules! { react::no_children_prop, react::no_dangerously_set_inner_html, react::no_find_dom_node, + react::no_render_return_value, import::named, import::no_cycle, import::no_self_import, diff --git a/crates/oxc_linter/src/rules/react/no_render_return_value.rs b/crates/oxc_linter/src/rules/react/no_render_return_value.rs new file mode 100644 index 000000000..f6c039a62 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_render_return_value.rs @@ -0,0 +1,135 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::ScopeFlags; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render.")] +#[diagnostic(severity(warning))] +struct NoRenderReturnValueDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoRenderReturnValue; + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule will warn you if you try to use the ReactDOM.render() return value. + /// + /// ### Example + /// ```javascript + /// // Bad + /// vaa inst =ReactDOM.render(, document.body); + /// function render() { + /// return ReactDOM.render(, document.body); + /// } + /// + /// // Good + /// ReactDOM.render(, document.body); + /// ``` + NoRenderReturnValue, + correctness +); + +impl Rule for NoRenderReturnValue { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { return }; + let Expression::MemberExpression(member_expr) = &call_expr.callee else { return }; + let Expression::Identifier(ident) = member_expr.object() else { return }; + if ident.name == "ReactDOM" { + if let Some((property_span, property_name)) = member_expr.static_property_info() { + if property_name == "render" { + if let Some(parent_node) = ctx.nodes().parent_node(node.id()) { + if matches!( + parent_node.kind(), + AstKind::VariableDeclarator(_) + | AstKind::ObjectProperty(_) + | AstKind::ReturnStatement(_) + | AstKind::AssignmentExpression(_) + ) { + ctx.diagnostic(NoRenderReturnValueDiagnostic( + ident.span.merge(&property_span), + )); + } + + let is_arrow_function = ctx + .scopes() + .get_flags(parent_node.scope_id()) + .contains(ScopeFlags::Arrow); + + if is_arrow_function { + ctx.nodes().ancestors(parent_node.id()).skip(1).find(|node_id| { + let parent_node = ctx.nodes().get_node(*node_id); + matches!(parent_node.kind(), AstKind::ArrowExpression(_)) + .then(|| { + ctx.diagnostic(NoRenderReturnValueDiagnostic( + ident.span.merge(&property_span), + )); + }) + .is_some() + }); + } + } + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("ReactDOM.render(
, document.body);", None), + ( + " + let node; + ReactDOM.render(
node = ref}/>, document.body); + ", + None, + ), + ("ReactDOM.render(
this.node = ref}/>, document.body);", None), + ("React.render(
this.node = ref}/>, document.body);", None), + ("React.render(
this.node = ref}/>, document.body);", None), + ("var foo = React.render(
, root);", None), + ("var foo = render(
, root)", None), + ("var foo = ReactDom.renderder(
, root)", None), + ]; + + let fail = vec![ + ("var Hello = ReactDOM.render(
, document.body);", None), + ( + " + var o = { + inst: ReactDOM.render(
, document.body) + }; + ", + None, + ), + ( + " + function render () { + return ReactDOM.render(
, document.body) + } + ", + None, + ), + ("var render = (a, b) => ReactDOM.render(a, b)", None), + ("this.o = ReactDOM.render(
, document.body);", None), + ("var v; v = ReactDOM.render(
, document.body);", None), + ("var inst = ReactDOM.render(
, document.body);", None), + // This rule is only supported for react versions >= 15.0.0, so the following are not supported. + // See https://github.com/web-infra-dev/oxc/pull/1042#discussion_r1369762147 + // ("var inst = React.render(
, document.body);", None), + // ("var inst = React.render(
, document.body);", None), + ]; + + Tester::new(NoRenderReturnValue::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_render_return_value.snap b/crates/oxc_linter/src/snapshots/no_render_return_value.snap new file mode 100644 index 000000000..802adfd08 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_render_return_value.snap @@ -0,0 +1,51 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_render_return_value +--- + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:1:1] + 1 │ var Hello = ReactDOM.render(
, document.body); + · ─────────────── + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:2:1] + 2 │ var o = { + 3 │ inst: ReactDOM.render(
, document.body) + · ─────────────── + 4 │ }; + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:2:1] + 2 │ function render () { + 3 │ return ReactDOM.render(
, document.body) + · ─────────────── + 4 │ } + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:1:1] + 1 │ var render = (a, b) => ReactDOM.render(a, b) + · ─────────────── + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:1:1] + 1 │ this.o = ReactDOM.render(
, document.body); + · ─────────────── + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:1:1] + 1 │ var v; v = ReactDOM.render(
, document.body); + · ─────────────── + ╰──── + + ⚠ eslint-plugin-react(no-render-return-value): Do not depend on the return value from ReactDOM.render. + ╭─[no_render_return_value.tsx:1:1] + 1 │ var inst = ReactDOM.render(
, document.body); + · ─────────────── + ╰──── + + diff --git a/crates/oxc_span/src/span.rs b/crates/oxc_span/src/span.rs index be9660f9b..9c1f67280 100644 --- a/crates/oxc_span/src/span.rs +++ b/crates/oxc_span/src/span.rs @@ -27,6 +27,11 @@ impl Span { self.end - self.start } + #[must_use] + pub fn merge(&self, other: &Self) -> Self { + Self::new(self.start.min(other.start), self.end.max(other.end)) + } + pub fn source_text<'a>(&self, source_text: &'a str) -> &'a str { &source_text[self.start as usize..self.end as usize] }