diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 303b6ed55..e276326c9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -3193,6 +3193,22 @@ export function lowerShorthandValue( `generic functions as values (call '${propName.text}' directly)`, ); } + // A program class name (`{ parse, TomlError }`, an imported class + // included): the class object, exactly the long form `{ C: C }`. + // Generic classes keep the identifier path's contextual pinning. + const classInfo = lowerer.classBySymbol.get(resolved); + if (classInfo && !classInfo.generic) { + const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; + if (decoratedGlobal !== undefined) { + return { + kind: "varRef", + localId: decoratedGlobal, + type: { kind: "classval", className: classInfo.def.name }, + loc, + }; + } + return lowerer.classValueRef(classInfo, propName); + } } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index f0199d6a4..3a786aa4e 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1229,6 +1229,36 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi return false; } +/** A JavaScript default-export object literal holding a class the program + * could not lower (`export default { parse, TomlDate }` where TomlDate + * extends an unsupported base): the literal narrows that member away, its + * diagnostics deferred to the reads that reach it, so the binding's type + * drops it too. Otherwise the module's own initialization would fail the + * width check against members no build can supply. */ +function withoutRejectedClassMembers( + lowerer: Lowerer, + expression: ts.Expression, + type: IrType, +): IrType { + let literal = expression; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (!ts.isObjectLiteralExpression(literal) || type.kind !== "record") return type; + const shape = lowerer.shapes.get(type.shapeId); + if (!shape || shape.tuple) return type; + const rejected = (t: IrType): boolean => + t.kind === "classval" && + !lowerer.classes.has(t.className) && + lowerer.deferredClassByName.has(t.className); + if (!shape.fields.some((f) => rejected(f.type))) return type; + const kept = shape.fields.filter((f) => !rejected(f.type)); + const shapeId = lowerer.shapes.intern( + kept.map((f) => ({ name: f.name, type: f.type })), + false, + shape.indexValue, + ); + return { kind: "record", shapeId }; +} + export function collectGlobals( lowerer: Lowerer, sf: ts.SourceFile, @@ -1331,6 +1361,7 @@ export function collectGlobals( type = unitOnlyUnion(lowerer.unions); } if (type.kind === "void") lowerer.badType(stmt.expression, tsType); + if (isJsSourceFile(sf)) type = withoutRejectedClassMembers(lowerer, stmt.expression, type); const g: IrGlobal = { id: `%g.${tag}default`, name: "default", type, mutable: false }; lowerer.globalsBySymbol.set(symbol, g); lowerer.globalsList.push(g); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json new file mode 100644 index 000000000..24634cff1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6494-default-export-class-members/lib/date.js", + "/tests/corpus/6494-default-export-class-members/lib/error.js", + "/tests/corpus/6494-default-export-class-members/lib/index.js", + "/tests/corpus/6494-default-export-class-members/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6494-default-export-class-members/lib/date.js b/tests/corpus/6494-default-export-class-members/lib/date.js new file mode 100644 index 000000000..0c4a426f7 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/date.js @@ -0,0 +1,8 @@ +// A class whose base scriptc cannot extend yet: the default export object +// below narrows this member away, and only reads of it would fence. +export class StampDate extends Date { + constructor(text) { + super(text); + this.label = "stamp:" + text; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/error.js b/tests/corpus/6494-default-export-class-members/lib/error.js new file mode 100644 index 000000000..7f2ac5744 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/error.js @@ -0,0 +1,7 @@ +export class ParseError extends Error { + constructor(message, line) { + super(message); + this.name = "ParseError"; + this.line = line; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/index.js b/tests/corpus/6494-default-export-class-members/lib/index.js new file mode 100644 index 000000000..66935fac6 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/index.js @@ -0,0 +1,14 @@ +import { StampDate } from "./date.js"; +import { ParseError } from "./error.js"; +function parse(text) { + if (text === "") throw new ParseError("empty document", 1); + const [key, value] = text.split("=").map((part) => part.trim()); + return { key, value }; +} +function stringify(value) { + return JSON.stringify(value); +} +// A default export object of functions and class values (the shorthand +// spelling of `{ ParseError: ParseError }`). +export default { parse, stringify, StampDate, ParseError }; +export { parse, stringify, StampDate, ParseError }; diff --git a/tests/corpus/6494-default-export-class-members/main.ts b/tests/corpus/6494-default-export-class-members/main.ts new file mode 100644 index 000000000..1548e6415 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/main.ts @@ -0,0 +1,15 @@ +// A JavaScript module's default export object holding functions and class +// values builds at module initialization, and its members work through +// both the default object and the named exports. +import lib, { parse, ParseError } from "./lib/index.js"; + +console.log(JSON.stringify(parse("a = 1"))); +console.log(lib.stringify(lib.parse("b = 2"))); +try { + lib.parse(""); +} catch (e) { + if (e instanceof ParseError) console.log(e.name, e.message, e.line); +} +const Ctor = lib.ParseError; +const made = new Ctor("built through the default object", 7); +console.log(made instanceof ParseError, made.message, made.line, Ctor === ParseError); diff --git a/tests/corpus/6494-default-export-class-members/package.json b/tests/corpus/6494-default-export-class-members/package.json new file mode 100644 index 000000000..ca00e63c9 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/package.json @@ -0,0 +1,5 @@ +{ + "name": "default-export-class-members-corpus", + "private": true, + "type": "module" +}