Skip to content

fix(eslint-plugin): [no-misused-promises] the inheritedMethods and properties options to check all statically analyzable declarations #10310

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

ronami
Copy link
Member

@ronami ronami commented Nov 8, 2024

PR Checklist

Overview

This PR resolves #10211 and uses the existing getStaticMemberAccessValue() utility instead of tsutils.getPropertyOfType() to handle most statically analyzable declarations/properties:

const staticSymbol = Symbol.for('static symbol');

interface Interface {
  identifier: () => void;
  1: () => void;
  2: () => void;
  stringLiteral: () => void;
  computedStringLiteral: () => void;
  // well known symbol
  [Symbol.iterator]: () => void;

  // less sure if this one is possible to lint for
  [staticSymbol]: () => void;
}


class Clazz implements Interface {
  async identifier() { }
  async 1() { }
  async [2]() { }
  async 'stringLiteral'() { }
  async ['computedStringLiteral']() { }
  async [Symbol.iterator]() { };
  async [staticSymbol]() { };
}

I added some PR review comments on parts of the PR I'm not sure are correct, please let me know what you think.

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @ronami!

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 Nov 8, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 6c48db9
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/6772ea964c1c98000821d3c8
😎 Deploy Preview https://deploy-preview-10310--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 8 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 98 (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 Nov 8, 2024

View your CI Pipeline Execution ↗ for commit 6c48db9.

Command Status Duration Result
nx test eslint-plugin --coverage=false ✅ Succeeded 7m 15s View ↗
nx test eslint-plugin ✅ Succeeded 5m 52s View ↗
nx test type-utils ✅ Succeeded <1s View ↗
nx test utils ✅ Succeeded <1s View ↗
nx run types:build ✅ Succeeded <1s View ↗
nx test visitor-keys ✅ Succeeded <1s View ↗
nx run-many --target=build --exclude website --... ✅ Succeeded 27s View ↗
nx test typescript-eslint ✅ Succeeded 4s View ↗
Additional runs (24) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2024-12-30 19:01:53 UTC

@ronami ronami changed the title fix(eslint-plugin): [no-misused-promises] inheritedMethods and properties to check all statically analyzable declarations fix(eslint-plugin): [no-misused-promises] the inheritedMethods and properties options to check all statically analyzable declarations Nov 8, 2024
Comment on lines 969 to 978
function getWellKnownStringOfSymbol(symbol: symbol): string | null {
switch (symbol) {
case Symbol.iterator:
return 'iterator';
case Symbol.asyncIterator:
return 'asyncIterator';
}

return null;
}
Copy link
Member Author

@ronami ronami Nov 8, 2024

Choose a reason for hiding this comment

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

I'm not sure if there's a better way to map a symbol to its string counterpart tsutils.getWellKnownSymbolPropertyOfType is expecting.

The list for well-known symbols isn't long, but this will still require maintaining this mapping.

Copy link
Member Author

Choose a reason for hiding this comment

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

If this approach makes sense, this list can be expanded to support other well-known symbols.

Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we need to stick to well-known symbols? I'd think we should go with any detected symbol, no?

Copy link
Member Author

@ronami ronami Nov 14, 2024

Choose a reason for hiding this comment

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

Hmm, I might be missing something, but I'm not entirely sure how to check if a JavaScript symbol exists on another ts.Type 🤔

Here's how existing code checks if an inherited type has a string symbol:

const escapedMemberName = ts.escapeLeadingUnderscores(staticAccessValue);
const symbolMemberMatch = type.getSymbol()?.members?.get(escapedMemberName);

return (
  symbolMemberMatch ?? tsutils.getPropertyOfType(type, escapedMemberName)
);

I've looked into the SymbolObject but couldn't find a way to check against an arbitrary symbol (not to be confused with ts.Symbol).

Because of this, I've created a function to map between known symbols and their string representation, which are passed to tsutils.getWellKnownSymbolPropertyOfType to check if they exist on the inherited type.

Edit: I don't think it's feasible to iterate the inherited parent's nodes, as a function may be higher up the inheritance chain:

const staticSymbol = Symbol.for('static symbol');

interface Bar {
  [staticSymbol]: () => void;
}

interface Foo extends Bar {}

class Clazz implements Foo {
  // should be reported
  async [staticSymbol]() {}
}

Copy link
Member

Choose a reason for hiding this comment

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

Hmm gotcha thanks. cc @kirkwaiblinger - I'm fine with this as a patchwork helper, with as little hardcoding as possible.

