From 1856309d9d04cffcfeb42febf7e990daebd107ee Mon Sep 17 00:00:00 2001 From: Ashley Hunter Date: Sat, 21 Mar 2026 22:32:37 +0000 Subject: [PATCH] fix(compiler): strip abstract keyword from class expressions in JIT mode --- .../src/component/transform.rs | 17 ++++++- .../tests/integration_test.rs | 46 +++++++++++++++++++ .../integration_test__jit_abstract_class.snap | 21 +++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_abstract_class.snap diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 636906434..c2bfbba42 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -695,6 +695,8 @@ struct JitClassInfo { is_exported: bool, /// Whether the class is export default. is_default_export: bool, + /// Whether the class is abstract. + is_abstract: bool, /// Constructor parameter info for ctorParameters. ctor_params: std::vec::Vec, /// Member decorator info for propDecorators. @@ -1224,6 +1226,7 @@ fn transform_angular_file_jit( class_body_end: class.body.span.end, is_exported, is_default_export, + is_abstract: class.r#abstract, ctor_params, member_decorators, decorator_text, @@ -1343,15 +1346,25 @@ fn transform_angular_file_jit( } // 4c. Class restructuring: `export class X` → `let X = class X` + // For abstract classes, also strip the `abstract` keyword since class expressions can't be abstract. + let class_keyword_start = if jit_info.is_abstract { + let rest = &source[jit_info.class_start as usize..]; + let offset = rest.find("class").unwrap_or(0); + jit_info.class_start + offset as u32 + } else { + jit_info.class_start + }; + if jit_info.is_exported || jit_info.is_default_export { edits.push(Edit::replace( jit_info.stmt_start, - jit_info.class_start, + class_keyword_start, format!("let {} = ", jit_info.class_name), )); } else { - edits.push(Edit::insert( + edits.push(Edit::replace( jit_info.class_start, + class_keyword_start, format!("let {} = ", jit_info.class_name), )); } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 490d5c8e8..4be2806dc 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -6406,6 +6406,52 @@ export class TestComponent { insta::assert_snapshot!("jit_union_type_ctor_params", result.code); } +#[test] +fn test_jit_abstract_class() { + let allocator = Allocator::default(); + let source = r#" +import { Injectable } from '@angular/core'; + +@Injectable() +export abstract class BaseProvider { + protected abstract get name(): string; + protected abstract initialize(): void; + + public greet(): string { + return `Hello from ${this.name}`; + } +} +"#; + + let options = ComponentTransformOptions { jit: true, ..Default::default() }; + let result = transform_angular_file(&allocator, "base.provider.ts", source, &options, None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + + // The abstract keyword should NOT appear before "class" in the output + // (JIT converts to class expression which can't be abstract) + assert!( + !result.code.contains("abstract class"), + "JIT output should not contain 'abstract class'. Got:\n{}", + result.code + ); + + // Should have proper class expression + assert!( + result.code.contains("let BaseProvider = class BaseProvider"), + "JIT output should have class expression. Got:\n{}", + result.code + ); + + // Should have __decorate call + assert!( + result.code.contains("__decorate("), + "JIT output should use __decorate. Got:\n{}", + result.code + ); + + insta::assert_snapshot!("jit_abstract_class", result.code); +} + // ========================================================================= // Source map tests // ========================================================================= diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_abstract_class.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_abstract_class.snap new file mode 100644 index 000000000..0a54dbb93 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_abstract_class.snap @@ -0,0 +1,21 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +assertion_line: 6452 +expression: result.code +--- + +import { Injectable } from '@angular/core'; +import { __decorate } from "tslib"; + +let BaseProvider = class BaseProvider { + protected abstract get name(): string; + protected abstract initialize(): void; + + public greet(): string { + return `Hello from ${this.name}`; + } +}; +BaseProvider = __decorate([ + Injectable() +], BaseProvider); +export { BaseProvider };