Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(eslint-plugin): [no-invalid-void-type] don't flag function overloading with void in return type annotation #10422

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

Merged

Conversation

ronami
Copy link
Member

@ronami ronami commented Nov 29, 2024

PR Checklist

Overview

This PR addresses #5752 and adjusts the rule so it doesn't report on void that's used as part of the return type of a function overload implementation:

function f(): void;
function f(x: string): string;
function f(x?: string): void | string {
  if (x !== undefined) {
    return x;
  }
}

@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 29, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 7887cc9
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/679fadc5f106910007f706b8
😎 Deploy Preview https://deploy-preview-10422--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: 92 (🔴 down 6 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 29, 2024

View your CI Pipeline Execution ↗ for commit 7887cc9.

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

☁️ Nx Cloud last updated this comment at 2025-02-02 17:54:57 UTC

Copy link

codecov bot commented Nov 29, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 87.30%. Comparing base (1c73754) to head (7887cc9).
Report is 65 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10422      +/-   ##
==========================================
+ Coverage   87.26%   87.30%   +0.03%     
==========================================
  Files         450      451       +1     
  Lines       15715    15750      +35     
  Branches     4601     4615      +14     
==========================================
+ Hits        13714    13750      +36     
  Misses       1645     1645              
+ Partials      356      355       -1     
Flag Coverage Δ
unittest 87.30% <100.00%> (+0.03%) ⬆️

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

Files with missing lines Coverage Δ
...es/eslint-plugin/src/rules/no-invalid-void-type.ts 100.00% <100.00%> (+2.17%) ⬆️
...es/eslint-plugin/src/util/hasOverloadSignatures.ts 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

@ronami ronami marked this pull request as ready for review November 30, 2024 13:43
Comment on lines 191 to 192
/* istanbul ignore next */
return [];
Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a preferred way to tackle this? I think this is currently an unreachable line.

Copy link
Member

Choose a reason for hiding this comment

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

#10388 - either it's reachable or it's not.

If it's not actually reachable then I'd say use type assertion(s) to tell TypeScript why it's wrong.

There might be opportunities to suggest improvements to AST types too. Example: the getMembers(node.parent.parent) later on has the provided type as TSESTree.Node. But maybe somewhere in there a type could be made more specific? Is TSESTree.ExportDefaultDeclaration's .parent always a TSESTree.BlockStatement | TSESTree.Program, rather than the catch-all TSESTree.Node?

Copy link
Member Author

@ronami ronami Dec 19, 2024

Choose a reason for hiding this comment

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

Interesting! I didn't consider proposing changes/improvements to the AST types. I've found #9560, and I think it would be a good option if I could be sure the changes are correct.

To my understanding, TypeScript's AST doesn't even have an actual AST node for ExportDefaultDeclaration or ExportNamedDeclaration as they're expressed as modifiers.

I've looked into @babel/parser, @swc/types, and @oxc-project/types (also the more technical 1, 2), but I didn't find anything that makes me confident this is valid in every case. I did verify @typescript-eslint itself is type-checked successfully with these assumptions.

