Skip to content

Clean up convert-to-async refactor #36858

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

Merged
merged 2 commits into from
Mar 3, 2020

Conversation

andrewbranch
Copy link
Member

I have several convert-to-async bugs assigned to me, and as I started to work on them, I was frustrated by vague names, and was also confused about what some blocks of code were doing. To figure it out, I set breakpoints in the confusing code and debugged all the tests, and the breakpoints were never hit. So, as I was unable to determine the utility of large chunks of logic, and they had zero test coverage (and yet the refactor on the whole has pretty good test coverage), I decided to delete those chunks of logic. I think that much of it was predicated on impossible conditions, but I can’t be 100% certain.

There was also a bit of logical branching that I determined was only necessary because identifiers weren’t being properly cloned in getSynthesizedDeepClone. After fixing that, two test baselines changed, revealing that they actually contained bugs before. (Remarkably, one was missing an await and seemed to be fundamentally broken; the other was just a comment being deleted when it should have been preserved. But otherwise, this PR does not change the behavior of the refactor as observable in any existing test.

@@ -32,7 +32,8 @@ namespace ts.codefix {
readonly kind: SynthBindingNameKind.Identifier;
readonly identifier: Identifier;
readonly types: Type[];
numberOfAssignmentsOriginal: number; // number of times the variable should be assigned in the refactor
/** A declaration for this identifier has already been generated */
hasBeenDeclared: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

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

This was used for determining whether to declare variables with let or const, but I determined the only time let ever occurred was after generating code like

let x: string | number;
try {
  x = await getString();
} catch {
  x = await getNumber();
}

and numberOfAssignmentsOriginal was being explicitly set to 2. In every other code path, the number was always 0. So, the property became a more descriptively-named boolean.

function getConstIdentifiers(synthNamesMap: ReadonlyMap<SynthIdentifier>): Identifier[] {
const constIdentifiers: Identifier[] = [];
synthNamesMap.forEach((val) => {
if (val.numberOfAssignmentsOriginal === 0) {
Copy link
Member Author

Choose a reason for hiding this comment

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

As mentioned earlier, at the time this function was called, this condition was always true.

Returns true if node is a promise returning expression
If name is not undefined, node is a promise returning call of name
*/
function isPromiseReturningExpression(node: Node, checker: TypeChecker, name?: string): boolean {
Copy link
Member Author

Choose a reason for hiding this comment

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

This function had a totally different meaning based on whether name was provided, so I split it up

Comment on lines -245 to -264
function deepCloneCallback(node: Node, clone: Node) {
if (isIdentifier(node)) {
const symbol = checker.getSymbolAtLocation(node);
const symboldIdString = symbol && getSymbolId(symbol).toString();
const renameInfo = symbol && synthNamesMap.get(symboldIdString!);

if (renameInfo) {
const type = checker.getTypeAtLocation(node);
originalType.set(getNodeId(clone).toString(), type);
}
}

const val = setOfAllExpressionsToReturn.get(getNodeId(node).toString());
if (val !== undefined) {
setOfAllExpressionsToReturn.delete(getNodeId(node).toString());
setOfAllExpressionsToReturn.set(getNodeId(clone).toString(), val);
}
}

}
Copy link
Member Author

Choose a reason for hiding this comment

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

This function runs for every (original, clone) pair of nodes during getSynthesizedDeepClone, and it was essentially just eagerly doing a lot of work that could be done on demand. I think the root reason it exists is because getDeepSynthesizedClone was failing to set node.original on identifiers, and so this was previously the last chance to get a reference to any identifiers in the original source. After fixing cloning identifiers, this function became useless, because you can always count on clonedNode.original to exist.

}
else {
const identifier = getSynthesizedDeepClone(node);
identsToRenameMap.set(symbolIdString, identifier);
synthNamesMap.set(symbolIdString, createSynthIdentifier(identifier, [], allVarNames.filter(elem => elem.identifier.text === node.text).length/*, numberOfAssignmentsSynthesized: 0*/));
Copy link
Member Author

Choose a reason for hiding this comment

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

This filter expression always evaluated to 0, and I couldn’t figure out a test case to make it not 0, but I’m not 100% convinced one doesn’t exist.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be noted that I did find an additional case where let should be used instead of const, but the code before this PR produces the wrong result (uses const):

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 add a test for this if it's not already covered?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I meant that both master and my PR produce an incorrect result on this pattern. I planned to fix it and add a test for it in a later PR. This PR attempts not to change the existing tested behavior at all (but two bugs were fixed accidentally by cloning identifiers correctly).

Comment on lines +251 to +252
Debug.assertNode(node.original!.parent, isPropertyAccessExpression);
return transformPromiseExpressionOfPropertyAccess(node, transformer, prevArgName);
Copy link
Member Author

Choose a reason for hiding this comment

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

This condition on node.original.parent always holds in the existing tests, but I’m not 100% sure why.

function getFlagOfBindingName(bindingName: SynthBindingName, constIdentifiers: readonly Identifier[]): NodeFlags {
const identifiers = getIdentifierTextsFromBindingName(getNode(bindingName));
const inArr: boolean = constIdentifiers.some(elem => contains(identifiers, elem.text));
return inArr ? NodeFlags.Const : NodeFlags.Let;
Copy link
Member Author

Choose a reason for hiding this comment

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

It was always NodeFlags.Const.

Comment on lines -414 to -417
function transformPromiseCall(node: Expression, transformer: Transformer, prevArgName?: SynthBindingName): readonly Statement[] {
const shouldReturn = transformer.setOfExpressionsToReturn.get(getNodeId(node).toString());
// the identifier is empty when the handler (.then()) ignores the argument - In this situation we do not need to save the result of the promise returning call
const originalNodeParent = node.original ? node.original.parent : node.parent;
Copy link
Member Author

Choose a reason for hiding this comment

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

This shows the confusion caused by node.original being missing for identifiers. There seems to be an assumption that if originalNodeParent was missing, it was probably a property access expression. What I found after ensuring that node.original.parent always exists is that it is always a property access expression, so parts of the following condition were always true.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, the original name was confusing—what is a “promise call”? The node passed here wasn’t even a call expression much of the time.

getSynthesizedDeepClone(getNode(variableName)),
/*type*/ undefined,
rightHandSide)],
NodeFlags.Const))];
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I'm a little surprised this always comes out const, but if nothing breaks...

Copy link
Contributor

@uniqueiniquity uniqueiniquity left a comment

Choose a reason for hiding this comment

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

Other than a general sense that you've removed every way that this could emit a let declaration, looks good to me.

@andrewbranch
Copy link
Member Author

Thanks @uniqueiniquity! Looking forward to seeing the future bug reports that demonstrate what all this deleted code was supposed to do in the first place 😄

@andrewbranch andrewbranch merged commit 3c468d2 into microsoft:master Mar 3, 2020
@andrewbranch andrewbranch deleted the bug/convert-to-async branch March 3, 2020 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants