Skip to content

Commit b688380

Browse files
authored
fix(eslint-plugin): [no-invalid-void-type] don't flag function overloading with void in return type annotation (#10422)
* initial implementation * add block test * refactor * refactor * use a recursive function to handle wrapped return types * report on method definitions that aren't overloads * handle export default declaration * handle named export declaration * refactor * fix test * use nullThrows instead of non-null assertions * add stricter parent types for various ast nodes and refactor accordingly * fix existing missing code coverage * refactor * move and rename hasOverloadSignatures * test void in non-implementation position * adjust hasOverloadSignatures to require a method definition with a body * expand comment a bit * Revert "adjust hasOverloadSignatures to require a method definition with a body" This reverts commit 8dd4707. * remove unnecessary comment * adjust hasOverloadSignatures to require a method definition with a body * Revert "adjust hasOverloadSignatures to require a method definition with a body" This reverts commit 350e82f. * refactor
1 parent f22de04 commit b688380

File tree

4 files changed

+357
-2
lines changed

4 files changed

+357
-2
lines changed

Diff for: packages/eslint-plugin/src/rules/no-invalid-void-type.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { TSESTree } from '@typescript-eslint/utils';
22

33
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
44

5-
import { createRule } from '../util';
5+
import { createRule, hasOverloadSignatures } from '../util';
66

77
export interface Options {
88
allowAsThisParameter?: boolean;
@@ -203,6 +203,20 @@ export default createRule<[Options], MessageIds>({
203203
return;
204204
}
205205

206+
// using `void` as part of the return type of function overloading implementation
207+
if (node.parent.type === AST_NODE_TYPES.TSUnionType) {
208+
const declaringFunction = getParentFunctionDeclarationNode(
209+
node.parent,
210+
);
211+
212+
if (
213+
declaringFunction &&
214+
hasOverloadSignatures(declaringFunction, context)
215+
) {
216+
return;
217+
}
218+
}
219+
206220
// this parameter is ok to be void.
207221
if (
208222
allowAsThisParameter &&
@@ -246,3 +260,25 @@ function getNotReturnOrGenericMessageId(
246260
? 'invalidVoidUnionConstituent'
247261
: 'invalidVoidNotReturnOrGeneric';
248262
}
263+
264+
function getParentFunctionDeclarationNode(
265+
node: TSESTree.Node,
266+
): TSESTree.FunctionDeclaration | TSESTree.MethodDefinition | null {
267+
let current = node.parent;
268+
while (current) {
269+
if (current.type === AST_NODE_TYPES.FunctionDeclaration) {
270+
return current;
271+
}
272+
273+
if (
274+
current.type === AST_NODE_TYPES.MethodDefinition &&
275+
current.value.body != null
276+
) {
277+
return current;
278+
}
279+
280+
current = current.parent;
281+
}
282+
283+
return null;
284+
}
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import type { RuleContext } from '@typescript-eslint/utils/ts-eslint';
3+
4+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
5+
6+
import { getStaticMemberAccessValue } from './misc';
7+
8+
/**
9+
* @return `true` if the function or method node has overload signatures.
10+
*/
11+
export function hasOverloadSignatures(
12+
node: TSESTree.FunctionDeclaration | TSESTree.MethodDefinition,
13+
context: RuleContext<string, unknown[]>,
14+
): boolean {
15+
// `export default function () {}`
16+
if (node.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
17+
return node.parent.parent.body.some(member => {
18+
return (
19+
member.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
20+
member.declaration.type === AST_NODE_TYPES.TSDeclareFunction
21+
);
22+
});
23+
}
24+
25+
// `export function f() {}`
26+
if (node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration) {
27+
return node.parent.parent.body.some(member => {
28+
return (
29+
member.type === AST_NODE_TYPES.ExportNamedDeclaration &&
30+
member.declaration?.type === AST_NODE_TYPES.TSDeclareFunction &&
31+
getFunctionDeclarationName(member.declaration, context) ===
32+
getFunctionDeclarationName(node, context)
33+
);
34+
});
35+
}
36+
37+
// either:
38+
// - `function f() {}`
39+
// - `class T { foo() {} }`
40+
41+
const nodeKey = getFunctionDeclarationName(node, context);
42+
43+
return node.parent.body.some(member => {
44+
return (
45+
(member.type === AST_NODE_TYPES.TSDeclareFunction ||
46+
(member.type === AST_NODE_TYPES.MethodDefinition &&
47+
member.value.body == null)) &&
48+
nodeKey === getFunctionDeclarationName(member, context)
49+
);
50+
});
51+
}
52+
53+
function getFunctionDeclarationName(
54+
node:
55+
| TSESTree.FunctionDeclaration
56+
| TSESTree.MethodDefinition
57+
| TSESTree.TSDeclareFunction,
58+
context: RuleContext<string, unknown[]>,
59+
): string | symbol | undefined {
60+
if (
61+
node.type === AST_NODE_TYPES.FunctionDeclaration ||
62+
node.type === AST_NODE_TYPES.TSDeclareFunction
63+
) {
64+
// For a `FunctionDeclaration` or `TSDeclareFunction` this may be `null` if
65+
// and only if the parent is an `ExportDefaultDeclaration`.
66+
//
67+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
68+
return node.id!.name;
69+
}
70+
71+
return getStaticMemberAccessValue(node, context);
72+
}

Diff for: packages/eslint-plugin/src/util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './getStringLength';
1111
export * from './getTextWithParentheses';
1212
export * from './getThisExpression';
1313
export * from './getWrappingFixer';
14+
export * from './hasOverloadSignatures';
1415
export * from './isArrayMethodCallWithPredicate';
1516
export * from './isAssignee';
1617
export * from './isNodeEqual';

0 commit comments

Comments
 (0)