Skip to main content

oxc_transformer/es2022/
class_static_block.rs

1//! ES2022: Class Static Block
2//!
3//! This plugin transforms class static blocks (`class C { static { foo } }`) to an equivalent
4//! using private fields (`class C { static #_ = foo }`).
5//!
6//! > This plugin is included in `preset-env`, in ES2022
7//!
8//! ## Example
9//!
10//! Input:
11//! ```js
12//! class C {
13//!   static {
14//!     foo();
15//!   }
16//!   static {
17//!     foo();
18//!     bar();
19//!   }
20//! }
21//! ```
22//!
23//! Output:
24//! ```js
25//! class C {
26//!   static #_ = foo();
27//!   static #_2 = (() => {
28//!     foo();
29//!     bar();
30//!   })();
31//! }
32//! ```
33//!
34//! ## Implementation
35//!
36//! Implementation based on [@babel/plugin-transform-class-static-block](https://babel.dev/docs/babel-plugin-transform-class-static-block).
37//!
38//! ## References:
39//! * Babel plugin implementation: <https://github.com/babel/babel/tree/v7.26.2/packages/babel-plugin-transform-class-static-block>
40//! * Class static initialization blocks TC39 proposal: <https://github.com/tc39/proposal-class-static-block>
41
42use itoa::Buffer as ItoaBuffer;
43
44use oxc_allocator::TakeIn;
45use oxc_ast::{NONE, ast::*};
46use oxc_span::SPAN;
47use oxc_syntax::scope::{ScopeFlags, ScopeId};
48use oxc_traverse::Traverse;
49
50use crate::{
51    context::TraverseCtx, state::TransformState,
52    utils::ast_builder::wrap_statements_in_arrow_function_iife,
53};
54
55pub struct ClassStaticBlock;
56
57impl ClassStaticBlock {
58    pub fn new() -> Self {
59        Self
60    }
61}
62
63impl<'a> Traverse<'a, TransformState<'a>> for ClassStaticBlock {
64    fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
65        // Loop through class body elements and:
66        // 1. Find if there are any `StaticBlock`s.
67        // 2. Collate list of private keys matching `#_` or `#_[1-9]...`.
68        //
69        // Don't collate private keys list conditionally only if a static block is found, as usually
70        // there will be no matching private keys, so those checks are cheap and will not allocate.
71        let mut has_static_block = false;
72        let mut keys = Keys::default();
73        for element in &body.body {
74            let key = match element {
75                ClassElement::StaticBlock(_) => {
76                    has_static_block = true;
77                    continue;
78                }
79                ClassElement::MethodDefinition(def) => &def.key,
80                ClassElement::PropertyDefinition(def) => &def.key,
81                ClassElement::AccessorProperty(def) => &def.key,
82                ClassElement::TSIndexSignature(_) => continue,
83            };
84
85            if let PropertyKey::PrivateIdentifier(id) = key {
86                keys.reserve(id.name.as_str());
87            }
88        }
89
90        // Transform static blocks
91        if !has_static_block {
92            return;
93        }
94
95        for element in &mut body.body {
96            if let ClassElement::StaticBlock(block) = element {
97                *element = Self::convert_block_to_private_field(block, &mut keys, ctx);
98            }
99        }
100    }
101}
102
103impl ClassStaticBlock {
104    /// Convert static block to private field.
105    /// `static { foo }` -> `static #_ = foo;`
106    /// `static { foo; bar; }` -> `static #_ = (() => { foo; bar; })();`
107    fn convert_block_to_private_field<'a>(
108        block: &mut StaticBlock<'a>,
109        keys: &mut Keys<'a>,
110        ctx: &mut TraverseCtx<'a>,
111    ) -> ClassElement<'a> {
112        let expr = Self::convert_block_to_expression(block, ctx);
113
114        let key = keys.get_unique(ctx);
115        let key = ctx.ast.property_key_private_identifier(SPAN, key);
116
117        ctx.ast.class_element_property_definition(
118            block.span,
119            PropertyDefinitionType::PropertyDefinition,
120            ctx.ast.vec(),
121            key,
122            NONE,
123            Some(expr),
124            false,
125            true,
126            false,
127            false,
128            false,
129            false,
130            false,
131            None,
132        )
133    }
134
135    /// Convert static block to expression which will be value of private field.
136    /// `static { foo }` -> `foo`
137    /// `static { foo; bar; }` -> `(() => { foo; bar; })()`
138    fn convert_block_to_expression<'a>(
139        block: &mut StaticBlock<'a>,
140        ctx: &mut TraverseCtx<'a>,
141    ) -> Expression<'a> {
142        let scope_id = block.scope_id();
143
144        // If block contains only a single `ExpressionStatement`, no need to wrap in an IIFE.
145        // `static { foo }` -> `foo`
146        // TODO(improve-on-babel): If block has no statements, could remove it entirely.
147        let stmts = &mut block.body;
148        if stmts.len() == 1
149            && let Statement::ExpressionStatement(stmt) = stmts.first_mut().unwrap()
150        {
151            return Self::convert_block_with_single_expression_to_expression(
152                &mut stmt.expression,
153                scope_id,
154                ctx,
155            );
156        }
157
158        // Convert block to arrow function IIFE.
159        // `static { foo; bar; }` -> `(() => { foo; bar; })()`
160
161        // Re-use the static block's scope for the arrow function.
162        // Always strict mode since we're in a class.
163        *ctx.scoping_mut().scope_flags_mut(scope_id) =
164            ScopeFlags::Function | ScopeFlags::Arrow | ScopeFlags::StrictMode;
165        wrap_statements_in_arrow_function_iife(stmts.take_in(ctx.ast), scope_id, block.span, ctx)
166    }
167
168    /// Convert static block to expression which will be value of private field,
169    /// where the static block contains only a single expression.
170    /// `static { foo }` -> `foo`
171    fn convert_block_with_single_expression_to_expression<'a>(
172        expr: &mut Expression<'a>,
173        scope_id: ScopeId,
174        ctx: &mut TraverseCtx<'a>,
175    ) -> Expression<'a> {
176        let expr = expr.take_in(ctx.ast);
177
178        // Remove the scope for the static block from the scope chain
179        ctx.remove_scope_for_expression(scope_id, &expr);
180
181        expr
182    }
183}
184
185/// Store of private identifier keys matching `#_` or `#_[1-9]...`.
186///
187/// Most commonly there will be no existing keys matching this pattern
188/// (why would you prefix a private key with `_`?).
189/// It's also uncommon to have more than 1 static block in a class.
190///
191/// Therefore common case is only 1 static block, which will use key `#_`.
192/// So store whether `#_` is in set as a separate `bool`, to make a fast path this common case,
193/// which does not involve any allocations (`numbered` will remain empty).
194///
195/// Use a `Vec` rather than a `HashMap`, because number of matching private keys is usually small,
196/// and `Vec` is lower overhead in that case.
197#[derive(Default)]
198struct Keys<'a> {
199    /// `true` if keys includes `#_`.
200    underscore: bool,
201    /// Keys matching `#_[1-9]...`. Stored without the `_` prefix.
202    numbered: Vec<&'a str>,
203}
204
205impl<'a> Keys<'a> {
206    /// Add a key to set.
207    ///
208    /// Key will only be added to set if it's `_`, or starts with `_[1-9]`.
209    fn reserve(&mut self, key: &'a str) {
210        let mut bytes = key.as_bytes().iter().copied();
211        if bytes.next() != Some(b'_') {
212            return;
213        }
214
215        match bytes.next() {
216            None => {
217                self.underscore = true;
218            }
219            Some(b'1'..=b'9') => {
220                self.numbered.push(&key[1..]);
221            }
222            _ => {}
223        }
224    }
225
226    /// Get a key which is not in the set.
227    ///
228    /// Returned key will be either `_`, or `_<integer>` starting with `_2`.
229    #[inline]
230    fn get_unique(&mut self, ctx: &TraverseCtx<'a>) -> Str<'a> {
231        #[expect(clippy::if_not_else)]
232        if !self.underscore {
233            self.underscore = true;
234            Str::from("_")
235        } else {
236            self.get_unique_slow(ctx)
237        }
238    }
239
240    // `#[cold]` and `#[inline(never)]` as it should be very rare to need a key other than `#_`.
241    #[cold]
242    #[inline(never)]
243    fn get_unique_slow(&mut self, ctx: &TraverseCtx<'a>) -> Str<'a> {
244        // Source text length is limited to `u32::MAX` so impossible to have more than `u32::MAX`
245        // private keys. So `u32` is sufficient here.
246        let mut i = 2u32;
247        let mut buffer = ItoaBuffer::new();
248        let mut num_str;
249        loop {
250            num_str = buffer.format(i);
251            if !self.numbered.contains(&num_str) {
252                break;
253            }
254            i += 1;
255        }
256
257        let key = ctx.ast.str_from_strs_array(["_", num_str]);
258        self.numbered.push(&key.as_str()[1..]);
259
260        key
261    }
262}
263
264#[cfg(test)]
265mod test {
266    use oxc_allocator::Allocator;
267    use oxc_semantic::Scoping;
268    use oxc_traverse::ReusableTraverseCtx;
269
270    use crate::state::TransformState;
271
272    use super::Keys;
273
274    macro_rules! setup {
275        ($ctx:ident) => {
276            let allocator = Allocator::default();
277            let scoping = Scoping::default();
278            let state = TransformState::default();
279            let ctx = ReusableTraverseCtx::new(state, scoping, &allocator);
280            // SAFETY: Macro user only gets a `&mut TransCtx`, which cannot be abused
281            let mut ctx = unsafe { ctx.unwrap() };
282            let $ctx = &mut ctx;
283        };
284    }
285
286    #[test]
287    fn keys_no_reserved() {
288        setup!(ctx);
289
290        let mut keys = Keys::default();
291
292        assert_eq!(keys.get_unique(ctx), "_");
293        assert_eq!(keys.get_unique(ctx), "_2");
294        assert_eq!(keys.get_unique(ctx), "_3");
295        assert_eq!(keys.get_unique(ctx), "_4");
296        assert_eq!(keys.get_unique(ctx), "_5");
297        assert_eq!(keys.get_unique(ctx), "_6");
298        assert_eq!(keys.get_unique(ctx), "_7");
299        assert_eq!(keys.get_unique(ctx), "_8");
300        assert_eq!(keys.get_unique(ctx), "_9");
301        assert_eq!(keys.get_unique(ctx), "_10");
302        assert_eq!(keys.get_unique(ctx), "_11");
303        assert_eq!(keys.get_unique(ctx), "_12");
304    }
305
306    #[test]
307    fn keys_no_relevant_reserved() {
308        setup!(ctx);
309
310        let mut keys = Keys::default();
311        keys.reserve("a");
312        keys.reserve("foo");
313        keys.reserve("__");
314        keys.reserve("_0");
315        keys.reserve("_1");
316        keys.reserve("_a");
317        keys.reserve("_foo");
318        keys.reserve("_2foo");
319
320        assert_eq!(keys.get_unique(ctx), "_");
321        assert_eq!(keys.get_unique(ctx), "_2");
322        assert_eq!(keys.get_unique(ctx), "_3");
323    }
324
325    #[test]
326    fn keys_reserved_underscore() {
327        setup!(ctx);
328
329        let mut keys = Keys::default();
330        keys.reserve("_");
331
332        assert_eq!(keys.get_unique(ctx), "_2");
333        assert_eq!(keys.get_unique(ctx), "_3");
334        assert_eq!(keys.get_unique(ctx), "_4");
335    }
336
337    #[test]
338    fn keys_reserved_numbers() {
339        setup!(ctx);
340
341        let mut keys = Keys::default();
342        keys.reserve("_2");
343        keys.reserve("_4");
344        keys.reserve("_11");
345
346        assert_eq!(keys.get_unique(ctx), "_");
347        assert_eq!(keys.get_unique(ctx), "_3");
348        assert_eq!(keys.get_unique(ctx), "_5");
349        assert_eq!(keys.get_unique(ctx), "_6");
350        assert_eq!(keys.get_unique(ctx), "_7");
351        assert_eq!(keys.get_unique(ctx), "_8");
352        assert_eq!(keys.get_unique(ctx), "_9");
353        assert_eq!(keys.get_unique(ctx), "_10");
354        assert_eq!(keys.get_unique(ctx), "_12");
355    }
356
357    #[test]
358    fn keys_reserved_later_numbers() {
359        setup!(ctx);
360
361        let mut keys = Keys::default();
362        keys.reserve("_5");
363        keys.reserve("_4");
364        keys.reserve("_12");
365        keys.reserve("_13");
366
367        assert_eq!(keys.get_unique(ctx), "_");
368        assert_eq!(keys.get_unique(ctx), "_2");
369        assert_eq!(keys.get_unique(ctx), "_3");
370        assert_eq!(keys.get_unique(ctx), "_6");
371        assert_eq!(keys.get_unique(ctx), "_7");
372        assert_eq!(keys.get_unique(ctx), "_8");
373        assert_eq!(keys.get_unique(ctx), "_9");
374        assert_eq!(keys.get_unique(ctx), "_10");
375        assert_eq!(keys.get_unique(ctx), "_11");
376        assert_eq!(keys.get_unique(ctx), "_14");
377    }
378
379    #[test]
380    fn keys_reserved_underscore_and_numbers() {
381        setup!(ctx);
382
383        let mut keys = Keys::default();
384        keys.reserve("_2");
385        keys.reserve("_4");
386        keys.reserve("_");
387
388        assert_eq!(keys.get_unique(ctx), "_3");
389        assert_eq!(keys.get_unique(ctx), "_5");
390        assert_eq!(keys.get_unique(ctx), "_6");
391    }
392
393    #[test]
394    fn keys_reserved_underscore_and_later_numbers() {
395        setup!(ctx);
396
397        let mut keys = Keys::default();
398        keys.reserve("_5");
399        keys.reserve("_4");
400        keys.reserve("_");
401
402        assert_eq!(keys.get_unique(ctx), "_2");
403        assert_eq!(keys.get_unique(ctx), "_3");
404        assert_eq!(keys.get_unique(ctx), "_6");
405    }
406}