The stricter parent types for the AST I'd like to propose:

  • ExportDefaultDeclaration and ExportNamedDeclaration should have the stricter parent type of TSESTree.BlockStatement | TSESTree.Program | TSESTree. TSModuleBlock (I'm not sure if TSESTree.BlockStatement is an option).

  • FunctionDeclaration should have the stricter parent type of TSESTree.BlockStatement | TSESTree.ExportDefaultDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.Program.

    TypeScript itself doesn't have a specific parent for FunctionDeclaration (it falls back to the generic ts.Node), although they do have stricter parent types for some other nodes.

How did we verify the correctness of the proposals in #6225? Also, would this type of change be considered a breaking change? I noticed that #9560 was released in v8, but it wasn't marked as a breaking change.

I'm a bit hesitant to change the AST's types, as it can potentially lead to bugs, and I would love to hear opinions on this.

I updated the PR with these changes, so it's easier to discuss them.

Copy link
Member

Choose a reason for hiding this comment

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

The stricter parent types for the AST I'd like to propose:

I'd say those proposals should be a separate issue. I'm generally in favor of stricter parent types in general but we should discuss.

So I think how to handle that in this PR is up to you. Maybe either:

  • Link to the issue with an as
  • Draft this PR and mark it blocked till that's resolved

How did we verify the correctness of the proposals in #6225?

Other than yarn build and yarn typecheck, pretty much just manual knowledge and testing.

breaking change?

I don't think improving the types of the AST to better reflect reality is breaking. In that sense it's a "feature".

Changing the runtime value would IMO be a breaking change.

Copy link
Member Author

@ronami ronami Jan 19, 2025

Choose a reason for hiding this comment

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

That sounds good; I'm not sure why I mixed both on the same PR; thanks! 👍

I've opened #10682, drafted this PR, and marked it as blocked.

If the issue is accepted, I'll create a new PR with the stricter parent node changes and eventually re-open this PR for review with only the relevant changes.

Comment on lines 227 to 232
const id = nullThrows(
node.id,
'This may be null if and only if the parent is an `ExportDefaultDeclaration`.',
);

return id.name;
Copy link
Member Author

@ronami ronami Nov 30, 2024

Choose a reason for hiding this comment

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

This currently only gets called for nodes whose parents aren't ExportDefaultDeclaration, and so this can never be null. If this code gets refactored, it may change, and this can throw.

I can change this to a chain-expression, though this will show up as a partially covered line by codecov.

Suggested change
const id = nullThrows(
node.id,
'This may be null if and only if the parent is an `ExportDefaultDeclaration`.',
);
return id.name;
return node.id?.name;

Copy link
Member

Choose a reason for hiding this comment

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

Similar to the other comment, my fallback would be to use a ! with a disable comment explaining why the types are wrong. If possible, fixing up the types to be correct would be even better.

You might find the existing uses of the MakeRequired utils type interesting. Some places in code use for node types in similar situations. E.g. no-confusing-void-expression has handling for return statements that must have an argument. Maybe there's a similar flow here for functions that must have an id?

I'm not worried about refactors. Our test coverage is so high that the cost of unnecessary safety precautions in code is IMO higher than the cost of the brittleness.

Copy link
Member Author

@ronami ronami Dec 19, 2024

Choose a reason for hiding this comment

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

I'm confident the only possibility of node.id being null (for FunctionDeclaration or TSDeclareFunction) is in cases where the parent node is an ExportDefaultDeclaration (also according to JavaScript's spec):

/**
* The function's name.
* - For an `ArrowFunctionExpression` this is always `null`.
* - For a `FunctionExpression` this may be `null` if the name is omitted.
* - For a `FunctionDeclaration` or `TSDeclareFunction` this may be `null` if
* and only if the parent is an `ExportDefaultDeclaration`.
*/
id: Identifier | null;

I think that updating the types to be stricter here would be more challenging. I've resorted to using a ! with a disable comment and explaining why this is necessary.

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.

LGTM overall! Just requesting touchups around the types / istanbul ignores. 🚀

Comment on lines 191 to 192
/* istanbul ignore next */
return [];
Copy link
Member

Choose a reason for hiding this comment

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

#10388 - either it's reachable or it's not.

If it's not actually reachable then I'd say use type assertion(s) to tell TypeScript why it's wrong.

There might be opportunities to suggest improvements to AST types too. Example: the getMembers(node.parent.parent) later on has the provided type as TSESTree.Node. But maybe somewhere in there a type could be made more specific? Is TSESTree.ExportDefaultDeclaration's .parent always a TSESTree.BlockStatement | TSESTree.Program, rather than the catch-all TSESTree.Node?

Comment on lines 227 to 232
const id = nullThrows(
node.id,
'This may be null if and only if the parent is an `ExportDefaultDeclaration`.',
);

