Skip to content

fix(assert): test a RegExp matcher against String(thrown), and make it terminal - #9228

Merged
proggeramlug merged 2 commits into
mainfrom
fix/assert-regexp-matcher
Aug 31, 2026
Merged

fix(assert): test a RegExp matcher against String(thrown), and make it terminal#9228
proggeramlug merged 2 commits into
mainfrom
fix/assert-regexp-matcher

Conversation

@TheHypnoo

@TheHypnoo TheHypnoo commented Aug 30, 2026

Copy link
Copy Markdown
Member

Summary

The first commit on this branch added return false after the RegExp block in
expected_error_matches. That stopped the fallthrough into instanceof, but
kept the line that caused it — a retry of the pattern against thrown.message
— so a whole second bug survived.

Node tests a RegExp matcher against String(thrown) and nothing else, and
treats RegExp as a terminal matcher category. The block collapses to one
statement:

if let Some(matches_thrown) = regex_test_value(expected, thrown) {
    return matches_thrown;
}

That fixes both halves at the single point throws, doesNotThrow, rejects,
and doesNotReject all route through.

The two bugs

Wrong input. An anchored pattern matching the bare message but not the
stringified error was wrongly accepted, because String(new Error("nope")) is
"Error: nope":

assert.throws(() => { throw new Error("nope") }, /^nope/)
  node 26.5.1 → AssertionError ERR_ASSERTION
  before      → passes silently

Same for a thrown non-error carrying a message property, whose String() is
"[object Object]".

Wrong control flow. A pattern matching neither input reached
js_instanceof_dynamic(thrown, regexp) and surfaced as a TypeError instead of
an AssertionError, so throws/rejects reported the wrong error class and
doesNotThrow/doesNotReject failed to rethrow the original error.

A RegExp value on a validator key ({ message: /bad/ }) is a different
matcher and is still tested against that property in object_matcher_matches.

The thrown.message retry dates to the original implementation in #1924 and was
never a workaround for anything else.

Why the existing fixture could not catch this

assert/errors/strict-throws-validation.ts uses /will-not-match/, a pattern
matching neither the message nor the stringified error, so it cannot tell the
two inputs apart — and it prints only name and code.

assert/errors/throws-regexp-matcher-input.ts covers both inputs across all
four entry points and matches Node 26.5.1 byte for byte on 9 cases. The unit
tests replace the setjmp-based one, which the previous fix also passed:

regexp_matcher_tests_the_stringified_error_not_the_message ... FAILED   ← previous fix
                                                            ... ok      ← this commit

Known remaining gap (not fixed here)

Node's generated message is The input did not match the regular expression /x/. Input:\n\n'Error: nope'\n; Perry emits the generic The thrown error did not match the expected matcher. That fallback is shared by every matcher
category, so patching only the regexp arm would leave the message layer
half-parity. Recorded in the changelog fragment as separate work.

Related issue

Fixes #9227

Refs #9202

Test plan

  • RUST_TEST_THREADS=1 cargo test -p perry-runtime regexp_matcher_tests — 3 passed
  • ./run_parity_tests.sh --suite node-suite --module assert71 PASS, 0 fail, 0 compile fail, 0 crash, 0 skip (100.0%), against the pinned Node 26.5.1
  • New fixture diffed byte-for-byte against Node 26.5.1 — identical on all 9 cases
  • cargo fmt clean; assert.rs is 1368 lines (under the 2000-line cap)
  • Added changelog.d/9228-assert-regexp-matcher-input.md (the lint job requires a fragment for any crates/ change)
  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md
  • Commit follows the conventional fix: format

Note on the oracle: run the suite with the pinned Node on PATH. With Node
v24 instead of 26.5.1, assert/imports/calltracker-shape reports a false
mismatch — assert.CallTracker still exists in v24 and was removed by v26, so
the oracle disagrees with itself, not with Perry. Same tree: 70/71 under v24,
71/71 under v26.5.1.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected regular expression matching in error assertions to consistently use the stringified thrown value.
    • Prevented failed regular expression matches from falling through to other matcher types.
    • Preserved property-specific matching for regular expressions used in validator objects.
    • Improved consistency across synchronous and asynchronous assertion APIs, including thrown strings and plain objects.
  • Tests

    • Added coverage for matching, non-matching, and error-reporting scenarios.

@TheHypnoo TheHypnoo added type:bug Bug fix parity Compatibility gap with Node.js, ECMAScript, or the supported ecosystem labels Aug 30, 2026
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 22683b90-0d77-4ffd-b855-53b5549a7761

📥 Commits

Reviewing files that changed from the base of the PR and between 6642990 and 10977c0.

📒 Files selected for processing (3)
  • changelog.d/9228-assert-regexp-matcher-input.md
  • crates/perry-runtime/src/object/assert.rs
  • test-parity/node-suite/assert/errors/throws-regexp-matcher-input.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test-parity/node-suite/assert/errors/throws-regexp-matcher-input.ts
  • crates/perry-runtime/src/object/assert.rs
  • changelog.d/9228-assert-regexp-matcher-input.md

Included review availability: Your plan provides up to 8 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

RegExp matchers in assert.throws and related APIs now test String(thrown) once and stop on a mismatch. Validator-key RegExp matching remains property-based. Runtime and Node parity tests cover synchronous and asynchronous assertions.

Changes

RegExp assertion matcher correction

