mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Run CommonJS top-level return, untyped assert forms, and untyped instanceof Object as Node does (#2101)
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`.
|
||||
Values whose type can be cyclic, such as recursive records, also read as `undefined`.
|
||||
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
|
||||
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
|
||||
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
|
||||
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
|
||||
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
|
||||
Checked-dynamic values compared deeply have no cycle detection.
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
`instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
|
||||
|
||||
export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
|
||||
"assert.throwsValidate": "scr_assert_throws_validate",
|
||||
"insp.jsonDyn": "scr_util_format_j",
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
@@ -402,6 +403,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"assert.eqBool": "scr_assert_eq_bool",
|
||||
"assert.eqSym": "scr_assert_eq_sym",
|
||||
"assert.eqDyn": "scr_assert_eq_dyn",
|
||||
"assert.messageValue": "scr_assert_message_value",
|
||||
"assert.methodValue": "scr_assert_method_value",
|
||||
"assert.messageClear": "scr_assert_message_clear",
|
||||
"assert.looseResult": "scr_assert_loose_result",
|
||||
"assert.deepResult": "scr_assert_deep_result",
|
||||
"assert.looseDeepResult": "scr_assert_loose_deep_result",
|
||||
|
||||
@@ -2228,23 +2228,10 @@ function lowerRuntimeKeyIn(
|
||||
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** Standard-library constructors the untyped instanceof helper has no
|
||||
* instance test for: an untyped `x instanceof Object` always refuses. */
|
||||
const DYN_INSTANCEOF_REFUSED: ReadonlySet<string> = new Set(["Object", "Function"]);
|
||||
|
||||
/** The SC1090 message `x instanceof C` throws, with the site's position,
|
||||
* when `C` is a runtime constructor with no instance test. A right-hand
|
||||
* side that names one of DYN_INSTANCEOF_REFUSED always reaches that
|
||||
* refusal, so the site is listed with the runtime fences. */
|
||||
* when `C` is a runtime constructor with no instance test. */
|
||||
function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string {
|
||||
const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor");
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr.right);
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
lowerer.isStdlibSymbol(symbol) &&
|
||||
DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol))
|
||||
)
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const sf = ts.getSourceFileOfNode(expr);
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1;
|
||||
return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`;
|
||||
|
||||
@@ -51,11 +51,15 @@ import {
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
isDynTypedRefType,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { boolLit, countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { everyExpr } from "../../ir/traverse.js";
|
||||
import { checkedClassInstanceOf } from "./class-construction.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
|
||||
/** node:assert/strict binds the loose NAMES to the strict comparisons —
|
||||
* Node's own aliasing (`strict.equal === assert.strictEqual`). */
|
||||
@@ -73,6 +77,63 @@ function canonicalAssertMember(module: string, member: string): string {
|
||||
return member;
|
||||
}
|
||||
|
||||
/** The assert methods that are function values (scr_assert_method_value). */
|
||||
const ASSERT_FUNCTION_VALUES: ReadonlySet<string> = new Set([
|
||||
"ok",
|
||||
"fail",
|
||||
"equal",
|
||||
"notEqual",
|
||||
"strictEqual",
|
||||
"notStrictEqual",
|
||||
"deepEqual",
|
||||
"notDeepEqual",
|
||||
"deepStrictEqual",
|
||||
"notDeepStrictEqual",
|
||||
"partialDeepStrictEqual",
|
||||
"ifError",
|
||||
]);
|
||||
|
||||
/** The assert method a `const` binding holds (`const ok = assert.ok`,
|
||||
* `const eq = strictEqual`), or null. */
|
||||
export function assertFunctionAliasOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): { module: string; member: string } | null {
|
||||
const decl = lowerer.checker.getSymbolAtLocation(ident)?.valueDeclaration;
|
||||
if (
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
!ts.isVariableDeclarationList(decl.parent) ||
|
||||
(decl.parent.flags & ts.NodeFlags.Const) === 0 ||
|
||||
!decl.initializer
|
||||
)
|
||||
return null;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
const bi = ts.isPropertyAccessExpression(init)
|
||||
? lowerer.builtinMemberOf(init)
|
||||
: ts.isIdentifier(init)
|
||||
? lowerer.builtinImportOf(init)
|
||||
: null;
|
||||
if (!bi || (bi.module !== "assert" && bi.module !== "assert/strict")) return null;
|
||||
return ASSERT_FUNCTION_VALUES.has(canonicalAssertMember(bi.module, bi.member)) ? bi : null;
|
||||
}
|
||||
|
||||
/** An assert method read as a value (`emitter.on("x", assert.fail)`,
|
||||
* `const eq = assert.strictEqual`): an untyped function that asserts as
|
||||
* the method does, the same value for every read. Null for other
|
||||
* members. */
|
||||
export function assertFunctionValue(
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
const member = canonicalAssertMember(bi.module, bi.member);
|
||||
if (!ASSERT_FUNCTION_VALUES.has(member)) return null;
|
||||
return { kind: "libCall", fn: "assert.methodValue", args: [strLit(member, loc)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/* Node's AssertionError carries actual, expected and operator. Where the
|
||||
* verdict is computed in compiled code, the failure branch records them
|
||||
* (assert.record) before the throwing call, so a passing assertion boxes
|
||||
@@ -166,28 +227,156 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha
|
||||
);
|
||||
}
|
||||
|
||||
/** The message the runtime reads from a registered value
|
||||
* (SCR_ASSERT_VALUE_MESSAGE in scr_assert.h). */
|
||||
const VALUE_MESSAGE = "\u0001scriptc:assert-message-value";
|
||||
|
||||
/** A message argument that is not a string, met while one assertion
|
||||
* lowers (withValueMessage). `fail` marks assert.fail's message, which
|
||||
* Node uses for every value but null and undefined. */
|
||||
let valueMessage: { value: IrExpr; has: string; text: string; fail: boolean } | null = null;
|
||||
|
||||
/** The optional trailing message argument: Node accepts a string or an
|
||||
* Error (throwing the Error itself) — the STRING form lowers; a literal
|
||||
* `undefined` counts as omitted (Node's `message == null` test). Returns
|
||||
* the libCall's (msg, hasMsg) pair — a "" dummy when omitted, so the
|
||||
* runtime can distinguish an omitted message from an empty one. */
|
||||
* runtime can distinguish an omitted message from an empty one. Any other
|
||||
* value that crosses into the checked-dynamic tree (an untyped
|
||||
* `failures.join('')`, an Error) is registered with the runtime at the call
|
||||
* instead (withValueMessage). */
|
||||
function lowerMessageArg(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression | undefined,
|
||||
loc: SrcLoc,
|
||||
fail = false,
|
||||
): { msg: IrExpr; hasMsg: IrExpr } {
|
||||
if (!node || (ts.isIdentifier(node) && node.text === "undefined")) {
|
||||
return { msg: { kind: "strLit", value: "", type: STRING, loc }, hasMsg: boolLit(false, loc) };
|
||||
}
|
||||
const msg = lowerer.lowerExpr(node);
|
||||
if (msg.type.kind !== "string") {
|
||||
if (msg.type.kind === "string") return { msg, hasMsg: boolLit(true, loc) };
|
||||
const value =
|
||||
valueMessage === null && (msg.type.kind === "dyn" || lowerer.dynConvertible(msg.type))
|
||||
? lowerer.coerceToExpected(msg, DYN)
|
||||
: null;
|
||||
if (value === null || value.type.kind !== "dyn") {
|
||||
lowerer.noLowering(
|
||||
`assert messages of type '${lowerer.fmt(msg.type)}'`,
|
||||
node,
|
||||
"only string messages lower (Node also accepts an Error to throw — construct and throw it directly)",
|
||||
);
|
||||
}
|
||||
return { msg, hasMsg: boolLit(true, loc) };
|
||||
const has = lowerer.declareHiddenLocal("assert.hasMessage", BOOL);
|
||||
const text = lowerer.declareHiddenLocal("assert.messageText", STRING);
|
||||
valueMessage = { value, has: has.id, text: text.id, fail };
|
||||
return { msg: varRef(text.id, STRING, loc), hasMsg: varRef(has.id, BOOL, loc) };
|
||||
}
|
||||
|
||||
/** Lowers one assertion; a message value met on the way is evaluated after
|
||||
* the assertion's other arguments, as Node evaluates them. A null or
|
||||
* undefined message is omitted (Node's `message == null` test); a truthy
|
||||
* one is registered with the runtime, which the failure reads; any other
|
||||
* value is the empty message, which Node's comparison diffs treat as no
|
||||
* message too (createErrDiff tests the message's truthiness). */
|
||||
function withValueMessage(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
lower: () => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
const outer = valueMessage;
|
||||
valueMessage = null;
|
||||
try {
|
||||
const lowered = lower();
|
||||
const message = valueMessage as {
|
||||
value: IrExpr;
|
||||
has: string;
|
||||
text: string;
|
||||
fail: boolean;
|
||||
} | null;
|
||||
if (lowered === null || message === null) return lowered;
|
||||
// assert.ok's statements evaluate the value first; its call is last.
|
||||
const before = lowered.kind === "seqExpr" ? lowered.stmts : [];
|
||||
const call = lowered.kind === "seqExpr" ? lowered.result : lowered;
|
||||
if (call.kind !== "libCall") {
|
||||
lowerer.noLowering(
|
||||
"assert messages that are not strings in this assertion form",
|
||||
expr,
|
||||
"pass a string message",
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [...before];
|
||||
// An argument computed from the message (assert.fail's generated flag)
|
||||
// reads it after the declarations below; the others are evaluated first.
|
||||
const readsMessage = (arg: IrExpr): boolean =>
|
||||
!everyExpr(arg, {
|
||||
stmt: () => true,
|
||||
expr: (e) =>
|
||||
!(e.kind === "varRef" && (e.localId === message.has || e.localId === message.text)),
|
||||
});
|
||||
const args = call.args.map((arg, index) =>
|
||||
readsMessage(arg) ? arg : stableValue(lowerer, stmts, arg, `assert.arg${index}`, loc),
|
||||
);
|
||||
const local = lowerer.declareHiddenLocal("assert.message", DYN);
|
||||
const value = varRef(local.id, DYN, loc);
|
||||
// The runtime reports a registered value: a truthy one for the other
|
||||
// methods, any value but null and undefined for assert.fail (Node's
|
||||
// `actual == null` test), whose other values read "Failed".
|
||||
const registered: IrExpr = message.fail
|
||||
? varRef(message.has, BOOL, loc)
|
||||
: { kind: "dynTest", test: "truthy", value, type: BOOL, loc };
|
||||
stmts.push(
|
||||
{ kind: "varDecl", localId: local.id, init: message.value, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.has,
|
||||
init: { kind: "dynTest", test: "nullish", negated: true, value, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: message.text,
|
||||
init: {
|
||||
kind: "ternary",
|
||||
cond: registered,
|
||||
then: strLit(VALUE_MESSAGE, loc),
|
||||
else_: strLit(message.fail ? "Failed" : "", loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: registered,
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "assert.messageValue",
|
||||
args: [value, { kind: "dynTest", test: "error", value, type: BOOL, loc }],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: { ...call, args }, loc },
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: { kind: "libCall", fn: "assert.messageClear", args: [], type: VOID, loc },
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
} finally {
|
||||
valueMessage = outer;
|
||||
}
|
||||
}
|
||||
|
||||
/** The module-function dispatch — the spoke's entry, called from both the
|
||||
@@ -201,6 +390,15 @@ export function lowerAssertModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module !== "assert" && bi.module !== "assert/strict") return null;
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertMember(lowerer, expr, bi, loc));
|
||||
}
|
||||
|
||||
function lowerAssertMember(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (canonicalAssertMember(bi.module, bi.member)) {
|
||||
case "ok":
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
@@ -880,7 +1078,7 @@ export function lowerAssertDirectCall(
|
||||
'use the default import: import assert from "node:assert")',
|
||||
);
|
||||
}
|
||||
return lowerAssertOk(lowerer, expr, loc);
|
||||
return withValueMessage(lowerer, expr, loc, () => lowerAssertOk(lowerer, expr, loc));
|
||||
}
|
||||
|
||||
/** assert(value[, message]) / assert.ok(value[, message]): the truthiness
|
||||
@@ -889,7 +1087,24 @@ export function lowerAssertDirectCall(
|
||||
* the same bytes at compile time). */
|
||||
function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression));
|
||||
if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) {
|
||||
if (expr.arguments.nodes.length === 0) {
|
||||
// Node's message for a call without arguments.
|
||||
const text = "No value argument passed to `assert.ok()`";
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [recordOnFailure(lowerer, boolLit(true, loc), null, boolLit(true, loc), "==", loc)],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "assert.ok",
|
||||
args: [boolLit(false, loc), strLit(text, loc), boolLit(true, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (expr.arguments.nodes.length > 2) {
|
||||
lowerer.noLowering(
|
||||
`${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`,
|
||||
expr,
|
||||
@@ -953,22 +1168,26 @@ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc)
|
||||
generated = boolLit(true, loc);
|
||||
} else {
|
||||
// Node's AssertionError sets generatedMessage to !message, so an
|
||||
// empty custom message counts as generated.
|
||||
msg = stableValue(
|
||||
lowerer,
|
||||
stmts,
|
||||
lowerMessageArg(lowerer, msgNode, loc).msg,
|
||||
"%assertFailMessage",
|
||||
loc,
|
||||
);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// empty custom message counts as generated. A message that is not a
|
||||
// string is read from the hidden local that withValueMessage declares
|
||||
// after these statements, so it is used as is.
|
||||
const { msg: lowered, hasMsg } = lowerMessageArg(lowerer, msgNode, loc, true);
|
||||
if (lowered.kind === "varRef" && hasMsg.kind === "varRef") {
|
||||
// A value message: generated when Node reads "Failed" for null or
|
||||
// undefined; the runtime sets the flag for a registered value.
|
||||
msg = lowered;
|
||||
generated = { kind: "unary", op: "!", operand: hasMsg, type: BOOL, loc };
|
||||
} else {
|
||||
msg = stableValue(lowerer, stmts, lowered, "%assertFailMessage", loc);
|
||||
generated = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: msg,
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const falseLit: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
stmts.push(recordOnFailure(lowerer, boolLit(true, loc), null, null, "fail", loc));
|
||||
@@ -1056,9 +1275,22 @@ function lowerAssertEqual(
|
||||
// would answer false where Node can answer true (the value's own
|
||||
// earlier crossing) — a miscompare, never lowered. Bytes fence both
|
||||
// ways (the dyn copy cannot carry the Buffer/Uint8Array brand the
|
||||
// checker type knows); class instances and everything non-convertible
|
||||
// keep the fence too.
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn") {
|
||||
// checker type knows); everything non-convertible keeps the fence too.
|
||||
// A class instance crosses as a reference to the instance itself, so
|
||||
// strict compares are honest reference equality, between two instances
|
||||
// too (assert.strictEqual(e, e.setMaxListeners(1))).
|
||||
const instanceType = (t: IrType): boolean =>
|
||||
isDynTypedRefType(t) && !lowerer.isSubclassOf(t.className, "%Error");
|
||||
const instance = (e: IrExpr): boolean =>
|
||||
!deep &&
|
||||
(instanceType(e.type) ||
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every((arm) => instanceType(arm) || isUnitType(arm)) ??
|
||||
false))) &&
|
||||
lowerer.dynConvertible(e.type);
|
||||
if (a.type.kind === "dyn" || b.type.kind === "dyn" || (instance(a) && instance(b))) {
|
||||
const box = (e: IrExpr, node: ts.Expression): IrExpr => {
|
||||
if (e.type.kind === "dyn") return e;
|
||||
const k = e.type.kind;
|
||||
@@ -1067,6 +1299,18 @@ function lowerAssertEqual(
|
||||
k === "f64" ||
|
||||
k === "string" ||
|
||||
k === "bool" ||
|
||||
// A union of scalars and units crosses by value, like its arms.
|
||||
(e.type.kind === "union" &&
|
||||
(lowerer.unions
|
||||
.get(e.type.unionId)
|
||||
?.arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "bool" ||
|
||||
isUnitType(arm),
|
||||
) ??
|
||||
false)) ||
|
||||
(k === "func" && lowerer.dynConvertible(e.type)) ||
|
||||
// Runtime handles cross by REFERENCE (identity preserved), so the
|
||||
// strict compare is honest: assert.strictEqual(this, server) —
|
||||
@@ -1082,6 +1326,9 @@ function lowerAssertEqual(
|
||||
// same). The deep walk compares the retained error data without
|
||||
// re-encoding and losing identity (SEMANTICS.md).
|
||||
(k === "object" && e.type.kind === "object" && e.type.className === "%Error") ||
|
||||
instance(e) ||
|
||||
// A symbol crosses as itself, so strict compares are identity.
|
||||
(!deep && k === "symbol") ||
|
||||
(deep && k !== "bytes" && k !== "object" && lowerer.dynConvertible(e.type)) ||
|
||||
// Under skipPrototype the prototype a class instance or Buffer
|
||||
// brand would carry is not compared.
|
||||
@@ -1470,17 +1717,29 @@ function classifyThrowsExpected(
|
||||
displayName: rec ? rec.lib : info.def.name.replace(/^%/, ""),
|
||||
};
|
||||
}
|
||||
if (
|
||||
(surface === "assert.doesNotThrow" || surface === "assert.doesNotReject") &&
|
||||
expectedT?.kind === "func"
|
||||
) {
|
||||
if (expectedT?.kind === "func") {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
// An untyped value there (a dynamic-tier function's result) is checked
|
||||
// at run time like any untyped expected value.
|
||||
if (value.type.kind === "jsval" || value.type.kind === "dyn") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return { form: "errValue", value: boxed };
|
||||
}
|
||||
// A JavaScript function accepts any argument; a typed one must take
|
||||
// unknown values (the caught value need not be an Error).
|
||||
if (
|
||||
value.type.kind === "func" &&
|
||||
value.type.params.every((param) => param.kind === "dyn") &&
|
||||
(value.type.params.every((param) => param.kind === "dyn") ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node))) &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
return { form: "predicate", value: lowerer.coerceToExpected(value, DYN) };
|
||||
// The boxed function carries its source name (Node's message names
|
||||
// a named validation function).
|
||||
const fnName = jsFuncNameOf(node);
|
||||
return {
|
||||
form: "predicate",
|
||||
value: { kind: "dynFrom", value, ...(fnName !== null ? { fnName } : {}), type: DYN, loc },
|
||||
};
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`${surface} with this validation callback`,
|
||||
@@ -1546,7 +1805,9 @@ function classifyThrowsExpected(
|
||||
// tracingChannel suite's shape): the value crosses as a dyn error and
|
||||
// the runtime compares per Node's expectsError key walk. A
|
||||
// checked-dynamic expected rides directly; a static %Error boxes.
|
||||
if (expectedT?.kind === "dyn") {
|
||||
// An untyped JavaScript value (a parameter) too: a function there is
|
||||
// checked at run time (assertThrowsHelper).
|
||||
if (expectedT?.kind === "dyn" || expectedT === null) {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "dyn") return { form: "errValue", value };
|
||||
}
|
||||
@@ -1943,6 +2204,46 @@ function assertThrowsHelper(
|
||||
return { e: strLit("", loc), has: false };
|
||||
})();
|
||||
|
||||
// expectedException's function branch: a function with a prototype that
|
||||
// the caught value is an instance of passes; anything else is called as
|
||||
// a validation function, whose result must be true.
|
||||
const validateStmts = (fn: IrExpr): IrStmt[] => {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
const prototype: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: fn,
|
||||
key: strLit("prototype", loc),
|
||||
optional: true,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "undefined", negated: true, value: prototype, type: BOOL, loc },
|
||||
[
|
||||
ifStmt(
|
||||
checkedClassInstanceOf(
|
||||
lowerer,
|
||||
actual(),
|
||||
fn,
|
||||
"assert.throws against this native constructor is not supported yet",
|
||||
loc,
|
||||
),
|
||||
[doReturn],
|
||||
),
|
||||
],
|
||||
),
|
||||
lib("assert.throwsValidate", [
|
||||
actual(),
|
||||
fn,
|
||||
strLit(mode === "rejects" ? "rejects" : "throws", loc),
|
||||
m(),
|
||||
hm(),
|
||||
]),
|
||||
doReturn,
|
||||
];
|
||||
};
|
||||
|
||||
let catchBody: IrStmt[];
|
||||
if (mode === "dnr" || mode === "dnt") {
|
||||
const actual = (): IrExpr => ({ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc });
|
||||
@@ -2116,7 +2417,13 @@ function assertThrowsHelper(
|
||||
// caught value crosses as a dyn value (identity-preserving) and
|
||||
// the runtime walks the expected's keys — a mismatch throws the
|
||||
// deep-equal AssertionError.
|
||||
// A function there is a validation function (or an error class
|
||||
// value) as in the predicate form.
|
||||
catchBody = [
|
||||
ifStmt(
|
||||
{ kind: "dynTest", test: "function", value: varRef("ev.0", DYN, loc), type: BOOL, loc },
|
||||
validateStmts(varRef("ev.0", DYN, loc)),
|
||||
),
|
||||
lib("assert.expectsErrDyn", [
|
||||
{ kind: "caughtToDyn", value: caughtRef(), type: DYN, loc },
|
||||
varRef("ev.0", DYN, loc),
|
||||
@@ -2127,7 +2434,8 @@ function assertThrowsHelper(
|
||||
];
|
||||
break;
|
||||
case "predicate":
|
||||
throw new InternalCompilerError("assert validation callback only enters a no-error helper");
|
||||
catchBody = validateStmts(varRef("predicate.0", DYN, loc));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { lowerDateToPrimitiveCall } from "./builtins/dates.js";
|
||||
@@ -1053,6 +1054,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
}
|
||||
const bi = lowerer.builtinImportOf(expr.expression);
|
||||
if (bi) return lowerBuiltinCall(lowerer, expr, bi, loc);
|
||||
// A constant bound to an assert method (`const ok = assert.ok`) called
|
||||
// directly is that assertion, with the message Node quotes the call for.
|
||||
{
|
||||
const alias = assertFunctionAliasOf(lowerer, expr.expression);
|
||||
const served = alias ? lowerer.lowerAssertModuleCall(expr, alias, loc) : null;
|
||||
if (served) return served;
|
||||
}
|
||||
// The assert module binding called DIRECTLY (`assert(x)` — a default
|
||||
// import or the CJS `const assert = require("assert")`): Node's
|
||||
// module object IS assert.ok; namespace-import bindings fence inside
|
||||
@@ -8709,7 +8717,12 @@ function lowerObjectStaticCall(
|
||||
(probed && isDynTypedRefType(probed.type)) ||
|
||||
(probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) ||
|
||||
(probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) ||
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))
|
||||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type)) ||
|
||||
// null, undefined and unions of untyped JavaScript values: the same
|
||||
// probe, whose ToObject throws Node's TypeError on nullish.
|
||||
(probed &&
|
||||
(isUnitType(probed.type) || probed.type.kind === "union") &&
|
||||
lowerer.dynConvertible(probed.type))
|
||||
) {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.coerceToExpected(probed, DYN);
|
||||
|
||||
@@ -1627,6 +1627,44 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"date",
|
||||
]);
|
||||
|
||||
/** A `return` at the top level of a CommonJS module (preflight admits it
|
||||
* there only): it ends the module's evaluation, and its value is
|
||||
* evaluated and discarded, as Node's module wrapper does. Top-level
|
||||
* bindings and export assignments after it would be left uninitialized
|
||||
* where Node leaves them in their temporal dead zone or absent, so a
|
||||
* module with any keeps a refusal. Null for a return inside a function. */
|
||||
function lowerModuleLevelReturn(lowerer: Lowerer, stmt: ts.ReturnStatement): IrStmt | null {
|
||||
let top: ts.Node = stmt;
|
||||
while (top.parent !== undefined && !ts.isSourceFile(top.parent)) {
|
||||
if (ts.isFunctionLike(top.parent) || ts.isClassStaticBlockDeclaration(top.parent)) return null;
|
||||
top = top.parent;
|
||||
}
|
||||
const sf = top.parent;
|
||||
if (sf === undefined || !ts.isSourceFile(sf)) return null;
|
||||
const stmts = sf.statements.nodes;
|
||||
const later = stmts.slice(stmts.indexOf(top as ts.Statement) + 1);
|
||||
if (
|
||||
later.some(
|
||||
(s) =>
|
||||
ts.isVariableStatement(s) || ts.isClassDeclaration(s) || cjsExportAssignmentOf(s) !== null,
|
||||
)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
stmt,
|
||||
"a top-level return in a CommonJS module that declares variables or classes, or assigns exports, after it",
|
||||
);
|
||||
}
|
||||
const loc = locOf(stmt);
|
||||
const ret: IrStmt = { kind: "return", value: null, loc };
|
||||
if (!stmt.expression) return ret;
|
||||
return {
|
||||
kind: "block",
|
||||
body: [{ kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc }, ret],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ownPrototypeResetStatement(lowerer, stmt))
|
||||
return { kind: "block", body: [], loc: locOf(stmt) };
|
||||
@@ -1736,6 +1774,8 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
if (ts.isForOfStatement(stmt)) return lowerer.lowerForOf(stmt);
|
||||
if (ts.isLabeledStatement(stmt)) return lowerLabeled(lowerer, stmt);
|
||||
if (ts.isReturnStatement(stmt)) {
|
||||
const moduleReturn = lowerModuleLevelReturn(lowerer, stmt);
|
||||
if (moduleReturn) return moduleReturn;
|
||||
// Implicit-any instance RETURN INFERENCE (function-returns.ts
|
||||
// resolveInferredReturn): the value lowers BARE and the statement
|
||||
// records itself — the post-pass settles the instance's return type
|
||||
|
||||
@@ -593,7 +593,11 @@ import {
|
||||
lowerTestMethodCall,
|
||||
lowerTestCtxProperty,
|
||||
} from "./lower-test.js";
|
||||
import { lowerAssertModuleCall, lowerAssertDirectCall } from "./lower-assert.js";
|
||||
import {
|
||||
assertFunctionValue,
|
||||
lowerAssertModuleCall,
|
||||
lowerAssertDirectCall,
|
||||
} from "./lower-assert.js";
|
||||
import type { AssertInstanceOptions } from "./assert-instances.js";
|
||||
import { lowerUtilModuleCall } from "./lower-inspect.js";
|
||||
import { lowerSystemErrorMapValue } from "./lower-system-errors.js";
|
||||
@@ -13989,6 +13993,8 @@ export class Lowerer {
|
||||
* deliberately narrower than table membership because many table rows
|
||||
* are only dispatch sentinels for call-site-specific lowering. */
|
||||
lowerBuiltinCallableValue(bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null {
|
||||
const assertion = assertFunctionValue(bi, loc);
|
||||
if (assertion) return assertion;
|
||||
if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc);
|
||||
if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member))
|
||||
return lowerUtilTypeValue(this, bi.member, loc);
|
||||
|
||||
@@ -873,6 +873,12 @@ export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined>
|
||||
result: DYN,
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
hasOwn: {
|
||||
fn: "dyn.hasOwnComputed",
|
||||
params: [DYN, DYN],
|
||||
result: BOOL,
|
||||
valueParams: exactValueParams(DYN, DYN),
|
||||
},
|
||||
keys: { fn: "dyn.objKeys", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
values: { fn: "dyn.objValues", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
|
||||
|
||||
@@ -5363,6 +5363,16 @@ function preflight7(load: LoadResult): {
|
||||
return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text);
|
||||
};
|
||||
|
||||
// A `return` at the top level of a CommonJS module (TS1108): Node runs
|
||||
// the module as a function body, so the return ends the module's
|
||||
// evaluation. ES modules and TypeScript keep the error.
|
||||
const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
||||
if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName))
|
||||
return false;
|
||||
const sf = p.getSourceFile(d.fileName);
|
||||
return sf !== undefined && isCjsJsFile7(sf, p);
|
||||
};
|
||||
|
||||
const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
||||
if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined)
|
||||
return false;
|
||||
@@ -5409,6 +5419,7 @@ function preflight7(load: LoadResult): {
|
||||
!isNpmStaticSubclassArgument(p, d) &&
|
||||
!workspaceImplicitAnySuppressed(p, d) &&
|
||||
!typeofTestedNameSuppressed(p, d) &&
|
||||
!cjsTopLevelReturnSuppressed(p, d) &&
|
||||
!jsdocTypeSuppressed(p, d, commentDup),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,6 +132,10 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
||||
// binding types as any and reached uses fence instead of passing unchecked
|
||||
2307,
|
||||
2792,
|
||||
// `delete` of a property the inference typed required (2790): JS deletes
|
||||
// any configurable property, and the lowering checks each delete's
|
||||
// receiver itself
|
||||
2790,
|
||||
// useUnknownInCatchVariables' annotation demand ("'e' is of type
|
||||
// 'unknown'", "Object is of type 'unknown'"): a JS catch clause CANNOT
|
||||
// annotate its variable, so the demand is unsatisfiable in source —
|
||||
|
||||
@@ -977,6 +977,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"assert.noErrorPredicate",
|
||||
"assert.regexDynTest",
|
||||
"assert.expectsErrDyn",
|
||||
"assert.throwsValidate",
|
||||
"assert.ifErrorErr",
|
||||
"assert.ifErrorF64",
|
||||
"assert.ifErrorStr",
|
||||
|
||||
@@ -4412,6 +4412,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
// The equality quartet over checked-dynamic operands (the frontend
|
||||
// boxes a static side into the checked-dynamic tree first).
|
||||
"assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID },
|
||||
/** A message argument that is not a string, registered for the next
|
||||
* failing assertion (scr_assert_message_value). */
|
||||
"assert.messageValue": { argTypes: [DYN, BOOL], result: VOID },
|
||||
/** An assert method as a function value, by canonical name. */
|
||||
"assert.methodValue": { argTypes: [STRING], result: DYN },
|
||||
"assert.messageClear": { argTypes: [], result: VOID },
|
||||
"assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID },
|
||||
// The throws(fn, {shape}) accumulator: begin/slot calls never throw;
|
||||
// shapeEnd throws the Comparison diff. The error slot is the
|
||||
@@ -4437,6 +4443,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* against the caught value's; a mismatch throws the deep-equal
|
||||
* AssertionError (scr_assert.c). MAY THROW by design. */
|
||||
"assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID },
|
||||
/** A validation function against the caught value (Node's expectedException). */
|
||||
"assert.throwsValidate": { argTypes: [DYN, DYN, STRING, STRING, BOOL], result: VOID },
|
||||
// assert.ifError's typed entries (always throw; unit args never
|
||||
// lower). The error slot is program-dependent — any %Error-hierarchy
|
||||
// class, root or subclass (the insp.error precedent, null slot).
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8062-assert-message-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8065-cjs-top-level-return/early.js",
|
||||
"<repo>/tests/corpus/8065-cjs-top-level-return/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8066-object-hasown-delete.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8067-assert-throws-validators.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8076-assert-instance-identity.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8078-instanceof-object-function.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8190-assert-function-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8191-assert-function-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -212,7 +212,56 @@ bool scr_assert_skip_prototype(void) {
|
||||
return (g_instance_options & SCR_ASSERT_SKIP_PROTOTYPE) != 0;
|
||||
}
|
||||
|
||||
/* A message argument that is not a string (an untyped value, or an Error
|
||||
* to throw): the compiled call registers it before the assertion and passes
|
||||
* SCR_ASSERT_VALUE_MESSAGE as its message, which the failure paths compose
|
||||
* like any custom message. A failure whose message starts with it throws
|
||||
* the Error itself (innerFail's `message instanceof Error`) or puts
|
||||
* String(value) in its place with generatedMessage `!value`, as Node's
|
||||
* AssertionError does. Matching the sentinel keeps a registration that no
|
||||
* failure consumed from reaching another assertion. */
|
||||
static SCR_TL ScrDyn *g_message_value;
|
||||
static SCR_TL bool g_message_error;
|
||||
|
||||
void scr_assert_message_value(ScrDyn *value, bool error) {
|
||||
if (g_message_value) scr_dyn_release(g_message_value);
|
||||
g_message_value = scr_dyn_retain(value);
|
||||
g_message_error = error;
|
||||
}
|
||||
|
||||
void scr_assert_message_clear(void) {
|
||||
if (g_message_value) scr_dyn_release(g_message_value);
|
||||
g_message_value = NULL;
|
||||
}
|
||||
|
||||
void scr_assert_fail(ScrStr *message, bool generated) {
|
||||
static const char sentinel[] = SCR_ASSERT_VALUE_MESSAGE;
|
||||
const size_t n = sizeof sentinel - 1;
|
||||
if (g_message_value && message->len >= n && memcmp(message->data, sentinel, n) == 0) {
|
||||
ScrDyn *value = g_message_value;
|
||||
g_message_value = NULL;
|
||||
if (g_message_error) {
|
||||
scr_str_release(message);
|
||||
scr_assert_record(NULL, NULL, NULL);
|
||||
g_instance_options = 0;
|
||||
scr_dyn_throw(value);
|
||||
return;
|
||||
}
|
||||
ScrStr *text = scr_dyn_string_coerce_js(value);
|
||||
generated = !scr_dyn_truthy(value);
|
||||
scr_dyn_release(value);
|
||||
if (!text) {
|
||||
scr_str_release(message);
|
||||
return;
|
||||
}
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_write(&b, text->data, text->len);
|
||||
scr_jb_write(&b, message->data + n, message->len - n);
|
||||
scr_str_release(text);
|
||||
scr_str_release(message);
|
||||
message = scr_jb_finish(&b);
|
||||
}
|
||||
ScrError *e = scr_assert_error(message, generated, scr_assert_diff_full() ? "full" : "simple");
|
||||
g_instance_options = 0;
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
@@ -2703,6 +2752,60 @@ bool scr_assert_no_error_predicate(ScrDyn *predicate, ScrDyn *actual) {
|
||||
return matched;
|
||||
}
|
||||
|
||||
/* assert.throws/rejects with a validation function (expectedException's
|
||||
* function branch, after its instanceof check): the function runs with a
|
||||
* fresh object as `this` and the caught value, and any result but true
|
||||
* fails, with Node's generated message unless the call passed one. The
|
||||
* caught value and the function are the AssertionError's actual and
|
||||
* expected. Borrows all. */
|
||||
void scr_assert_throws_validate(ScrDyn *actual, ScrDyn *validate, ScrStr *op, ScrStr *msg,
|
||||
bool has_msg) {
|
||||
ScrDyn *receiver = scr_dyn_new_obj();
|
||||
scr_dyn_this_push_dyn(receiver);
|
||||
ScrDyn *result = scr_dyn_call(validate, &actual, 1, "expected");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(receiver);
|
||||
if (!result) return;
|
||||
if (result->kind == SCR_DYN_BOOL && result->v.b) {
|
||||
scr_dyn_release(result);
|
||||
return;
|
||||
}
|
||||
ScrStr *message;
|
||||
bool generated = !(has_msg && msg->len > 0);
|
||||
if (!generated) {
|
||||
message = scr_str_retain(msg);
|
||||
} else {
|
||||
ScrAssertBuf b = {0};
|
||||
ab_cstr(&b, "The ");
|
||||
ScrDyn *name = scr_dyn_fn_get(validate, "name", 4);
|
||||
if (name && name->kind == SCR_DYN_STR && name->v.str->len > 0) {
|
||||
ab_cstr(&b, "\"");
|
||||
ab_bytes(&b, name->v.str->data, name->v.str->len);
|
||||
ab_cstr(&b, "\" ");
|
||||
}
|
||||
if (name) scr_dyn_release(name);
|
||||
ab_cstr(&b, "validation function is expected to return \"true\". Received ");
|
||||
ScrStr *shown = scr_assert_cf_inspect(result);
|
||||
ab_bytes(&b, shown->data, shown->len);
|
||||
scr_str_release(shown);
|
||||
if (scr_dyn_ref_is_error(actual)) {
|
||||
ScrStr *text = scr_dyn_string_coerce_js(actual);
|
||||
if (!text) {
|
||||
scr_dyn_release(result);
|
||||
scr_str_release(ab_take(&b));
|
||||
return;
|
||||
}
|
||||
ab_cstr(&b, "\n\nCaught error:\n\n");
|
||||
ab_bytes(&b, text->data, text->len);
|
||||
scr_str_release(text);
|
||||
}
|
||||
message = ab_take(&b);
|
||||
}
|
||||
scr_dyn_release(result);
|
||||
scr_assert_record_dyn(actual, validate, op);
|
||||
scr_assert_fail(message, generated);
|
||||
}
|
||||
|
||||
/* ── assert.ifError ───────────────────────────────────────────────────
|
||||
* Node throws for ANY value but null/undefined (falsy included); the
|
||||
* frontend routes null/undefined to a no-op and everything else here.
|
||||
@@ -2879,3 +2982,225 @@ ScrError *scr_assert_error_new(ScrDyn *options) {
|
||||
scr_assert_attach_values(e, generated, actual, expected, op, diff);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* ── assert functions as values ──────────────────────────────────────────
|
||||
* `assert.fail` stored or passed as a function (`emitter.on("x",
|
||||
* assert.fail)`): one function per method, which takes its arguments as
|
||||
* untyped values and asserts as the method does. */
|
||||
|
||||
/* Node's message argument: absent or null/undefined (a generated message),
|
||||
* a string, an Error (thrown when the assertion fails), or another value
|
||||
* (String(value)). The value is registered as scr_assert_fail reads it; the
|
||||
* caller clears it after the assertion. +1 into *msg. */
|
||||
static void scr_av_message(ScrDyn *const *args, size_t argc, size_t i, ScrStr **msg, bool *has) {
|
||||
const ScrDyn *m = i < argc ? args[i] : NULL;
|
||||
if (!m || m->kind == SCR_DYN_UNDEF || m->kind == SCR_DYN_NULL) {
|
||||
*msg = scr_str_new("", 0);
|
||||
*has = false;
|
||||
return;
|
||||
}
|
||||
*has = true;
|
||||
if (m->kind == SCR_DYN_STR) {
|
||||
*msg = scr_str_retain(m->v.str);
|
||||
return;
|
||||
}
|
||||
bool error = scr_dyn_ref_is_error(m) ||
|
||||
(m->kind == SCR_DYN_OBJ && scr_dyn_obj_get(m, "%error", 6) != NULL);
|
||||
scr_assert_message_value((ScrDyn *)m, error);
|
||||
*msg = scr_str_new(SCR_ASSERT_VALUE_MESSAGE, sizeof SCR_ASSERT_VALUE_MESSAGE - 1);
|
||||
}
|
||||
|
||||
static const ScrDyn *scr_av_arg(ScrDyn *const *args, size_t argc, size_t i) {
|
||||
return i < argc ? args[i] : scr_dyn_undefined();
|
||||
}
|
||||
|
||||
static ScrDyn *scr_av_done(ScrStr *msg) {
|
||||
scr_str_release(msg);
|
||||
scr_assert_message_clear();
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
/* assert(value, message) and assert.ok: Node's generated message quotes
|
||||
* the call's source, which a function value has no fixed site for; the
|
||||
* message here is AssertionError's own `value == true`. */
|
||||
static ScrDyn *scr_av_ok(ScrClosure *c, ScrDyn *const *args, size_t argc) {
|
||||
(void)c;
|
||||
const ScrDyn *value = scr_av_arg(args, argc, 0);
|
||||
if (scr_dyn_truthy(value)) return scr_dyn_retain(scr_dyn_undefined());
|
||||
scr_assert_record(scr_dyn_retain((ScrDyn *)value), scr_dyn_new_bool(true), "==");
|
||||
if (argc == 0) {
|
||||
static const char text[] = "No value argument passed to `assert.ok()`";
|
||||
scr_assert_fail(scr_str_new(text, sizeof text - 1), true);
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *msg;
|
||||
bool has;
|
||||
scr_av_message(args, argc, 1, &msg, &has);
|
||||
if (has) {
|
||||
scr_assert_fail(scr_str_retain(msg), false);
|
||||
return scr_av_done(msg);
|
||||
}
|
||||
ScrStr *shown = scr_assert_cf_inspect(value);
|
||||
ScrAssertBuf b = {0};
|
||||
ab_str(&b, shown);
|
||||
ab_cstr(&b, " == true");
|
||||
scr_str_release(shown);
|
||||
scr_assert_fail(ab_take(&b), true);
|
||||
return scr_av_done(msg);
|
||||
}
|
||||
|
||||
/* assert.fail(message): "Failed" without a message. */
|
||||
static ScrDyn *scr_av_fail(ScrClosure *c, ScrDyn *const *args, size_t argc) {
|
||||
(void)c;
|
||||
if (argc > 1) {
|
||||
static const char text[] = "assert.fail with more than one argument through a function value is not supported yet";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, text, sizeof text - 1, "SC2020");
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *msg;
|
||||
bool has;
|
||||
scr_av_message(args, argc, 0, &msg, &has);
|
||||
scr_assert_record(scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain(scr_dyn_undefined()), "fail");
|
||||
if (has) scr_assert_fail(scr_str_retain(msg), false);
|
||||
else scr_assert_fail(scr_str_new("Failed", 6), true);
|
||||
return scr_av_done(msg);
|
||||
}
|
||||
|
||||
/* The comparison methods take both operands: Node's ERR_MISSING_ARGS
|
||||
* TypeError otherwise. False with it pending. */
|
||||
static bool scr_av_operands(size_t argc) {
|
||||
if (argc >= 2) return true;
|
||||
static const char text[] = "The \"actual\" and \"expected\" arguments must be specified";
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, text, sizeof text - 1, "ERR_MISSING_ARGS");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* equal / notEqual: Node's loose comparison, NaN equal to NaN. */
|
||||
static ScrDyn *scr_av_loose(ScrDyn *const *args, size_t argc, bool negated) {
|
||||
if (!scr_av_operands(argc)) return NULL;
|
||||
const ScrDyn *a = scr_av_arg(args, argc, 0), *b = scr_av_arg(args, argc, 1);
|
||||
bool nan = a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM && isnan(a->v.num) && isnan(b->v.num);
|
||||
bool equal = nan || scr_dyn_abstract_eq(a, b);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
if (negated ? !equal : equal) return scr_dyn_retain(scr_dyn_undefined());
|
||||
ScrStr *msg;
|
||||
bool has;
|
||||
scr_av_message(args, argc, 2, &msg, &has);
|
||||
scr_assert_record(scr_dyn_retain((ScrDyn *)a), scr_dyn_retain((ScrDyn *)b), negated ? "!=" : "==");
|
||||
ScrStr *ia = scr_assert_cf_inspect(a), *ib = scr_assert_cf_inspect(b);
|
||||
scr_assert_loose_result(equal, negated, ia, ib, msg, has);
|
||||
scr_str_release(ia);
|
||||
scr_str_release(ib);
|
||||
return scr_av_done(msg);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_av_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_loose(a, n, false);
|
||||
}
|
||||
static ScrDyn *scr_av_not_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_loose(a, n, true);
|
||||
}
|
||||
|
||||
/* The strict and deep comparisons, through the untyped assert paths. */
|
||||
enum { SCR_AV_STRICT, SCR_AV_DEEP_STRICT, SCR_AV_DEEP, SCR_AV_PARTIAL };
|
||||
|
||||
static ScrDyn *scr_av_compare(ScrDyn *const *args, size_t argc, int form, bool negated) {
|
||||
if (!scr_av_operands(argc)) return NULL;
|
||||
ScrDyn *a = (ScrDyn *)scr_av_arg(args, argc, 0), *b = (ScrDyn *)scr_av_arg(args, argc, 1);
|
||||
ScrStr *msg;
|
||||
bool has;
|
||||
scr_av_message(args, argc, 2, &msg, &has);
|
||||
if (form == SCR_AV_DEEP) scr_assert_loose_deep_dyn(a, b, negated, msg, has);
|
||||
else if (form == SCR_AV_PARTIAL) scr_assert_partial_deep_dyn(a, b, msg, has);
|
||||
else scr_assert_eq_dyn(a, b, negated, form == SCR_AV_DEEP_STRICT, msg, has);
|
||||
return scr_av_done(msg);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_av_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_STRICT, false);
|
||||
}
|
||||
static ScrDyn *scr_av_not_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_STRICT, true);
|
||||
}
|
||||
static ScrDyn *scr_av_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, false);
|
||||
}
|
||||
static ScrDyn *scr_av_not_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_DEEP_STRICT, true);
|
||||
}
|
||||
static ScrDyn *scr_av_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_DEEP, false);
|
||||
}
|
||||
static ScrDyn *scr_av_not_deep_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_DEEP, true);
|
||||
}
|
||||
static ScrDyn *scr_av_partial_deep_strict_equal(ScrClosure *c, ScrDyn *const *a, size_t n) {
|
||||
(void)c;
|
||||
return scr_av_compare(a, n, SCR_AV_PARTIAL, false);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_av_if_error(ScrClosure *c, ScrDyn *const *args, size_t argc) {
|
||||
(void)c;
|
||||
scr_assert_iferror_dyn(scr_av_arg(args, argc, 0));
|
||||
return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
static const struct {
|
||||
const char *name;
|
||||
ScrDynThunk thunk;
|
||||
uint32_t arity;
|
||||
} scr_av_methods[] = {
|
||||
{"ok", &scr_av_ok, 0},
|
||||
{"fail", &scr_av_fail, 1},
|
||||
{"equal", &scr_av_equal, 3},
|
||||
{"notEqual", &scr_av_not_equal, 3},
|
||||
{"strictEqual", &scr_av_strict_equal, 3},
|
||||
{"notStrictEqual", &scr_av_not_strict_equal, 3},
|
||||
{"deepEqual", &scr_av_deep_equal, 3},
|
||||
{"notDeepEqual", &scr_av_not_deep_equal, 3},
|
||||
{"deepStrictEqual", &scr_av_deep_strict_equal, 3},
|
||||
{"notDeepStrictEqual", &scr_av_not_deep_strict_equal, 3},
|
||||
{"partialDeepStrictEqual", &scr_av_partial_deep_strict_equal, 3},
|
||||
{"ifError", &scr_av_if_error, 1},
|
||||
};
|
||||
#define SCR_AV_COUNT (sizeof scr_av_methods / sizeof scr_av_methods[0])
|
||||
static SCR_TL ScrDyn *scr_av_values[SCR_AV_COUNT];
|
||||
static SCR_TL bool scr_av_registered;
|
||||
|
||||
static void scr_av_cleanup(void) {
|
||||
for (size_t i = 0; i < SCR_AV_COUNT; i++) {
|
||||
scr_dyn_release(scr_av_values[i]);
|
||||
scr_av_values[i] = NULL;
|
||||
}
|
||||
scr_av_registered = false;
|
||||
}
|
||||
|
||||
/* The function value of an assert method by its canonical name (the frontend
|
||||
* maps assert/strict's aliases), +1; one value per method, so stored copies
|
||||
* compare equal. */
|
||||
ScrDyn *scr_assert_method_value(ScrStr *name) {
|
||||
for (size_t i = 0; i < SCR_AV_COUNT; i++) {
|
||||
if (strlen(scr_av_methods[i].name) != name->len ||
|
||||
memcmp(scr_av_methods[i].name, name->data, name->len) != 0)
|
||||
continue;
|
||||
if (!scr_av_values[i]) {
|
||||
if (!scr_av_registered) {
|
||||
scr_av_registered = true;
|
||||
scr_atexit(scr_av_cleanup);
|
||||
}
|
||||
scr_av_values[i] = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_av_methods[i].thunk,
|
||||
scr_av_methods[i].arity, "native:assert",
|
||||
scr_av_methods[i].name);
|
||||
}
|
||||
return scr_dyn_retain(scr_av_values[i]);
|
||||
}
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
@@ -24,6 +24,19 @@ void scr_assert_fail(ScrStr *message, bool generated); /* takes ownership; alway
|
||||
* consumes them; a passing call clears them. */
|
||||
void scr_assert_set_options(int options);
|
||||
void scr_assert_clear_options(void);
|
||||
/* A message argument that is not a string, for the next assertion that
|
||||
* fails with SCR_ASSERT_VALUE_MESSAGE as its message (scr_assert.c): an
|
||||
* Error (`error`) is thrown as it is, anything else reports String(value).
|
||||
* Borrows value; clear drops a registration the call did not consume. */
|
||||
#define SCR_ASSERT_VALUE_MESSAGE "\x01scriptc:assert-message-value"
|
||||
void scr_assert_message_value(struct ScrDyn *value, bool error);
|
||||
void scr_assert_message_clear(void);
|
||||
/* An assert method as a function value, by canonical name (+1). */
|
||||
struct ScrDyn *scr_assert_method_value(ScrStr *name);
|
||||
/* assert.throws/rejects with a validation function: its result must be
|
||||
* true (Node's expectedException), else the AssertionError. Borrows all. */
|
||||
void scr_assert_throws_validate(struct ScrDyn *actual, struct ScrDyn *validate, ScrStr *op,
|
||||
ScrStr *msg, bool has_msg);
|
||||
bool scr_assert_diff_full(void);
|
||||
bool scr_assert_skip_prototype(void);
|
||||
void scr_assert_record(ScrDyn *actual, ScrDyn *expected, const char *op); /* values move in */
|
||||
|
||||
@@ -4719,6 +4719,115 @@ int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) {
|
||||
return truthy ? 1 : 0;
|
||||
}
|
||||
|
||||
/* `value instanceof Object`: whether Object.prototype is on the value's
|
||||
* prototype chain. Primitives answer false, and so does an object whose
|
||||
* chain ends in null first (Object.create(null), a null-prototype record).
|
||||
* Objects without a chain the runtime walks (class instances, native
|
||||
* handles, errors, promises) inherit Object.prototype. False with the
|
||||
* exception pending when a prototype read throws. */
|
||||
static bool scr_dyn_instanceof_object(const ScrDyn *value) {
|
||||
switch (value->kind) {
|
||||
case SCR_DYN_UNDEF:
|
||||
case SCR_DYN_NULL:
|
||||
case SCR_DYN_BOOL:
|
||||
case SCR_DYN_NUM:
|
||||
case SCR_DYN_STR:
|
||||
case SCR_DYN_BIGINT:
|
||||
case SCR_DYN_SYMBOL:
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/* Object.prototype is on the chain from the value's own prototype on:
|
||||
* Object.prototype itself has a null prototype, so it is not an Object. */
|
||||
ScrDyn *current = scr_dyn_retain((ScrDyn *)value);
|
||||
for (bool first = true;; first = false) {
|
||||
if (!first && current == scr_builtin_object_prototype) {
|
||||
scr_dyn_release(current);
|
||||
return true;
|
||||
}
|
||||
if (current == scr_builtin_object_prototype) {
|
||||
scr_dyn_release(current);
|
||||
return false;
|
||||
}
|
||||
ScrDyn *next = NULL;
|
||||
if (current->kind == SCR_DYN_FUNC) {
|
||||
/* A function's prototype is Function.prototype, whose own is
|
||||
* Object.prototype, unless the program set it. */
|
||||
if (scr_closure_identity(current->v.fn.clo)->props) {
|
||||
ScrDyn *table = scr_dyn_fn_properties(current);
|
||||
next = table->prototype ? scr_dyn_retain(table->prototype)
|
||||
: table->null_proto ? scr_dyn_new_null() : NULL;
|
||||
scr_dyn_release(table);
|
||||
}
|
||||
} else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR ||
|
||||
(current->kind == SCR_DYN_TYPED_REF &&
|
||||
strncmp(current->v.typed_ref.type_key, "object:", 7) != 0)) {
|
||||
next = scr_dyn_get_prototype(current);
|
||||
if (!next) {
|
||||
scr_dyn_release(current);
|
||||
return false; /* the read threw: pending */
|
||||
}
|
||||
}
|
||||
scr_dyn_release(current);
|
||||
if (!next) return true;
|
||||
if (next->kind == SCR_DYN_NULL) {
|
||||
scr_dyn_release(next);
|
||||
return false;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
/* `value instanceof Function`: whether a function is on the value's
|
||||
* prototype chain. A function's own prototype is Function.prototype unless
|
||||
* the program set it, so the walk ends true at the first function that
|
||||
* kept its prototype; it follows set prototypes through functions and
|
||||
* objects alike (`Object.create(function () {})` is a Function instance),
|
||||
* and ends false at null or at a value without a chain the runtime walks.
|
||||
* False with the exception pending when a prototype read throws. */
|
||||
static bool scr_dyn_instanceof_function(const ScrDyn *value) {
|
||||
ScrDyn *current = NULL;
|
||||
if (value->kind == SCR_DYN_FUNC) {
|
||||
if (!scr_closure_identity(value->v.fn.clo)->props) return true;
|
||||
ScrDyn *table = scr_dyn_fn_properties((ScrDyn *)value);
|
||||
current = table->prototype ? scr_dyn_retain(table->prototype) : NULL;
|
||||
bool unset = !table->prototype && !table->null_proto;
|
||||
scr_dyn_release(table);
|
||||
if (unset) return true;
|
||||
} else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR) {
|
||||
current = scr_dyn_get_prototype((ScrDyn *)value);
|
||||
if (!current) return false; /* the read threw: pending */
|
||||
}
|
||||
while (current && current->kind != SCR_DYN_NULL) {
|
||||
ScrDyn *next = NULL;
|
||||
if (current->kind == SCR_DYN_FUNC) {
|
||||
if (!scr_closure_identity(current->v.fn.clo)->props) {
|
||||
scr_dyn_release(current);
|
||||
return true;
|
||||
}
|
||||
ScrDyn *table = scr_dyn_fn_properties(current);
|
||||
bool unset = !table->prototype && !table->null_proto;
|
||||
next = table->prototype ? scr_dyn_retain(table->prototype) : NULL;
|
||||
scr_dyn_release(table);
|
||||
if (unset) {
|
||||
scr_dyn_release(current);
|
||||
return true;
|
||||
}
|
||||
} else if (current->kind == SCR_DYN_OBJ || current->kind == SCR_DYN_ARR) {
|
||||
next = scr_dyn_get_prototype(current);
|
||||
if (!next) {
|
||||
scr_dyn_release(current);
|
||||
return false; /* the read threw: pending */
|
||||
}
|
||||
}
|
||||
scr_dyn_release(current);
|
||||
current = next;
|
||||
}
|
||||
if (current) scr_dyn_release(current);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) {
|
||||
if (!callee || callee->kind != SCR_DYN_FUNC) {
|
||||
static const char message[] = "Right-hand side of 'instanceof' is not callable";
|
||||
@@ -4727,6 +4836,13 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc
|
||||
}
|
||||
int hooked = scr_dyn_has_instance_hook(value, callee);
|
||||
if (hooked >= 0) return hooked == 1;
|
||||
if (callee->v.fn.sig && callee->v.fn.name) {
|
||||
if (strcmp(callee->v.fn.sig, "native:constructor") == 0 && strcmp(callee->v.fn.name, "Object") == 0)
|
||||
return scr_dyn_instanceof_object(value);
|
||||
if (strcmp(callee->v.fn.sig, "native:function-constructor") == 0 &&
|
||||
strcmp(callee->v.fn.name, "Function") == 0)
|
||||
return scr_dyn_instanceof_function(value);
|
||||
}
|
||||
/* An error constructor held as a value (`inst instanceof params.Parent`
|
||||
* with Parent === Error): the error kind's interval, and for Error itself
|
||||
* objects inheriting Error.prototype. */
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
// node:assert messages that are not string literals: an untyped string, an
|
||||
// Error to throw, null/undefined (omitted) and falsy values, and
|
||||
// scalar-union operands against untyped values.
|
||||
const assert = require('assert');
|
||||
|
||||
function attempt(label, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(label, 'passed');
|
||||
} catch (e) {
|
||||
console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.code);
|
||||
}
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
for (const x of ['a', 'b']) if (x === 'c') failures.push(`\n${x}`);
|
||||
attempt('joined passes', () => assert.strictEqual(failures.length, 0, failures.join('')));
|
||||
const untyped = JSON.parse('"untyped message"');
|
||||
attempt('untyped', () => assert.strictEqual(1, 2, untyped));
|
||||
attempt('untyped deep', () => assert.deepStrictEqual([1], [2], untyped));
|
||||
attempt('untyped notStrict', () => assert.notStrictEqual(1, 1, untyped));
|
||||
attempt('error message', () => assert.strictEqual(1, 2, new TypeError('own error')));
|
||||
attempt('error deep', () => assert.deepStrictEqual({ a: 1 }, { a: 2 }, new RangeError('own range')));
|
||||
const none = JSON.parse('null');
|
||||
attempt('null message', () => assert.strictEqual(1, 2, none));
|
||||
const zero = JSON.parse('0');
|
||||
attempt('zero message deep', () => assert.deepStrictEqual([1], [2], zero));
|
||||
attempt('passing error message', () => assert.strictEqual(1, 1, new Error('unused')));
|
||||
attempt('later string message', () => assert.strictEqual(1, 2, 'plain'));
|
||||
|
||||
// Operand order: both operands evaluate before the message.
|
||||
const order = [];
|
||||
const mark = (label, value) => (order.push(label), value);
|
||||
attempt('order', () => assert.strictEqual(mark('a', 1), mark('b', 2), mark('m', JSON.parse('"m"'))));
|
||||
console.log(order.join(','));
|
||||
|
||||
const row = [['a', 'b', true], ['c', 'd', 'x']];
|
||||
for (const [, , expected] of row) {
|
||||
attempt('union operand', () => assert.strictEqual(JSON.parse(JSON.stringify(expected)), expected));
|
||||
}
|
||||
|
||||
// assert.fail with a message that is not a string: an Error is thrown as
|
||||
// is; other values become the message, generated when falsy.
|
||||
attempt('fail error', () => assert.fail(new RangeError('re')));
|
||||
attempt('fail value', () => assert.fail(JSON.parse('{"a":1}')));
|
||||
attempt('fail zero', () => assert.fail(zero));
|
||||
attempt('fail null', () => assert.fail(JSON.parse('null')));
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
exports.ready = true;
|
||||
console.log('early module start', typeof helper);
|
||||
if (exports.ready) return;
|
||||
console.log('early module end');
|
||||
function helper() {}
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
// A CommonJS module may end its evaluation with a top-level return: the
|
||||
// rest of the module does not run, while timers, exit handlers and the
|
||||
// modules it already required keep their effects.
|
||||
const early = require('./early.js');
|
||||
console.log('early exports', early.ready);
|
||||
setTimeout(() => console.log('timer ran'), 1);
|
||||
process.on('exit', () => console.log('exit handler'));
|
||||
for (const value of [1, 2, 3]) {
|
||||
if (value === 2) return 'ignored value';
|
||||
console.log('value', value);
|
||||
}
|
||||
console.log('not reached');
|
||||
function hoisted() {
|
||||
return 'hoisted';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
// Object.hasOwn over nullish receivers and as a value, and `delete` of
|
||||
// properties JavaScript inference types as required.
|
||||
const o = { a: 1, b: undefined };
|
||||
console.log(Object.hasOwn(o, 'a'), Object.hasOwn(o, 'b'), Object.hasOwn(o, 'c'));
|
||||
console.log(Object.hasOwn([1, 2], 1), Object.hasOwn([1, 2], 2), Object.hasOwn(Object.create({ x: 1 }), 'x'));
|
||||
for (const value of [null, undefined]) {
|
||||
try {
|
||||
Object.hasOwn(value, 'a');
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message);
|
||||
}
|
||||
}
|
||||
const pick = Math.random() < 2 ? { k: 1 } : null;
|
||||
console.log(Object.hasOwn(pick, 'k'));
|
||||
const hasOwn = Object.hasOwn;
|
||||
console.log(hasOwn({ k: 1 }, 'k'), hasOwn({}, 'k'), [{ a: 1 }, {}].map((x) => hasOwn(x, 'a')).join());
|
||||
const record = { a: 1, b: 2 };
|
||||
delete record.a;
|
||||
console.log(Object.keys(record).join(), Object.hasOwn(record, 'a'), record.a);
|
||||
const table = { one: 1, two: 2 };
|
||||
for (const key of Object.keys(table)) if (key === 'one') delete table[key];
|
||||
console.log(JSON.stringify(table));
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
// assert.throws and assert.rejects with validation functions, constructor
|
||||
// functions, and untyped expected values (Node's expectedException).
|
||||
const assert = require('assert');
|
||||
function show(label, fn) {
|
||||
try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, JSON.stringify(e.message), e.generatedMessage, e.operator, e.actual && e.actual.message); }
|
||||
}
|
||||
function check(err) { return err.message === 'x'; }
|
||||
show('named false', () => assert.throws(() => { throw new Error('y'); }, check));
|
||||
show('named true', () => assert.throws(() => { throw new Error('x'); }, check));
|
||||
show('custom message', () => assert.throws(() => { throw new Error('y'); }, check, 'custom'));
|
||||
show('returns 1', () => assert.throws(() => { throw new Error('y'); }, () => 1));
|
||||
show('non-error thrown', () => assert.throws(() => { throw 'str'; }, (v) => v === 'str'));
|
||||
show('non-error fails', () => assert.throws(() => { throw 42; }, () => undefined));
|
||||
function MyErr(m) { this.message = m; }
|
||||
show('constructor', () => assert.throws(() => { throw new MyErr('q'); }, MyErr));
|
||||
function untyped(expected) { assert.throws(() => { throw new Error('u'); }, expected); }
|
||||
show('untyped validator', () => untyped((e) => e.message === 'u'));
|
||||
show('untyped validator fails', () => untyped(function v() { return false; }));
|
||||
show('untyped object', () => untyped({ message: 'u' }));
|
||||
assert.rejects(Promise.reject(new Error('r')), (e) => e.message === 'r').then(() => console.log('rejects passed'));
|
||||
assert.rejects(Promise.reject(new Error('r')), () => false).catch((e) => console.log('rejects', JSON.stringify(e.message), e.operator));
|
||||
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
// assert.strictEqual and notStrictEqual compare class instances by
|
||||
// reference: two instances, an instance and the same instance read back
|
||||
// through an untyped value, and an instance a method returns. Symbols
|
||||
// compare by identity too.
|
||||
const assert = require('assert');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Box {
|
||||
constructor(v) {
|
||||
this.v = v;
|
||||
}
|
||||
self() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Box(1);
|
||||
const c = new Box(1);
|
||||
assert.strictEqual(b, b.self());
|
||||
assert.notStrictEqual(b, c);
|
||||
const e = new EventEmitter();
|
||||
assert.strictEqual(e, e.setMaxListeners(1));
|
||||
const untyped = [e, b][0];
|
||||
assert.strictEqual(untyped, e);
|
||||
assert.strictEqual(e, untyped);
|
||||
assert.notStrictEqual(untyped, b);
|
||||
|
||||
try {
|
||||
assert.strictEqual(b, c);
|
||||
} catch (error) {
|
||||
console.log(error.code);
|
||||
console.log(error.message);
|
||||
}
|
||||
try {
|
||||
assert.notStrictEqual(b, b.self(), 'same box');
|
||||
} catch (error) {
|
||||
console.log(error.code, error.message);
|
||||
}
|
||||
// A symbol read back through an untyped value is the same symbol.
|
||||
const symbol = Symbol('s');
|
||||
const read = Reflect.get({ k: symbol }, 'k');
|
||||
assert.strictEqual(read, symbol);
|
||||
assert.notStrictEqual(read, Symbol('s'));
|
||||
try {
|
||||
assert.strictEqual(read, Symbol('other'));
|
||||
} catch (error) {
|
||||
console.log(error.code, error.message.split('\n')[0]);
|
||||
}
|
||||
console.log('done');
|
||||
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
// `x instanceof Object` and `x instanceof Function` on untyped values follow
|
||||
// Node: objects are instances of Object unless their prototype chain ends
|
||||
// in null first, and functions (classes and arrows included) are instances
|
||||
// of Function.
|
||||
class Shape {}
|
||||
class Square extends Shape {}
|
||||
|
||||
const nullProto = Object.create(null);
|
||||
const underNull = Object.create(nullProto);
|
||||
const regrown = Object.create(null);
|
||||
Object.setPrototypeOf(regrown, {});
|
||||
const orphan = function orphan() {};
|
||||
Object.setPrototypeOf(orphan, null);
|
||||
function declared() {}
|
||||
|
||||
const values = {
|
||||
'plain object': {},
|
||||
array: [1, 2],
|
||||
'Object.create(null)': nullProto,
|
||||
'object under a null-prototype object': underNull,
|
||||
'Object.create(null) given a prototype': regrown,
|
||||
'Object.create(plain object)': Object.create({ a: 1 }),
|
||||
'parsed JSON': JSON.parse('{"a":[1]}'),
|
||||
'function declaration': declared,
|
||||
'bound function': declared.bind(null),
|
||||
'arrow function': () => 1,
|
||||
'async function': async () => {},
|
||||
class: Square,
|
||||
'class instance': new Square(),
|
||||
'function with a null prototype': orphan,
|
||||
map: new Map(),
|
||||
date: new Date(0),
|
||||
'regular expression': /x/,
|
||||
error: new TypeError('t'),
|
||||
'typed array': new Uint8Array(2),
|
||||
promise: Promise.resolve(1),
|
||||
number: 1,
|
||||
string: 's',
|
||||
boolean: false,
|
||||
null: null,
|
||||
undefined: undefined,
|
||||
symbol: Symbol('s'),
|
||||
bigint: 10n,
|
||||
};
|
||||
|
||||
for (const [label, value] of Object.entries(values)) {
|
||||
console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`);
|
||||
}
|
||||
|
||||
function describe(value) {
|
||||
if (!(value instanceof Object)) return 'primitive or null-prototype';
|
||||
return value instanceof Function ? 'function' : 'object';
|
||||
}
|
||||
console.log([nullProto, {}, describe, 0].map(describe).join(', '));
|
||||
|
||||
// Prototype chains through functions and objects, and Object.prototype
|
||||
// itself, whose own prototype is null.
|
||||
const base = function base() {};
|
||||
const fromFunction = {};
|
||||
Object.setPrototypeOf(fromFunction, base);
|
||||
const throughObject = {};
|
||||
Object.setPrototypeOf(throughObject, fromFunction);
|
||||
const nullRooted = function nullRooted() {};
|
||||
Object.setPrototypeOf(nullRooted, null);
|
||||
const reparented = function reparented() {};
|
||||
Object.setPrototypeOf(reparented, fromFunction);
|
||||
const chains = {
|
||||
objectPrototype: Object.getPrototypeOf({}),
|
||||
fromFunction,
|
||||
throughObject,
|
||||
nullRooted,
|
||||
reparented,
|
||||
};
|
||||
for (const [label, value] of Object.entries(chains)) {
|
||||
console.log(`${label}: Object ${value instanceof Object}, Function ${value instanceof Function}`);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
// assert methods are function values: stored, passed and called through
|
||||
// aliases, they assert as the methods do, and every read is the same
|
||||
// function. A constant bound to a method and called directly reports
|
||||
// the call as Node quotes it.
|
||||
const assert = require('assert');
|
||||
const { strictEqual, deepStrictEqual } = require('assert');
|
||||
const strict = require('assert/strict');
|
||||
const EventEmitter = require('events');
|
||||
console.log(typeof assert.fail, assert.fail === assert.fail, strict.equal === assert.strictEqual, strictEqual === assert.strictEqual);
|
||||
const ok = assert.ok;
|
||||
ok(1);
|
||||
const tryIt = (label, fn) => { try { fn(); console.log(label, 'passed'); } catch (e) { console.log(label, e.name, e.code, JSON.stringify(e.message), e.generatedMessage); } };
|
||||
tryIt('ok', () => ok(0));
|
||||
tryIt('ok none', () => ok());
|
||||
tryIt('ok msg', () => ok(false, 'nope'));
|
||||
tryIt('ok err', () => ok(false, new TypeError('own')));
|
||||
tryIt('fail', () => assert.fail.call(null));
|
||||
tryIt('fail msg', () => (0, assert.fail)('boom'));
|
||||
tryIt('equal', () => assert.equal.call(null, 1, '1'));
|
||||
tryIt('equal fail', () => [assert.equal][0](1, 2));
|
||||
tryIt('notEqual fail', () => [assert.notEqual][0](1, '1'));
|
||||
tryIt('strict', () => strictEqual(1, 2));
|
||||
tryIt('deep', () => deepStrictEqual({ a: 1 }, { a: 1 }));
|
||||
tryIt('deep fail', () => [deepStrictEqual][0]({ a: 1 }, { a: 2 }, 'differs'));
|
||||
tryIt('ifError', () => [assert.ifError][0](null));
|
||||
const checks = [assert.ok, assert.fail, assert.strictEqual];
|
||||
checks.forEach((check, i) => console.log(i, typeof check, check === checks[i], check.name));
|
||||
[1, 'x', true].forEach((value) => assert.ok(value));
|
||||
console.log('done');
|
||||
|
||||
// The comparison methods need both operands, as Node's ERR_MISSING_ARGS
|
||||
// TypeError says, also through function values.
|
||||
const comparisons = [assert.equal, assert.notEqual, assert.strictEqual, assert.notStrictEqual,
|
||||
assert.deepEqual, assert.notDeepEqual, assert.deepStrictEqual, assert.notDeepStrictEqual];
|
||||
for (const compare of comparisons) {
|
||||
for (const args of [[], [1]]) {
|
||||
try {
|
||||
compare(...args);
|
||||
console.log(compare.name, args.length, 'passed');
|
||||
} catch (e) {
|
||||
console.log(compare.name, args.length, e.name, e.code, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// assert methods read as values in TypeScript: a constant alias called
|
||||
// directly, a stored value, and a method passed as a callback.
|
||||
import assert from "node:assert";
|
||||
const eq = assert.strictEqual;
|
||||
eq(1, 1);
|
||||
const fail = assert.fail;
|
||||
const checks: unknown[] = [assert.ok, assert.fail];
|
||||
console.log(typeof checks[0], checks[1] === fail);
|
||||
try {
|
||||
eq(1, 2, "one is not two");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
const run = (fn: (v: unknown) => void) => fn(true);
|
||||
run(assert.ok);
|
||||
console.log("done");
|
||||
@@ -2,8 +2,8 @@
|
||||
// a fence inside a function runs when the function is called.
|
||||
"use strict";
|
||||
var value = JSON.parse("{}");
|
||||
console.log(value instanceof Object);
|
||||
console.log(value instanceof WeakRef);
|
||||
function check(other) {
|
||||
return other instanceof Object;
|
||||
return other instanceof WeakRef;
|
||||
}
|
||||
console.log(check([]));
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// (typed arrays, class instances), namespace-object calls, unsupported
|
||||
// throws/rejects expectations (properties outside the static error
|
||||
// surface, empty shapes, non-literal objects, doesNotReject shapes —
|
||||
// Node's own TypeError), ifError over uncomparable types, and assert
|
||||
// functions as values. Every reached site terminates in a named
|
||||
// Node's own TypeError), ifError over uncomparable types, and assert.throws
|
||||
// as a value. Every reached site terminates in a named
|
||||
// diagnostic — never a miscompare, never silence.
|
||||
import assert from "node:assert";
|
||||
import * as assertNs from "node:assert";
|
||||
@@ -47,8 +47,8 @@ assert.throws(() => { throw new Error("x"); }, {}); // Node rejects the empty sh
|
||||
void assert.doesNotReject(async () => {}, { name: "TypeError" }); // Node's own TypeError (function or RegExp only)
|
||||
assert.ifError(new Map<string, number>()); // no honest ifError rendering for this type
|
||||
|
||||
const eq = assert.strictEqual; // assert functions as values
|
||||
eq(1, 1);
|
||||
const throwsFn = assert.throws; // assert.throws as a value: it takes the function to run
|
||||
throwsFn(() => {});
|
||||
|
||||
interface Tree { label: string; kids: Tree[] }
|
||||
const tree: Tree = { label: "t", kids: [] };
|
||||
|
||||
@@ -104,12 +104,12 @@ assert-fences.ts:48:16 - error SC2020: 'assert.ifError over 'Map<string, number>
|
||||
|
||||
hint: numbers, strings, booleans, Errors, null/undefined, and unions of those are the lowered forms
|
||||
|
||||
assert-fences.ts:50:12 - error SC2020: 'assert.strictEqual' is part of the standard library types but has no scriptc lowering yet
|
||||
assert-fences.ts:50:18 - error SC2020: 'assert.throws' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
49 |
|
||||
50 | const eq = assert.strictEqual; // assert functions as values
|
||||
| ^~~~~~~~~~~~~~~~~~
|
||||
51 | eq(1, 1);
|
||||
50 | const throwsFn = assert.throws; // assert.throws as a value: it takes the function to run
|
||||
| ^~~~~~~~~~~~~
|
||||
51 | throwsFn(() => {});
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
|
||||
@@ -221,10 +221,10 @@ test("deferred fences in a module's top-level code report as running at startup"
|
||||
]);
|
||||
const out = report(fixture("startup-fences.js"));
|
||||
expect(out).toMatch(
|
||||
/runs at startup {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:13/,
|
||||
/runs at startup {3}1 site .*\n.*'instanceof' right-hand sides other than classes declared in the program\n\s+at tests\/coverage-fixtures\/startup-fences\.js:5:30/,
|
||||
);
|
||||
expect(out).toMatch(
|
||||
/deferred to runtime {3}1 site .*\n.*instanceof against this native constructor\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:10/,
|
||||
/deferred to runtime {3}1 site .*\n.*'instanceof' right-hand sides other than classes declared in the program\n\s+at tests\/coverage-fixtures\/startup-fences\.js:7:27/,
|
||||
);
|
||||
const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts });
|
||||
expect(envelope.diagnostics.map((d) => [d.scope, d.line, d.startup])).toEqual([
|
||||
|
||||
Reference in New Issue
Block a user