return id.name;
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the other comment, my fallback would be to use a ! with a disable comment explaining why the types are wrong. If possible, fixing up the types to be correct would be even better.

You might find the existing uses of the MakeRequired utils type interesting. Some places in code use for node types in similar situations. E.g. no-confusing-void-expression has handling for return statements that must have an argument. Maybe there's a similar flow here for functions that must have an id?

I'm not worried about refactors. Our test coverage is so high that the cost of unnecessary safety precautions in code is IMO higher than the cost of the brittleness.

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Dec 14, 2024
Comment on lines +582 to +591
{
code: 'type invalidVoidUnion = void | Map;',
errors: [
{
column: 25,
line: 1,
messageId: 'invalidVoidUnionConstituent',
},
],
},
Copy link
Member Author

@ronami ronami Dec 19, 2024

Choose a reason for hiding this comment

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

This test is unrelated to the PR, but it adds coverage to part of the code that is currently uncovered (it seemed small enough to be part of the PR, even if it's unrelated).

Please let me know if I should revert this part.

@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Dec 19, 2024
return getStaticMemberAccessValue(node, context);
}

function hasOverloadMethods(
Copy link
Member Author

@ronami ronami Dec 20, 2024

Choose a reason for hiding this comment

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

I think this may be useful as a shared utility function for different rules. Re-using this function (pretty much as is) simplifies tackling #4586 (which I'll be happy to push a PR for), for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding that this attempts to emulate checker.isImplementationOfOverload() for rules which aren't type-aware.

tommie added a commit to tommie/stm-ts that referenced this pull request Dec 23, 2024
The no-invalid-void-type issue is being fixed: typescript-eslint/typescript-eslint#10422
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.

Commented inline. Exciting! 🚀

I think just #10422 (comment) is left.

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Jan 7, 2025
@ronami ronami added blocked by another issue Issues which are not ready because another issue needs to be resolved first and removed awaiting response Issues waiting for a reply from the OP or another party labels Jan 19, 2025
@ronami ronami marked this pull request as draft January 19, 2025 18:17
@ronami ronami removed the blocked by another issue Issues which are not ready because another issue needs to be resolved first label Jan 29, 2025
@ronami
Copy link
Member Author

ronami commented Feb 2, 2025

Opening for re-review, please note:

  • I've renamed and extracted hasOverloadSignatures to a separate utility because I think it may be useful in other places (e.g. [explicit-module-boundary-types] New option to allow unspecified return type if there are overload signatures #4586).

  • Unlike FunctionDeclaration, which has a different type of node for its definition (TSDeclareFunction), MethodDefinition doesn't. MethodDefinition seems to either have its value be a FunctionExpression for implementations or TSEmptyBodyFunctionExpression in case of overloading (ignoring type/interface's TSMethodSignature, which seems like it cannot be part of a class's body).

    hasOverloadSignatures currently requests a node of type TSESTree.FunctionDeclaration | TSESTree.MethodDefinition, which means that it's possible to pass an overload method definition, for which the function will wrongly return true:

    class T {
      // compiles, even though this is a type error
      foo(): string;
    }

    This doesn't affect the no-invalid-void-type rule, as it only passes function/method implementations, but it may be an issue for future rules that use this. This can have a stricter type, but it does add a bit of complexity (see 350e82f).

    I'm not sure if this is worth the extra complexity, would love to hear another opinion on this 🙃

@ronami ronami marked this pull request as ready for review February 2, 2025 17:52
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.

Yeah I think this is great as-is. The utility has no known issues as used right now so we can cross that bridge when we get to it IMO. Awesome stuff! 🙌

@JoshuaKGoldberg JoshuaKGoldberg merged commit b688380 into typescript-eslint:main Feb 24, 2025
63 checks passed
@ronami ronami deleted the no-invalid-void-type-overloading branch February 24, 2025 17:48
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 4, 2025
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.

Bug: [no-invalid-void-type] Can't use void in return type union for overloaded function
2 participants