Layer / File(s) Summary
Terminal RegExp matching
crates/perry-runtime/src/object/assert.rs
RegExp mismatches now return false without message retries or instanceof fallthrough. Runtime tests cover errors, non-error values, terminal mismatches, and validator-key matching.
Assertion API parity coverage
test-parity/node-suite/assert/errors/throws-regexp-matcher-input.ts, changelog.d/9228-assert-regexp-matcher-input.md
Parity tests cover throws, doesNotThrow, rejects, and doesNotReject. The changelog records the corrected matching behavior and remaining assertion-message difference.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 10977

The PR makes RegExp thrown-error matching terminal and aligns its input with the stringified error across the assertion APIs; no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: RegExp matchers now test String(thrown) and terminate matcher evaluation.
Description check ✅ Passed The description includes the required Summary, Changes, Related issue, Test plan, and Checklist information. It documents the behavior, scope, tests, known gap, and repository checklist items.
Linked Issues check ✅ Passed The implementation satisfies issue #9227. RegExp mismatches now return assertion failures without instanceof fallthrough, and coverage includes throws, doesNotThrow, rejects, and doesNotReject. The re…
Out of Scope Changes check ✅ Passed The code changes, regression tests, parity fixture, unit tests, and changelog fragment directly support the linked issue and stated objectives. No unrelated changes are identified.
Full details: Linked Issues check

Explanation

The implementation satisfies issue #9227. RegExp mismatches now return assertion failures without instanceof fallthrough, and coverage includes throws, doesNotThrow, rejects, and doesNotReject. The reported parity suite passes with no non-pass outcomes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/assert-regexp-matcher

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TheHypnoo TheHypnoo changed the title fix(assert): stop regexp matcher fallthrough fix(assert): test a RegExp matcher against String(thrown), and make it terminal Aug 31, 2026
@TheHypnoo
TheHypnoo marked this pull request as ready for review August 31, 2026 11:30
…t terminal

The previous commit on this branch added `return false` after the RegExp
block. That stopped the fallthrough into `instanceof`, but kept the line that
caused it: a retry of the pattern against `thrown.message`.

Node tests a RegExp matcher against `String(thrown)` and nothing else, and
treats RegExp as a terminal matcher category. The whole block collapses to one
statement, which fixes both halves at the single point `throws`,
`doesNotThrow`, `rejects`, and `doesNotReject` all route through:

- An anchored pattern matching the bare message but not the stringified error
  was wrongly accepted. `assert.throws(() => { throw new Error("nope") },
  /^nope/)` passed, where Node reports an AssertionError because `String(err)`
  is "Error: nope". Same for a thrown non-error carrying a `message` property,
  whose `String()` is "[object Object]".
- A pattern matching neither reached `js_instanceof_dynamic(thrown, regexp)`
  and surfaced as a TypeError instead of an AssertionError, so throws/rejects
  reported the wrong error class and doesNotThrow/doesNotReject failed to
  rethrow the original error.

A RegExp value on a validator key (`{ message: /bad/ }`) is a different matcher
and is still tested against that property in `object_matcher_matches`.

The `thrown.message` retry dates to the original implementation in #1924 and
was never a workaround for anything else.

The pre-existing `assert/errors/strict-throws-validation.ts` could not catch
either bug: `/will-not-match/` matches neither input, so it cannot tell them
apart, and it prints only `name` and `code`. The new fixture covers both inputs
across all four entry points and matches Node 26.5.1 byte for byte. The unit
tests replace the setjmp-based one, which the previous fix also passed.

Fixes #9227
Refs #9202

Claude-Session: https://claude.ai/code/session_01QFubuLjMeJBhrHSfCuoPMp
@proggeramlug
proggeramlug force-pushed the fix/assert-regexp-matcher branch from 79ff37b to 10977c0 Compare August 31, 2026 13:01
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@proggeramlug

Copy link
Copy Markdown
Contributor

Merged.

Validated on a shared branch with #9228, #9257, #9263, #9271, #9272, #9274, #9277, #9279 and #9280 — one build, one validation pass, then split back out and merged individually.

Results across the batch:

  • perry-runtime 2885 passed / 0 failed at RUST_TEST_THREADS=1
  • perry-codegen 30 suites green (the one failure was a doc-test reporting a missing libperry_codegen-*.rlib — an artifact of my own cleanup of stale build dirs, confirmed by a clean re-run at 31/31, not a code defect)
  • all 60 lint gates plus the check_thread_locals / tls_budget checkers
  • a nine-row differential probe byte-identical to node 26.5.1, covering every changed area: RegExp \w/\b/. ASCII and LineTerminator semantics, offset (a[i±1]) and length-bounded array reads, the assert RegExp matcher, and closure identity across a 40k-allocation GC churn
  • seven earlier regression probes re-run at zero diff lines: tagged and scalar array stores, pointer↔scalar transition churn, growth-forwarding receivers, BigInt negation, iterator protocols, field shadowing

One probe (protorepl) moved from 2 to 4 diff lines and I ran it down rather than waving it through: both divergences are accepted trades already on main — the fresh-instance case (#9239) and #9247's deliberate change of a custom-chain miss from Some(undefined) to None, which it made because swallowing the miss left everything Perry synthesizes unreachable (Object(true).valueOf(), plain-function .prototype, iterator helpers). Neither is anything in this batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parity Compatibility gap with Node.js, ECMAScript, or the supported ecosystem type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node:assert RegExp mismatch falls through to instanceof and throws TypeError

2 participants