mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Build JavaScript default-export objects that hold class values
`export default { parse, stringify, DateType, ParseError }` failed at
module initialization: the literal's width check against the default
binding's type found class members missing.
- A class name in a shorthand property (`{ ParseError }`) now evaluates
to the class object, exactly like the long form `{ ParseError:
ParseError }`; it had no lowering, so the JavaScript literal dropped
the member.
- A class the program could not lower (one extending Date) is still
dropped from the literal, its diagnostic deferred to the reads that
reach it. The default binding of a JavaScript object literal now
registers its type without such members, so the module initializes
and the named exports work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
139542bcf1
commit
d1e39f3d8c
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6494-default-export-class-members/lib/date.js",
|
||||
"<repo>/tests/corpus/6494-default-export-class-members/lib/error.js",
|
||||
"<repo>/tests/corpus/6494-default-export-class-members/lib/index.js",
|
||||
"<repo>/tests/corpus/6494-default-export-class-members/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class ParseError extends Error {
|
||||
constructor(message, line) {
|
||||
super(message);
|
||||
this.name = "ParseError";
|
||||
this.line = line;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "default-export-class-members-corpus",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
Reference in New Issue
Block a user