Skip to content

Support NamedEvaluation for decorated anonymous class expression#15122

Closed
JLHwung wants to merge 11 commits intobabel:mainfrom
JLHwung:named-evaluation-decorated-anonymous-class-expression
Closed

Support NamedEvaluation for decorated anonymous class expression#15122
JLHwung wants to merge 11 commits intobabel:mainfrom
JLHwung:named-evaluation-decorated-anonymous-class-expression

Conversation

@JLHwung
Copy link
Contributor

@JLHwung JLHwung commented Nov 4, 2022

Q                       A
Fixed Issues? Ensures context.name is correct for decorated anonymous class expression
Patch: Bug Fix? Yes
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

This PR ensures that the context.name is correctly assigned when an anonymous class expression is decorated. Note that current main already works in the following case:

function dec(value, context) {
  context.name // <-- context.name should be "a"
}
var a = @dec class {}

This PR supports more cases (See testcases for a complete set):

a = @dec class {}
{ a: @dec class {} }

To support computed key and the default export, a new helper setFunctionName is introduced, which is very similar to the TS compiler utility __setFunctionName:

https://github.com/microsoft/TypeScript/blob/c837d78d7ee5bab2ab6f2c3c7c24882ca9c31dcf/src/compiler/factory/emitHelpers.ts#L899-L909

This PR also fixes that context.name is not a string when decorating a computed numerical key:

function dec(value, context) {
  context.name // <-- previously 1n, now "1"
}
class C { @dec 1n }

Limit

  • This PR currently does not support NamedEvaluation for auto-accessors:
class { static accessor key = @dec class {}

The context.name available in dec will be the backed private key storage name. Per spec, it should be the accessor name key.

  • This PR does not support preserving name after es2015 transpilation.

@JLHwung JLHwung added PR: Bug Fix 🐛 A type of pull request used for our changelog categories Spec: Decorators labels Nov 4, 2022
path.parentPath.node.type === "VariableDeclarator" &&
path.parentPath.node.id.type === "Identifier"
) {
className = path.parentPath.node.id.name;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is how we previously handle the namedEvaluation in the case var c = @dec class {}.

}

// Todo: unify name references logic with helper-function-name
function NamedEvaluationVisitoryFactory(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first I tried to extract logic from helper-function-name but it turns out that helper is out-of-dated. Since any exports in helpers must await a new minor release, I end up implementing a new namedEvaluationVisitor.

Copy link
Member

Choose a reason for hiding this comment

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

If we were to put this in helper-function-name waiting for a minor, would we need to keep both implementations or can we just update the old one?

@babel-bot
Copy link
Collaborator

babel-bot commented Nov 4, 2022

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/53477/

},
// We listen on ObjectExpression so that we don't have to visit
// the object properties under object patterns
ObjectExpression(path, state) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guess I have to handle RecordExpression here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A record expression can not contain function as value anyway, so we can ignore that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nicolo-ribaudo The current record spec requires NamedEvaluation for the initializer. https://tc39.es/proposal-record-tuple/#sec-record-initializer-runtime-semantics-recordpropertydefinitionevaluation Is that observable now that function / class can not be a record value?

`;

helpers.setFunctionName = helper("7.20.1")`
export default function _setFunctionName(f, n) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To mirror https://tc39.es/ecma262/#sec-setfunctionname, the helper should support prefix used by getters / setters, though it is currently not used by this plugin.

Comment on lines +7 to +8
var key = toPrimitive(arg, "string");
return typeof key === "symbol" ? key : String(key);
Copy link
Contributor Author

@JLHwung JLHwung Nov 4, 2022

Choose a reason for hiding this comment

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

