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

Commit

Permalink
fix(rome_js_analyze): useExhaustiveDependencies support function synt…
Browse files Browse the repository at this point in the history
…ax, or React.use* hooks and more. (#4571)

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
  • Loading branch information
nissy-dev and ematipico authored Jun 21, 2023
1 parent a34cdcc commit 3d61b99
Show file tree
Hide file tree
Showing 19 changed files with 889 additions and 395 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ parameter decorators:
The _Rome_ formatter takes care of removing extra semicolons.
Thus, there is no need for this rule.


#### Other changes

- [`noRedeclare`](https://docs.rome.tools/lint/rules/noredeclare/): allow redeclare of index signatures are in different type members [#4478](https://github.com/rome/tools/issues/4478)

- Fix a crash in the [`NoParameterAssign`](https://docs.rome.tools/lint/rules/noparameterassign/) rule that occurred when there was a bogus binding. [#4323](https://github.com/rome/tools/issues/4323)

- Fix [`useExhaustiveDependencies`](https://docs.rome.tools/lint/rules/useexhaustivedependencies/) rule in the following cases [#4330](https://github.com/rome/tools/issues/4330)
- when the first argument of hooks is a named function
- inside an export default function
- for React.use* hooks

- Improve the diagnostic and the code action of [`useDefaultParameterLast`](https://docs.rome.tools/lint/rules/usedefaultparameterlast/).

The diagnostic now reports the last required parameter which should precede optional and default parameters.
Expand Down
17 changes: 16 additions & 1 deletion crates/rome_js_analyze/src/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ impl ReactLibrary {
/// List of valid [`React` API]
///
/// [`React` API]: https://reactjs.org/docs/react-api.html
const VALID_REACT_API: [&str; 14] = [
const VALID_REACT_API: [&str; 29] = [
"Component",
"PureComponent",
"memo",
Expand All @@ -157,6 +157,21 @@ const VALID_REACT_API: [&str; 14] = [
"Suspense",
"startTransition",
"Children",
"useEffect",
"useLayoutEffect",
"useInsertionEffect",
"useCallback",
"useMemo",
"useImperativeHandle",
"useState",
"useContext",
"useReducer",
"useRef",
"useDebugValue",
"useDeferredValue",
"useTransition",
"useId",
"useSyncExternalStore",
];

/// Checks if the current [JsCallExpression] is a potential [`React` API].
Expand Down
77 changes: 64 additions & 13 deletions crates/rome_js_analyze/src/react/hooks.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::react::{is_react_call_api, ReactLibrary};
use std::collections::{HashMap, HashSet};

use rome_js_semantic::{Capture, ClosureExtensions, SemanticModel};
use rome_js_semantic::{Capture, Closure, ClosureExtensions, SemanticModel};
use rome_js_syntax::{
binding_ext::AnyJsIdentifierBinding, AnyJsExpression, JsArrayBindingPattern,
JsArrayBindingPatternElementList, JsCallExpression, JsVariableDeclarator, TextRange,
JsArrayBindingPatternElementList, JsArrowFunctionExpression, JsCallExpression,
JsFunctionExpression, JsVariableDeclarator, TextRange,
};
use rome_rowan::AstNode;
use serde::{Deserialize, Serialize};
Expand All @@ -15,15 +17,45 @@ pub(crate) struct ReactCallWithDependencyResult {
pub(crate) dependencies_node: Option<AnyJsExpression>,
}

pub enum AnyJsFunctionExpression {
JsArrowFunctionExpression(JsArrowFunctionExpression),
JsFunctionExpression(JsFunctionExpression),
}

impl AnyJsFunctionExpression {
fn closure(&self, model: &SemanticModel) -> Closure {
match self {
Self::JsArrowFunctionExpression(arrow_function) => arrow_function.closure(model),
Self::JsFunctionExpression(function) => function.closure(model),
}
}
}

impl TryFrom<AnyJsExpression> for AnyJsFunctionExpression {
type Error = ();

fn try_from(expression: AnyJsExpression) -> Result<Self, Self::Error> {
match expression {
AnyJsExpression::JsArrowFunctionExpression(arrow_function) => {
Ok(Self::JsArrowFunctionExpression(arrow_function))
}
AnyJsExpression::JsFunctionExpression(function) => {
Ok(Self::JsFunctionExpression(function))
}
_ => Err(()),
}
}
}

impl ReactCallWithDependencyResult {
/// Returns all [Reference] captured by the closure argument of the React hook.
/// See [react_hook_with_dependency].
pub fn all_captures(&self, model: &SemanticModel) -> impl Iterator<Item = Capture> {
self.closure_node
.as_ref()
.and_then(|node| node.as_js_arrow_function_expression())
.map(|arrow_function| {
let closure = arrow_function.closure(model);
.and_then(|node| AnyJsFunctionExpression::try_from(node.clone()).ok())
.map(|function_expression| {
let closure = function_expression.closure(model);
let range = *closure.closure_range();
closure
.descendents()
Expand Down Expand Up @@ -78,6 +110,15 @@ pub(crate) fn react_hook_configuration<'a>(
hooks.get(name)
}

const HOOKS_WITH_DEPS_API: [&str; 6] = [
"useEffect",
"useLayoutEffect",
"useInsertionEffect",
"useCallback",
"useMemo",
"useImperativeHandle",
];

/// Returns the [TextRange] of the hook name; the node of the
/// expression of the argument that correspond to the closure of
/// the hook; and the node of the dependency list of the hook.
Expand All @@ -95,18 +136,28 @@ pub(crate) fn react_hook_configuration<'a>(
pub(crate) fn react_hook_with_dependency(
call: &JsCallExpression,
hooks: &HashMap<String, ReactHookConfiguration>,
model: &SemanticModel,
) -> Option<ReactCallWithDependencyResult> {
let name = call
.callee()
.ok()?
.as_js_identifier_expression()?
.name()
.ok()?
.value_token()
.ok()?;
let expression = call.callee().ok()?;
let name = match &expression {
AnyJsExpression::JsIdentifierExpression(identifier) => {
Some(identifier.name().ok()?.value_token().ok()?)
}
AnyJsExpression::JsStaticMemberExpression(member) => {
Some(member.member().ok()?.as_js_name()?.value_token().ok()?)
}
_ => None,
}?;
let function_name_range = name.text_trimmed_range();
let name = name.text_trimmed();

// check if the hooks api is imported from the react library
if HOOKS_WITH_DEPS_API.contains(&name)
&& !is_react_call_api(expression, model, ReactLibrary::React, name)
{
return None;
}

let hook = hooks.get(name)?;
let closure_index = hook.closure_index?;
let dependencies_index = hook.dependencies_index?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ declare_rule! {
/// ### Invalid
///
/// ```js,expect_diagnostic
/// import { useEffect } from "react";
///
/// function component() {
/// let a = 1;
/// useEffect(() => {
Expand All @@ -36,6 +38,8 @@ declare_rule! {
/// ```
///
/// ```js,expect_diagnostic
/// import { useEffect } from "react";
///
/// function component() {
/// let b = 1;
/// useEffect(() => {
Expand All @@ -44,6 +48,8 @@ declare_rule! {
/// ```
///
/// ```js,expect_diagnostic
/// import { useEffect, useState } from "react";
///
/// function component() {
/// const [name, setName] = useState();
/// useEffect(() => {
Expand All @@ -54,6 +60,8 @@ declare_rule! {
/// ```
///
/// ```js,expect_diagnostic
/// import { useEffect } from "react";
///
/// function component() {
/// let a = 1;
/// const b = a + 1;
Expand All @@ -66,6 +74,8 @@ declare_rule! {
/// ## Valid
///
/// ```js
/// import { useEffect } from "react";
///
/// function component() {
/// let a = 1;
/// useEffect(() => {
Expand All @@ -75,6 +85,8 @@ declare_rule! {
/// ```
///
/// ```js
/// import { useEffect } from "react";
///
/// function component() {
/// const a = 1;
/// useEffect(() => {
Expand All @@ -84,6 +96,8 @@ declare_rule! {
/// ```
///
/// ```js
/// import { useEffect } from "react";
///
/// function component() {
/// const [name, setName] = useState();
/// useEffect(() => {
Expand Down Expand Up @@ -489,6 +503,7 @@ fn function_of_hook_call(call: &JsCallExpression) -> Option<JsSyntaxNode> {
matches!(
x.kind(),
JsSyntaxKind::JS_FUNCTION_DECLARATION
| JsSyntaxKind::JS_FUNCTION_EXPORT_DEFAULT_DECLARATION
| JsSyntaxKind::JS_FUNCTION_EXPRESSION
| JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION
)
Expand All @@ -508,9 +523,9 @@ impl Rule for UseExhaustiveDependencies {
let mut signals = vec![];

let call = ctx.query();
if let Some(result) = react_hook_with_dependency(call, &options.hooks_config) {
let model = ctx.model();
let model = ctx.model();

if let Some(result) = react_hook_with_dependency(call, &options.hooks_config, model) {
let Some(component_function) = function_of_hook_call(call) else {
return vec![]
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function MyComponent1() {
let a = 1;
React.useEffect(() => {
console.log(a);
});

// the rule doesn't show the warnings because the hooks are not imported from react.
useEffect(() => {
console.log(a);
});
}

function MyComponent2() {
let a = 1;
const React = { useEffect() {} }
// the rule doesn't show the warnings because `React` is defined by the user.
React.useEffect(() => {
console.log(a);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
source: crates/rome_js_analyze/tests/spec_tests.rs
expression: checkHooksImportedFromReact.js
---
# Input
```js
function MyComponent1() {
let a = 1;
React.useEffect(() => {
console.log(a);
});

// the rule doesn't show the warnings because the hooks are not imported from react.
useEffect(() => {
console.log(a);
});
}

function MyComponent2() {
let a = 1;
const React = { useEffect() {} }
// the rule doesn't show the warnings because `React` is defined by the user.
React.useEffect(() => {
console.log(a);
});
}

```

# Diagnostics
```
checkHooksImportedFromReact.js:3:9 lint/nursery/useExhaustiveDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━
! This hook do not specify all of its dependencies.
1 │ function MyComponent1() {
2 │ let a = 1;
> 3 │ React.useEffect(() => {
│ ^^^^^^^^^
4 │ console.log(a);
5 │ });
i This dependency is not specified in the hook dependency list.
2 │ let a = 1;
3 │ React.useEffect(() => {
> 4 │ console.log(a);
│ ^
5 │ });
6 │
```


Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect } from "react";

function MyComponent() {
let a = 1;
useEffect(() => {
Expand All @@ -6,4 +8,4 @@ function MyComponent() {
useMyEffect(() => {
console.log(a);
});
}
}
Loading

0 comments on commit 3d61b99

Please sign in to comment.