Skip to content

Expose detection of closure comparisons - #163

Closed
sebastianbergmann wants to merge 1 commit into
mainfrom
expose-detection-of-closure-comparisons
Closed

Expose detection of closure comparisons#163
sebastianbergmann wants to merge 1 commit into
mainfrom
expose-detection-of-closure-comparisons

Conversation

@sebastianbergmann

Copy link
Copy Markdown
Owner

ClosureComparator decides closure equality with a heuristic: two closures are "equal" when they share the same declaration site, the same bound $this, and equal captured use variables. That can quietly return equal for closures that are not meaningfully interchangeable.

PHPUnit wants to surface this and raise a (possibly opt-out) warning when assertEquals() (and the other comparator-based assertions) end up comparing closures so users learn that they rely on fragile closure comparison rather than getting a silent pass.

Equality assertions run constantly, often over large arrays and object graphs. PHPUnit must not pay a second full traversal of the operands just to ask "was a closure involved?" as that tax would be paid on every assertion, including the overwhelming majority that contain no closures at all.

The comparison already walks the entire graph: ArrayComparator / ObjectComparator dispatch every nested node through Factory::getComparatorFor(), which routes closure-vs-closure nodes to ClosureComparator. So the cheap place to detect closure comparison is inside the comparison that's already happening. We can let ClosureComparator record the fact, and let the consumer read it afterward. No second traversal, effectively free.

Factory gains a per-instance boolean flag and a small API (all additive, Factory is under the backward compatibility promise, so this is BC-safe):

  • recordClosureComparison() sets the flag. Marked @internal; it's the write side, called only by ClosureComparator
  • closureComparisonOccurred() is the consumer-facing query
  • resetClosureComparisonTracking() clears the flag so a consumer can scope it to a single comparison

ClosureComparator::assertEquals() calls $this->factory()->recordClosureComparison() as its first statement. Because every default comparator is registered into the same Factory instance via setFactory(), the flag set deep inside a nested comparison is visible to the Factory the consumer holds — even when the closures are buried inside arrays or object properties.

The flag is set to true only when a closure is compared against a closure. This is exactly the silent, heuristic-driven case worth warning about. The flag is not set to true for one-sided cases (a closure compared against a scalar / null, or under a key that exists on only one side), because those already produce a visible assertion failure. So the cheap signal targets the genuinely silent risk, not the cases that already fail loudly.

@github-actions

Copy link
Copy Markdown

API Surface Changes

If any of the additions below are not intended as public API, mark them with @internal in the docblock.

New API Surface

Methods

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.02%. Comparing base (f07eb19) to head (3d901cc).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #163      +/-   ##
============================================
+ Coverage     94.96%   95.02%   +0.05%     
- Complexity      229      232       +3     
============================================
  Files            18       18              
  Lines           616      623       +7     
============================================
+ Hits            585      592       +7     
  Misses           31       31              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@sebastianbergmann
sebastianbergmann deleted the expose-detection-of-closure-comparisons branch May 23, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant