Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Promises to Async Await Code Fix #24939

Conversation

elizabethdinella
Copy link
Contributor

Fixes #

@elizabethdinella
Copy link
Contributor Author

Adding @bterlson

@mhegazy mhegazy requested a review from a user June 13, 2018 23:30
}

function getPromiseCall(node:Node): CallExpression{
if(node.kind == SyntaxKind.CallExpression){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. mind running the formatter on the changed files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also mind running the linter task, jake lint

@@ -1050,6 +1051,12 @@ namespace ts.Completions {
}
}

if (isMetaProperty(node) && (node.keywordToken === SyntaxKind.NewKeyword || node.keywordToken === SyntaxKind.ImportKeyword)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you merge from master one more time. i think these should already be in master now.

break;
}

const returnType = checker.getReturnTypeOfSignature(checker.getSignatureFromDeclaration(<FunctionDeclaration | FunctionExpression> node))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit these to non-ambient functions. an easy check is if the function does not have a body, break

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about methods, and arrow functions?

if (isJsFile) {
const symbol = node.symbol;
if (symbol.members && (symbol.members.size > 0)) {
diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration));
}
}

if(node.modifiers && containsAsync(node.modifiers)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasModifier(node, ModifierFlags.Async)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually you can just call isAsyncFunction

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also think about async generators.. i would exclude these for now.

return false;
}

function isPromiseType(T:Type):boolean{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a better check is to expose the checker's getPromisedTypeOfPromise, call that, if it returns a type, then it is Promise, if it returns undefined it is not.

break;
}

