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

[ESLint] Handle optional member chains #19275

Merged
merged 3 commits into from
Jul 7, 2020
Merged

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Jul 7, 2020

Follow-up to #19273.

Our algorithm treats a.b.c.d as unique paths in a many places and those parts should work the same way regardless of whether these are optional or required members. So in #19273, I removed ?. from the internal representation.

However, ideally we still want to print ?. where needed if all usages of that member in the user code used optional chaining. For example, if we use pizza?.crust and pizza?.crust.density, then we should suggest pizza?.crust in deps, but if one of them didn’t have ?, then we should suggest pizza.crust instead. Because then, at least one of them is required anyway. None of this should have any effects on the dependency logic — both variations, if typed by user, should satisfy the constraint.

The implementation is split in two stages.

It intentionally doesn’t interfere with the main algorithm that checks the dependencies. Instead, when we initially generate member paths, we also fill a Map. In that Map, we mark for each deep path whether it was a required or an optional one. By the time we have collected all dependencies, the Map will contain a mapping from the key path to a Boolean: true if all usages were optional, and false if some of them were required.

Finally, when we're ready to print our suggestions and/or fix the code, we will use that Map to pretty-print our paths. We will consult it to see if each segment should be concatenated using ?. or .. If at least one usage was required, or if that path was not found in the code, then we will default to . as a default path separator. So if you don't use ?. you won’t see it.

This adds a few tests and also fixes a bunch of existing tests to use better output.

This is split in two commits. The first one is just a bunch of renames because "optional" clashed with another concept we previously called the same way. Review commits individually.

This disambiguates "optional"/"required" because that terminology is taken by optional chaining.
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jul 7, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 916b82b:

Sandbox Source
React Configuration

desc:
'Update the dependencies array to be: [pizza.crust, pizza.toppings]',
'Update the dependencies array to be: [pizza.crust, pizza?.toppings]',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note: technically this could be further improved to not add ? at all because another dep didn't use it. However, I don't think it matters because at least this doesn't cause a crash. Additionally, this points your attention to unnecessary ? inside the function — since presumably if data is immutable, you shouldn't need it anyway.

* foo.(bar) -> 'foo.bar'
* foo.bar.(baz) -> 'foo.bar.baz'
* foo(.)bar -> 'foo.bar'
* foo.bar(.)baz -> 'foo.bar.baz'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This comment didn't make sense so I fixed it.

@sizebot
Copy link

sizebot commented Jul 7, 2020

Details of bundled changes.

Comparing: 7ca1d86...916b82b

eslint-plugin-react-hooks

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
eslint-plugin-react-hooks.development.js +2.1% +2.3% 82.48 KB 84.25 KB 18.9 KB 19.33 KB NODE_DEV
eslint-plugin-react-hooks.production.min.js 🔺+1.4% 🔺+1.5% 22.95 KB 23.26 KB 7.75 KB 7.87 KB NODE_PROD

Size changes (stable)

Generated by 🚫 dangerJS against 916b82b

@ljharb
Copy link
Contributor

ljharb commented Jul 7, 2020

Because then, at least one of them is required anyway.

That's not how optional chaining works, if I'm reading the OP right. a?.b.c will short-circuit, and not evaluate .c, if a is nullish (and will throw if a is non-nullish and a.b is nullish)

@sizebot
Copy link

sizebot commented Jul 7, 2020

Details of bundled changes.

Comparing: 7ca1d86...916b82b

eslint-plugin-react-hooks

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
eslint-plugin-react-hooks.development.js +2.1% +2.3% 82.5 KB 84.27 KB 18.9 KB 19.34 KB NODE_DEV
eslint-plugin-react-hooks.production.min.js 🔺+1.4% 🔺+1.5% 22.96 KB 23.27 KB 7.76 KB 7.88 KB NODE_PROD

Size changes (experimental)

Generated by 🚫 dangerJS against 916b82b

@gaearon
Copy link
Collaborator Author

gaearon commented Jul 7, 2020

That's not how optional chaining works, if I'm reading the OP right. a?.b.c will short-circuit, and not evaluate .c, if a is nullish (and will throw if a is non-nullish and a.b is nullish)

Hmm. Maybe I wasn't clear or maybe I'm missing something.

I'm referring to this scenario:

useEffect(() => {
  console.log(a?.b.x)
  console.log(a?.b.x)
})

If we were to auto-fill dependencies, we'd want to suggest a?.b.x. Because suggesting a.b.x would lead to a crash.

However, with this scenario:

useEffect(() => {
  console.log(a?.b.x)
  console.log(a.b.x)
})

In this case, suggesting a?.b.x as a dep may be a bit misleading because we do use a.b.x inside, too. So it is better to call user’s attention to that by suggesting the "stricter" a.b.x as a dep. They can still edit it if they want to.


Similarly, if the user code has a?.b and a.b.x, suggesting a.b as their "union" is safer because a.b.x requires it. But if the user code has a?.b.x and a?.b then it's okay to suggest a?.b as their "union".

Copy link
Member

@rickhanlonii rickhanlonii left a comment

Choose a reason for hiding this comment

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

Looks great

errors: [
{
message:
"React Hook useEffect has a missing dependency: 'pizza.crust'. " +
Copy link
Member

Choose a reason for hiding this comment

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

We may want this to explain why only one is included with something like:

React Hook useEffect has a missing dependencies: 'pizza?.crust' and 'pizza.crust' (which can be reduced to a dependency on 'pizza.crust'). Either include it or remove the dependency array.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Meh. I don't think this is useful enough to clutter the message.

packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js Outdated Show resolved Hide resolved
@ljharb
Copy link
Contributor

ljharb commented Jul 7, 2020

@gaearon as long as there's no getters in the chain that might change the nullishness of properties on later evaluation, and no assignments or mutations in between the two statements, you're right :-)

@gaearon
Copy link
Collaborator Author

gaearon commented Jul 7, 2020

Yeah. This rule in general assumes that render-scope objects you read inside Hook callbacks are immutable POJOs or treated as such. Otherwise dependencies don't work as a concept in general.

Co-authored-by: Ricky <rickhanlonii@gmail.com>
@gaearon gaearon merged commit 7c35cb2 into facebook:master Jul 7, 2020
@gaearon gaearon deleted the optt branch July 7, 2020 20:44
trueadm pushed a commit to trueadm/react that referenced this pull request Jul 8, 2020
* Rename internal variables

This disambiguates "optional"/"required" because that terminology is taken by optional chaining.

* Handle optional member chains

* Update comment

Co-authored-by: Ricky <rickhanlonii@gmail.com>

Co-authored-by: Ricky <rickhanlonii@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants