Rework computed property name + decorator emit#18350
Conversation
|
@rbuckton can you please review |
f2ad6c8 to
6b1d89b
Compare
| } | ||
|
|
||
| export function nodeCanBeDecorated(node: Node): boolean { | ||
| export function nodeCanBeDecorated(node: Node, parent: Node = node.parent, grandparent: Node = parent.parent): boolean { |
There was a problem hiding this comment.
Rather than add parent and grandparent as optional arguments, I'd rather make them required. We can just modify the checker to pass in node.parent and node.parent.parent to the call. That way the function won't be possibly misused in other places later.
| } | ||
|
|
||
| export function childIsDecorated(node: Node): boolean { | ||
| export function childIsDecorated(node: Node, parent?: Node): boolean { |
There was a problem hiding this comment.
Can we remove the optionality?
| } | ||
|
|
||
| export function nodeIsDecorated(node: Node): boolean { | ||
| export function nodeIsDecorated(node: Node, parent?: Node, grandparent?: Node): boolean { |
There was a problem hiding this comment.
Can we remove the optionality, or possibly add overloads for cases where we don't need to know parent or grandparent?
|
|
||
| export function nodeOrChildIsDecorated(node: Node): boolean { | ||
| return nodeIsDecorated(node) || childIsDecorated(node); | ||
| export function nodeOrChildIsDecorated(node: Node, parent?: Node, grandparent?: Node): boolean { |
There was a problem hiding this comment.
Can we remove the optionality, or possibly add overloads for cases where we don't need to know parent or grandparent?
| * @param isStatic A value indicating whether the member should be a static or instance member. | ||
| */ | ||
| function isInitializedProperty(member: ClassElement, isStatic: boolean) { | ||
| function isInitializedProperty(member: ClassElement, isStatic: boolean): member is PropertyDeclaration { |
There was a problem hiding this comment.
This type predicate is somewhat inaccurate, since it digs further into a PropertyDeclaration. This results in narrowing at the call site such that PropertyDeclaration is possibly removed from the list of constituents of member even though it still could contain other PropertyDeclaration nodes.
| && (<PropertyDeclaration>member).initializer !== undefined; | ||
| } | ||
|
|
||
| function isComputedNameWhichRequiresHoisting(member: ClassElement, isStatic: boolean): member is PropertyDeclaration { |
There was a problem hiding this comment.
Same as above, the type predicate is inaccurate.
There was a problem hiding this comment.
Judging from the one place I see this called, isStatic is only ever false.
aa68d8a to
44d3d8e
Compare
| statements = [varStatement]; | ||
| } | ||
| if (hoistedNameAssignments.length) { | ||
| statements.unshift(createVariableStatement(/*modifiers*/ undefined, map(hoistedNameAssignments, a => createVariableDeclaration(a.left as Identifier, /*type*/ undefined, a.right)))); |
There was a problem hiding this comment.
Seems a bit wasteful to create a BinaryExpression just to drop it. In destructuring.ts we have to do something similar, so we pass a callback that creates the correct assignment in each context.
| if (!node.members) return; | ||
| const result: PropertyDeclaration[] = []; | ||
| for (let member of node.members) { | ||
| if (!isStatic && hasComputedNameWhichRequiresHoisting(member)) { |
There was a problem hiding this comment.
It feels like this function now has too many responsibilities. I'd much rather keep the hoistedComputedNames and decoratedMembers parts separate.
| // If the class does not contain nodes that require a synthesized constructor, | ||
| // accept the current constructor if it exists. | ||
| if (!hasInstancePropertyWithInitializer && !hasParameterPropertyAssignments) { | ||
| getInitializedProperties(node, /*isStatic*/ false, hoistedComputedNames, decoratedMembers); |
There was a problem hiding this comment.
The fact we don't use the return value here indicates getInitializedProperties now has too many responsibilities.
There was a problem hiding this comment.
If I rename it to collectPropertyTransformationResults and make it output all three things through output array arguments (or does it make more sense to return an object with three array members?), does it become acceptable? Because it certainly doesn't only apply for initialized properties anymore, and all the work still needs to be done.
There was a problem hiding this comment.
Possibly, if you are able to pass the results around rather than recalculate them. In general though I'd rather see smaller and more focused methods, e.g.:
getInitializedProperties(node, isStatic)getHoistedComputedNames(properties)getDecoratedMembers(properties)
| * @param receiver The object receiving the property assignment. | ||
| */ | ||
| function transformInitializedProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) { | ||
| const propertyName = visitPropertyNameOfClassElement(property); |
There was a problem hiding this comment.
We don't need to do this for fields anymore?
There was a problem hiding this comment.
Correct. The work is done inside the hoisting of computed names/decorated names. If you still do it, you get redundant output like so:
this[_a = _c] = null;
this[_b = _f] = null;
var _a, _b;We know we don't need to transform computed property names on initialized properties here because we've already dealt with them in the section where we hoist them.
44d3d8e to
f7c935d
Compare
|
@rbuckton I think I've refactored away all your concerns. |
|
I was thinking something more like this: // at the top of visitClassDeclaration/visitClassExpression
const allInitializedProperties = filter(node.members, isInitializedProperty);
const staticProperties = filter(allInitializedProperties, isStaticMember);
const nonStaticProperties = filter(allInitializedProperties, isNonStaticMember);
const hoistedMembers = filter(nonStaticProperties, hasComputedNameWhichRequiresHoisting);
const decoratedMembers = filter(node.members, isDecoratedClassElement);
// in visitClassDeclaration
const hoistingDeclarations = hoistComputedExpressions(hoistedMembers, createVariableDeclaration);
if (some(hoistingDeclarations)) {
statements.push(createVariableStatement(hoistingDeclarations));
}
// in visitClassExpression
const hoistingAssignments = hoistComputedExpressions(hoistedMembers, hoistAssignment);
// prefix these before classExpression. hoistAssignment takes care of the temporary variables.
// in visitPropertyNameOfClassElement
if (hasComputedNameWhichRequiresHoisting(member)) {
return updateComputedPropertyName(member.name, getGeneratedNameForNode(member.name));
}
// elsewhere in ts.ts
function isStaticMember(member: ClassElement) { return hasModifier(member, ModifierFlags.Static); }
function isNonStaticMember(member: ClassElement) { return !hasModifier(member, ModifierFlags.Static); }
function hoistComputedExpressions<T extends Expression | VariableDeclaration>(hoistedMembers: PropertyDeclaration[], createAssignmentCallback: (name: Identifier, expression: Expression) => T): T {
let result: T[];
for (const member of hoistedMembers) {
const tempId = getGeneratedNameForNode(member.name);
const assignment = createAssignmentCallback(tempId, visitNode((<ComputedPropertyName>member.name).expression, visitor, isExpression));
result = append(result, assignment);
}
return result;
}
function hoistAssignment(name: Identifier, expression: Expression) {
hoistVariableDeclaration(name);
return createAssignment(name, expression);
} |
|
@rbuckton The only issue with what you have there is that we have to hoist all computed names which could have sideeffects, not just initialized ones. So you have this matrix where:
Right now I'm just doing this by applying two (well, three) small transforms in succession - one to hoist and transform computed names, then one to handle all initialized properties (then one to write decorators, but that's mostly independent now except that it needs the output of the last transform to get its property name references right). Thinking on it, |
… array allocations using callback
|
@rbuckton I think this is much cleaner now. |
|
After conversing with @rbuckton, we're going to change this to respect execution order in the event of an |
|
Superseded by #19430. |
Fixes #17028
Computed property names for properties were being elided if uninitialized, or, if initialized, run within the constructor of the class. This is incorrect - for reference, computed property names for methods/getters/setters execute outside the class, and only once. This change makes a computed property name be hoisted to outside of the class declaration if it is not "simple" (and could have observable side effects from being invoked multiple times on each construction of the class). Then, if hoisted, the hoisted identifier is used in the decorator emit if need be. If not hoisted, but still decorated (if the expression is simple), then the simple expression is duplicated into the decorator emit. As an added bonus, we also no longer omit a computed property name expression whose execution may have observable side effects if there is no initializer.
We issue errors on the usage of most complex expressions in computed property names today, but this brings the property computed name emit (when it could matter) in-line with the method/getter/setter computed name runtime behavior. This shouldn't be considered a breaking change, since we issue an error on computed names on properties which are not directly a string/number/symbol; but should now have correct emit for if/when we allow it as part of allowing local symbol types as computed property names or other proposals around expanding what we allow for computed property names.
As of now, a "simple" computed property is defined as a numeric literal, a string literal, a no substitution template literal, a keyword, or a syntactically well-known symbol - it is possible that there is room to expand this if anyone has further suggestions.