if(isPromiseType(returnType)){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure this is sufficient check to be able convert it. e.g.:

function f() {
    return g();
}

async function g() { return 0; }

f returns a Promise, but there is no reason why this should be converted to an async function.

I think a better way to look at this is not the async part but rather the await part. if the function includes a possible use of await. in other words, there a call to .then on a promise in the function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are talking about it should be the use of any of .then, .catch and .finally

@@ -104,4 +119,18 @@ namespace ts {
function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node {
return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator;
}

function containsAsync(arr: NodeArray<Modifier>): boolean{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the hasModifier helper.

return false;
}

function isPromiseType(T:Type):boolean{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to think about a few things:

  1. There could be multiple types named Promise, and they might not be the ones of interest. So you need some other API to introspect the type.
  2. Usually with async/await, we're less concerned with whether the type is a Promise and more about whether it is "thenable". Yes, that means that converting from async/await changes your types in some cases, but this is probably okay.

return (<PropertyAccessExpression>node.expression).name.text === funcName && isPromiseType(checker.getTypeAtLocation(node));
}

function isPromiseType(T:Type):boolean{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You defined this below


function parseThen(node:CallExpression, checker:TypeChecker): Statement[]{
let res = node.arguments[0];
let rej = node.arguments[1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let [res, rej] = node.arguments

return node as CallExpression;
}

for( let child of node.getChildren().filter(node => isNode(node)) ){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're looking for the forEachChild in parser.ts. But if not you can also just use

return node.getChildren().find(isNode);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getChildren() is the wrong method to use since it returns the tokens, such as an opening parenthesis.

/// <reference path='fourslash.ts' />

////function f():Promise<any> {
//// return fetch('http://yahoo.com').then(res);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just avoid potential trademark issues altogether and find/replace these with typescriptlang.org

reportsUnnecessary: true,
}]);

verify.codeFix({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description here looks wrong.

},
"Convert to use async and await":{
"category": "Message",
"code": 95055

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an extra space here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer "Convert to async function", in case anyone else does.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for @bterlson comment. also for the suggestion message as well.

index: 0,
newFileContent:
`async function f() {
var result = await fetch('http://yahoo.com);
Copy link

@ghost ghost Jun 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should default to const over var. But a const can't be reassigned, so you'd have to not use the name result twice if there is more than one awaited result.

Actually, the variable is unnecessary here since return res(await fetch()) is closer to what the original code did. So maybe it's never necessary to synthesize a name.

}catch(rejection){
console.log("rejected", rejection);
}
console.log(result);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an exception then result will be undefined -- don't think the original code had that issue.

@@ -0,0 +1,6 @@
/// <reference path='fourslash.ts' />

////import./**/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a separate PR?

function isPromiseType(T:Type):boolean{
return T.flags === TypeFlags.Object && T.symbol.name === "Promise";
function getReturnStmts(node: Node, retStmts: ReturnStatement[]) {
for (const child of node.getChildren().filter(node => isNode(node))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to getChildren. this is a call that will reiterate the tokens in the tree, and since we do not care about tokens, only return statement nodes, we do not need to do that, and we do not need the .filter either.

also, you can use forEachReturnStatement instead.

// collect all the return statements
// check that a property access expression exists in there and that it is a handler
const retStmts: ReturnStatement[] = [];
getReturnStmts(node, retStmts);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would recommend having all this logic in one function, and sharing it between the quick fix and the suggestion code. this way we can use them to report the suggestion, and check in the quick fix if the assumptions are still valid.

this comes up when VSCode, say call us to apply a fix after a code change, so it is possible that the previous code where we reported the suggestion has change.

@elizabethdinella elizabethdinella force-pushed the promisesToAsyncAwaitCodeFix branch from 0719ad2 to e6c676e Compare August 9, 2018 18:28
Copy link
Member

@RyanCavanaugh RyanCavanaugh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments

return setOfExpressionsToReturn;
}

function isPromiseReturningExpression(node: Node, checker: TypeChecker, name?: string): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For any of these predicate-type functions, an explanatory comment showing a 'true' and 'false' case would be really helpful. For this function, for example, it's not obvious to me what name is for

const symbolIdString = getSymbolId(symbol).toString();

// if the identifier refers to a function we want to add the new synthesized variable for the declaration (ex. blob in let blob = res(arg))
// Note - the choice of the first call signature is arbitrary
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given a list of call signatures where you don't know which to pick, it's generally best in TypeScript to pick the last rather than the first.

}

let nodeType = transformer.checker.getTypeAtLocation(node);
if (nodeType && nodeType.flags & 1 && (<IntrinsicType>nodeType).intrinsicName === "error" && isIdentifier(node)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1?

const numberOfAssignmentsOriginal = 0, numberOfAssignmentsSynthesized = 0;
const types: Type[] = [];

function getMapEntryIfExists(node: Identifier): SynthIdentifier {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have helper functions entirely at the bottom of their containing functions - it's confusing to have imperative lines both above and below them

@@ -1626,14 +1626,34 @@ namespace ts {
* WARNING: This is an expensive operation and is only intended to be used in refactorings
* and code fixes (because those are triggered by explicit user actions).
*/
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true): T {
const clone = node && getSynthesizedDeepCloneWorker(node as NonNullable<T>);
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true, renameMap?: Map<Identifier>, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather this be left as-is and have the extra logic in the refactor code file. I don't want getSynthesizedDeepClone to turn into a Swiss Army Knife of Arbitrary Transforms

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggested putting it here because it requires a deep clone. Are you okay with the code-fix-specific helper being largely copied from the shared helper?

async function f(){
let result;
try {
let result_1 = await fetch("https://typescriptlang.org");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not const?

interface SynthIdentifier {
identifier: Identifier;
types: Type[];
numberOfAssignmentsOriginal: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is numberOfAssignmentsOriginal still useful or can you just use types.length?

@@ -1626,14 +1626,34 @@ namespace ts {
* WARNING: This is an expensive operation and is only intended to be used in refactorings
* and code fixes (because those are triggered by explicit user actions).
*/
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true): T {
const clone = node && getSynthesizedDeepCloneWorker(node as NonNullable<T>);
export function getSynthesizedDeepClone<T extends Node | undefined>(node: T, includeTrivia = true, renameMap?: Map<Identifier>, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggested putting it here because it requires a deep clone. Are you okay with the code-fix-specific helper being largely copied from the shared helper?

@elizabethdinella elizabethdinella force-pushed the promisesToAsyncAwaitCodeFix branch from 8b17532 to 25edf4c Compare August 9, 2018 21:29
synthNamesMap: Map<SynthIdentifier>; // keys are the symbol id of the identifier
allVarNames: SymbolAndIdentifier[];
setOfExpressionsToReturn: Map<true>; // keys are the node ids of the expressions
context: CodeFixContextBase;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be unused.

if (hasPrevArgName && !shouldReturn) {
const type = transformer.checker.getTypeAtLocation(func);
const callSignatures = type && transformer.checker.getSignaturesOfType(type, SignatureKind.Call);
const returnType = callSignatures && callSignatures[0].getReturnType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think all overloads have to have the same return type in TS.

}

function declaredInFile(symbol: Symbol, sourceFile: SourceFile): boolean {
return symbol.valueDeclaration && symbol.valueDeclaration.getSourceFile() === sourceFile;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like symbol.valueDeclaration can be undefined.


return getSynthesizedDeepClone(nodeToRename, /*includeTrivia*/ true, identsToRenameMap, checker, deepCloneCallback);

function isExpressionOrCallOnTypePromise(child: Node): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like child: SignatureDeclaration.

}

// dispatch function to recursively build the refactoring
function transformExpression(node: Expression, transformer: Transformer, outermostParent: CallExpression, prevArgName?: SynthIdentifier): Statement[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should prevArgName be in Transformer?

}

let nodeType = transformer.checker.getTypeAtLocation(node);
if (nodeType && nodeType.flags & TypeFlags.Any && (<IntrinsicType>nodeType).intrinsicName === "error" && isIdentifier(node)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about eliminating the error-type check.


function transformPromiseCall(node: Expression, transformer: Transformer, prevArgName?: SynthIdentifier): Statement[] {
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(node).toString());
const hasPrevArgName = prevArgName && prevArgName.identifier.text.length > 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what circumstances is the identifier empty?

return createNodeArray([]);
}

function getReturnStatementsWithPromiseHandlersIndices(block: Block): number[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just check getReturnStatementsWithPromiseHandlers in the loop above?

const temp = transformExpression(node, transformer, node, prevArgName);
innerCbBody = innerCbBody.concat(temp);
if (innerCbBody.length > 0) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this early return have any effect?


if (isFunctionLikeDeclaration(funcNode)) {
if (funcNode.parameters.length > 0) {
const param = funcNode.parameters[0].name as Identifier;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this cast safe?

}

function getSymbol(node: Node): Symbol | undefined {
return node.symbol ? node.symbol : transformer.checker.getSymbolAtLocation(node);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check node.symbol in the other places where getSymbolAtLocation is called (or in getSymbolAtLocation itself)? Note: this is a question, not a prompt - I don't understand the difference between the two mechanisms.

function getMapEntryIfExists(node: Identifier): SynthIdentifier {
const originalNode = getOriginalNode(node);
const symbol = getSymbol(originalNode);
const identifier = node;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call the parameter "identifier"?

@@ -109,7 +113,64 @@ namespace ts {
}
}

function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: DiagnosticWithLocation[]): void {

const functionType = node.type ? checker.getTypeFromTypeNode(node.type) : undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should get the type of node and not consume node.type directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        if (isAsyncFunction(node) || !node.body) {
            return;
        }

        const functionType = checker.getTypeAtLocation(node);
        if (!functionType)
        {
            return;
        }

@amcasey
Copy link
Member

amcasey commented Aug 9, 2018

function f() {
	return Promise.resolve().then(() => 1, () => "a"); 
}

Generates catch(), which is an error.

Same thing if the parameter is named _.

@amcasey
Copy link
Member

amcasey commented Aug 9, 2018

function f() {
	return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x); 
}

Generates

async function f() {
	let x_1: string | number;
    try {
        const x = await Promise.resolve();
        x_1 = 1;
    }
    catch (x_1) {
        x_1 = "a";
    }
    return !!x_1; 
}

There's a collision between the catch parameter and let x_1.

@amcasey
Copy link
Member

amcasey commented Aug 10, 2018

function f() {
	return Promise.resolve().then(function g(x) {
		try {
			return Promise.resolve().then(y => 1);
		}
		catch (e) {
			e;
		}
	}); 
}

The try-catch is eliminated, which seems questionable.

@amcasey
Copy link
Member

amcasey commented Aug 10, 2018

function f() {
	return Promise.resolve().then(f ? (x => 1) : (y => 2));
}

Generates

async function f() {
	await Promise.resolve();
}

@billti
Copy link
Member

billti commented Aug 10, 2018

Re the comment above that

"There's a collision between the catch parameter and let x_1"

A catch clause introduces a new lexical scope, and its parameters don't impact outside the scope. (It's a little more nuanced than that - the answer on https://stackoverflow.com/questions/7926237/javascript-catch-clause-scope is a good reference).

@amcasey
Copy link
Member

amcasey commented Aug 10, 2018

@billti My concern was that the assignment in the catch block seemed to update the catch parameter, rather than the local being used for the return value.

@amcasey
Copy link
Member

amcasey commented Aug 10, 2018

Subsumed by #26373

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants