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

fix(rome_js_analyze): false negatives of noArrayIndexKey #3681

Merged
merged 7 commits into from
Nov 18, 2022
Merged

fix(rome_js_analyze): false negatives of noArrayIndexKey #3681

merged 7 commits into from
Nov 18, 2022

Conversation

lucasweng
Copy link
Contributor

@lucasweng lucasweng commented Nov 11, 2022

Summary

  • Improves the detection of the following cases
    // Return statement
    return array.map((_, index) => <Component key={index} />);
    
    // Assignment expression
    let element = null
    if (condition) {
        elements = array.map((_, index) => <Component key={index} />);
    }
    
    // Parenthesized expression
    function App({things}) {
        const elements = useMemo(() => (
            things.map((_, index) => <Component key={index} />)
        ), [things]);
        return elements;
     }
    function App() {
        return (
            <HoC>
                {({things}) => (
                    things.map((_, index) => <Component key={index} />)
                )}
            </HoC>
        )
    }
    
    // Call expression
    const Component = () => array.map((_, index) => <Component key={index} />);
    
    // Variable declaration
    let elements = array.map((_, index) => <Component key={index} />);
    
    // `flatMap` or `Array.from` method call
    array.flatMap((_, index) => {
        return <Component key={index} />;
    })
    Array.from(sets, (_, index) => {
        return <Component key={index} />;
    })

Test Plan

Added new test cases

@netlify
Copy link

netlify bot commented Nov 11, 2022

Deploy Preview for docs-rometools failed.

Name Link
🔨 Latest commit
🔍 Latest deploy log https://app.netlify.com/sites/docs-rometools/deploys/637781eff15ce21882c2c3d7

@lucasweng lucasweng marked this pull request as ready for review November 12, 2022 01:03
Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your PR. I think there are some more issues with the rule (unrelated to your changes) and it may be possible to simplify the finding of the closest call expression.

