Minor fixes to "Convert To Async" refactor (#45536)

* Minor fixes to convertToAsync

* Back out on nested return in inner continuation

* Baseline update

* Verify type argument for call can be used, add a few more early exit shortcuts
This commit is contained in:
Ron Buckton 2021-09-01 13:13:12 -07:00 committed by GitHub
parent f9a3d85b00
commit 6f7f3b1775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 691 additions and 273 deletions

View file

@ -634,6 +634,8 @@ namespace ts {
getESSymbolType: () => esSymbolType,
getNeverType: () => neverType,
getOptionalType: () => optionalType,
getPromiseType: () => getGlobalPromiseType(/*reportErrors*/ false),
getPromiseLikeType: () => getGlobalPromiseLikeType(/*reportErrors*/ false),
isSymbolAccessible,
isArrayType,
isTupleType,

View file

@ -4939,14 +4939,16 @@ namespace ts {
}
// @api
function createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block) {
function createCatchClause(variableDeclaration: string | BindingName | VariableDeclaration | undefined, block: Block) {
const node = createBaseNode<CatchClause>(SyntaxKind.CatchClause);
variableDeclaration = !isString(variableDeclaration) ? variableDeclaration : createVariableDeclaration(
variableDeclaration,
/*exclamationToken*/ undefined,
/*type*/ undefined,
/*initializer*/ undefined
);
if (typeof variableDeclaration === "string" || variableDeclaration && !isVariableDeclaration(variableDeclaration)) {
variableDeclaration = createVariableDeclaration(
variableDeclaration,
/*exclamationToken*/ undefined,
/*type*/ undefined,
/*initializer*/ undefined
);
}
node.variableDeclaration = variableDeclaration;
node.block = block;
node.transformFlags |=

View file

@ -4269,6 +4269,8 @@ namespace ts {
/* @internal */ createArrayType(elementType: Type): Type;
/* @internal */ getElementTypeOfArrayType(arrayType: Type): Type | undefined;
/* @internal */ createPromiseType(type: Type): Type;
/* @internal */ getPromiseType(): Type;
/* @internal */ getPromiseLikeType(): Type;
/* @internal */ isTypeAssignableTo(source: Type, target: Type): boolean;
/* @internal */ createAnonymousType(symbol: Symbol | undefined, members: SymbolTable, callSignatures: Signature[], constructSignatures: Signature[], indexInfos: IndexInfo[]): Type;
@ -7454,7 +7456,7 @@ namespace ts {
updateDefaultClause(node: DefaultClause, statements: readonly Statement[]): DefaultClause;
createHeritageClause(token: HeritageClause["token"], types: readonly ExpressionWithTypeArguments[]): HeritageClause;
updateHeritageClause(node: HeritageClause, types: readonly ExpressionWithTypeArguments[]): HeritageClause;
createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block): CatchClause;
createCatchClause(variableDeclaration: string | BindingName | VariableDeclaration | undefined, block: Block): CatchClause;
updateCatchClause(node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block): CatchClause;
//

View file

@ -1364,7 +1364,7 @@ namespace ts {
// Warning: This has the same semantics as the forEach family of functions,
// in that traversal terminates in the event that 'visitor' supplies a truthy value.
export function forEachReturnStatement<T>(body: Block, visitor: (stmt: ReturnStatement) => T): T | undefined {
export function forEachReturnStatement<T>(body: Block | Statement, visitor: (stmt: ReturnStatement) => T): T | undefined {
return traverse(body);

View file

@ -34,6 +34,7 @@ namespace ts.codefix {
readonly types: Type[];
/** A declaration for this identifier has already been generated */
hasBeenDeclared: boolean;
hasBeenReferenced: boolean;
}
interface Transformer {
@ -43,6 +44,12 @@ namespace ts.codefix {
readonly isInJSFile: boolean;
}
interface PromiseReturningCallExpression<Name extends string> extends CallExpression {
readonly expression: PropertyAccessExpression & {
readonly escapedText: Name;
};
}
function convertToAsyncFunction(changes: textChanges.ChangeTracker, sourceFile: SourceFile, position: number, checker: TypeChecker): void {
// get the function declaration - returns a promise
const tokenAtPosition = getTokenAtPosition(sourceFile, position);
@ -84,13 +91,22 @@ namespace ts.codefix {
for (const returnStatement of returnStatements) {
forEachChild(returnStatement, function visit(node) {
if (isCallExpression(node)) {
const newNodes = transformExpression(node, transformer);
const newNodes = transformExpression(node, node, transformer, /*hasContinuation*/ false);
if (hasFailed()) {
return true; // return something truthy to shortcut out of more work
}
changes.replaceNodeWithNodes(sourceFile, returnStatement, newNodes);
}
else if (!isFunctionLike(node)) {
forEachChild(node, visit);
if (hasFailed()) {
return true; // return something truthy to shortcut out of more work
}
}
});
if (hasFailed()) {
return; // shortcut out of more work
}
}
}
@ -116,9 +132,10 @@ namespace ts.codefix {
setOfExpressionsToReturn.add(getNodeId(node));
forEach(node.arguments, visit);
}
else if (isPromiseReturningCallExpression(node, checker, "catch")) {
else if (isPromiseReturningCallExpression(node, checker, "catch") ||
isPromiseReturningCallExpression(node, checker, "finally")) {
setOfExpressionsToReturn.add(getNodeId(node));
// if .catch() is the last call in the chain, move leftward in the chain until we hit something else that should be returned
// if .catch() or .finally() is the last call in the chain, move leftward in the chain until we hit something else that should be returned
forEachChild(node, visit);
}
else if (isPromiseTypedExpression(node, checker)) {
@ -133,13 +150,48 @@ namespace ts.codefix {
return setOfExpressionsToReturn;
}
function isPromiseReturningCallExpression(node: Node, checker: TypeChecker, name: string): node is CallExpression {
function isPromiseReturningCallExpression<Name extends string>(node: Node, checker: TypeChecker, name: Name): node is PromiseReturningCallExpression<Name> {
if (!isCallExpression(node)) return false;
const isExpressionOfName = hasPropertyAccessExpressionWithName(node, name);
const nodeType = isExpressionOfName && checker.getTypeAtLocation(node);
return !!(nodeType && checker.getPromisedTypeOfPromise(nodeType));
}
// NOTE: this is a mostly copy of `isReferenceToType` from checker.ts. While this violates DRY, it keeps
// `isReferenceToType` in checker local to the checker to avoid the cost of a property lookup on `ts`.
function isReferenceToType(type: Type, target: Type) {
return (getObjectFlags(type) & ObjectFlags.Reference) !== 0
&& (type as TypeReference).target === target;
}
function getExplicitPromisedTypeOfPromiseReturningCallExpression(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, callback: Expression, checker: TypeChecker) {
if (node.expression.name.escapedText === "finally") {
// for a `finally`, there's no type argument
return undefined;
}
// If the call to `then` or `catch` comes from the global `Promise` or `PromiseLike` type, we can safely use the
// type argument supplied for the callback. For other promise types we would need a more complex heuristic to determine
// which type argument is safe to use as an annotation.
const promiseType = checker.getTypeAtLocation(node.expression.expression);
if (isReferenceToType(promiseType, checker.getPromiseType()) ||
isReferenceToType(promiseType, checker.getPromiseLikeType())) {
if (node.expression.name.escapedText === "then") {
if (callback === elementAt(node.arguments, 0)) {
// for the `onfulfilled` callback, use the first type argument
return elementAt(node.typeArguments, 0);
}
else if (callback === elementAt(node.arguments, 1)) {
// for the `onrejected` callback, use the second type argument
return elementAt(node.typeArguments, 1);
}
}
else {
return elementAt(node.typeArguments, 0);
}
}
}
function isPromiseTypedExpression(node: Node, checker: TypeChecker): node is Expression {
if (!isExpression(node)) return false;
return !!checker.getPromisedTypeOfPromise(checker.getTypeAtLocation(node));
@ -229,6 +281,10 @@ namespace ts.codefix {
return createSynthIdentifier(identifier);
}
function hasFailed() {
return !codeActionSucceeded;
}
function silentFail() {
codeActionSucceeded = false;
return emptyArray;
@ -236,77 +292,40 @@ namespace ts.codefix {
// dispatch function to recursively build the refactoring
// should be kept up to date with isFixablePromiseHandler in suggestionDiagnostics.ts
function transformExpression(node: Expression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this expression belongs.
* @param continuationArgName The argument name for the continuation that follows this call.
*/
function transformExpression(returnContextNode: Expression, node: Expression, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] {
if (isPromiseReturningCallExpression(node, transformer.checker, "then")) {
if (node.arguments.length === 0) return silentFail();
return transformThen(node, transformer, prevArgName);
return transformThen(node, elementAt(node.arguments, 0), elementAt(node.arguments, 1), transformer, hasContinuation, continuationArgName);
}
if (isPromiseReturningCallExpression(node, transformer.checker, "catch")) {
return transformCatch(node, transformer, prevArgName);
return transformCatch(node, elementAt(node.arguments, 0), transformer, hasContinuation, continuationArgName);
}
if (isPromiseReturningCallExpression(node, transformer.checker, "finally")) {
return transformFinally(node, elementAt(node.arguments, 0), transformer, hasContinuation, continuationArgName);
}
if (isPropertyAccessExpression(node)) {
return transformExpression(node.expression, transformer, prevArgName);
return transformExpression(returnContextNode, node.expression, transformer, hasContinuation, continuationArgName);
}
const nodeType = transformer.checker.getTypeAtLocation(node);
if (nodeType && transformer.checker.getPromisedTypeOfPromise(nodeType)) {
Debug.assertNode(node.original!.parent, isPropertyAccessExpression);
return transformPromiseExpressionOfPropertyAccess(node, transformer, prevArgName);
Debug.assertNode(getOriginalNode(node).parent, isPropertyAccessExpression);
return transformPromiseExpressionOfPropertyAccess(returnContextNode, node, transformer, hasContinuation, continuationArgName);
}
return silentFail();
}
function transformCatch(node: CallExpression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
const func = singleOrUndefined(node.arguments);
const argName = func ? getArgBindingName(func, transformer) : undefined;
let possibleNameForVarDecl: SynthIdentifier | undefined;
// If there is another call in the chain after the .catch() we are transforming, we will need to save the result of both paths (try block and catch block)
// To do this, we will need to synthesize a variable that we were not aware of while we were adding identifiers to the synthNamesMap
// We will use the prevArgName and then update the synthNamesMap with a new variable name for the next transformation step
if (prevArgName && !shouldReturn(node, transformer)) {
if (isSynthIdentifier(prevArgName)) {
possibleNameForVarDecl = prevArgName;
transformer.synthNamesMap.forEach((val, key) => {
if (val.identifier.text === prevArgName.identifier.text) {
const newSynthName = createUniqueSynthName(prevArgName);
transformer.synthNamesMap.set(key, newSynthName);
}
});
}
else {
possibleNameForVarDecl = createSynthIdentifier(factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic), prevArgName.types);
}
// We are about to write a 'let' variable declaration, but `transformExpression` for both
// the try block and catch block will assign to this name. Setting this flag indicates
// that future assignments should be written as `name = value` instead of `const name = value`.
possibleNameForVarDecl.hasBeenDeclared = true;
function isNullOrUndefined({ checker }: Transformer, node: Expression) {
if (node.kind === SyntaxKind.NullKeyword) return true;
if (isIdentifier(node) && !isGeneratedIdentifier(node) && idText(node) === "undefined") {
const symbol = checker.getSymbolAtLocation(node);
return !symbol || checker.isUndefinedSymbol(symbol);
}
const tryBlock = factory.createBlock(transformExpression(node.expression, transformer, possibleNameForVarDecl));
const transformationBody = func ? getTransformationBody(func, possibleNameForVarDecl, argName, node, transformer) : emptyArray;
const catchArg = argName ? isSynthIdentifier(argName) ? argName.identifier.text : argName.bindingPattern : "e";
const catchVariableDeclaration = factory.createVariableDeclaration(catchArg);
const catchClause = factory.createCatchClause(catchVariableDeclaration, factory.createBlock(transformationBody));
// In order to avoid an implicit any, we will synthesize a type for the declaration using the unions of the types of both paths (try block and catch block)
let varDeclList: VariableStatement | undefined;
let varDeclIdentifier: Identifier | undefined;
if (possibleNameForVarDecl && !shouldReturn(node, transformer)) {
varDeclIdentifier = getSynthesizedDeepClone(possibleNameForVarDecl.identifier);
const typeArray: Type[] = possibleNameForVarDecl.types;
const unionType = transformer.checker.getUnionType(typeArray, UnionReduction.Subtype);
const unionTypeNode = transformer.isInJSFile ? undefined : transformer.checker.typeToTypeNode(unionType, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
const varDecl = [factory.createVariableDeclaration(varDeclIdentifier, /*exclamationToken*/ undefined, unionTypeNode)];
varDeclList = factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList(varDecl, NodeFlags.Let));
}
const tryStatement = factory.createTryStatement(tryBlock, catchClause, /*finallyBlock*/ undefined);
const destructuredResult = prevArgName && varDeclIdentifier && isSynthBindingPattern(prevArgName)
&& factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(getSynthesizedDeepClone(prevArgName.bindingPattern), /*exclamationToken*/ undefined, /*type*/ undefined, varDeclIdentifier)], NodeFlags.Const));
return compact([varDeclList, tryStatement, destructuredResult]);
return false;
}
function createUniqueSynthName(prevArgName: SynthIdentifier): SynthIdentifier {
@ -314,31 +333,168 @@ namespace ts.codefix {
return createSynthIdentifier(renamedPrevArg);
}
function transformThen(node: CallExpression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
const [onFulfilled, onRejected] = node.arguments;
const onFulfilledArgumentName = getArgBindingName(onFulfilled, transformer);
const transformationBody = getTransformationBody(onFulfilled, prevArgName, onFulfilledArgumentName, node, transformer);
if (onRejected) {
const onRejectedArgumentName = getArgBindingName(onRejected, transformer);
const tryBlock = factory.createBlock(transformExpression(node.expression, transformer, onFulfilledArgumentName).concat(transformationBody));
const transformationBody2 = getTransformationBody(onRejected, prevArgName, onRejectedArgumentName, node, transformer);
const catchArg = onRejectedArgumentName ? isSynthIdentifier(onRejectedArgumentName) ? onRejectedArgumentName.identifier.text : onRejectedArgumentName.bindingPattern : "e";
const catchVariableDeclaration = factory.createVariableDeclaration(catchArg);
const catchClause = factory.createCatchClause(catchVariableDeclaration, factory.createBlock(transformationBody2));
return [factory.createTryStatement(tryBlock, catchClause, /* finallyBlock */ undefined)];
function getPossibleNameForVarDecl(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer, continuationArgName?: SynthBindingName) {
let possibleNameForVarDecl: SynthIdentifier | undefined;
// If there is another call in the chain after the .catch() or .finally() we are transforming, we will need to save the result of both paths
// (try block and catch/finally block). To do this, we will need to synthesize a variable that we were not aware of while we were adding
// identifiers to the synthNamesMap. We will use the continuationArgName and then update the synthNamesMap with a new variable name for
// the next transformation step
if (continuationArgName && !shouldReturn(node, transformer)) {
if (isSynthIdentifier(continuationArgName)) {
possibleNameForVarDecl = continuationArgName;
transformer.synthNamesMap.forEach((val, key) => {
if (val.identifier.text === continuationArgName.identifier.text) {
const newSynthName = createUniqueSynthName(continuationArgName);
transformer.synthNamesMap.set(key, newSynthName);
}
});
}
else {
possibleNameForVarDecl = createSynthIdentifier(factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic), continuationArgName.types);
}
// We are about to write a 'let' variable declaration, but `transformExpression` for both
// the try block and catch/finally block will assign to this name. Setting this flag indicates
// that future assignments should be written as `name = value` instead of `const name = value`.
declareSynthIdentifier(possibleNameForVarDecl);
}
return transformExpression(node.expression, transformer, onFulfilledArgumentName).concat(transformationBody);
return possibleNameForVarDecl;
}
function finishCatchOrFinallyTransform(node: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer, tryStatement: TryStatement, possibleNameForVarDecl: SynthIdentifier | undefined, continuationArgName?: SynthBindingName) {
const statements: Statement[] = [];
// In order to avoid an implicit any, we will synthesize a type for the declaration using the unions of the types of both paths (try block and catch block)
let varDeclIdentifier: Identifier | undefined;
if (possibleNameForVarDecl && !shouldReturn(node, transformer)) {
varDeclIdentifier = getSynthesizedDeepClone(declareSynthIdentifier(possibleNameForVarDecl));
const typeArray: Type[] = possibleNameForVarDecl.types;
const unionType = transformer.checker.getUnionType(typeArray, UnionReduction.Subtype);
const unionTypeNode = transformer.isInJSFile ? undefined : transformer.checker.typeToTypeNode(unionType, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
const varDecl = [factory.createVariableDeclaration(varDeclIdentifier, /*exclamationToken*/ undefined, unionTypeNode)];
const varDeclList = factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList(varDecl, NodeFlags.Let));
statements.push(varDeclList);
}
statements.push(tryStatement);
if (continuationArgName && varDeclIdentifier && isSynthBindingPattern(continuationArgName)) {
statements.push(factory.createVariableStatement(
/*modifiers*/ undefined,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(
getSynthesizedDeepClone(declareSynthBindingPattern(continuationArgName)),
/*exclamationToken*/ undefined,
/*type*/ undefined,
varDeclIdentifier
)],
NodeFlags.Const)));
}
return statements;
}
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation.
* @param continuationArgName The argument name for the continuation that follows this call.
*/
function transformFinally(node: PromiseReturningCallExpression<"finally">, onFinally: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] {
if (!onFinally || isNullOrUndefined(transformer, onFinally)) {
// Ignore this call as it has no effect on the result
return transformExpression(/* returnContextNode */ node, node.expression.expression, transformer, hasContinuation, continuationArgName);
}
const possibleNameForVarDecl = getPossibleNameForVarDecl(node, transformer, continuationArgName);
// Transform the left-hand-side of `.finally` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation.
const inlinedLeftHandSide = transformExpression(/*returnContextNode*/ node, node.expression.expression, transformer, /*hasContinuation*/ true, possibleNameForVarDecl);
if (hasFailed()) return silentFail(); // shortcut out of more work
// Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here
// as that indicates whether `return` is valid.
const inlinedCallback = transformCallbackArgument(onFinally, hasContinuation, /*continuationArgName*/ undefined, /*argName*/ undefined, node, transformer);
if (hasFailed()) return silentFail(); // shortcut out of more work
const tryBlock = factory.createBlock(inlinedLeftHandSide);
const finallyBlock = factory.createBlock(inlinedCallback);
const tryStatement = factory.createTryStatement(tryBlock, /*catchClause*/ undefined, finallyBlock);
return finishCatchOrFinallyTransform(node, transformer, tryStatement, possibleNameForVarDecl, continuationArgName);
}
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation.
* @param continuationArgName The argument name for the continuation that follows this call.
*/
function transformCatch(node: PromiseReturningCallExpression<"then" | "catch">, onRejected: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] {
if (!onRejected || isNullOrUndefined(transformer, onRejected)) {
// Ignore this call as it has no effect on the result
return transformExpression(/* returnContextNode */ node, node.expression.expression, transformer, hasContinuation, continuationArgName);
}
const inputArgName = getArgBindingName(onRejected, transformer);
const possibleNameForVarDecl = getPossibleNameForVarDecl(node, transformer, continuationArgName);
// Transform the left-hand-side of `.then`/`.catch` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation.
const inlinedLeftHandSide = transformExpression(/*returnContextNode*/ node, node.expression.expression, transformer, /*hasContinuation*/ true, possibleNameForVarDecl);
if (hasFailed()) return silentFail(); // shortcut out of more work
// Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here
// as that indicates whether `return` is valid.
const inlinedCallback = transformCallbackArgument(onRejected, hasContinuation, possibleNameForVarDecl, inputArgName, node, transformer);
if (hasFailed()) return silentFail(); // shortcut out of more work
const tryBlock = factory.createBlock(inlinedLeftHandSide);
const catchClause = factory.createCatchClause(inputArgName && getSynthesizedDeepClone(declareSynthBindingName(inputArgName)), factory.createBlock(inlinedCallback));
const tryStatement = factory.createTryStatement(tryBlock, catchClause, /*finallyBlock*/ undefined);
return finishCatchOrFinallyTransform(node, transformer, tryStatement, possibleNameForVarDecl, continuationArgName);
}
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows this continuation.
* @param continuationArgName The argument name for the continuation that follows this call.
*/
function transformThen(node: PromiseReturningCallExpression<"then">, onFulfilled: Expression | undefined, onRejected: Expression | undefined, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] {
if (!onFulfilled || isNullOrUndefined(transformer, onFulfilled)) {
// If we don't have an `onfulfilled` callback, try treating this as a `.catch`.
return transformCatch(node, onRejected, transformer, hasContinuation, continuationArgName);
}
// We don't currently support transforming a `.then` with both onfulfilled and onrejected handlers, per GH#38152.
if (onRejected && !isNullOrUndefined(transformer, onRejected)) {
return silentFail();
}
const inputArgName = getArgBindingName(onFulfilled, transformer);
// Transform the left-hand-side of `.then` into an array of inlined statements. We pass `true` for hasContinuation as `node` is the outer continuation.
const inlinedLeftHandSide = transformExpression(node.expression.expression, node.expression.expression, transformer, /*hasContinuation*/ true, inputArgName);
if (hasFailed()) return silentFail(); // shortcut out of more work
// Transform the callback argument into an array of inlined statements. We pass whether we have an outer continuation here
// as that indicates whether `return` is valid.
const inlinedCallback = transformCallbackArgument(onFulfilled, hasContinuation, continuationArgName, inputArgName, node, transformer);
if (hasFailed()) return silentFail(); // shortcut out of more work
return concatenate(inlinedLeftHandSide, inlinedCallback);
}
/**
* Transforms the 'x' part of `x.then(...)`, or the 'y()' part of `y().catch(...)`, where 'x' and 'y()' are Promises.
*/
function transformPromiseExpressionOfPropertyAccess(node: Expression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
if (shouldReturn(node, transformer)) {
return [factory.createReturnStatement(getSynthesizedDeepClone(node))];
function transformPromiseExpressionOfPropertyAccess(returnContextNode: Expression, node: Expression, transformer: Transformer, hasContinuation: boolean, continuationArgName?: SynthBindingName): readonly Statement[] {
if (shouldReturn(returnContextNode, transformer)) {
let returnValue = getSynthesizedDeepClone(node);
if (hasContinuation) {
returnValue = factory.createAwaitExpression(returnValue);
}
return [factory.createReturnStatement(returnValue)];
}
return createVariableOrAssignmentOrExpressionStatement(prevArgName, factory.createAwaitExpression(node), /*typeAnnotation*/ undefined);
return createVariableOrAssignmentOrExpressionStatement(continuationArgName, factory.createAwaitExpression(node), /*typeAnnotation*/ undefined);
}
function createVariableOrAssignmentOrExpressionStatement(variableName: SynthBindingName | undefined, rightHandSide: Expression, typeAnnotation: TypeNode | undefined): readonly Statement[] {
@ -349,7 +505,7 @@ namespace ts.codefix {
if (isSynthIdentifier(variableName) && variableName.hasBeenDeclared) {
// if the variable has already been declared, we don't need "let" or "const"
return [factory.createExpressionStatement(factory.createAssignment(getSynthesizedDeepClone(variableName.identifier), rightHandSide))];
return [factory.createExpressionStatement(factory.createAssignment(getSynthesizedDeepClone(referenceSynthIdentifier(variableName)), rightHandSide))];
}
return [
@ -357,14 +513,14 @@ namespace ts.codefix {
/*modifiers*/ undefined,
factory.createVariableDeclarationList([
factory.createVariableDeclaration(
getSynthesizedDeepClone(getNode(variableName)),
getSynthesizedDeepClone(declareSynthBindingName(variableName)),
/*exclamationToken*/ undefined,
typeAnnotation,
rightHandSide)],
NodeFlags.Const))];
}
function maybeAnnotateAndReturn(expressionToReturn: Expression | undefined, typeAnnotation: TypeNode | undefined): readonly Statement[] {
function maybeAnnotateAndReturn(expressionToReturn: Expression | undefined, typeAnnotation: TypeNode | undefined): Statement[] {
if (typeAnnotation && expressionToReturn) {
const name = factory.createUniqueName("result", GeneratedIdentifierFlags.Optimistic);
return [
@ -376,21 +532,27 @@ namespace ts.codefix {
}
// should be kept up to date with isFixablePromiseArgument in suggestionDiagnostics.ts
function getTransformationBody(func: Expression, prevArgName: SynthBindingName | undefined, argName: SynthBindingName | undefined, parent: CallExpression, transformer: Transformer): readonly Statement[] {
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this callback belongs.
* @param continuationArgName The argument name for the continuation that follows this call.
* @param inputArgName The argument name provided to this call
*/
function transformCallbackArgument(func: Expression, hasContinuation: boolean, continuationArgName: SynthBindingName | undefined, inputArgName: SynthBindingName | undefined, parent: PromiseReturningCallExpression<"then" | "catch" | "finally">, transformer: Transformer): readonly Statement[] {
switch (func.kind) {
case SyntaxKind.NullKeyword:
// do not produce a transformed statement for a null argument
break;
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.Identifier: // identifier includes undefined
if (!argName) {
if (!inputArgName) {
// undefined was argument passed to promise handler
break;
}
const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier | PropertyAccessExpression), /*typeArguments*/ undefined, isSynthIdentifier(argName) ? [argName.identifier] : []);
const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier | PropertyAccessExpression), /*typeArguments*/ undefined, isSynthIdentifier(inputArgName) ? [referenceSynthIdentifier(inputArgName)] : []);
if (shouldReturn(parent, transformer)) {
return maybeAnnotateAndReturn(synthCall, parent.typeArguments?.[0]);
return maybeAnnotateAndReturn(synthCall, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker));
}
const type = transformer.checker.getTypeAtLocation(func);
@ -400,9 +562,9 @@ namespace ts.codefix {
return silentFail();
}
const returnType = callSignatures[0].getReturnType();
const varDeclOrAssignment = createVariableOrAssignmentOrExpressionStatement(prevArgName, factory.createAwaitExpression(synthCall), parent.typeArguments?.[0]);
if (prevArgName) {
prevArgName.types.push(returnType);
const varDeclOrAssignment = createVariableOrAssignmentOrExpressionStatement(continuationArgName, factory.createAwaitExpression(synthCall), getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker));
if (continuationArgName) {
continuationArgName.types.push(transformer.checker.getAwaitedType(returnType) || returnType);
}
return varDeclOrAssignment;
@ -419,13 +581,53 @@ namespace ts.codefix {
if (isReturnStatement(statement)) {
seenReturnStatement = true;
if (isReturnStatementWithFixablePromiseHandler(statement, transformer.checker)) {
refactoredStmts = refactoredStmts.concat(getInnerTransformationBody(transformer, [statement], prevArgName));
refactoredStmts = refactoredStmts.concat(transformReturnStatementWithFixablePromiseHandler(transformer, statement, hasContinuation, continuationArgName));
}
else {
const possiblyAwaitedRightHandSide = returnType && statement.expression ? getPossiblyAwaitedRightHandSide(transformer.checker, returnType, statement.expression) : statement.expression;
refactoredStmts.push(...maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, parent.typeArguments?.[0]));
refactoredStmts.push(...maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker)));
}
}
else if (hasContinuation && forEachReturnStatement(statement, returnTrue)) {
// If there is a nested `return` in a callback that has a trailing continuation, we don't transform it as the resulting complexity is too great. For example:
//
// source | result
// -------------------------------------| ---------------------------------------
// function f(): Promise<number> { | async function f9(): Promise<number> {
// return foo().then(() => { | await foo();
// if (Math.random()) { | if (Math.random()) {
// return 1; | return 1; // incorrect early return
// } | }
// return 2; | return 2; // incorrect early return
// }).then(a => { | const a = undefined;
// return a + 1; | return a + 1;
// }); | }
// } |
//
// However, branching returns in the outermost continuation are acceptable as no other continuation follows it:
//
// source | result
//--------------------------------------|---------------------------------------
// function f() { | async function f() {
// return foo().then(res => { | const res = await foo();
// if (res.ok) { | if (res.ok) {
// return 1; | return 1;
// } | }
// else { | else {
// if (res.buffer.length > 5) { | if (res.buffer.length > 5) {
// return 2; | return 2;
// } | }
// else { | else {
// return 3; | return 3;
// } | }
// } | }
// }); | }
// } |
//
// We may improve this in the future, but for now the heuristics are too complex
return silentFail();
}
else {
refactoredStmts.push(statement);
}
@ -435,29 +637,31 @@ namespace ts.codefix {
? refactoredStmts.map(s => getSynthesizedDeepClone(s))
: removeReturns(
refactoredStmts,
prevArgName,
continuationArgName,
transformer,
seenReturnStatement);
}
else {
const innerRetStmts = isFixablePromiseHandler(funcBody, transformer.checker) ? [factory.createReturnStatement(funcBody)] : emptyArray;
const innerCbBody = getInnerTransformationBody(transformer, innerRetStmts, prevArgName);
const inlinedStatements = isFixablePromiseHandler(funcBody, transformer.checker) ?
transformReturnStatementWithFixablePromiseHandler(transformer, factory.createReturnStatement(funcBody), hasContinuation, continuationArgName) :
emptyArray;
if (innerCbBody.length > 0) {
return innerCbBody;
if (inlinedStatements.length > 0) {
return inlinedStatements;
}
if (returnType) {
const possiblyAwaitedRightHandSide = getPossiblyAwaitedRightHandSide(transformer.checker, returnType, funcBody);
if (!shouldReturn(parent, transformer)) {
const transformedStatement = createVariableOrAssignmentOrExpressionStatement(prevArgName, possiblyAwaitedRightHandSide, /*typeAnnotation*/ undefined);
if (prevArgName) {
prevArgName.types.push(returnType);
const transformedStatement = createVariableOrAssignmentOrExpressionStatement(continuationArgName, possiblyAwaitedRightHandSide, /*typeAnnotation*/ undefined);
if (continuationArgName) {
continuationArgName.types.push(transformer.checker.getAwaitedType(returnType) || returnType);
}
return transformedStatement;
}
else {
return maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, parent.typeArguments?.[0]);
return maybeAnnotateAndReturn(possiblyAwaitedRightHandSide, getExplicitPromisedTypeOfPromiseReturningCallExpression(parent, func, transformer.checker));
}
}
else {
@ -491,9 +695,12 @@ namespace ts.codefix {
if (prevArgName === undefined) {
ret.push(factory.createExpressionStatement(possiblyAwaitedExpression));
}
else if (isSynthIdentifier(prevArgName) && prevArgName.hasBeenDeclared) {
ret.push(factory.createExpressionStatement(factory.createAssignment(referenceSynthIdentifier(prevArgName), possiblyAwaitedExpression)));
}
else {
ret.push(factory.createVariableStatement(/*modifiers*/ undefined,
(factory.createVariableDeclarationList([factory.createVariableDeclaration(getNode(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, possiblyAwaitedExpression)], NodeFlags.Const))));
(factory.createVariableDeclarationList([factory.createVariableDeclaration(declareSynthBindingName(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, possiblyAwaitedExpression)], NodeFlags.Const))));
}
}
}
@ -505,29 +712,30 @@ namespace ts.codefix {
// if block has no return statement, need to define prevArgName as undefined to prevent undeclared variables
if (!seenReturnStatement && prevArgName !== undefined) {
ret.push(factory.createVariableStatement(/*modifiers*/ undefined,
(factory.createVariableDeclarationList([factory.createVariableDeclaration(getNode(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, factory.createIdentifier("undefined"))], NodeFlags.Const))));
(factory.createVariableDeclarationList([factory.createVariableDeclaration(declareSynthBindingName(prevArgName), /*exclamationToken*/ undefined, /*type*/ undefined, factory.createIdentifier("undefined"))], NodeFlags.Const))));
}
return ret;
}
function getInnerTransformationBody(transformer: Transformer, innerRetStmts: readonly Node[], prevArgName?: SynthBindingName) {
/**
* @param hasContinuation Whether another `then`, `catch`, or `finally` continuation follows the continuation to which this statement belongs.
* @param continuationArgName The argument name for the continuation that follows this call.
*/
function transformReturnStatementWithFixablePromiseHandler(transformer: Transformer, innerRetStmt: ReturnStatement, hasContinuation: boolean, continuationArgName?: SynthBindingName) {
let innerCbBody: Statement[] = [];
for (const stmt of innerRetStmts) {
forEachChild(stmt, function visit(node) {
if (isCallExpression(node)) {
const temp = transformExpression(node, transformer, prevArgName);
innerCbBody = innerCbBody.concat(temp);
if (innerCbBody.length > 0) {
return;
}
forEachChild(innerRetStmt, function visit(node) {
if (isCallExpression(node)) {
const temp = transformExpression(node, node, transformer, hasContinuation, continuationArgName);
innerCbBody = innerCbBody.concat(temp);
if (innerCbBody.length > 0) {
return;
}
else if (!isFunctionLike(node)) {
forEachChild(node, visit);
}
});
}
}
else if (!isFunctionLike(node)) {
forEachChild(node, visit);
}
});
return innerCbBody;
}
@ -597,18 +805,35 @@ namespace ts.codefix {
return every(bindingName.elements, isEmptyBindingName);
}
function getNode(bindingName: SynthBindingName) {
return isSynthIdentifier(bindingName) ? bindingName.identifier : bindingName.bindingPattern;
}
function createSynthIdentifier(identifier: Identifier, types: Type[] = []): SynthIdentifier {
return { kind: SynthBindingNameKind.Identifier, identifier, types, hasBeenDeclared: false };
return { kind: SynthBindingNameKind.Identifier, identifier, types, hasBeenDeclared: false, hasBeenReferenced: false };
}
function createSynthBindingPattern(bindingPattern: BindingPattern, elements: readonly SynthBindingName[] = emptyArray, types: Type[] = []): SynthBindingPattern {
return { kind: SynthBindingNameKind.BindingPattern, bindingPattern, elements, types };
}
function referenceSynthIdentifier(synthId: SynthIdentifier) {
synthId.hasBeenReferenced = true;
return synthId.identifier;
}
function declareSynthBindingName(synthName: SynthBindingName) {
return isSynthIdentifier(synthName) ? declareSynthIdentifier(synthName) : declareSynthBindingPattern(synthName);
}
function declareSynthBindingPattern(synthPattern: SynthBindingPattern) {
for (const element of synthPattern.elements) {
declareSynthBindingName(element);
}
return synthPattern.bindingPattern;
}
function declareSynthIdentifier(synthId: SynthIdentifier) {
synthId.hasBeenDeclared = true;
return synthId.identifier;
}
function isSynthIdentifier(bindingName: SynthBindingName): bindingName is SynthIdentifier {
return bindingName.kind === SynthBindingNameKind.Identifier;
}

View file

@ -139,33 +139,40 @@ namespace ts {
// Should be kept up to date with transformExpression in convertToAsyncFunction.ts
export function isFixablePromiseHandler(node: Node, checker: TypeChecker): boolean {
// ensure outermost call exists and is a promise handler
if (!isPromiseHandler(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
if (!isPromiseHandler(node) || !hasSupportedNumberOfArguments(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
return false;
}
// ensure all chained calls are valid
let currentNode = node.expression;
let currentNode = node.expression.expression;
while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
if (isCallExpression(currentNode) && !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
return false;
if (isCallExpression(currentNode)) {
if (!hasSupportedNumberOfArguments(currentNode) || !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
return false;
}
currentNode = currentNode.expression.expression;
}
else {
currentNode = currentNode.expression;
}
currentNode = currentNode.expression;
}
return true;
}
function isPromiseHandler(node: Node): node is CallExpression {
function isPromiseHandler(node: Node): node is CallExpression & { readonly expression: PropertyAccessExpression } {
return isCallExpression(node) && (
hasPropertyAccessExpressionWithName(node, "then") && hasSupportedNumberOfArguments(node) ||
hasPropertyAccessExpressionWithName(node, "catch"));
hasPropertyAccessExpressionWithName(node, "then") ||
hasPropertyAccessExpressionWithName(node, "catch") ||
hasPropertyAccessExpressionWithName(node, "finally"));
}
function hasSupportedNumberOfArguments(node: CallExpression) {
if (node.arguments.length > 2) return false;
if (node.arguments.length < 2) return true;
return some(node.arguments, arg => {
return arg.kind === SyntaxKind.NullKeyword ||
isIdentifier(arg) && arg.text === "undefined";
function hasSupportedNumberOfArguments(node: CallExpression & { readonly expression: PropertyAccessExpression }) {
const name = node.expression.name.text;
const maxArguments = name === "then" ? 2 : name === "catch" ? 1 : name === "finally" ? 1 : 0;
if (node.arguments.length > maxArguments) return false;
if (node.arguments.length < maxArguments) return true;
return maxArguments === 1 || some(node.arguments, arg => {
return arg.kind === SyntaxKind.NullKeyword || isIdentifier(arg) && arg.text === "undefined";
});
}

View file

@ -53,6 +53,13 @@ interface Promise<T> {
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
/**
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
* resolved value cannot be modified from the callback.
* @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
* @returns A Promise for the completion of the callback.
*/
finally(onfinally?: (() => void) | undefined | null): Promise<T>
}
interface PromiseConstructor {
/**
@ -277,7 +284,30 @@ interface Array<T> {}`
}
}
function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, includeLib?: boolean, includeModule?: boolean, expectFailure = false, onlyProvideAction = false) {
const enum ConvertToAsyncTestFlags {
None,
IncludeLib = 1 << 0,
IncludeModule = 1 << 1,
ExpectSuggestionDiagnostic = 1 << 2,
ExpectNoSuggestionDiagnostic = 1 << 3,
ExpectAction = 1 << 4,
ExpectNoAction = 1 << 5,
ExpectSuccess = ExpectSuggestionDiagnostic | ExpectAction,
ExpectFailed = ExpectNoSuggestionDiagnostic | ExpectNoAction,
}
function testConvertToAsyncFunction(it: Mocha.PendingTestFunction, caption: string, text: string, baselineFolder: string, flags: ConvertToAsyncTestFlags) {
const includeLib = !!(flags & ConvertToAsyncTestFlags.IncludeLib);
const includeModule = !!(flags & ConvertToAsyncTestFlags.IncludeModule);
const expectSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic);
const expectNoSuggestionDiagnostic = !!(flags & ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic);
const expectAction = !!(flags & ConvertToAsyncTestFlags.ExpectAction);
const expectNoAction = !!(flags & ConvertToAsyncTestFlags.ExpectNoAction);
const expectFailure = expectNoSuggestionDiagnostic || expectNoAction;
Debug.assert(!(expectSuggestionDiagnostic && expectNoSuggestionDiagnostic), "Cannot combine both 'ExpectSuggestionDiagnostic' and 'ExpectNoSuggestionDiagnostic'");
Debug.assert(!(expectAction && expectNoAction), "Cannot combine both 'ExpectAction' and 'ExpectNoAction'");
const t = extractTest(text);
const selectionRange = t.ranges.get("selection")!;
if (!selectionRange) {
@ -320,35 +350,47 @@ interface Array<T> {}`
const diagnostics = languageService.getSuggestionDiagnostics(f.path);
const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === Diagnostics.This_may_be_converted_to_an_async_function.message &&
diagnostic.start === context.span.start && diagnostic.length === context.span.length);
if (expectFailure) {
assert.isUndefined(diagnostic);
}
else {
assert.exists(diagnostic);
}
const actions = codefix.getFixes(context);
const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message);
if (expectFailure && !onlyProvideAction) {
assert.isNotTrue(action && action.changes.length > 0);
return;
let outputText: string | null;
if (action?.changes.length) {
const data: string[] = [];
data.push(`// ==ORIGINAL==`);
data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/"));
const changes = action.changes;
assert.lengthOf(changes, 1);
data.push(`// ==ASYNC FUNCTION::${action.description}==`);
const newText = textChanges.applyChanges(sourceFile.text, changes[0].textChanges);
data.push(newText);
const diagProgram = makeLanguageService({ path, content: newText }, includeLib, includeModule).getProgram()!;
assert.isFalse(hasSyntacticDiagnostics(diagProgram));
outputText = data.join(newLineCharacter);
}
else {
// eslint-disable-next-line no-null/no-null
outputText = null;
}
assert.isTrue(action && action.changes.length > 0);
Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, outputText);
const data: string[] = [];
data.push(`// ==ORIGINAL==`);
data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/"));
const changes = action!.changes;
assert.lengthOf(changes, 1);
if (expectNoSuggestionDiagnostic) {
assert.isUndefined(diagnostic, "Expected code fix to not provide a suggestion diagnostic");
}
else if (expectSuggestionDiagnostic) {
assert.exists(diagnostic, "Expected code fix to provide a suggestion diagnostic");
}
data.push(`// ==ASYNC FUNCTION::${action!.description}==`);
const newText = textChanges.applyChanges(sourceFile.text, changes[0].textChanges);
data.push(newText);
const diagProgram = makeLanguageService({ path, content: newText }, includeLib, includeModule).getProgram()!;
assert.isFalse(hasSyntacticDiagnostics(diagProgram));
Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter));
if (expectNoAction) {
assert.isNotTrue(!!action?.changes.length, "Expected code fix to not provide an action");
assert.isNotTrue(typeof outputText === "string", "Expected code fix to not apply changes");
}
else if (expectAction) {
assert.isTrue(!!action?.changes.length, "Expected code fix to provide an action");
assert.isTrue(typeof outputText === "string", "Expected code fix to apply changes");
}
}
function makeLanguageService(file: TestFSWithWatch.File, includeLib?: boolean, includeModule?: boolean) {
@ -372,19 +414,23 @@ interface Array<T> {}`
}
const _testConvertToAsyncFunction = createTestWrapper((it, caption: string, text: string) => {
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true);
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuccess);
});
const _testConvertToAsyncFunctionFailed = createTestWrapper((it, caption: string, text: string) => {
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ false, /*expectFailure*/ true);
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectFailed);
});
const _testConvertToAsyncFunctionFailedSuggestion = createTestWrapper((it, caption: string, text: string) => {
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ false, /*expectFailure*/ true, /*onlyProvideAction*/ true);
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectNoSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectAction);
});
const _testConvertToAsyncFunctionFailedAction = createTestWrapper((it, caption: string, text: string) => {
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.ExpectSuggestionDiagnostic | ConvertToAsyncTestFlags.ExpectNoAction);
});
const _testConvertToAsyncFunctionWithModule = createTestWrapper((it, caption: string, text: string) => {
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*includeModule*/ true);
testConvertToAsyncFunction(it, caption, text, "convertToAsyncFunction", ConvertToAsyncTestFlags.IncludeLib | ConvertToAsyncTestFlags.IncludeModule | ConvertToAsyncTestFlags.ExpectSuccess);
});
describe("unittests:: services:: convertToAsyncFunction", () => {
@ -435,11 +481,11 @@ function [#|f|](): Promise<void>{
function [#|f|]():Promise<void> {
return fetch('https://typescriptlang.org').then(result => { console.log(result); }).catch(err => { console.log(err); });
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRej", `
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchAndRej", `
function [#|f|]():Promise<void> {
return fetch('https://typescriptlang.org').then(result => { console.log(result); }, rejection => { console.log("rejected:", rejection); }).catch(err => { console.log(err) });
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRejRef", `
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchAndRejRef", `
function [#|f|]():Promise<void> {
return fetch('https://typescriptlang.org').then(res, rej).catch(catch_err)
}
@ -1718,5 +1764,76 @@ function [#|foo|](p: Promise<string[]>) {
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_thenNoArguments", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then();
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_catchNoArguments", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().catch();
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_chainedThenCatchThen", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then(x => Promise.resolve(x + 1)).catch(() => 1).then(y => y + 2);
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_finally", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().finally(() => console.log("done"));
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_finallyNoArguments", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().finally();
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_finallyNull", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().finally(null);
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_finallyUndefined", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().finally(undefined);
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_thenFinally", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then(x => x + 1).finally(() => console.log("done"));
}`);
_testConvertToAsyncFunction("convertToAsyncFunction_thenFinallyThen", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then(x => Promise.resolve(x + 1)).finally(() => console.log("done")).then(y => y + 2);
}`);
_testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_returnInBranch", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then(() => {
if (Math.random()) {
return 1;
}
return 2;
}).then(a => {
return a + 1;
});
}
`);
_testConvertToAsyncFunctionFailedAction("convertToAsyncFunction_partialReturnInBranch", `
declare function foo(): Promise<number>;
function [#|f|](): Promise<number> {
return foo().then(() => {
if (Math.random()) {
return 1;
}
console.log("foo");
}).then(a => {
return a + 1;
});
}
`);
});
}

View file

@ -3622,7 +3622,7 @@ declare namespace ts {
updateDefaultClause(node: DefaultClause, statements: readonly Statement[]): DefaultClause;
createHeritageClause(token: HeritageClause["token"], types: readonly ExpressionWithTypeArguments[]): HeritageClause;
updateHeritageClause(node: HeritageClause, types: readonly ExpressionWithTypeArguments[]): HeritageClause;
createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block): CatchClause;
createCatchClause(variableDeclaration: string | BindingName | VariableDeclaration | undefined, block: Block): CatchClause;
updateCatchClause(node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block): CatchClause;
createPropertyAssignment(name: string | PropertyName, initializer: Expression): PropertyAssignment;
updatePropertyAssignment(node: PropertyAssignment, name: PropertyName, initializer: Expression): PropertyAssignment;
@ -11074,7 +11074,7 @@ declare namespace ts {
/** @deprecated Use `factory.updateHeritageClause` or the factory supplied by your transformation context instead. */
const updateHeritageClause: (node: HeritageClause, types: readonly ExpressionWithTypeArguments[]) => HeritageClause;
/** @deprecated Use `factory.createCatchClause` or the factory supplied by your transformation context instead. */
const createCatchClause: (variableDeclaration: string | VariableDeclaration | undefined, block: Block) => CatchClause;
const createCatchClause: (variableDeclaration: string | VariableDeclaration | BindingName | undefined, block: Block) => CatchClause;
/** @deprecated Use `factory.updateCatchClause` or the factory supplied by your transformation context instead. */
const updateCatchClause: (node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block) => CatchClause;
/** @deprecated Use `factory.createPropertyAssignment` or the factory supplied by your transformation context instead. */

View file

@ -3622,7 +3622,7 @@ declare namespace ts {
updateDefaultClause(node: DefaultClause, statements: readonly Statement[]): DefaultClause;
createHeritageClause(token: HeritageClause["token"], types: readonly ExpressionWithTypeArguments[]): HeritageClause;
updateHeritageClause(node: HeritageClause, types: readonly ExpressionWithTypeArguments[]): HeritageClause;
createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block): CatchClause;
createCatchClause(variableDeclaration: string | BindingName | VariableDeclaration | undefined, block: Block): CatchClause;
updateCatchClause(node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block): CatchClause;
createPropertyAssignment(name: string | PropertyName, initializer: Expression): PropertyAssignment;
updatePropertyAssignment(node: PropertyAssignment, name: PropertyName, initializer: Expression): PropertyAssignment;
@ -7274,7 +7274,7 @@ declare namespace ts {
/** @deprecated Use `factory.updateHeritageClause` or the factory supplied by your transformation context instead. */
const updateHeritageClause: (node: HeritageClause, types: readonly ExpressionWithTypeArguments[]) => HeritageClause;
/** @deprecated Use `factory.createCatchClause` or the factory supplied by your transformation context instead. */
const createCatchClause: (variableDeclaration: string | VariableDeclaration | undefined, block: Block) => CatchClause;
const createCatchClause: (variableDeclaration: string | VariableDeclaration | BindingName | undefined, block: Block) => CatchClause;
/** @deprecated Use `factory.updateCatchClause` or the factory supplied by your transformation context instead. */
const updateCatchClause: (node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block) => CatchClause;
/** @deprecated Use `factory.createPropertyAssignment` or the factory supplied by your transformation context instead. */

View file

@ -1,19 +0,0 @@
// ==ORIGINAL==
function /*[#|*/f/*|]*/():Promise<void> {
return fetch('https://typescriptlang.org').then(result => { console.log(result); }, rejection => { console.log("rejected:", rejection); }).catch(err => { console.log(err) });
}
// ==ASYNC FUNCTION::Convert to async function==
async function f():Promise<void> {
try {
try {
const result = await fetch('https://typescriptlang.org');
console.log(result);
} catch (rejection) {
console.log("rejected:", rejection);
}
} catch (err) {
console.log(err);
}
}

View file

@ -1,37 +0,0 @@
// ==ORIGINAL==
function /*[#|*/f/*|]*/():Promise<void> {
return fetch('https://typescriptlang.org').then(res, rej).catch(catch_err)
}
function res(result){
console.log(result);
}
function rej(rejection){
return rejection.ok;
}
function catch_err(err){
console.log(err);
}
// ==ASYNC FUNCTION::Convert to async function==
async function f():Promise<void> {
try {
try {
const result = await fetch('https://typescriptlang.org');
return res(result);
} catch (rejection) {
return rej(rejection);
}
} catch (err) {
return catch_err(err);
}
}
function res(result){
console.log(result);
}
function rej(rejection){
return rejection.ok;
}
function catch_err(err){
console.log(err);
}

View file

@ -15,7 +15,7 @@ function rej(reject){
// ==ASYNC FUNCTION::Convert to async function==
async function f(){
let result: number | Promise<number>;
let result: number;
try {
const result_1 = await fetch("https://typescriptlang.org");
result = await res(result_1);

View file

@ -11,7 +11,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
let x = fetch("https://typescriptlang.org").then(res => console.log(res));
try {
return x;
return await x;
} catch (err) {
return console.log("Error!", err);
}

View file

@ -11,7 +11,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
let x = fetch("https://typescriptlang.org").then(res => console.log(res));
try {
return x;
return await x;
} catch (err) {
return console.log("Error!", err);
}

View file

@ -7,8 +7,6 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
const x = await fetch('https://typescriptlang.org');
return x.statusText;
} catch (e) { }
const x = await fetch('https://typescriptlang.org');
return x.statusText;
}

View file

@ -7,8 +7,6 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
const x = await fetch('https://typescriptlang.org');
return x.statusText;
} catch (e) { }
const x = await fetch('https://typescriptlang.org');
return x.statusText;
}

View file

@ -8,7 +8,7 @@ function /*[#|*/f/*|]*/():Promise<void | Response> {
async function f():Promise<void | Response> {
try {
await fetch('https://typescriptlang.org');
return await fetch('https://typescriptlang.org');
} catch (rejection) {
return console.log("rejected:", rejection);
}

View file

@ -8,7 +8,7 @@ function /*[#|*/f/*|]*/():Promise<void | Response> {
async function f():Promise<void | Response> {
try {
await fetch('https://typescriptlang.org');
return await fetch('https://typescriptlang.org');
} catch (rej) {
return console.log(rej);
}

View file

@ -8,7 +8,7 @@ function /*[#|*/f/*|]*/():Promise<void | Response> {
async function f():Promise<void | Response> {
try {
return fetch('https://typescriptlang.org');
return await fetch('https://typescriptlang.org');
} catch (rej) {
return console.log(rej);
}

View file

@ -8,7 +8,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
try {
await fetch('https://typescriptlang.org');
return await fetch('https://typescriptlang.org');
} catch (rejection) {
return console.log("rejected:", rejection);
}

View file

@ -8,7 +8,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
try {
await fetch('https://typescriptlang.org');
return await fetch('https://typescriptlang.org');
} catch (rejection) {
return console.log("rejected:", rejection);
}

View file

@ -16,7 +16,7 @@ function my_print (resp): Promise<void> {
async function f() {
try {
return my_print(fetch("https://typescriptlang.org").then(res => console.log(res)));
return await my_print(fetch("https://typescriptlang.org").then(res => console.log(res)));
} catch (err) {
return console.log("Error!", err);
}

View file

@ -9,7 +9,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
try {
return fetch(Promise.resolve(1).then(res => "https://typescriptlang.org"));
return await fetch(Promise.resolve(1).then(res => "https://typescriptlang.org"));
} catch (err) {
return console.log(err);
}

View file

@ -9,7 +9,7 @@ function /*[#|*/f/*|]*/() {
async function f() {
try {
return fetch(Promise.resolve(1).then(res => "https://typescriptlang.org"));
return await fetch(Promise.resolve(1).then(res => "https://typescriptlang.org"));
} catch (err) {
return console.log(err);
}

View file

@ -9,7 +9,7 @@ function /*[#|*/f/*|]*/(p: Promise<unknown>) {
async function f(p: Promise<unknown>) {
try {
return p;
return await p;
} catch (error) {
return await Promise.reject(error);
}

View file

@ -7,7 +7,7 @@ function /*[#|*/f/*|]*/(p: Promise<unknown>) {
async function f(p: Promise<unknown>) {
try {
return p;
return await p;
} catch (error) {
return await Promise.reject(error);
}

View file

@ -9,7 +9,7 @@ function /*[#|*/f/*|]*/(p: Promise<unknown>) {
async function f(p: Promise<unknown>) {
try {
return p;
return await p;
} catch (error) {
return await Promise.reject(error);
}

View file

@ -11,7 +11,7 @@ async function f() {
try {
await Promise.resolve();
result = ({ x: 3 });
} catch (e) {
} catch {
result = ({ x: "a" });
}
const { x } = result;

View file

@ -11,7 +11,7 @@ async function f() {
try {
await Promise.resolve();
result = ({ x: 3 });
} catch (e) {
} catch {
result = ({ x: "a" });
}
const { x } = result;

View file

@ -0,0 +1,12 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().catch();
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
return foo();
}

View file

@ -14,9 +14,9 @@ type APIResponse<T> = { success: true, data: T } | { success: false };
async function get() {
try {
return Promise
return await Promise
.resolve<APIResponse<{ email: string; }>>({ success: true, data: { email: "" } });
} catch (e) {
} catch {
const result: APIResponse<{ email: string; }> = ({ success: false });
return result;
}

View file

@ -0,0 +1,19 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().then(x => Promise.resolve(x + 1)).catch(() => 1).then(y => y + 2);
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
let y: number;
try {
const x = await foo();
y = await Promise.resolve(x + 1);
} catch {
y = 1;
}
return y + 2;
}

View file

@ -7,7 +7,5 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
return Promise.resolve();
} catch (e) { }
return Promise.resolve();
}

View file

@ -7,7 +7,5 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
return Promise.resolve();
} catch (e) { }
return Promise.resolve();
}

View file

@ -7,8 +7,6 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
const x = await Promise.resolve(0);
return x;
} catch (e) { }
const x = await Promise.resolve(0);
return x;
}

View file

@ -7,8 +7,6 @@ function /*[#|*/f/*|]*/() {
// ==ASYNC FUNCTION::Convert to async function==
async function f() {
try {
const x = await Promise.resolve(0);
return x;
} catch (e) { }
const x = await Promise.resolve(0);
return x;
}

View file

@ -0,0 +1,16 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().finally(() => console.log("done"));
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
try {
return await foo();
} finally {
return console.log("done");
}
}

View file

@ -0,0 +1,12 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().finally();
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
return foo();
}

View file

@ -0,0 +1,12 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().finally(null);
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
return foo();
}

View file

@ -0,0 +1,12 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().finally(undefined);
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
return foo();
}

View file

@ -0,0 +1,17 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().then(x => x + 1).finally(() => console.log("done"));
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
try {
const x = await foo();
return x + 1;
} finally {
return console.log("done");
}
}

View file

@ -0,0 +1,19 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().then(x => Promise.resolve(x + 1)).finally(() => console.log("done")).then(y => y + 2);
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
let y: number;
try {
const x = await foo();
y = await Promise.resolve(x + 1);
} finally {
console.log("done");
}
return y + 2;
}

View file

@ -0,0 +1,12 @@
// ==ORIGINAL==
declare function foo(): Promise<number>;
function /*[#|*/f/*|]*/(): Promise<number> {
return foo().then();
}
// ==ASYNC FUNCTION::Convert to async function==
declare function foo(): Promise<number>;
async function f(): Promise<number> {
return foo();
}