Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

fix(rome_js_analyze): improve the detection of ReactDOM.render calls in noRenderReturnValue #3626

Merged
merged 2 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions crates/rome_js_analyze/src/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ impl ReactCreateElementCall {
model: &SemanticModel,
) -> Option<Self> {
let callee = call_expression.callee().ok()?;
let is_react_create_element = is_react_call_api(&callee, model, "createElement")?;
let is_react_create_element =
is_react_call_api(&callee, model, ReactLibrary::React, "createElement")?;

if is_react_create_element {
let arguments = call_expression.arguments().ok()?.args();
Expand Down Expand Up @@ -156,7 +157,8 @@ impl ReactCloneElementCall {
model: &SemanticModel,
) -> Option<Self> {
let callee = call_expression.callee().ok()?;
let is_react_clone_element = is_react_call_api(&callee, model, "cloneElement")?;
let is_react_clone_element =
is_react_call_api(&callee, model, ReactLibrary::React, "cloneElement")?;

if is_react_clone_element {
let arguments = call_expression.arguments().ok()?.args();
Expand Down Expand Up @@ -216,6 +218,28 @@ impl ReactApiCall for ReactCloneElementCall {
}
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum ReactLibrary {
React,
ReactDOM,
}

impl ReactLibrary {
const fn import_name(self) -> &'static str {
match self {
ReactLibrary::React => "react",
ReactLibrary::ReactDOM => "react-dom",
}
}

const fn global_name(self) -> &'static str {
match self {
ReactLibrary::React => "React",
ReactLibrary::ReactDOM => "ReactDOM",
}
}
}

/// List of valid [`React` API]
///
/// [`React` API]: https://reactjs.org/docs/react-api.html
Expand Down Expand Up @@ -243,10 +267,13 @@ const VALID_REACT_API: [&str; 14] = [
pub(crate) fn is_react_call_api(
expression: &JsAnyExpression,
model: &SemanticModel,
lib: ReactLibrary,
api_name: &str,
) -> Option<bool> {
// we bail straight away if the API doesn't exists in React
debug_assert!(VALID_REACT_API.contains(&api_name));
if matches!(lib, ReactLibrary::React) {
// we bail straight away if the API doesn't exists in React
debug_assert!(VALID_REACT_API.contains(&api_name));
}

Some(match expression {
JsAnyExpression::JsStaticMemberExpression(node) => {
Expand All @@ -269,12 +296,12 @@ pub(crate) fn is_react_call_api(
.ancestors()
.find_map(|ancestor| JsImport::cast_ref(&ancestor))
{
js_import.source_is("react").ok()?
js_import.source_is(lib.import_name()).ok()?
} else {
false
}
}
None => identifier.has_name("React"),
None => identifier.has_name(lib.global_name()),
}
}

Expand All @@ -283,7 +310,7 @@ pub(crate) fn is_react_call_api(

model
.declaration(&name)
.and_then(|binding| is_react_export(binding, api_name))
.and_then(|binding| is_react_export(binding, lib, api_name))
.unwrap_or(false)
}
_ => false,
Expand Down Expand Up @@ -335,7 +362,7 @@ pub(crate) fn jsx_reference_identifier_is_fragment(
model: &SemanticModel,
) -> Option<bool> {
match model.declaration(name) {
Some(reference) => is_react_export(reference, "Fragment"),
Some(reference) => is_react_export(reference, ReactLibrary::React, "Fragment"),
None => {
let value_token = name.value_token().ok()?;
let is_fragment = value_token.text_trimmed() == "Fragment";
Expand All @@ -344,7 +371,7 @@ pub(crate) fn jsx_reference_identifier_is_fragment(
}
}

fn is_react_export(binding: Binding, name: &str) -> Option<bool> {
fn is_react_export(binding: Binding, lib: ReactLibrary, name: &str) -> Option<bool> {
let ident = JsIdentifierBinding::cast_ref(binding.syntax())?;
let import_specifier = ident.parent::<JsAnyNamedImportSpecifier>()?;
let name_token = match &import_specifier {
Expand All @@ -366,5 +393,5 @@ fn is_react_export(binding: Binding, name: &str) -> Option<bool> {
let import_clause = import_specifiers.parent::<JsImportNamedClause>()?;
let import = import_clause.parent::<JsImport>()?;

import.source_is("react").ok()
import.source_is(lib.import_name()).ok()
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::react::{is_react_call_api, ReactApiCall, ReactCloneElementCall};
use crate::react::{is_react_call_api, ReactApiCall, ReactCloneElementCall, ReactLibrary};
use crate::semantic_services::Semantic;
use rome_analyze::context::RuleContext;
use rome_analyze::{declare_rule, Rule, RuleDiagnostic};
Expand Down Expand Up @@ -333,7 +333,7 @@ fn find_react_children_function_argument(
let object = member_expression.object().ok()?;

// React.Children.forEach/map or Children.forEach/map
if is_react_call_api(&object, model, "Children")? {
if is_react_call_api(&object, model, ReactLibrary::React, "Children")? {
let arguments = call_expression.arguments().ok()?;
let arguments = arguments.args();
let mut arguments = arguments.into_iter();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
use crate::react::{is_react_call_api, ReactLibrary};
use crate::semantic_services::Semantic;
use rome_analyze::context::RuleContext;
use rome_analyze::{declare_rule, Rule, RuleDiagnostic};
use rome_console::markup;
use rome_js_semantic::SemanticModel;
use rome_js_syntax::JsSyntaxKind::JS_IMPORT;
use rome_js_syntax::{
JsAnyExpression, JsCallExpression, JsExpressionStatement, JsIdentifierBinding,
JsIdentifierExpression, JsStaticMemberExpression,
};
use rome_rowan::{declare_node_union, AstNode};
use rome_js_syntax::{JsCallExpression, JsExpressionStatement};
use rome_rowan::AstNode;

declare_rule! {
/// Prevent the usage of the return value of `React.render`.
Expand Down Expand Up @@ -50,16 +46,7 @@ impl Rule for NoRenderReturnValue {
let node = ctx.query();
let callee = node.callee().ok()?;
let model = ctx.model();
let is_react_render = match callee {
JsAnyExpression::JsStaticMemberExpression(static_member) => {
is_react_render(PossibleReactRender::from(static_member), model)
}
JsAnyExpression::JsIdentifierExpression(identifier_expression) => {
is_react_render(PossibleReactRender::from(identifier_expression), model)
}
_ => return None,
}?;
if is_react_render {
if is_react_call_api(&callee, model, ReactLibrary::ReactDOM, "render")? {
let parent = node.syntax().parent()?;

if !JsExpressionStatement::can_cast(parent.kind()) {
Expand All @@ -85,60 +72,3 @@ Check the "<Hyperlink href="https://facebook.github.io/react/docs/react-dom.html
)
}
}

declare_node_union! {
pub(crate) PossibleReactRender = JsStaticMemberExpression | JsIdentifierExpression
}

fn is_react_render(node: PossibleReactRender, model: &SemanticModel) -> Option<bool> {
let result = match node {
PossibleReactRender::JsStaticMemberExpression(node) => {
let object = node.object().ok()?;
let member = node.member().ok()?;
let member = member.as_js_name()?;
let identifier = object.as_js_identifier_expression()?.name().ok()?;

let maybe_from_react = identifier.syntax().text_trimmed() == "ReactDOM"
&& member.syntax().text_trimmed() == "render";

if maybe_from_react {
let identifier_binding = model.declaration(&identifier);
if let Some(binding_identifier) = identifier_binding {
let binding_identifier =
JsIdentifierBinding::cast_ref(binding_identifier.syntax())?;
for ancestor in binding_identifier.syntax().ancestors() {
if ancestor.kind() == JS_IMPORT {
return Some(
binding_identifier.syntax().text_trimmed()
== identifier.syntax().text_trimmed(),
);
}
}
}
}
maybe_from_react
}
PossibleReactRender::JsIdentifierExpression(identifier) => {
let maybe_react_render = identifier.syntax().text_trimmed() == "render";
let name = identifier.name().ok()?;
if maybe_react_render {
let declaration = model.declaration(&name);
if let Some(declaration) = declaration {
let identifier_binding = JsIdentifierBinding::cast_ref(declaration.syntax())?;
for ancestor in identifier_binding.syntax().ancestors() {
if ancestor.kind() == JS_IMPORT {
return Some(
identifier_binding.syntax().text_trimmed()
== identifier.syntax().text_trimmed(),
);
}
}
}
}

maybe_react_render
}
};

Some(result)
}

This file was deleted.

Loading