Only provide suggestion for outermost async fix

This commit is contained in:
Benjamin Lichtman 2018-12-28 16:37:59 -08:00
parent 4a664d690b
commit 2dd6e20ef9
22 changed files with 148 additions and 67 deletions

View file

@ -60,7 +60,7 @@ namespace ts.codefix {
const setOfExpressionsToReturn = getAllPromiseExpressionsToReturn(functionToConvert, checker);
const functionToConvertRenamed = renameCollidingVarNames(functionToConvert, checker, synthNamesMap, context, setOfExpressionsToReturn, originalTypeMap, allVarNames);
const constIdentifiers = getConstIdentifiers(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, allVarNames, setOfExpressionsToReturn, constIdentifiers, originalTypeMap, isInJSFile: isInJavascript };
if (!returnStatements.length) {
@ -87,10 +87,10 @@ namespace ts.codefix {
}
}
function getReturnStatementsWithPromiseHandlers(body: Block): ReadonlyArray<ReturnStatement> {
function getReturnStatementsWithPromiseHandlers(body: Block, checker: TypeChecker): ReadonlyArray<ReturnStatement> {
const res: ReturnStatement[] = [];
forEachReturnStatement(body, ret => {
if (isReturnStatementWithFixablePromiseHandler(ret)) res.push(ret);
if (isReturnStatementWithFixablePromiseHandler(ret, checker)) res.push(ret);
});
return res;
}
@ -450,7 +450,7 @@ namespace ts.codefix {
seenReturnStatement = true;
}
if (isReturnStatementWithFixablePromiseHandler(statement)) {
if (isReturnStatementWithFixablePromiseHandler(statement, transformer.checker)) {
refactoredStmts = refactoredStmts.concat(getInnerTransformationBody(transformer, [statement], prevArgName));
}
else {
@ -466,7 +466,7 @@ namespace ts.codefix {
seenReturnStatement);
}
else {
const innerRetStmts = isFixablePromiseHandler(funcBody) ? [createReturn(funcBody)] : emptyArray;
const innerRetStmts = isFixablePromiseHandler(funcBody, transformer.checker) ? [createReturn(funcBody)] : emptyArray;
const innerCbBody = getInnerTransformationBody(transformer, innerRetStmts, prevArgName);
if (innerCbBody.length > 0) {

View file

@ -1,5 +1,7 @@
/* @internal */
namespace ts {
const visitedNestedConvertibleFunctions = createMap<true>();
export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] {
program.getSemanticDiagnostics(sourceFile, cancellationToken);
const diags: DiagnosticWithLocation[] = [];
@ -13,6 +15,7 @@ namespace ts {
const isJsFile = isSourceFileJS(sourceFile);
visitedNestedConvertibleFunctions.clear();
check(sourceFile);
if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) {
@ -114,17 +117,21 @@ namespace ts {
}
function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push<DiagnosticWithLocation>): void {
if (!isAsyncFunction(node) &&
node.body &&
isBlock(node.body) &&
hasReturnStatementWithPromiseHandler(node.body) &&
returnsPromise(node, checker)) {
if (!visitedNestedConvertibleFunctions.has(getKeyFromNode(node)) && isConvertableFunction(node, checker)) {
diags.push(createDiagnosticForNode(
!node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node,
Diagnostics.This_may_be_converted_to_an_async_function));
}
}
function isConvertableFunction(node: FunctionLikeDeclaration, checker: TypeChecker) {
return !isAsyncFunction(node) &&
node.body &&
isBlock(node.body) &&
hasReturnStatementWithPromiseHandler(node.body, checker) &&
returnsPromise(node, checker);
}
function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean {
const functionType = checker.getTypeAtLocation(node);
const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call);
@ -136,25 +143,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, stmt => isReturnStatementWithFixablePromiseHandler(stmt, checker));
}
export function isReturnStatementWithFixablePromiseHandler(node: Node): node is ReturnStatement {
return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression);
export function isReturnStatementWithFixablePromiseHandler(node: Node, checker: TypeChecker): node is ReturnStatement {
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;
@ -167,16 +174,24 @@ 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.NullKeyword:
case SyntaxKind.Identifier: // identifier includes undefined
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
if (isConvertableFunction(arg as FunctionLikeDeclaration, checker)) {
visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true);
}
/* falls through */
case SyntaxKind.NullKeyword:
case SyntaxKind.Identifier: // identifier includes undefined
return true;
default:
return false;
}
}
function getKeyFromNode(exp: FunctionLikeDeclaration) {
return `${exp.pos.toString()}:${exp.end.toString()}`;
}
}

View file

@ -255,7 +255,7 @@ interface String { charAt: any; }
interface Array<T> {}`
};
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false) {
function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) {
const t = extractTest(text);
const selectionRange = t.ranges.get("selection")!;
if (!selectionRange) {
@ -307,7 +307,7 @@ interface Array<T> {}`
const actions = codefix.getFixes(context);
const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message);
if (expectFailure) {
if (expectFailure && !onlyProvideAction) {
assert.isNotTrue(action && action.changes.length > 0);
return;
}
@ -1151,25 +1151,25 @@ function [#|f|]() {
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", `
const [#|foo|] = function () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionWithName", `
const foo = function [#|f|]() {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionAssignedToBindingPattern", `
const { length } = [#|function|] () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParams", `
function [#|f|]() {
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_bindingPattern", `
@ -1178,7 +1178,7 @@ function [#|f|]() {
}
function res({ status, trailer }){
console.log(status);
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", `
@ -1188,7 +1188,7 @@ function [#|f|]() {
}
function res({ status, trailer }){
console.log(status);
}
}
`);
_testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", `
@ -1209,7 +1209,7 @@ function [#|f|]() {
}
function res(result) {
return Promise.resolve().then(x => console.log(result));
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromise", `
@ -1241,7 +1241,7 @@ function [#|f|]() {
return Promise.resolve(1)
.then(x => Promise.reject(x))
.catch(err => console.log(err));
}
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", `
@ -1266,6 +1266,22 @@ _testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", `
export function [#|foo|]() {
return fetch('https://typescriptlang.org').then(s => console.log(s));
}
`);
_testConvertToAsyncFunction("convertToAsyncFunction_OutermostOnlySuccess", `
function [#|foo|]() {
return fetch('a').then(() => {
return fetch('b').then(() => 'c');
})
}
`);
_testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", `
function foo() {
return fetch('a').then([#|() => {|]
return fetch('b').then(() => 'c');
})
}
`);
});
@ -1276,4 +1292,8 @@ export function [#|foo|]() {
function _testConvertToAsyncFunctionFailed(caption: string, text: string) {
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true);
}
function _testConvertToAsyncFunctionFailedSuggestion(caption: string, text: string) {
testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true);
}
}

View file

@ -0,0 +1,16 @@
// ==ORIGINAL==
function foo() {
return fetch('a').then(/*[#|*/() => {/*|]*/
return fetch('b').then(() => 'c');
})
}
// ==ASYNC FUNCTION::Convert to async function==
function foo() {
return fetch('a').then(async () => {
await fetch('b');
return 'c';
})
}

View file

@ -0,0 +1,15 @@
// ==ORIGINAL==
function /*[#|*/foo/*|]*/() {
return fetch('a').then(() => {
return fetch('b').then(() => 'c');
})
}
// ==ASYNC FUNCTION::Convert to async function==
async function foo() {
await fetch('a');
await fetch('b');
return 'c';
}

View file

@ -0,0 +1,15 @@
// ==ORIGINAL==
function /*[#|*/foo/*|]*/() {
return fetch('a').then(() => {
return fetch('b').then(() => 'c');
})
}
// ==ASYNC FUNCTION::Convert to async function==
async function foo() {
await fetch('a');
await fetch('b');
return 'c';
}

View file

@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
}
function res({ status, trailer }){
console.log(status);
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -15,4 +15,4 @@ async function f() {
}
function res({ status, trailer }){
console.log(status);
}
}

View file

@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
}
function res({ status, trailer }){
console.log(status);
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -15,4 +15,4 @@ async function f() {
}
function res({ status, trailer }){
console.log(status);
}
}

View file

@ -6,7 +6,7 @@ function /*[#|*/f/*|]*/() {
}
function res({ status, trailer }){
console.log(status);
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -17,4 +17,4 @@ async function f() {
}
function res({ status, trailer }){
console.log(status);
}
}

View file

@ -6,7 +6,7 @@ function /*[#|*/f/*|]*/() {
}
function res({ status, trailer }){
console.log(status);
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -17,4 +17,4 @@ async function f() {
}
function res({ status, trailer }){
console.log(status);
}
}

View file

@ -4,7 +4,7 @@ function /*[#|*/f/*|]*/() {
return Promise.resolve(1)
.then(x => Promise.reject(x))
.catch(err => console.log(err));
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -16,4 +16,4 @@ async function f() {
catch (err) {
return console.log(err);
}
}
}

View file

@ -4,7 +4,7 @@ function /*[#|*/f/*|]*/() {
return Promise.resolve(1)
.then(x => Promise.reject(x))
.catch(err => console.log(err));
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -16,4 +16,4 @@ async function f() {
catch (err) {
return console.log(err);
}
}
}

View file

@ -1,8 +1,8 @@
// ==ORIGINAL==
function /*[#|*/f/*|]*/() {
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
// ==ASYNC FUNCTION::Convert to async function==
@ -15,5 +15,5 @@ async function f() {
catch (x_1) {
x_2 = "a";
}
return !!x_2;
}
return !!x_2;
}

View file

@ -1,8 +1,8 @@
// ==ORIGINAL==
function /*[#|*/f/*|]*/() {
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x);
}
// ==ASYNC FUNCTION::Convert to async function==
@ -15,5 +15,5 @@ async function f() {
catch (x_1) {
x_2 = "a";
}
return !!x_2;
}
return !!x_2;
}

View file

@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
}
function res(result) {
return Promise.resolve().then(x => console.log(result));
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -16,4 +16,4 @@ async function f() {
}
function res(result) {
return Promise.resolve().then(x => console.log(result));
}
}

View file

@ -5,7 +5,7 @@ function /*[#|*/f/*|]*/() {
}
function res(result) {
return Promise.resolve().then(x => console.log(result));
}
}
// ==ASYNC FUNCTION::Convert to async function==
@ -16,4 +16,4 @@ async function f() {
}
function res(result) {
return Promise.resolve().then(x => console.log(result));
}
}

View file

@ -2,11 +2,11 @@
const /*[#|*/foo/*|]*/ = function () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const foo = async function () {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}

View file

@ -2,11 +2,11 @@
const /*[#|*/foo/*|]*/ = function () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const foo = async function () {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}

View file

@ -2,11 +2,11 @@
const { length } = /*[#|*/function/*|]*/ () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const { length } = async function () {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}

View file

@ -2,11 +2,11 @@
const { length } = /*[#|*/function/*|]*/ () {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const { length } = async function () {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}

View file

@ -2,11 +2,11 @@
const foo = function /*[#|*/f/*|]*/() {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const foo = async function f() {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}

View file

@ -2,11 +2,11 @@
const foo = function /*[#|*/f/*|]*/() {
return fetch('https://typescriptlang.org').then(result => { console.log(result) });
}
}
// ==ASYNC FUNCTION::Convert to async function==
const foo = async function f() {
const result = await fetch('https://typescriptlang.org');
console.log(result);
}
}