Files
oxc-project__oxc/packages/codegen/src-js/print/class.ts
camc314 a111b5b340 fix(packages/codegen): print accessibility modifiers before abstract (#26402)
`packages/codegen` emits invalid TypeScript for abstract accessors with an explicit accessibility modifier. The JavaScript printer writes `abstract` before `public` or `protected`, but TypeScript requires accessibility to come first and reports TS1029 for the generated declaration.

For example, this valid input:

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

was printed as:

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

The printer now preserves the valid modifier order:

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

`printAccessorProperty` now emits accessibility before `abstract`, matching the neighboring method and property printers. The same ordering also handles declarations such as `protected abstract override accessor x: number` and applies to both the ordinary and source-map-enabled builds.

This brings the JavaScript printer in line with the Rust codegen correction in #26392. The change is confined to accessor modifier ordering; subsequent `static`, `override`, and `accessor` emission and source-map anchoring retain their existing behavior.
2026-09-07 15:13:55 +00:00

437 lines
12 KiB
TypeScript

// Classes.
import { typeAssertIs } from "../asserts.ts";
import { printPropertyKey } from "./binding_pattern.ts";
import {
CAT_CLOSE_BRACKET,
CAT_IDENT,
CAT_OP_UN_NOT,
CAT_OTHER,
CAT_QUESTION,
CAT_START_OF_STMT,
} from "./categories.ts";
import {
debugAssertLastFresh,
markMapStart,
write,
writeIdent,
writeNoLast,
writeWithMap,
writeWithMapEnd,
writeWithMapNamed,
writeWithMapNoLast,
} from "./write.ts";
import { printExpression } from "./expression.ts";
import { printFunctionBody, printParenParams } from "./function.ts";
import { printSpaceBeforeIdentifier } from "./space.ts";
import { printIndent } from "./indent.ts";
import { CTX_NONE } from "./operators.ts";
import { PREC_CALL, PREC_COMMA, PREC_LOWEST, PREC_POSTFIX } from "./precedence.ts";
import { printStatement } from "./statement.ts";
import {
printTSIndexSignature,
printTypeAnnotation,
printTypeArguments,
printTypeParameters,
} from "./typescript.ts";
import type { State } from "../state.ts";
import type {
AccessorPropertyNode,
ClassBodyNode,
MethodDefinitionNode,
PropertyDefinitionNode,
} from "./types.ts";
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
/**
* Print a class declaration or expression, decorators and heritage clauses included.
*
* A class expression is parenthesized where the statement or an `export default` starts with it,
* as `printFunction` does for functions.
*/
export function printClass(node: ESTree.Class, state: State): void {
let wrap = false;
if (node.type === "ClassExpression") {
debugAssertLastFresh(state);
// `CAT_START_OF_STMT` or `CAT_START_OF_DEFAULT_EXPORT`, which are adjacent - see `categories.ts`
wrap = (state.last | 1) === CAT_START_OF_STMT;
}
if (wrap) write(state, "(", CAT_OTHER);
const { decorators } = node;
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
printSpaceBeforeIdentifier(state);
const declare = TS && node.declare;
const abstract = TS && node.abstract;
// The node's mapping goes on whichever of these is written first
if (declare) writeWithMap(state, "declare ", CAT_OTHER, node.start, node.end, node);
if (abstract) {
if (declare) {
write(state, "abstract ", CAT_OTHER);
} else {
writeWithMap(state, "abstract ", CAT_OTHER, node.start, node.end, node);
}
}
if (declare || abstract) {
writeIdent(state, "class");
} else {
writeWithMap(state, "class", CAT_IDENT, node.start, node.end, node);
}
if (node.id != null) {
write(state, " ", CAT_OTHER);
writeWithMapNamed(state, node.id.name, node.id.start, node.id.end, node.id);
}
if (TS) printTypeParameters(node.typeParameters, state);
if (node.superClass != null) {
write(state, " extends ", CAT_OTHER);
printExpression(node.superClass, state, PREC_POSTFIX, CTX_NONE);
if (TS) printTypeArguments(node.superTypeArguments, state);
}
const { implements: implementsClauses } = node;
if (TS && implementsClauses != null && implementsClauses.length > 0) {
write(state, " implements ", CAT_OTHER);
const { length } = implementsClauses;
for (let i = 0; i < length; i++) {
if (i > 0) write(state, ", ", CAT_OTHER);
const clause = implementsClauses[i];
printExpression(clause.expression, state, PREC_CALL, CTX_NONE);
printTypeArguments(clause.typeArguments, state);
}
}
write(state, " ", CAT_OTHER);
printClassBody(node.body, state);
if (wrap) write(state, ")", CAT_CLOSE_BRACKET);
}
/**
* Print a run of decorators separated by spaces, parenthesizing any whose expression needs it.
*/
export function printDecorators(decorators: ESTree.Decorator[], state: State): void {
const { length } = decorators;
for (let i = 0; i < length; i++) {
const decorator = decorators[i];
writeWithMap(state, "@", CAT_OTHER, decorator.start, decorator.end, decorator);
const { expression } = decorator;
const wrap = decoratorNeedsWrap(expression);
if (wrap) write(state, "(", CAT_OTHER);
printExpression(expression, state, PREC_LOWEST, CTX_NONE);
if (wrap) write(state, ")", CAT_CLOSE_BRACKET);
write(state, " ", CAT_OTHER);
}
}
/**
* Whether a decorator's expression has to be parenthesized.
*
* The grammar allows a name, a chain of plain member accesses, and a call on either, and nothing
* else - so a computed access needs parentheses even though a static one does not.
*/
function decoratorNeedsWrap(expr: ESTree.Expression): boolean {
for (;;) {
switch (expr.type) {
case "Identifier":
return false;
case "MemberExpression":
// `@a.b` / `@this.b` / `@x!.y` need no parens; computed access does
return expr.computed;
case "CallExpression":
expr = expr.callee;
break;
default:
return true;
}
}
}
/**
* Print the members of a class in braces, empty ones tight.
*/
function printClassBody(node: ClassBodyNode, state: State): void {
const { body } = node;
const { length } = body;
if (length === 0) {
writeWithMapNoLast(state, "{", node.start, node.end, node);
writeWithMapEnd(state, "}", CAT_OTHER, node.start, node.end, node);
return;
}
writeWithMap(state, "{\n", CAT_OTHER, node.start, node.end, node);
state.indentLevel++;
for (let i = 0; i < length; i++) {
printIndent(state);
const element = body[i];
switch (element.type) {
case "MethodDefinition":
/* IF TS */
case "TSAbstractMethodDefinition":
/* END_IF */
printMethodDefinition(element, state);
write(state, "\n", CAT_OTHER);
break;
case "PropertyDefinition":
/* IF TS */
case "TSAbstractPropertyDefinition":
/* END_IF */
printPropertyDefinition(element, state);
write(state, ";\n", CAT_OTHER);
break;
case "StaticBlock":
printStaticBlock(element, state);
write(state, "\n", CAT_OTHER);
break;
case "AccessorProperty":
/* IF TS */
case "TSAbstractAccessorProperty":
/* END_IF */
printAccessorProperty(element, state);
write(state, ";\n", CAT_OTHER);
break;
/* IF TS */
case "TSIndexSignature":
printTSIndexSignature(element, state);
write(state, ";\n", CAT_OTHER);
break;
/* END_IF */
default:
throw new Error(`Unknown class element type: ${element.type}`);
}
}
state.indentLevel--;
printIndent(state);
writeWithMapEnd(state, "}", CAT_OTHER, node.start, node.end, node);
}
/**
* Print a method, including getters, setters, constructors and their modifiers.
*/
function printMethodDefinition(node: MethodDefinitionNode, state: State): void {
markMapStart(state, node.start, node.end, node);
const { decorators } = node;
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
if (TS && node.accessibility != null) {
printSpaceBeforeIdentifier(state);
writeNoLast(state, node.accessibility);
write(state, " ", CAT_OTHER);
}
if (TS && (node.type === "TSAbstractMethodDefinition" || node.abstract)) {
printSpaceBeforeIdentifier(state);
write(state, "abstract ", CAT_OTHER);
}
if (node.static) {
printSpaceBeforeIdentifier(state);
write(state, "static ", CAT_OTHER);
}
if (TS && node.override) {
printSpaceBeforeIdentifier(state);
write(state, "override ", CAT_OTHER);
}
const { kind } = node;
if (kind === "get") {
printSpaceBeforeIdentifier(state);
write(state, "get ", CAT_OTHER);
} else if (kind === "set") {
printSpaceBeforeIdentifier(state);
write(state, "set ", CAT_OTHER);
}
const func = node.value;
if (func.async) {
printSpaceBeforeIdentifier(state);
write(state, "async ", CAT_OTHER);
}
if (func.generator) write(state, "*", CAT_OTHER);
if (node.computed) {
write(state, "[", CAT_OTHER);
typeAssertIs<ESTree.Expression>(node.key);
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
write(state, "]", CAT_CLOSE_BRACKET);
} else {
printPropertyKey(node.key, state);
}
if (TS && node.optional) write(state, "?", CAT_QUESTION);
if (TS) printTypeParameters(func.typeParameters, state);
printParenParams(func.params, state);
if (TS && func.returnType != null) {
printTypeAnnotation(func.returnType, state);
}
if (func.body != null) {
write(state, " ", CAT_OTHER);
printFunctionBody(func.body, state);
} else {
write(state, ";", CAT_OTHER);
}
}
/**
* Print a class field, with its modifiers and initializer.
*/
function printPropertyDefinition(node: PropertyDefinitionNode, state: State): void {
markMapStart(state, node.start, node.end, node);
const { decorators } = node;
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
if (TS && node.declare) {
printSpaceBeforeIdentifier(state);
write(state, "declare ", CAT_OTHER);
}
if (TS && node.accessibility != null) {
printSpaceBeforeIdentifier(state);
writeNoLast(state, node.accessibility);
write(state, " ", CAT_OTHER);
}
if (TS && (node.type === "TSAbstractPropertyDefinition" || node.abstract)) {
printSpaceBeforeIdentifier(state);
write(state, "abstract ", CAT_OTHER);
}
if (node.static) {
printSpaceBeforeIdentifier(state);
write(state, "static ", CAT_OTHER);
}
if (TS && node.override) {
printSpaceBeforeIdentifier(state);
write(state, "override ", CAT_OTHER);
}
if (TS && node.readonly) {
printSpaceBeforeIdentifier(state);
write(state, "readonly ", CAT_OTHER);
}
if (node.computed) {
write(state, "[", CAT_OTHER);
typeAssertIs<ESTree.Expression>(node.key);
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
write(state, "]", CAT_CLOSE_BRACKET);
} else {
printPropertyKey(node.key, state);
}
if (TS && node.optional) write(state, "?", CAT_QUESTION);
if (TS && node.definite) write(state, "!", CAT_OP_UN_NOT);
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
if (node.value != null) {
write(state, " = ", CAT_OTHER);
printExpression(node.value, state, PREC_COMMA, CTX_NONE);
}
}
/**
* Print a `static { … }` block.
*
* Its body has no directive prologue, unlike a function body, so a string statement at the top of
* one needs no protection from being read back as a directive.
*/
function printStaticBlock(node: ESTree.StaticBlock, state: State): void {
printSpaceBeforeIdentifier(state);
writeWithMap(state, "static ", CAT_OTHER, node.start, node.end, node);
const { body } = node;
const { length } = body;
if (length === 0) {
writeWithMapNoLast(state, "{", node.start, node.end, node);
writeWithMapEnd(state, "}", CAT_OTHER, node.start, node.end, node);
return;
}
writeWithMap(state, "{\n", CAT_OTHER, node.start, node.end, node);
state.indentLevel++;
for (let i = 0; i < length; i++) {
printStatement(body[i], state);
}
state.indentLevel--;
printIndent(state);
writeWithMapEnd(state, "}", CAT_OTHER, node.start, node.end, node);
}
/**
* Print an `accessor` field.
*/
function printAccessorProperty(node: AccessorPropertyNode, state: State): void {
markMapStart(state, node.start, node.end, node);
const { decorators } = node;
if (decorators != null && decorators.length > 0) printDecorators(decorators, state);
if (TS && node.accessibility != null) {
printSpaceBeforeIdentifier(state);
writeNoLast(state, node.accessibility);
write(state, " ", CAT_OTHER);
}
if (TS && (node.type === "TSAbstractAccessorProperty" || node.abstract)) {
printSpaceBeforeIdentifier(state);
write(state, "abstract ", CAT_OTHER);
}
if (node.static) {
printSpaceBeforeIdentifier(state);
write(state, "static ", CAT_OTHER);
}
if (TS && node.override) {
printSpaceBeforeIdentifier(state);
write(state, "override ", CAT_OTHER);
}
printSpaceBeforeIdentifier(state);
writeIdent(state, "accessor");
if (node.computed) {
write(state, " [", CAT_OTHER);
typeAssertIs<ESTree.Expression>(node.key);
printExpression(node.key, state, PREC_COMMA, CTX_NONE);
write(state, "]", CAT_CLOSE_BRACKET);
} else {
write(state, " ", CAT_OTHER);
printPropertyKey(node.key, state);
}
if (TS && node.definite) write(state, "!", CAT_OP_UN_NOT);
if (TS && node.typeAnnotation != null) printTypeAnnotation(node.typeAnnotation, state);
if (node.value != null) {
write(state, " = ", CAT_OTHER);
printExpression(node.value, state, PREC_COMMA, CTX_NONE);
}
}