function getWellKnownStringOfSymbol(symbol: symbol): string | null {
  for (const [key, value] of Object.entries(Symbol)) {
    if (symbol === value) {
      return key;
    }
  }

  return null;
}

And, sorry for dropping the ball on this again 🤦 there's something about symbols that makes me instinctively forget the conversation...

Copy link
Member Author

Choose a reason for hiding this comment

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

I've updated the PR to not hard-code well-known symbols, thanks!

The only alternative I had in mind for not hard-coding was to extract the value from Symbol.prototype.toString() with a regex (which I really didn't want to do), but I didn't think of iterating the global Symbol's keys.

This means that arbitrary symbols (e.g. Symbol.for('foo')) won't be reported by the rule though.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I don't see a way to work for arbitrary symbols 😞

Copy link
Member

Choose a reason for hiding this comment

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

Copy link

codecov bot commented Nov 8, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 86.92%. Comparing base (3ae4148) to head (6c48db9).
Report is 71 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10310      +/-   ##
==========================================
+ Coverage   86.86%   86.92%   +0.06%     
==========================================
  Files         445      446       +1     
  Lines       15455    15514      +59     
  Branches     4507     4520      +13     
==========================================
+ Hits        13425    13486      +61     
+ Misses       1675     1673       -2     
  Partials      355      355              
Flag Coverage Δ
unittest 86.92% <100.00%> (+0.06%) ⬆️

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

Files with missing lines Coverage Δ
...ges/eslint-plugin/src/rules/no-misused-promises.ts 98.56% <100.00%> (+0.83%) ⬆️
packages/eslint-plugin/src/util/misc.ts 98.85% <ø> (ø)

... and 9 files with indirect coverage changes

@ronami ronami marked this pull request as ready for review November 9, 2024 13:59
Comment on lines 575 to 579
node.type !== AST_NODE_TYPES.MethodDefinition &&
node.type !== AST_NODE_TYPES.TSAbstractMethodDefinition &&
node.type !== AST_NODE_TYPES.TSMethodSignature &&
node.type !== AST_NODE_TYPES.TSPropertySignature &&
node.type !== AST_NODE_TYPES.PropertyDefinition
Copy link
Member Author

Choose a reason for hiding this comment

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

This is enough to pass the test suit but there are additional types available to satisfy getStaticMemberAccessValue.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is necessary mainly for type-checking, and I don't know if this can realistically have a runtime scenario. Please let me know if this should be handled differently (currently, there is no coverage for this).

Copy link
Member

Choose a reason for hiding this comment

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

I don't believe this can realistically be a runtime scenario. I think the real issue here is that although nodeMember is type ts.TypeElement | ts.ClassElement, node is type TSESTree.Node.

The two strategies I'd think could be good would be:

  • Simpler: use an as to correct TypeScript's understanding of node's type to be NodeWithKey (which will need to be exported from misc.ts)
  • More aligned with how most of our typed lint rules go: instead of iterating over the tsNode.members, iterate over the members of node

The latter is probably a lot more work and changes the existing code. So I think the as strategy would be reasonable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I've started with a type-assertion on my initial implementation but I wasn't sure if there's a realistic case where this would explode.

I updated the code to use a type assertion, as the second option seems to be a lot more work.

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.

I like where this is going!

Copy link
Member

Choose a reason for hiding this comment

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

[Testing] One thing under test per case please (https://typescript-eslint.io/contributing/pull-requests#granular-unit-tests)

Comment on lines 575 to 579
node.type !== AST_NODE_TYPES.MethodDefinition &&
node.type !== AST_NODE_TYPES.TSAbstractMethodDefinition &&
node.type !== AST_NODE_TYPES.TSMethodSignature &&
node.type !== AST_NODE_TYPES.TSPropertySignature &&
node.type !== AST_NODE_TYPES.PropertyDefinition
Copy link
Member

Choose a reason for hiding this comment

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

I don't believe this can realistically be a runtime scenario. I think the real issue here is that although nodeMember is type ts.TypeElement | ts.ClassElement, node is type TSESTree.Node.

The two strategies I'd think could be good would be:

  • Simpler: use an as to correct TypeScript's understanding of node's type to be NodeWithKey (which will need to be exported from misc.ts)
  • More aligned with how most of our typed lint rules go: instead of iterating over the tsNode.members, iterate over the members of node

The latter is probably a lot more work and changes the existing code. So I think the as strategy would be reasonable.

Comment on lines 969 to 978
function getWellKnownStringOfSymbol(symbol: symbol): string | null {
switch (symbol) {
case Symbol.iterator:
return 'iterator';
case Symbol.asyncIterator:
return 'asyncIterator';
}

return null;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we need to stick to well-known symbols? I'd think we should go with any detected symbol, no?

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Nov 10, 2024
@JoshuaKGoldberg
Copy link
Member

👋 Hey @ronami, did you want to re-request review on this?

No pressure, just asking because it's been a bit and I've seen other things from you in the meantime. 🙂

@ronami
Copy link
Member Author

ronami commented Dec 2, 2024

👋 Hey @ronami, did you want to re-request review on this?

No pressure, just asking because it's been a bit and I've seen other things from you in the meantime. 🙂

Hey @JoshuaKGoldberg, thanks for checking up on this ❤️

I've commented here on something I'm not sure how to implement (I might be missing something 🙈).

I didn't want to ask for a re-review since it's still an open discussion. This is the only remaining open issue before the PR can be re-reviewed.

@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Dec 26, 2024
context,
);

if (!staticAccessValue) {
Copy link
Member

Choose a reason for hiding this comment

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

(same as other comment)

Suggested change
if (!staticAccessValue) {
if (staticAccessValue == null) {

@@ -492,9 +492,14 @@ export default createRule<Options, MessageId>({
if (objType == null) {
return;
}
const propertySymbol = checker.getPropertyOfType(
const staticAccessValue = getStaticMemberAccessValue(node, context);
if (!staticAccessValue) {
Copy link
Member

Choose a reason for hiding this comment

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

(bug)

Suggested change
if (!staticAccessValue) {
if (staticAccessValue == null) {

because

class MyClass {
  ''(): void { }
}
class MySubclass extends MyClass {
  async ''(): Promise<void> { }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, nice catch! Is there any reason we don't have strict-boolean-expressions enabled?

Copy link
Member

Choose a reason for hiding this comment

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

Because IIRC both Joshs don't like it hehe. Brad and I both do like it. 🤷‍♂️

Comment on lines 969 to 978
function getWellKnownStringOfSymbol(symbol: symbol): string | null {
switch (symbol) {
case Symbol.iterator:
return 'iterator';
case Symbol.asyncIterator:
return 'asyncIterator';
}

return null;
}
Copy link
Member

Choose a reason for hiding this comment

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

@kirkwaiblinger
Copy link
Member

Hey @ronami, do you plan to make further changes regarding #10310 (comment) or is this intended to be reviewed as is?

@ronami
Copy link
Member Author

ronami commented Jan 19, 2025

Hey @ronami, do you plan to make further changes regarding #10310 (comment) or is this intended to be reviewed as is?

Oh, I'm sorry; I've looked into it but completely forgot to get back to it.


I didn't consider the issue with the current approach, which could result in different values on different machines and NodeJS versions (though looking into MDN, it seems pretty much all well-known symbols are supported on rather old Node versions already).

I haven't noticed heritageType.getProperties().map(sym => sym.links.nameType), which, while not part of the API, does seem to work in various edge cases, and types do seem === to each other, interesting 🙃.

Having a way to get the key type of a property Symbol would be useful. I'll be happy to check if this can be added to the public API or if there's a reasonable hack we can currently use. Should this be filed as a GitHub issue or discussed on the TypeScript Discord channel?

Thanks for your help with this ❤️

@kirkwaiblinger
Copy link
Member

I didn't consider the issue with the current approach, which could result in different values on different machines and NodeJS versions (though looking into MDN, it seems pretty much all well-known symbols are supported on rather old Node versions already).

Ok, that definitely alleviates some of the concern 👍 Though I guess there's maybe still a forwards-compatibility question.

I haven't noticed heritageType.getProperties().map(sym => sym.links.nameType), which, while not part of the API, does seem to work in various edge cases, and types do seem === to each other, interesting 🙃.

Having a way to get the key type of a property Symbol would be useful. I'll be happy to check if this can be added to the public API or if there's a reasonable hack we can currently use. Should this be filed as a GitHub issue or discussed on the TypeScript Discord channel?

What I usually do is just ask questions on the TS discord and copy down the link on the issue/PR I'm working on - partly for visibility and partly just so I can find it again myself! 😅

Thanks for your help with this ❤️

❤️

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Jan 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting response Issues waiting for a reply from the OP or another party
Projects
None yet
3 participants