fix(codegen): order accessibility before abstract on accessors (#26392)

Codegen emits invalid TypeScript when an abstract accessor has an explicit accessibility modifier. Both normal and minified output put `abstract` before `public` or `protected`, causing TS1029 because accessibility must precede `abstract`.

For this valid input:

```ts
abstract class C {
  public abstract accessor x: number;
  protected abstract accessor y: string;
}
```

The previous output reordered the members to:

```ts
abstract public accessor x: number;
abstract protected accessor y: string;
```

`AccessorProperty::gen` now prints accessibility before `abstract`, matching the neighboring method and property emitters and preserving the input's valid modifier order. The same ordering applies in minified output:

```ts
abstract class C{public abstract accessor x:number;protected abstract accessor y:string}
```

The change is confined to accessor modifier ordering; subsequent `static`, `override`, and `accessor` emission remains unchanged.
This commit is contained in:
camc314 committed 2026-09-07 13:27:26 +00:00
1 parent 34a242e134
commit b156333a23
3 files changed
+65 -8

No files matched your search

+5 -5
View File
@@ -3021,16 +3021,16 @@ impl Gen for AccessorProperty<'_> {
fn r#gen(&self, p: &mut Codegen, ctx: Context) {
p.add_source_mapping(self.span);
p.print_decorators(&self.decorators, ctx);
if self.r#type.is_abstract() {
p.print_space_before_identifier();
p.print_str("abstract");
p.print_soft_space();
}
if let Some(accessibility) = self.accessibility {
p.print_space_before_identifier();
p.print_str(accessibility.as_str());
p.print_soft_space();
}
if self.r#type.is_abstract() {
p.print_space_before_identifier();
p.print_str("abstract");
p.print_soft_space();
}
if self.r#static {
p.print_space_before_identifier();
p.print_str("static");
@@ -39,6 +39,11 @@ pub fn test_tsx(source_text: &str, expected: &str) {
test_options_with_source_type(source_text, expected, SourceType::tsx(), default_options());
}
#[track_caller]
pub fn test_ts(source_text: &str, expected: &str) {
test_options_with_source_type(source_text, expected, SourceType::ts(), default_options());
}
#[track_caller]
pub fn test_options_with_source_type(
source_text: &str,
+55 -3
View File
@@ -3,13 +3,65 @@ use oxc_parser::ParseOptions;
use oxc_span::SourceType;
use crate::{
snapshot, snapshot_options,
snapshot, snapshot_options, test_ts,
tester::{
default_options, test, test_idempotency, test_options_with_source_type, test_same,
test_tsx, test_with_parse_options,
default_options, test, test_idempotency, test_idempotency_options,
test_options_with_source_type, test_same, test_tsx, test_with_parse_options,
},
};
#[test]
fn abstract_accessor_modifiers() {
test_ts(
"abstract class C { public abstract accessor x: number; protected abstract accessor y: string; }",
"abstract class C {\n\tpublic abstract accessor x: number;\n\tprotected abstract accessor y: string;\n}\n",
);
}
#[test]
fn minify_abstract_accessor_modifiers() {
test_options_with_source_type(
"abstract class C { public abstract accessor x: number; protected abstract accessor y: string; }",
"abstract class C{public abstract accessor x:number;protected abstract accessor y:string}",
SourceType::ts(),
CodegenOptions::minify(),
);
}
#[test]
fn accessor_modifier_combinations() {
let check = |source: &str, minified: &str| {
test_ts(source, source);
test_idempotency(source);
test_options_with_source_type(source, minified, SourceType::ts(), CodegenOptions::minify());
test_idempotency_options(source, &CodegenOptions::minify());
};
check(
"abstract class C {\n\tabstract accessor x: number;\n}\n",
"abstract class C{abstract accessor x:number}",
);
check(
"abstract class C extends B {\n\tpublic abstract override accessor x: number;\n}\n",
"abstract class C extends B{public abstract override accessor x:number}",
);
check(
"abstract class C extends B {\n\tprotected abstract override accessor x: number;\n}\n",
"abstract class C extends B{protected abstract override accessor x:number}",
);
check("class C {\n\tpublic static accessor x = 1;\n}\n", "class C{public static accessor x=1}");
check(
"class C {\n\tprotected static accessor x = 1;\n}\n",
"class C{protected static accessor x=1}",
);
check("class C {\n\tprivate accessor x = 1;\n}\n", "class C{private accessor x=1}");
check(
"class C extends B {\n\tpublic override accessor x = 1;\n}\n",
"class C extends B{public override accessor x=1}",
);
check("class C {\n\taccessor #x = 1;\n}\n", "class C{accessor#x=1}");
check("class C {\n\tpublic accessor x!: number;\n}\n", "class C{public accessor x!:number}");
}
#[test]
fn cases() {
test_same("({ foo(): string {} });\n");