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

fix(eslint-plugin): [unbound-method] report on destructuring in function parameters #8952

Merged
merged 8 commits into from
Jul 29, 2024

Conversation

auvred
Copy link
Member

@auvred auvred commented Apr 19, 2024

PR Checklist

Overview

Old approach: query for VariableDeclarator, AssignmentExpression and check if they have ObjectPattern

New approach: query for ObjectPattern, check if VariableDeclarator, AssignmentExpression, AssignmentPattern its parent


We're iterating over ObjectPattern's properties:

  1. If ObjectPatterns's parent has init node, check if it's safe to destructure this init node:
    • if it's unsafe, then report on this property and go to the next property
    • if it's safe, then check if ObjectPatterns's parent is AssignmentPattern:
      • if false, go to the next property
      • if true, continue working on this property
  2. If ObjectPattern has a type annotation, then check if it's safe to destructure this type
  3. If there is no type annotation, check the inferred property type

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @auvred!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

Copy link

netlify bot commented Apr 19, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit e7e3603
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/6697a209efc42f000886b854
😎 Deploy Preview https://deploy-preview-8952--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 99 (🟢 up 5 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 90 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

nx-cloud bot commented Apr 19, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit e7e3603. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 1 target

Sent with 💌 from NxCloud.

Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

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

I really like these changes! The algorithm change makes sense to me, the code is easy to follow, and the new code for type inspection is very thorough.

Added a few questions and some test cases to flesh out edge cases.

packages/eslint-plugin/src/rules/unbound-method.ts Outdated Show resolved Hide resolved
packages/eslint-plugin/src/rules/unbound-method.ts Outdated Show resolved Hide resolved
packages/eslint-plugin/src/rules/unbound-method.ts Outdated Show resolved Hide resolved
packages/eslint-plugin/src/rules/unbound-method.ts Outdated Show resolved Hide resolved
@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Apr 20, 2024
nativelyBoundMembers.has(getMemberFullName(node)) &&
isNotImported(objectSymbol, currentSourceFile)
notImported &&
nativelyBoundMembers.has(`${object.name}.${property.name}`)
Copy link
Member Author

Choose a reason for hiding this comment

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

const { log } = console;

In our current configuration, the console.log declarations are under @types/node. So if we remove this check, the rule will report on this case. Not sure what's the best solution there..

Copy link
Member

Choose a reason for hiding this comment

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

Are you saying, you'd like to get rid of the first half of this function and only have the part after the return statement, but that that would cause bugs to do so? I'm not 100% following.

Copy link
Member Author

Choose a reason for hiding this comment

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

you'd like to get rid of the first half of this function and only have the part after the return statement, but that that would cause bugs to do so?

Yup, exactly!

I'd prefer to use only type checking here for obvious reasons, as it is more flexible and robust than just checking identifier names.

But this type checking-based approach works by checking whether a method is defined in the default library. This can be a problem for us, especially in the case of console methods.

Imagine the following situation:

// tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "types": ["node"]
  }
}
// index.ts
const { log } = console

The log method of the Console interface is defined in the node_modules/@types/node/console.d.ts, not in the default TypeScript library. Therefore, the rule will false-positively report on this destructuring because it knows that this method is defined outside the default lib.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I'm not sure I have enough knowledge to know what to do here. Maybe someone from @typescript-eslint/triage-team will have a suggestion?

Copy link
Member

Choose a reason for hiding this comment

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

Just to make sure I understand: we're saying that:

  1. console's log is a known exception defined in lib.dom.d.ts
  2. A log override is defined in @types/node'
  3. We'd like to still allow the exception for console's log, even though type info thinks it's a @types/node thing

...is that right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that makes sense. So, I guess the plan would be as follows:

  • Make exceptions only for known methods from the default TS library.
  • Remove purely syntactic checks so that we only rely on type-level checks
  • For now, treat types coming from @types/* as invalid and let the rule report them
  • Report invalid types on the DT side

Am i right?


I wonder if this would be too breaking a change for people, though. We'll make them update @types/node to get rid of lint errors in Node.js projects.

Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg Jul 1, 2024

Choose a reason for hiding this comment

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

I would say that Report invalid types on the DT side should be done ASAP, so folks who keep up-to-date-ish dev dependencies are less likely to be impacted. And maybe delaying merging the change on our end till a couple of months after that? Would that be overkill?

Copy link
Member

@kirkwaiblinger kirkwaiblinger Jul 7, 2024

Choose a reason for hiding this comment

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

My opinion - keep the first check so we can merge without regression (it may not be "ideal" but it already exists). File DT issue, and put a link to it in code with an explanation that the first check is necessary due that issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, I just realized that Console is an interface, and we can't rely on function signatures declared in interfaces, because classes implementing those interfaces can still access this even if this: void is declared in an interface:

interface Aaa {
  unbound: (this: void) => void
}

class Foo implements Aaa {
  data = 'string'
  unbound() {
    console.log(this.data) // runtime error!
  }
}

// not reported by unbound-method rule
const { unbound } = {} as Aaa
unbound()

playground

This is a bug in a rule. See #3503. The rule should rely on function signatures only when dealing with classes, not with interfaces.

So even if we ask DT to change the types on their side, we have to make exceptions for those types because they do not guarantee runtime safety.


So I think now we have two options from #8952 (comment):

  1. We can use the current method: pure AST node name comparison - the log method of console variable was called

  2. We can try to do some heuristics based on the .d.ts file path: for example, if it includes node_modules or something like that

Personally, I'm in favor of the second option, because it covers more cases and does so more reliably. On the other hand, pure AST checks have been around for a long time and no one has complained about them yet.

What do you think of that?

Copy link
Member

Choose a reason for hiding this comment

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

Similarly to my opinion in #8952 (comment), I think that Option 2 here is just out of scope for this issue, granted that existing checks are based on Option 1 -like behavior. So my preference would be to go with Option 1 and discuss Option 2 in a followup.

@auvred auvred requested a review from kirkwaiblinger May 3, 2024 18:43
@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label May 3, 2024
Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

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

A couple questions/commenting requests, but no significant concrete changes being requested at this time.

isBuiltinSymbolLike(
services.program,
services.getTypeAtLocation(object),
[
Copy link
Member

Choose a reason for hiding this comment

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

What's the difference between this array literal and the SUPPORTED_GLOBALS array? Do we need both? Perhaps giving it a good name will make it clear?

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we need both?

I really don't know 🙁

For more reasoning on this, see #8952 (comment)

Perhaps giving it a good name will make it clear?

+1 👍 Will rename it

}

const objectSymbol = services.getSymbolAtLocation(node.object);
function isNativelyBound(
Copy link
Member

Choose a reason for hiding this comment

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

It's not super obvious how this function works. I cloned it down to play around with the tests, and I'm still not 100% sure, but my rough understanding is that the first part checks by identity whether something is a natively bound member, and the second part checks via the type system, in order to catch a more general set of cases where the builtin namespace objects are potentially renamed.

Could you add some comments to help the reader 🙏 ?

nativelyBoundMembers.has(getMemberFullName(node)) &&
isNotImported(objectSymbol, currentSourceFile)
notImported &&
nativelyBoundMembers.has(`${object.name}.${property.name}`)
Copy link
Member

Choose a reason for hiding this comment

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

Are you saying, you'd like to get rid of the first half of this function and only have the part after the return statement, but that that would cause bugs to do so? I'm not 100% following.

packages/eslint-plugin/src/rules/unbound-method.ts Outdated Show resolved Hide resolved
@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label May 9, 2024
Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
Copy link

codecov bot commented May 12, 2024

Codecov Report

Attention: Patch coverage is 93.61702% with 3 lines in your changes missing coverage. Please review.

Project coverage is 88.39%. Comparing base (90bacee) to head (e7e3603).
Report is 27 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8952   +/-   ##
=======================================
  Coverage   88.38%   88.39%           
=======================================
  Files         419      419           
  Lines       14620    14644   +24     
  Branches     4279     4288    +9     
=======================================
+ Hits        12922    12944   +22     
- Misses       1374     1376    +2     
  Partials      324      324           
Flag Coverage Δ
unittest 88.39% <93.61%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
packages/eslint-plugin/src/rules/unbound-method.ts 96.26% <100.00%> (+0.96%) ⬆️
packages/type-utils/src/builtinSymbolLikes.ts 3.63% <0.00%> (-0.14%) ⬇️

@bradzacher bradzacher added the bug Something isn't working label May 28, 2024
@kirkwaiblinger kirkwaiblinger removed the awaiting response Issues waiting for a reply from the OP or another party label Jun 15, 2024
@auvred auvred requested a review from kirkwaiblinger July 17, 2024 10:52
Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

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

I think we're there! Nice work!
engage

@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label Jul 26, 2024
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

So thorough!! 🙌

@JoshuaKGoldberg JoshuaKGoldberg merged commit 64b4e43 into typescript-eslint:main Jul 29, 2024
66 checks passed
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: [unbound-method] Does not fail for destructured parameters
4 participants