fix(transformer): improve legacy decorator handling and fix constructor parameter decorators#13632
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
CodSpeed Instrumentation Performance ReportMerging #13632 will not alter performanceComparing Summary
Footnotes |
There was a problem hiding this comment.
Pull Request Overview
This PR fixes an issue in the legacy decorator transformation where decorated classes without names weren't being handled correctly, and improves the overall handling of decorator metadata by refactoring the transformation flow from complex upfront analysis to incremental processing.
- Refactored decorator transformation to use a stack-based approach for processing nested classes
- Fixed constructor parameter decorator handling by ensuring metadata is processed in the correct order
- Improved handling of classes without names by generating appropriate bindings
Reviewed Changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/metadata/private-in-expression-in-decorator/* | New test case for private expressions in decorators |
| tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/metadata/class-and-method-decorators/* | New test case for combined class and method decorators |
| tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/class-without-name-with-decorated_* | New test cases for unnamed decorated classes |
| tasks/transform_conformance/snapshots/oxc.snap.md | Updated snapshot reflecting improved test coverage |
| crates/oxc_transformer/src/lib.rs | Added exit methods for decorator processing |
| crates/oxc_transformer/src/decorator/mod.rs | Added exit method hooks for incremental decorator processing |
| crates/oxc_transformer/src/decorator/legacy/mod.rs | Major refactor from upfront analysis to stack-based incremental processing |
| crates/oxc_transformer/src/decorator/legacy/metadata.rs | Simplified metadata handling with separate stacks for methods and constructors |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
There was a problem hiding this comment.
This PR changes quite a lot, and I'm not that familiar with decorators transform to start with, so it's hard to review properly without spending hours on it.
There's one bug related to stacks (see below).
Also, a couple of comments/questions:
-
Are you sure that decorators always get transformed by other transforms before they're removed from AST? (e.g. in a case like
@(x ?? y)) -
How
method_metadata_stackandconstructor_metadata_stackare pushed/popped looks a little fragile. Is it guaranteed that everypushhas a correspondingpop, even if some other transform e.g. adds/removes a constructor or method in between, or if some classes are class expressions?
This decorators transform is pretty damn tricky!
(I hope you don't mind I pushed 1 unimportant commit, just to reformat the test cases. Conformance CI fail is not related to that commit - it passes locally. Something must have changed on main since PR was originally submitted.)
49c39d7 to
f7526d6
Compare
f7526d6 to
54d9766
Compare
Ideally, yes, the decorators are transformed in the However, there is a weird situation that will break this. For example, if expressions are handled by the sub-visitor and it is called in the |
|
@overlookmotel I've resolved your requested changes. Can you take another look? I am happy if you want to make a change directly in this PR or in the follow-up PRs. |
OK great!
Hmm. Yes, you're right. But I'm not sure what we could do to solve this. There is a period in time between when the decorators are extracted from the AST, and when they're inserted again. Any code examining the AST during that window of time won't see them. But in practice, I don't think this will present a problem. |
…nd `SparseStack` (#13672) When using stacks, typically you create a stack at start of AST traversal, and then `debug_assert!` that the stack has been emptied again at the end of traversal, to make sure that the stack has not got out of sync - that every `push` is followed by a corresponding `pop`. With `NonEmptyStack` and `SparseStack` it's a little confusing how to do this, because these stacks can never be empty. `stack.is_empty()` is not correct. `stack.len() == 1` is correct, but it's unclear when reading that code what exactly this is testing. Add `is_exhausted` methods to `NonEmptyStack` and `SparseStack` to fulfil this role. Naming: `is_exhausted` is perhaps not the best name, but I couldn't come up with a better one. I considered `is_emptied`, which is more descriptive, but I think it's too close to `is_empty`. (prompted by #13632)
|
This PR made me realize that it's confusing how to check that stacks are back to initial state at end of AST traversal, because it's different depending on the stack type:
This is not obvious, and it's really error-prone (bad design on my part). #13672 adds an Once that PR is merged, I suggest using |
454c379 to
fa907f4
Compare
Thank you for adding this method. I had incorrectly used |
overlookmotel
left a comment
There was a problem hiding this comment.
I added a commit just to rename a var to be more accurate description of what it is.
One nit comment below. If you're willing to make that change, please go ahead and merge afterwards. Or if you can't be bothered, just merge it now!
|
Thank you for reviewing. Merging now! |
Merge activity
|
…or parameter decorators (#13632) Fixes rolldown/rolldown#6079. Regressed introduced by #13227. The root cause is that the class plugin will insert a constructor method in some cases, but metadata handling is processed before the constructor is inserted, which causes the method count not to be exactly the same as the method metadata count. Thus, decorator metadata is inserted in the incorrect place. This PR refactors the core transformation logic to ensure that the new methods and properties inserted by other plugins won't affect the decorator transformation. ### Transformation Flow Changes Before: Complex upfront analysis with separate processing paths - Used check_class_has_decorated() to analyze entire class tree - Separate handling for class vs element decorators - Multiple disconnected stacks for state tracking After: Unified incremental processing - Process decorators as we traverse (exit_method_definition, exit_property_definition, etc.) - Accumulate decoration statements in single stack during traversal - Unified processing in transform_class() using consolidated state
b488892 to
bb2bcf0
Compare

Fixes rolldown/rolldown#6079. Regressed introduced by #13227.
The root cause is that the class plugin will insert a constructor method in some cases, but metadata handling is processed before the constructor is inserted, which causes the method count not to be exactly the same as the method metadata count. Thus, decorator metadata is inserted in the incorrect place.
This PR refactors the core transformation logic to ensure that the new methods and properties inserted by other plugins won't affect the decorator transformation.
Transformation Flow Changes
Before: Complex upfront analysis with separate processing paths
After: Unified incremental processing
exit_property_definition, etc.)