Skip to content

Commit 83bca0b

Browse files
ashley-hunterclaude
andcommitted
fix: preserve private class fields (#foo) during field stripping
Private fields (#foo) are JavaScript runtime syntax that declares a private slot on the class. They must not be stripped even without an initializer, unlike public fields which are TypeScript type annotations under useDefineForClassFields: false. Stripping them causes rolldown to panic because this.#foo references remain but the declaration is gone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2078247 commit 83bca0b

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2350,6 +2350,12 @@ pub fn transform_angular_file(
23502350
if prop.r#static {
23512351
continue;
23522352
}
2353+
// Skip private fields (#foo) — these are JS runtime syntax,
2354+
// not TS type annotations. They declare a private slot on
2355+
// the class and must be preserved even without an initializer.
2356+
if matches!(prop.key, PropertyKey::PrivateIdentifier(_)) {
2357+
continue;
2358+
}
23532359
// Strip if: no initializer (value is None) OR has `declare` keyword
23542360
if prop.value.is_none() || prop.declare {
23552361
let field_span = prop.span;

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9379,3 +9379,60 @@ export class TestComponent {
93799379
result.code
93809380
);
93819381
}
9382+
9383+
#[test]
9384+
fn test_private_fields_not_stripped() {
9385+
// JavaScript private fields (#foo) are real runtime declarations, not TypeScript
9386+
// type annotations. Even without an initializer, #foo declares a private slot
9387+
// on the class and must NOT be stripped. Stripping them causes rolldown to panic
9388+
// because this.#foo references remain but the declaration is gone.
9389+
let allocator = Allocator::default();
9390+
let source = r#"
9391+
import { Component } from '@angular/core';
9392+
9393+
@Component({ selector: 'test', template: '' })
9394+
export class TestComponent {
9395+
#privateUninitialized: string;
9396+
#privateInitialized = 'hello';
9397+
publicUninitialized: string;
9398+
publicInitialized = 'world';
9399+
9400+
method() {
9401+
console.log(this.#privateUninitialized);
9402+
}
9403+
}
9404+
"#;
9405+
let options = ComponentTransformOptions {
9406+
strip_uninitialized_fields: true,
9407+
..Default::default()
9408+
};
9409+
let result =
9410+
transform_angular_file(&allocator, "test.component.ts", source, Some(&options), None);
9411+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
9412+
9413+
// Private fields must NOT be stripped (they are JS runtime syntax).
9414+
// Check for the field DECLARATION (not just any reference like this.#foo in methods).
9415+
// A private field declaration looks like " #privateUninitialized;" on its own line.
9416+
assert!(
9417+
result.code.contains(" #privateUninitialized"),
9418+
"Uninitialized private field declaration must be preserved. Got:\n{}",
9419+
result.code
9420+
);
9421+
assert!(
9422+
result.code.contains("#privateInitialized = 'hello'"),
9423+
"Initialized private field must be preserved. Got:\n{}",
9424+
result.code
9425+
);
9426+
// Public uninitialized field SHOULD be stripped (TS type annotation)
9427+
assert!(
9428+
!result.code.contains("publicUninitialized"),
9429+
"Public uninitialized field should be stripped. Got:\n{}",
9430+
result.code
9431+
);
9432+
// Public initialized field must be preserved
9433+
assert!(
9434+
result.code.contains("publicInitialized = 'world'"),
9435+
"Public initialized field must be preserved. Got:\n{}",
9436+
result.code
9437+
);
9438+
}

0 commit comments

Comments
 (0)