@lucasweng lucasweng requested a review from ematipico as a code owner November 16, 2022 13:46
@@ -7,8 +7,8 @@ use rome_js_semantic::SemanticModel;
use rome_js_syntax::{
JsAnyCallArgument, JsArrowFunctionExpression, JsCallExpression, JsExpressionStatement,
JsFunctionDeclaration, JsFunctionExpression, JsIdentifierBinding, JsMethodClassMember,
JsMethodObjectMember, JsParameterList, JsPropertyObjectMember, JsReferenceIdentifier,
JsxAttribute, JsxOpeningElement, JsxSelfClosingElement,
JsMethodObjectMember, JsParameterList, JsParenthesizedExpression, JsPropertyObjectMember,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry for commenting here but GitHub doesn't allow me to comment unchanged lines.

We should use the semantic model to resolve where the expression assigned to key is declared. This replaces the find_function_parent call on line 135

        let Some(parameter) = model
            .declaration(&reference)
            .and_then(|declaration| declaration.syntax().parent())
            .and_then(JsFormalParameter::cast);

        let Some(function) = parameter
            .parent::<JsParameterList>()
            .and_then(|list| list.parent::<JsParameters>())
            .and_then(|parameters| parameters.parent::<JsAnyFunction>()) else { return None; };

        let Some(call_expression) = parameter
            .parent::<JsCallArgumentList>()
            .and_then(|arguments| arguments.parent::<JsCallArguments>())
            .and_then(|arguments| arguments.parent::<JsCallExpression>()) else { return None };

Copy link
Contributor Author

@lucasweng lucasweng Nov 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries and thanks @MichaReiser!

I was trying to refactor the rule by matching on JsxAttribute and JsPropertyObjectMember following suggestions in #3681 (comment) and #3681 (comment).

  1. Use the semantic model to find the declaration of index that is inside a JsArrowFunctionExpression or JsFunctionExpression
  2. From the function expression above, find the closest call expression that belongs to an array method
  3. Then traverse down to check if the declaration is the second argument of the function call to avoid false positive like the following
things.map((id) => {
    return <Jsx key={id} />
})
  1. For JsPropertyObjectMember we need to navigate down further to check if the JsObjectExpression is the second argument of a React.cloneElement call
React.Children.forEach(this.props.children, (_, index) => {
  const obj = {key: index}
  // we don't know if the key above is used as `JsxAttribute`
  return notCloneElement(obj)
})

But I'm not sure if there is a better way to address the false positives without navigating up and then down like the above steps 🤔
Or should we address the more common use cases first without handling edge cases that are less likely to appear in userland?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then traverse down to check if the declaration is the second argument of the function call to avoid false positive like the following

You can probably use parameter.syntax().index(). It gives you the index in the enclosing parameter list (you have to divide it by two because the list elements are parameter, comma, parameter2`.

React.Children.forEach(this.props.children, (_, index) => {
const obj = {key: index}
// we don't know if the key above is used as JsxAttribute
return notCloneElement(obj)
})

I'm not sure if I understand the question/problem here. I believe it should be sufficient to only detect calls to cloneElement where the object is the argument cloneElement { key: index }.

The way I would expect cloneElement to work if you match on object member is that:

  • You test if the object member name is key
  • You navigate upwards from the object member until you find the enclosing call expression
  • You test if it is a react API call
  • If that's the case, do the same as for JSX elements by resolving the binding for index
    • then finding the enclosing call expression
    • test if the call expression is an array method.

Copy link
Contributor Author

@lucasweng lucasweng Nov 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bc7a03c now the rule is matching on JsxAttribute and JsPropertyObjectMember, and it will

  1. Find if it's a key property
  2. Retrieve the reference identifier from the key property,
  3. Use the semantic model to find the declaration of the reference is a parameter in a function expression
  4. Find the enclosing call expression
  5. Check if the call expression is an array method and the parameter is the array index of that method
  6. If it's matching on JsPropertyObjectMember, detect calls to cloneElement

Also, added tests for flatMap and Array.from

How do we test if the current implementation is faster or slower than the previous one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I can remove ReactCloneElementCall::from_call_expression, it's no longer used because of this refactor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReactCloneElementCall

It's safe to remove the method as it is no longer used.

@MichaReiser
Copy link
Contributor

!bench_analyzer

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! Nice to see how you managed to shorten the implementation by 100 lines of code!

@github-actions
Copy link

Analyzer Benchmark Results

group                     main                                   pr
-----                     ----                                   --
analyzer/css.js           1.03      2.8±0.06ms     4.1 MB/sec    1.00      2.7±0.13ms     4.3 MB/sec
analyzer/index.js         1.06      8.1±0.33ms     4.0 MB/sec    1.00      7.7±0.33ms     4.3 MB/sec
analyzer/lint.ts          1.00      3.8±0.09ms    10.9 MB/sec    1.01      3.8±0.07ms    10.8 MB/sec
analyzer/parser.ts        1.05      9.4±0.25ms     5.2 MB/sec    1.00      8.9±0.22ms     5.5 MB/sec
analyzer/router.ts        1.01      6.3±0.22ms    10.0 MB/sec    1.00      6.2±0.18ms    10.1 MB/sec
analyzer/statement.ts     1.04      9.1±0.28ms     3.9 MB/sec    1.00      8.8±0.21ms     4.0 MB/sec
analyzer/typescript.ts    1.03     13.8±0.41ms     3.9 MB/sec    1.00     13.5±0.49ms     4.0 MB/sec

@MichaReiser
Copy link
Contributor

Analyzer Benchmark Results

group                     main                                   pr
-----                     ----                                   --
analyzer/css.js           1.03      2.8±0.06ms     4.1 MB/sec    1.00      2.7±0.13ms     4.3 MB/sec
analyzer/index.js         1.06      8.1±0.33ms     4.0 MB/sec    1.00      7.7±0.33ms     4.3 MB/sec
analyzer/lint.ts          1.00      3.8±0.09ms    10.9 MB/sec    1.01      3.8±0.07ms    10.8 MB/sec
analyzer/parser.ts        1.05      9.4±0.25ms     5.2 MB/sec    1.00      8.9±0.22ms     5.5 MB/sec
analyzer/router.ts        1.01      6.3±0.22ms    10.0 MB/sec    1.00      6.2±0.18ms    10.1 MB/sec
analyzer/statement.ts     1.04      9.1±0.28ms     3.9 MB/sec    1.00      8.8±0.21ms     4.0 MB/sec
analyzer/typescript.ts    1.03     13.8±0.41ms     3.9 MB/sec    1.00     13.5±0.49ms     4.0 MB/sec

Nice!

@MichaReiser
Copy link
Contributor

And there are now errors that Rome detects in the playground because of your fixes :D Awesome job

@MichaReiser MichaReiser requested a review from a team November 18, 2022 12:53
@MichaReiser MichaReiser changed the title fix(rome_js_analyze): improve the detection of invalid cases in noArrayIndexKey fix(rome_js_analyze): false negatives of noArrayIndexKey Nov 18, 2022
@MichaReiser MichaReiser merged commit 861eb07 into rome:main Nov 18, 2022
@lucasweng lucasweng deleted the fix/no-array-index-key branch November 18, 2022 14:52
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

🐛 noArrayIndexKey ignores valid cases
2 participants