fix(41405): allow using property access as reference to function (#41429)

This commit is contained in:
Oleksandr T
2021-01-15 03:02:48 +02:00
committed by GitHub
parent 33ea6c581a
commit c83f769850
7 changed files with 202 additions and 16 deletions

View File

@@ -65,7 +65,7 @@ namespace ts.codefix {
const isInJavascript = isInJSFile(functionToConvert);
const setOfExpressionsToReturn = getAllPromiseExpressionsToReturn(functionToConvert, checker);
const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap);
const returnStatements = functionToConvertRenamed.body && isBlock(functionToConvertRenamed.body) ? getReturnStatementsWithPromiseHandlers(functionToConvertRenamed.body) : emptyArray;
const returnStatements = functionToConvertRenamed.body && isBlock(functionToConvertRenamed.body) ? getReturnStatementsWithPromiseHandlers(functionToConvertRenamed.body, checker) : emptyArray;
const transformer: Transformer = { checker, synthNamesMap, setOfExpressionsToReturn, isInJSFile: isInJavascript };
if (!returnStatements.length) {
return;
@@ -90,10 +90,10 @@ namespace ts.codefix {
}
}
function getReturnStatementsWithPromiseHandlers(body: Block): readonly ReturnStatement[] {
function getReturnStatementsWithPromiseHandlers(body: Block, checker: TypeChecker): readonly ReturnStatement[] {
const res: ReturnStatement[] = [];
forEachReturnStatement(body, ret => {
if (isReturnStatementWithFixablePromiseHandler(ret)) res.push(ret);
if (isReturnStatementWithFixablePromiseHandler(ret, checker)) res.push(ret);
});
return res;
}
@@ -374,13 +374,14 @@ namespace ts.codefix {
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) {
// undefined was argument passed to promise handler
break;
}
const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier), /*typeArguments*/ undefined, isSynthIdentifier(argName) ? [argName.identifier] : []);
const synthCall = factory.createCallExpression(getSynthesizedDeepClone(func as Identifier | PropertyAccessExpression), /*typeArguments*/ undefined, isSynthIdentifier(argName) ? [argName.identifier] : []);
if (shouldReturn(parent, transformer)) {
return maybeAnnotateAndReturn(synthCall, parent.typeArguments?.[0]);
}
@@ -410,7 +411,7 @@ namespace ts.codefix {
for (const statement of funcBody.statements) {
if (isReturnStatement(statement)) {
seenReturnStatement = true;
if (isReturnStatementWithFixablePromiseHandler(statement)) {
if (isReturnStatementWithFixablePromiseHandler(statement, transformer.checker)) {
refactoredStmts = refactoredStmts.concat(getInnerTransformationBody(transformer, [statement], prevArgName));
}
else {
@@ -432,7 +433,7 @@ namespace ts.codefix {
seenReturnStatement);
}
else {
const innerRetStmts = isFixablePromiseHandler(funcBody) ? [factory.createReturnStatement(funcBody)] : emptyArray;
const innerRetStmts = isFixablePromiseHandler(funcBody, transformer.checker) ? [factory.createReturnStatement(funcBody)] : emptyArray;
const innerCbBody = getInnerTransformationBody(transformer, innerRetStmts, prevArgName);
if (innerCbBody.length > 0) {
@@ -536,6 +537,9 @@ namespace ts.codefix {
else if (isIdentifier(funcNode)) {
name = getMapEntryOrDefault(funcNode);
}
else if (isPropertyAccessExpression(funcNode) && isIdentifier(funcNode.name)) {
name = getMapEntryOrDefault(funcNode.name);
}
// return undefined argName when arg is null or undefined
// eslint-disable-next-line no-in-operator

View File

@@ -114,7 +114,7 @@ namespace ts {
return !isAsyncFunction(node) &&
node.body &&
isBlock(node.body) &&
hasReturnStatementWithPromiseHandler(node.body) &&
hasReturnStatementWithPromiseHandler(node.body, checker) &&
returnsPromise(node, checker);
}
@@ -129,25 +129,25 @@ namespace ts {
return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator;
}
function hasReturnStatementWithPromiseHandler(body: Block): boolean {
return !!forEachReturnStatement(body, isReturnStatementWithFixablePromiseHandler);
function hasReturnStatementWithPromiseHandler(body: Block, checker: TypeChecker): boolean {
return !!forEachReturnStatement(body, statement => isReturnStatementWithFixablePromiseHandler(statement, checker));
}
export function isReturnStatementWithFixablePromiseHandler(node: Node): node is ReturnStatement & { expression: CallExpression } {
return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression);
export function isReturnStatementWithFixablePromiseHandler(node: Node, checker: TypeChecker): node is ReturnStatement & { expression: CallExpression } {
return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression, checker);
}
// Should be kept up to date with transformExpression in convertToAsyncFunction.ts
export function isFixablePromiseHandler(node: Node): boolean {
export function isFixablePromiseHandler(node: Node, checker: TypeChecker): boolean {
// ensure outermost call exists and is a promise handler
if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) {
if (!isPromiseHandler(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
return false;
}
// ensure all chained calls are valid
let currentNode = node.expression;
while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) {
if (isCallExpression(currentNode) && !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
return false;
}
currentNode = currentNode.expression;
@@ -171,7 +171,7 @@ namespace ts {
}
// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
function isFixablePromiseArgument(arg: Expression): boolean {
function isFixablePromiseArgument(arg: Expression, checker: TypeChecker): boolean {
switch (arg.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
@@ -179,8 +179,16 @@ namespace ts {
visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true);
// falls through
case SyntaxKind.NullKeyword:
case SyntaxKind.Identifier: // identifier includes undefined
return true;
case SyntaxKind.Identifier:
case SyntaxKind.PropertyAccessExpression: {
const symbol = checker.getSymbolAtLocation(arg);
if (!symbol) {
return false;
}
return checker.isUndefinedSymbol(symbol) ||
some(skipAlias(symbol, checker).declarations, d => isFunctionLike(d) || hasInitializer(d) && !!d.initializer && isFunctionLike(d.initializer));
}
default:
return false;
}

View File

@@ -539,6 +539,7 @@ function [#|f|]():Promise<void | Response> {
}
`
);
_testConvertToAsyncFunction("convertToAsyncFunction_NoRes3", `
function [#|f|]():Promise<void | Response> {
return fetch('https://typescriptlang.org').catch(rej => console.log(rej));
@@ -600,6 +601,7 @@ function [#|f|]():Promise<void> {
}
`
);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRef", `
function [#|f|]():Promise<boolean> {
return fetch('https://typescriptlang.org').then(res);
@@ -609,6 +611,75 @@ function res(result){
}
`
);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRef1", `
class Foo {
public [#|method|](): Promise<boolean> {
return fetch('a').then(this.foo);
}
private foo(res) {
return res.ok;
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRef2", `
class Foo {
public [#|method|](): Promise<Response> {
return fetch('a').then(this.foo);
}
private foo = res => res;
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRef3", `
const res = (result) => {
return result.ok;
}
function [#|f|](): Promise<boolean> {
return fetch('https://typescriptlang.org').then(res);
}
`
);
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef1", `
const res = 1;
function [#|f|]() {
return fetch('https://typescriptlang.org').then(res);
}
`
);
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef2", `
class Foo {
private foo = 1;
public [#|method|](): Promise<boolean> {
return fetch('a').then(this.foo);
}
}
`
);
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef3", `
const res = undefined;
function [#|f|]() {
return fetch('https://typescriptlang.org').then(res);
}
`
);
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionResRef4", `
class Foo {
private foo = undefined;
public [#|method|](): Promise<boolean> {
return fetch('a').then(this.foo);
}
}
`
);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal", `
function [#|f|]():Promise<void> {
return fetch('https://typescriptlang.org').then(res);
@@ -618,6 +689,19 @@ function res(result){
}
`
);
_testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal1", `
class Foo {
public [#|method|](): Promise<void> {
return fetch('a').then(this.foo);
}
private foo(res) {
console.log(res);
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_NoBrackets", `
function [#|f|]():Promise<void> {
return fetch('https://typescriptlang.org').then(result => console.log(result));