Revised emit for computed property names, including with decorators#19430
Conversation
9ed0edb to
b3a1cf2
Compare
|
@rbuckton This PR implements the emit as you suggested in-person. Is anything missing? |
| write("enum "); | ||
| emit(node.name); | ||
| pushNameGenerationScope(); | ||
| pushNameGenerationScope(node); |
There was a problem hiding this comment.
I suppose enums aren't really a place you can inject temporary variables either, so they probably shouldn't be a name-generation scope (even though that only really matters for javavascript output and enums are typescript only).
| Iterator = 1 << 23, // The expression to a `yield*` should be treated as an Iterator when down-leveling, not an Iterable. | ||
| NoAsciiEscaping = 1 << 24, // When synthesizing nodes that lack an original node or textSourceNode, we want to write the text on the node with ASCII escaping substitutions. | ||
| /*@internal*/ TypeScriptClassWrapper = 1 << 25, // The node is an IIFE class wrapper created by the ts transform. | ||
| /*@internal*/ NoNameGenerationScope = 1 << 26, // Used to indicate when a function or other wrapper shouldn't be considered a name generation scope because its generated locals are merged with a higher scope |
There was a problem hiding this comment.
We already have ReuseTempVariableScope which is supposed to mean the same thing. I like your approach where you added node to pushNameGenerationScope and popNameGenerationScope. Perhaps we can combine the two approaches and deduplicate the behavior in the emitter?
| if (name.autoGenerateKind === GeneratedIdentifierKind.Node) { | ||
| // Node names generate unique names based on their original node | ||
| // and are cached based on that node's id. | ||
| let flags: TempFlags; |
There was a problem hiding this comment.
Rather than read twice, the approach we've taken in other places (such as with context flags in the parser) is to do something like this:
if (name.skipNameGenerationScope) {
const savedTempFlags = tempFlags;
popNameGenerationScope(/*node*/ undefined);
const result = generateNameCached(getNodeForGeneratedName(name));
pushNameGenerationScope(/*node*/ undefined);
tempFlags = savedTempFlags;
}
else {
return generateNameCached(getNodeForGeneratedName(name));
}| // transformation. | ||
| if (getEmitFlags(node) & EmitFlags.Indented) { | ||
| setEmitFlags(classFunction, EmitFlags.Indented); | ||
| setEmitFlags(classFunction, EmitFlags.Indented | EmitFlags.NoNameGenerationScope); |
There was a problem hiding this comment.
Perhaps collapse/remove the if:
setEmitFlags(classFunction, (getEmitFlags(node) & EmitFlags.Indented) | EmitFlags.NoNameGenerationScope);|
|
||
| case SyntaxKind.PropertyDeclaration: | ||
| // TypeScript property declarations are elided. | ||
| // TypeScript property declarations are elided. However their names are still visited, and can potentially be retained if they could have sideeffects |
There was a problem hiding this comment.
Can we do this in a visitPropertyDeclaration function instead?
| } | ||
| return C; | ||
| }()); | ||
| var _a; |
There was a problem hiding this comment.
We seem to now have an unnecessary variable here.
| //// [parserComputedPropertyName31.js] | ||
| class C { | ||
| } | ||
| var _a, _b; |
There was a problem hiding this comment.
We seem to now have unnecessary variables here.
| this[_a] = id++; | ||
| } | ||
| } | ||
| _a = e, _b = e2; |
There was a problem hiding this comment.
_b isn't even used and e2 itself has no side effects.
| //// [parserComputedPropertyName36.js] | ||
| class C { | ||
| } | ||
| var _a; |
There was a problem hiding this comment.
We seem to now have unnecessary variables here.
| this[_a] = 0; | ||
| } | ||
| [Symbol()]() { } | ||
| [_a = Symbol(), _b = Symbol(), Symbol()]() { } |
|
@rbuckton Got 'em all. Made the consolidations and style changes you requested, and updated the transform to only create an assignment when the assignment result is used, and only retain the expression if there is no assignment use if the expression looks like it could be side-effecting. |
| }()); } | ||
| if (y === void 0) { y = (_a = /** @class */ (function () { | ||
| function class_2() { | ||
| this[_b] = x; |
There was a problem hiding this comment.
I suppose this means that computed key names are evaluated only once, at declaration time, instead of at construction time?
There was a problem hiding this comment.
Correct, just like computed names for methods already are. We were inconsistent here.
| } | ||
|
|
||
| function isSimpleInlineableExpression(expression: Expression) { | ||
| return !isIdentifier(expression) && isSimpleCopiableExpression(expression) || |
There was a problem hiding this comment.
Would a GeneratedIdentifier be "inlineable"?
There was a problem hiding this comment.
Only if such a Generatedidentifier has all of its assignments occur before any intended usages of the identifier. Not something we check for in a uniform way, I think, but it may happen to be true for all generated identifiers at present?
| */ | ||
| function transformInitializedProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) { | ||
| const propertyName = visitPropertyNameOfClassElement(property); | ||
| const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression) |
There was a problem hiding this comment.
Add comments to explain why we are generating the name here.
| ); | ||
| } | ||
|
|
||
| function isSimpleInlineableExpression(expression: Expression) { |
There was a problem hiding this comment.
Document what it means to be a "simple inlineable expression"
|
|
||
| let statements: Statement[] = [classStatement]; | ||
|
|
||
| // Write any pending expressions |
There was a problem hiding this comment.
Add more context as to what these "pending expressions" are.
| // the body of a class with static initializers. | ||
| setEmitFlags(classExpression, EmitFlags.Indented | getEmitFlags(classExpression)); | ||
| expressions.push(startOnNewLine(createAssignment(temp, classExpression))); | ||
| addRange(expressions, pendingExpressions); |
There was a problem hiding this comment.
As with visitClassDeclaration, add a comment to explain what the "pending expressions" are.
| } | ||
| } | ||
|
|
||
| function getPropertyNameExpressionIfNeeded(name: PropertyName, shouldHoist: boolean, omitSimple: boolean): Expression { |
There was a problem hiding this comment.
Add documentation to explain this function's purpose. Also omitSimple is not very clear, either give it a better name or document it.
| expression = createAssignment(generatedName, expression); | ||
| let expr = getPropertyNameExpressionIfNeeded(name, some(member.decorators), /*omitSimple*/ false); | ||
| if (expr) { // expr only exists if `name` is a computed property name | ||
| if (some(pendingExpressions)) { |
There was a problem hiding this comment.
Add comments to explain why we are inlining the existing pending expressions.
| this[_b] = x; | ||
| } | ||
| return class_2; | ||
| }()), _b = x, |
There was a problem hiding this comment.
Should _b = x, be on its own line?
There was a problem hiding this comment.
I'm more befuddled as to why _a appears on the following line, actually (because they should all be inline). AFAIK the comma list doesn't specify multiline (nor should it), so I'm not sure where the break is coming from.
There was a problem hiding this comment.
Then again statics appear on their own lines in the same context, apparently, so I suppose so?
| } | ||
| var f2 = function _a() { | ||
| var _newTarget = this && this instanceof _a ? this.constructor : void 0; | ||
| var f2 = function _b() { |
There was a problem hiding this comment.
What changed the names here?
There was a problem hiding this comment.
_a was always used by the name for the static function expression on A - now there's no name generation scope for a class body, so it's the same scope as here, thereby bumping these names to the next character.
|
@rbuckton Done. As mentioned in person, I had to migrate |
| constantValue?: string | number; // The constant value of an expression | ||
| externalHelpersModuleName?: Identifier; // The local name for an imported helpers module | ||
| helpers?: EmitHelper[]; // Emit helpers for the node | ||
| startsOnNewLine?: boolean; // If the ndoe should begin on a new line |
This is a more correct alternative to #18350 based on discussions we had around it.
Computed property names on elided declarations (namely property declarations when transpiling to es6 or below), if they could have sideffects, are now moved into the next expression execution position available for them to execute in. This could be in a computed method name, or after the end of the class declaration/expression. As a sideeffect, the
tstransform now must visit methods to place any pending expressions in their computed names, if needed. This is important for compliance with the public fields proposal, as computed names for public fields are supposed to work the same as computed names for methods.Fixes #17028.