Further assumption idea: _toPropertyKey can be simplified to typeof key === "symbol" ? key : "".concat(key) (aka TS's __propKey) if [Symbol.toPrimitive] method does not return a symbol.

@nicolo-ribaudo
Copy link
Member

We could consider adding extra.name to anonymous functions&classes at the parser level, which is easier to track than making sure to name the function before transforming the code around it.

@JLHwung
Copy link
Contributor Author

JLHwung commented Nov 7, 2022

We could consider adding extra.name to anonymous functions&classes at the parser level, which is easier to track than making sure to name the function before transforming the code around it.

While engines like V8 retains such information in their AST ScopeInfo, the Babel's challenge here is that we are not handling the final JS code. The inferred function name can be easily out-of-sync because of any AST changes, and historically we are not good at syncing other scope info e.g. bindings, too. So I think it is fine to do it at the plugin level: we infer the function name only when we need such info.

@liuxingbaoyu liuxingbaoyu self-requested a review November 9, 2022 21:50
@JLHwung JLHwung force-pushed the named-evaluation-decorated-anonymous-class-expression branch from 7ce9211 to 699b31f Compare November 9, 2022 22:09
@JLHwung JLHwung force-pushed the named-evaluation-decorated-anonymous-class-expression branch from 699b31f to da24744 Compare November 14, 2022 22:45
return babelHelpers.classPrivateFieldGet(this, _C);
}
set ['c'](v) {
set [_computedKey](v) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new output aligns to the decorated variant of this test case.


// Recrawl the scope to make sure new identifiers are properly synced
path.scope.crawl();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are copied from the epilogue of this transform.

Also fixed non-memoised computed key for undecorated accessor
@JLHwung JLHwung force-pushed the named-evaluation-decorated-anonymous-class-expression branch from 4839e26 to 920073e Compare November 15, 2022 20:59
@JLHwung JLHwung force-pushed the named-evaluation-decorated-anonymous-class-expression branch from ad89112 to be9293f Compare November 15, 2022 21:19
@JLHwung JLHwung force-pushed the named-evaluation-decorated-anonymous-class-expression branch from 7e2b9dd to 248fb76 Compare November 15, 2022 22:40
const valueNode = value ? t.cloneNode(value) : undefined;

const newField = generateClassProperty(newId, valueNode, isStatic);
const newField = generateClassProperty(newId, value, isStatic);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we avoid cloning the accessor property's initializer because

  • cloning is unnecessary since the initializer is moved to the proxy setter body
  • If the initializer has been transformed by propertyVisitor.ClassAccessorProperty (e.g. the case of nested decorator @noop accessor foo = @dec class {}), the cloned node will result to a different NodePath, which bypasses the VISITED check and gets the inner class @dec class {} transformed twice.

}
hasElementDecorators = true;
} else if (element.node.type === "ClassAccessorProperty") {
// @ts-expect-error todo: propertyVisitor.ClassAccessorProperty should be callable. Improve typings.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Help wanted: I don't know how to make the visitor callable.

Copy link
Member

Choose a reason for hiding this comment

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

I tried this, but I also failed.😅

return this.#C;
}
set ['c'](v) {
set [_computedKey](v) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test captured the bug that computed key in a non-decorated accessor is not properly memoised.

Comment on lines +566 to +570
let newKey = key;
if (computed && !scopeParent.isStatic(key)) {
newKey = memoiseExpression(key as t.Expression, "computedKey");
}
addProxyAccessorsFor(newPath, newKey, newId, computed);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let newKey = key;
if (computed && !scopeParent.isStatic(key)) {
newKey = memoiseExpression(key as t.Expression, "computedKey");
}
addProxyAccessorsFor(newPath, newKey, newId, computed);
addProxyAccessorsFor(
newPath,
computed && !scopeParent.isStatic(key)
? memoiseExpression(key as t.Expression, "computedKey")
: key,
newId,
computed,
);

Would this be better?

Comment on lines +108 to +109
const id = path.node.id;
const className = id.name;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const id = path.node.id;
const className = id.name;
const className = path.node.id.name;

It seems that id is only used once?

helpers.setFunctionName = helper("7.20.1")`
export default function _setFunctionName(f, n) {
if (typeof n === "symbol") n = n.description ? "[" + n.description + "]" : "";
return Object.defineProperty(f, "name", { configurable: true, value: n + "" });
Copy link
Member

Choose a reason for hiding this comment

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

In some older browsers .name was non-configurable, so we should wrap this in try/catch.

/* @minVersion 7.1.5 */

// https://tc39.es/ecma262/#sec-toprimitive
export default function _toPrimitive(
Copy link
Member

Choose a reason for hiding this comment

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

Can we inline this in the toPropertyKey helper?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This helper has been exposed since 7.1.5, so probably not in the patch release. Here I extracted toPrimitive and toPropertyKey into single files.

}

// Todo: unify name references logic with helper-function-name
function NamedEvaluationVisitoryFactory(
Copy link
Member

Choose a reason for hiding this comment

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

If we were to put this in helper-function-name waiting for a minor, would we need to keep both implementations or can we just update the old one?

@JLHwung
Copy link
Contributor Author

JLHwung commented Nov 28, 2023

This PR is a rebase nightmare. I will try to extract some commits into separate PRs.

@nicolo-ribaudo
Copy link
Member

Ok, sorry for not reviewing it earlier 😅

@JLHwung
Copy link
Contributor Author

JLHwung commented Nov 28, 2023

@nicolo-ribaudo No problem. Maybe the one-year-older me can find new bugs hidden in this PR. 😄

@nicolo-ribaudo nicolo-ribaudo deleted the named-evaluation-decorated-anonymous-class-expression branch December 4, 2023 18:03
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Mar 5, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Bug Fix 🐛 A type of pull request used for our changelog categories Spec: Decorators

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants