GROOVY-12140: normalize reflectively boxed primitive returns through …#2676
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes GROOVY-12140 by ensuring primitive return values that were boxed via Method#invoke are re-normalized through the JDK valueOf caches, restoring consistent reference identity (===) across Groovy dispatch paths (reflective vs. indy/bytecode).
Changes:
- Add
MetaClassHelper#normalizeBoxedReturn(Object, Class<?>)to re-normalize reflectively boxed primitive returns viavalueOfcaches (excluding float/double). - Apply normalization at two reflective chokepoints:
CachedMethod#invokeandPlainObjectMetaMethodSite#doInvoke. - Add a regression test covering dynamic-name calls and classic-mode varargs reflective call sites.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/test/groovy/bugs/Groovy12140.groovy | Adds regression coverage for boxed primitive identity across reflective dispatch paths. |
| src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java | Introduces shared helper to normalize reflectively boxed primitive returns through valueOf caches. |
| src/main/java/org/codehaus/groovy/runtime/callsite/PlainObjectMetaMethodSite.java | Normalizes reflective Method.invoke results to preserve === identity for primitive returns. |
| src/main/java/org/codehaus/groovy/reflection/CachedMethod.java | Normalizes reflective cached-method invocation results used by doMethodInvoke routes. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2676 +/- ##
==================================================
+ Coverage 68.6904% 68.6972% +0.0068%
- Complexity 34105 34120 +15
==================================================
Files 1536 1536
Lines 128903 128912 +9
Branches 23363 23369 +6
==================================================
+ Hits 88544 88559 +15
+ Misses 32529 32521 -8
- Partials 7830 7832 +2
🚀 New features to boost your workflow:
|
…valueOf Method#invoke boxes primitive return values with fresh instances, unlike MethodHandle-based and generated-bytecode invocation, which box through the valueOf caches. Dispatch paths that invoke reflectively therefore broke reference identity (===) of primitive returns: classic-mode reflective call sites (e.g. varargs methods), and — in both compilation modes — anything routed through MetaMethod#doMethodInvoke, notably dynamic-name calls such as obj."$name"(), where a constant-name call to the same method returns the cached box. Reflective results are now re-normalized through the valueOf caches at the two chokepoints: CachedMethod#invoke (all doMethodInvoke routes) and PlainObjectMetaMethodSite#doInvoke (classic call sites holding a raw Method). The shared helper lives in MetaClassHelper#normalizeBoxedReturn. float/double are left as-is since valueOf does not cache them on any path. Also reproducible on GROOVY_5_0_X, so a candidate for backport.
🚨 TestLens detected 1 failed test 🚨Here is what you can do:
Test SummaryBuild and test / lts (17, macos-latest) > :groovy-ant:test
🏷️ Commit: e3a5c70 Test FailuresGroovyTest > testGroovyCodeExternalURLResource (:groovy-ant:test in Build and test / lts (17, macos-latest))Muted TestsNote Checks are currently running using the configuration below. Select tests to mute in this pull request: 🔲 GroovyTest > testGroovyCodeExternalURLResource Reuse successful test results: 🔲 ♻️ Only rerun the tests that failed or were muted before Click the checkbox to trigger a rerun: 🔲 Rerun jobs Learn more about TestLens at testlens.app. |
…valueOf
Method#invoke boxes primitive return values with fresh instances, unlike MethodHandle-based and generated-bytecode invocation, which box through the valueOf caches. Dispatch paths that invoke reflectively therefore broke reference identity (===) of primitive returns: classic-mode reflective call sites (e.g. varargs methods), and — in both compilation modes — anything routed through MetaMethod#doMethodInvoke, notably dynamic-name calls such as obj."$name"(), where a constant-name call to the same method returns the cached box.
Reflective results are now re-normalized through the valueOf caches at the two chokepoints: CachedMethod#invoke (all doMethodInvoke routes) and PlainObjectMetaMethodSite#doInvoke (classic call sites holding a raw Method). The shared helper lives in MetaClassHelper#normalizeBoxedReturn. float/double are left as-is since valueOf does not cache them on any path. Also reproducible on GROOVY_5_0_X, so a candidate for backport.