-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
[WIP] Promises to Async Await Code Fix #24939
Conversation
Adding @bterlson |
} | ||
|
||
function getPromiseCall(node:Node): CallExpression{ | ||
if(node.kind == SyntaxKind.CallExpression){ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/services/completions.ts
Outdated
@@ -1050,6 +1051,12 @@ namespace ts.Completions { | |||
} | |||
} | |||
|
|||
if (isMetaProperty(node) && (node.keywordToken === SyntaxKind.NewKeyword || node.keywordToken === SyntaxKind.ImportKeyword)) { |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasModifier(node, ModifierFlags.Async)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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)){ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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:
- 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. - 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{ |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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)) ){ |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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.
src/compiler/diagnosticMessages.json
Outdated
}, | ||
"Convert to use async and await":{ | ||
"category": "Message", | ||
"code": 95055 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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./**/ |
There was a problem hiding this comment.
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))) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
0719ad2
to
e6c676e
Compare
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
src/services/utilities.ts
Outdated
@@ -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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
?
src/services/utilities.ts
Outdated
@@ -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 { |
There was a problem hiding this comment.
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?
8b17532
to
25edf4c
Compare
…code review comments regarding the original types map
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
}
function f() {
return Promise.resolve().then(() => 1, () => "a");
} Generates Same thing if the parameter is named |
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 |
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. |
function f() {
return Promise.resolve().then(f ? (x => 1) : (y => 2));
} Generates async function f() {
await Promise.resolve();
} |
Re the comment above that "There's a collision between the catch parameter and let x_1" A |
@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. |
Subsumed by #26373 |
Fixes #