Skip to content

GROOVY-12140: normalize reflectively boxed primitive returns through …#2676

Merged
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy12140
Jul 8, 2026
Merged

GROOVY-12140: normalize reflectively boxed primitive returns through …#2676
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy12140

Conversation

@paulk-asert

Copy link
Copy Markdown
Contributor

…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via valueOf caches (excluding float/double).
  • Apply normalization at two reflective chokepoints: CachedMethod#invoke and PlainObjectMetaMethodSite#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.

Comment thread src/test/groovy/bugs/Groovy12140.groovy
@codecov-commenter

codecov-commenter commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.6972%. Comparing base (e3c89fd) to head (e3a5c70).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
...a/org/codehaus/groovy/reflection/CachedMethod.java 62.5899% <100.0000%> (+0.2711%) ⬆️
...a/org/codehaus/groovy/runtime/MetaClassHelper.java 35.0649% <100.0000%> (+1.1442%) ⬆️
...vy/runtime/callsite/PlainObjectMetaMethodSite.java 37.5000% <100.0000%> (ø)

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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.
@paulk-asert paulk-asert merged commit 23780cf into apache:master Jul 8, 2026
6 checks passed
@paulk-asert paulk-asert deleted the groovy12140 branch July 8, 2026 02:26
@testlens-app

testlens-app Bot commented Jul 8, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

Build and test / lts (17, macos-latest) > :groovy-ant:test

Test Runs
GroovyTest > testGroovyCodeExternalURLResource

🏷️ Commit: e3a5c70
▶️ Tests: 73 executed
🟡 Checks: 2/29 completed

Test Failures

GroovyTest > testGroovyCodeExternalURLResource (:groovy-ant:test in Build and test / lts (17, macos-latest))
/Users/runner/work/groovy/groovy/subprojects/groovy-ant/src/test-resources/org/codehaus/groovy/ant/GroovyTest.xml:65: Source resource does not exist!
	at org.codehaus.groovy.ant.Groovy.execute(Groovy.java:386)
	at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:299)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:99)
	at org.apache.tools.ant.Task.perform(Task.java:350)
	at org.apache.tools.ant.Target.execute(Target.java:449)
	at org.apache.tools.ant.Target.performTasks(Target.java:470)
	at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1401)
	at org.apache.tools.ant.Project.executeTarget(Project.java:1374)
	at org.codehaus.groovy.ant.GroovyTest.testGroovyCodeExternalURLResource(GroovyTest.java:114)

Muted Tests

Note

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants