diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index bc51a9382..b996abed7 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -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") { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json new file mode 100644 index 000000000..fcaacbbee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8160-assert-expected-from-fields.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/8160-assert-expected-from-fields.cjs b/tests/corpus/8160-assert-expected-from-fields.cjs new file mode 100644 index 000000000..4c16bd78f --- /dev/null +++ b/tests/corpus/8160-assert-expected-from-fields.cjs @@ -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"); diff --git a/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json b/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json new file mode 100644 index 000000000..cdfeabea9 --- /dev/null +++ b/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json @@ -0,0 +1 @@ +{ "plain": 2656, "san": 5234 }