Convert checked-dynamic expected errors in assert.throws and assert.match

An expected regex or string read from a JavaScript object literal's
field is a checked-dynamic value while its checker type is RegExp or
string. assert.throws chose its regex comparison from the checker type
and passed the value unconverted, an internal compiler error, and
assert.match and the expected-object form refused it. Such a value now
converts to the regex or string its type names.
This commit is contained in:
Chris Tate
2026-10-23 21:12:15 -05:00
parent a294403acd
commit 10e3476a55
4 changed files with 80 additions and 3 deletions
@@ -1355,7 +1355,7 @@ function lowerAssertMatch(
lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr);
}
const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const re = lowerer.lowerExpr(expr.arguments.nodes[1]!);
const re = lowerExpectedValue(lowerer, expr.arguments.nodes[1]!);
if (re.type.kind !== "regex") {
lowerer.noLowering(
`${surface} with a non-regex pattern`,
@@ -1420,6 +1420,18 @@ function inErrorHierarchy(lowerer: Lowerer, className: string): boolean {
return false;
}
/** An expected string or regex: a value JavaScript stores checked-dynamic
* (an object literal's field, an untyped binding) converts to the string
* or regex its checker type names, so the comparison takes that arm. */
function lowerExpectedValue(lowerer: Lowerer, node: ts.Expression): IrExpr {
const value = lowerer.lowerExpr(node);
if (value.type.kind !== "dyn") return value;
const expected = lowerer.mapTypeOf(lowerer.typeOf(node));
return expected?.kind === "string" || expected?.kind === "regex"
? lowerer.coerceToExpected(value, expected)
: value;
}
/** Classify the expected argument: string → message, regex value → the
* regex test over String(error), an error-class identifier → instanceof,
* an object LITERAL over name/message/code (string or regex values) →
@@ -1436,7 +1448,7 @@ function classifyThrowsExpected(
return { form: "message", msg: m.msg, hasMsg: m.hasMsg };
}
if (expectedT?.kind === "regex") {
return { form: "regex", value: lowerer.lowerExpr(node) };
return { form: "regex", value: lowerExpectedValue(lowerer, node) };
}
const sym = ts.isIdentifier(node) ? lowerer.resolveValueSymbol(node) : null;
const info =
@@ -1509,7 +1521,7 @@ function classifyThrowsExpected(
);
}
const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name;
const value = lowerer.lowerExpr(valueNode);
const value = lowerExpectedValue(lowerer, valueNode);
if (value.type.kind === "string") {
byId.set(id, { id, kind: "str", value, enameBake: null });
} else if (value.type.kind === "regex") {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8160-assert-expected-from-fields.cjs"
],
"diags": []
}
@@ -0,0 +1,58 @@
// Expected errors stored in an object literal's fields: a JavaScript
// object keeps its fields checked-dynamic, and assert.throws, assert.match
// and the expected-object form convert them to the regex or string the
// fields hold.
"use strict";
const assert = require("assert");
const errMessages = {
auth: / auth/,
state: / state/,
length: /Invalid initialization vector/,
code: "ERR_CRYPTO_INVALID_IV",
};
const error = (message, code) => {
const e = new Error(message);
if (code) e.code = code;
return e;
};
assert.throws(() => {
throw error("Unsupported state or unable to authenticate data");
}, errMessages.auth);
assert.throws(() => {
throw error("Invalid state for operation getAuthTag");
}, errMessages.state);
assert.throws(
() => {
throw error("Invalid initialization vector", "ERR_CRYPTO_INVALID_IV");
},
{ code: errMessages.code, message: errMessages.length },
);
assert.match("unable to auth", errMessages.auth);
assert.doesNotMatch("no match here", errMessages.state);
const report = (label, run) => {
try {
run();
console.log(label, "passed");
} catch (error) {
console.log(label, error.name, error.code, error.message.split("\n")[0]);
}
};
report("wrong message", () =>
assert.throws(() => {
throw error("something else");
}, errMessages.length),
);
report("wrong code", () =>
assert.throws(
() => {
throw error("Invalid initialization vector", "ERR_OTHER");
},
{ code: errMessages.code },
),
);
report("match", () => assert.match("plain text", errMessages.auth));
console.log("done");
@@ -0,0 +1 @@
{ "plain": 2656, "san": 5234 }