Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: react-hooks/exhaustive-deps #2637

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

alisnic
Copy link

@alisnic alisnic commented Mar 7, 2024

I gotta be honest with you - I don't know Rust much and this may be too much for me. But I decided at least I can move things forwarding by creating the scaffolding and porting the tests

Related to #2174

@github-actions github-actions bot added the A-linter Area - Linter label Mar 7, 2024
@alisnic alisnic marked this pull request as draft March 7, 2024 08:42
Copy link

codspeed-hq bot commented Mar 7, 2024

CodSpeed Performance Report

Merging #2637 will not alter performance

Comparing alisnic:feat/exhaustive-deps (bdc8a38) with main (8e3e404)

Summary

✅ 29 untouched benchmarks

@alisnic
Copy link
Author

alisnic commented Apr 5, 2024

@Boshen pushed my latest code. Currently stuck by 2 things:

  1. How do I check that the identifier id is defined within the component scope? So I don't generate false positives
function Counter() {
  let [count, setCount] = useState(0);

  function increment(x) {
    return x + 1;
  }

  useEffect(() => {
    let id = setInterval(() => {
      setCount(increment);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

  1. Given a AstNode, how to check that is it used inside of an expression that is assigned to itself?
function Example() {
  const foo = useCallback(() => {
    foo();
  }, []);
}

@alisnic
Copy link
Author

alisnic commented May 9, 2024

I think I need a completely different approach here. Instead I should iterate over all identifier references and traverse the tree upwards to detect whether I'm in a hook or not. This should remove most of traversal boilerplate I wrote

@Boshen
Copy link
Member

Boshen commented May 10, 2024

This rule is pretty complicated, let's see whether @rzvxa can help. Feel free to ask questions.

@rzvxa
Copy link
Collaborator

rzvxa commented May 11, 2024

I think I need a completely different approach here. Instead I should iterate over all identifier references and traverse the tree upwards to detect whether I'm in a hook or not. This should remove most of traversal boilerplate I wrote

Hey mate, First off thanks for this impressive work.

I was reading through the code when I got an idea that might make it easier to implement, Have you tried to create a custom visitor instead of using the check_statements function?

If you add something like this:

#[derive(Default)]
struct DependencyCollector<'a> {
    deps: DependencyList<'a>,
}

impl<'a> Visit<'a> for DependencyCollector<'a> {
    fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
        self.deps.insert(/* dependency */);
    }
    /* .... */
}

and use it like this:

DependencyCollector::default().visit_statements(&body_expr.statements);

instead of calling the check_statement, It can abstract away most of the walk functions you've used and as a result make it less cluttered and easier to read/write.

I think it can result in what you want from visiting every identifier, I'm not sure which one is more performant though, Since one skips redundant checks but requires additional visits and the other one has a lot of redundant checks and no additional visit.

@rzvxa
Copy link
Collaborator

rzvxa commented May 11, 2024

  1. How do I check that the identifier id is defined within the component scope? So I don't generate false positives
function Counter() {
  let [count, setCount] = useState(0);

  function increment(x) {
    return x + 1;
  }

  useEffect(() => {
    let id = setInterval(() => {
      setCount(increment);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

Given the scope_id of the id and the scope id for Counter, Can't we just check recursively whether this scope id has a direct ancestor which is the Counter?

But we have to check ScopeFlags and look for any shadowed variable in our path.

Maybe it can be easier if you go for checking the SymbolId instead of ScopeIds.

@alisnic
Copy link
Author

alisnic commented May 16, 2024

I was reading through the code when I got an idea that might make it easier to implement, Have you tried to create a custom visitor instead of using the check_statements function?

How would this approach be different from:

    ctx.semantic().nodes().iter().for_each(|node| {
            let AstKind::IdentifierReference(iref) = node.kind() else { return };
            // do stuff
        })

Will the visitor visit nodes that the iter above would not?

@rzvxa
Copy link
Collaborator

rzvxa commented May 16, 2024

I was reading through the code when I got an idea that might make it easier to implement, Have you tried to create a custom visitor instead of using the check_statements function?

How would this approach be different from:

    ctx.semantic().nodes().iter().for_each(|node| {
            let AstKind::IdentifierReference(iref) = node.kind() else { return };
            // do stuff
        })

Will the visitor visit nodes that the iter above would not?

I know there are some types missing from AstKind but to be honest I'm not sure if any of the vital ones are missing.

If there is nothing important missing in the AstKind then yeah your snippet above would basically do the same thing. But here is the thing, You didn't use the nodes iterator, Instead you rely on this recursive function which doesn't support all types containing an IdentifierReference - can't recall the exact type but I remember noticing one missing in my initial review - We are most probably going to use codegen for our visitors/AST since it is growing in complexity so using them would reduce the risk of human error.

Just to point out, We recently found out some types are missing a visit event in our Visit trait, So you can see even core stuff can be wrong when written by humans(since the visitor pattern is tedious and has a lot of boilerplate code).

But feel free to use either one of these approaches since they should perform the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-linter Area - Linter
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants