Convert checked-dynamic expected errors in assert.throws and assert.match (#2066)

This commit is contained in:
Chris Tate
2026-10-23 21:41:23 -05:00
committed by GitHub
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 }