Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Used checker.getTypeArguments in return-undefined rule when possible #4866

Merged
merged 5 commits into from
Oct 5, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Used checker.getTypeArguments in return-undefined rule when possible
TypeScript no longer guarantees a .typeArguments member on the Type object, as they're apparently capable of being lazily-defined (?). Instead we're to use a `checker.getTypeArguments` method.

Once 3.7 is out of beta/RC we'll be able to upgrade our dev dependency to rely on it.
  • Loading branch information
Josh Goldberg committed Oct 2, 2019
commit d4328a6be74b97bac5ee5f9452b6b2922ec61ef4
130 changes: 72 additions & 58 deletions src/rules/returnUndefinedRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker) {
return;
}

const returnKindFromType = getReturnKindFromFunction(functionReturningFrom, checker);
const returnKindFromType = getReturnKindFromFunction(functionReturningFrom);
if (returnKindFromType !== undefined && returnKindFromType !== actualReturnKind) {
ctx.addFailureAtNode(
node,
Expand All @@ -82,6 +82,77 @@ function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker) {
);
}
}

function getReturnKindFromFunction(node: FunctionLike): ReturnKind | undefined {
switch (node.kind) {
case ts.SyntaxKind.Constructor:
case ts.SyntaxKind.SetAccessor:
return ReturnKind.Void;
case ts.SyntaxKind.GetAccessor:
return ReturnKind.Value;
}

// Handle generator functions/methods:
if (node.asteriskToken !== undefined) {
return ReturnKind.Void;
}

const contextual =
isFunctionExpressionLike(node) && node.type === undefined
? tryGetReturnType(checker.getContextualType(node), checker)
: undefined;
const returnType =
contextual !== undefined
? contextual
: tryGetReturnType(checker.getTypeAtLocation(node), checker);

if (returnType === undefined || isTypeFlagSet(returnType, ts.TypeFlags.Any)) {
return undefined;
}

const effectivelyVoidChecker = hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword)
? isEffectivelyVoidPromise
: isEffectivelyVoid;

if (effectivelyVoidChecker(returnType)) {
return ReturnKind.Void;
}

return ReturnKind.Value;
}

/** True for `void`, `undefined`, Promise<void>, or `void | undefined | Promise<void>`. */
function isEffectivelyVoidPromise(type: ts.Type): boolean {
// Would need access to `checker.getPromisedTypeOfPromise` to do this properly.
// Assume that the return type is the global Promise (since this is an async function) and get its type argument.

// tslint:disable-next-line:no-bitwise
if (
isTypeFlagSet(type, ts.TypeFlags.Void | ts.TypeFlags.Undefined) ||
(isUnionType(type) && type.types.every(isEffectivelyVoidPromise))
) {
return true;
}

const typeArguments = getTypeArgumentsOfType(type);

return typeArguments !== undefined && typeArguments.length === 1 && isEffectivelyVoidPromise(typeArguments[0]);
}

function getTypeArgumentsOfType(type: ts.Type) {
if (!isTypeReference(type)) {
return undefined;
}

// Fixes for https://github.com/palantir/tslint/issues/4863
// type.typeArguments was replaced with checker.getTypeArguments:
// https://github.com/microsoft/TypeScript/commit/250d5a8229e17342f36fe52545bb68140db96a2e
if ((checker as any).getTypeArguments) {
return (checker as any).getTypeArguments(type) as readonly ts.Type[] | undefined;
}

return (type as any).typeArguments as readonly ts.Type[] | undefined;
}
}

function getReturnKindFromReturnStatement(node: ts.ReturnStatement): ReturnKind | undefined {
Expand All @@ -108,63 +179,6 @@ type FunctionLike =
| ts.GetAccessorDeclaration
| ts.SetAccessorDeclaration;

function getReturnKindFromFunction(
node: FunctionLike,
checker: ts.TypeChecker,
): ReturnKind | undefined {
switch (node.kind) {
case ts.SyntaxKind.Constructor:
case ts.SyntaxKind.SetAccessor:
return ReturnKind.Void;
case ts.SyntaxKind.GetAccessor:
return ReturnKind.Value;
}

// Handle generator functions/methods:
if (node.asteriskToken !== undefined) {
return ReturnKind.Void;
}

const contextual =
isFunctionExpressionLike(node) && node.type === undefined
? tryGetReturnType(checker.getContextualType(node), checker)
: undefined;
const returnType =
contextual !== undefined
? contextual
: tryGetReturnType(checker.getTypeAtLocation(node), checker);

if (returnType === undefined || isTypeFlagSet(returnType, ts.TypeFlags.Any)) {
return undefined;
}

const effectivelyVoidChecker = hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword)
? isEffectivelyVoidPromise
: isEffectivelyVoid;

if (effectivelyVoidChecker(returnType)) {
return ReturnKind.Void;
}

return ReturnKind.Value;
}

/** True for `void`, `undefined`, Promise<void>, or `void | undefined | Promise<void>`. */
function isEffectivelyVoidPromise(type: ts.Type): boolean {
// Would need access to `checker.getPromisedTypeOfPromise` to do this properly.
// Assume that the return type is the global Promise (since this is an async function) and get its type argument.

return (
// tslint:disable-next-line:no-bitwise
isTypeFlagSet(type, ts.TypeFlags.Void | ts.TypeFlags.Undefined) ||
(isUnionType(type) && type.types.every(isEffectivelyVoidPromise)) ||
(isTypeReference(type) &&
type.typeArguments !== undefined &&
type.typeArguments.length === 1 &&
isEffectivelyVoidPromise(type.typeArguments[0]))
);
}

/** True for `void`, `undefined`, or `void | undefined`. */
function isEffectivelyVoid(type: ts.Type): boolean {
return (
Expand Down