Paul King created GROOVY-12584:
----------------------------------

             Summary: Indy cast of a Collection to an abstract subclass of 
HashSet or ArrayList throws a bare ClassCastException
                 Key: GROOVY-12584
                 URL: https://issues.apache.org/jira/browse/GROOVY-12584
             Project: Groovy
          Issue Type: Bug
          Components: Compiler
            Reporter: Paul King


h3. Problem

The invokedynamic cast selector (\{{Selector.CastSelector.handleCollections}}) 
has a fast path that is meant to turn a \{{Collection}} into a \{{HashSet}} or 
\{{ArrayList}} for an abstract target type. Its check is the wrong way round:

{code:java}
private static boolean isAbstractClassOf(final Class<?> type, final Class<?> 
callSiteType) {
    return (callSiteType.isInterface() || 
Modifier.isAbstract(callSiteType.getModifiers())) && 
type.isAssignableFrom(callSiteType);
}
{code}

{\{isAbstractClassOf(HashSet.class, target)}} is true only when the target is 
an abstract *subclass* of \{{HashSet}}. So for the targets it was presumably 
written for (\{{Set}}, \{{AbstractSet}}, \{{Collection}}, \{{List}}) the fast 
path never fires, and the cast goes to 
\{{DefaultTypeTransformation.castToType}}, as it does with classic call sites. 
For an abstract subclass of \{{HashSet}} or \{{ArrayList}} it does fire. It 
builds a plain \{{HashSet}}/\{{ArrayList}}, which then fails the cast to the 
target type with a bare \{{ClassCastException}}, where classic call sites throw 
\{{GroovyCastException}}.

h3. Reproducer

{code:groovy}
abstract class ASet extends HashSet {}
abstract class AList extends ArrayList {}

Object list = [3, 1, 2, 1]
Object set = [3, 1, 2] as LinkedHashSet

ASet s = list   // or: AList l = set
{code}

||Cast||indy||classic call sites||
|\{{ASet s = list}}|\{{ClassCastException: Cannot cast java.util.HashSet to 
ASet}}|\{{GroovyCastException: ... Could not find matching constructor for: 
ASet(Integer, Integer, Integer, Integer)}}|
|\{{AList l = set}}|\{{ClassCastException: Cannot cast java.util.ArrayList to 
AList}}|\{{GroovyCastException: ... Could not find matching constructor for: 
AList(Integer, Integer, Integer)}}|
|\{{Set s = list}}, \{{AbstractSet s = list}}|\{{LinkedHashSet [3, 1, 2]}}|same|
|\{{List l = set}}|\{{GroovyCastException}}|same|

The cast fails either way; only the exception type and message differ. 
\{{GroovyCastException}} extends \{{ClassCastException}}, so existing 
\{{catch}} blocks are unaffected by a fix.

The code dates back to the early indy work and is identical on 
\{{GROOVY_2_5_X}} (\{{vmplugin.v7}}), \{{GROOVY_3_0_X}}, \{{GROOVY_4_0_X}}, 
\{{GROOVY_5_1_X}}, \{{GROOVY_6_0_X}} and master.

h3. Proposed fix

Remove \{{handleCollections()}} and \{{isAbstractClassOf()}} so that every 
collection cast goes through \{{castToType}}, matching classic call sites.

Correcting the check instead would make indy diverge from classic call sites:
* \{{Set s = list}} would produce a \{{HashSet}} (losing order) instead of 
today's \{{LinkedHashSet}}.
* \{{List l = set}} would start succeeding, where it currently throws 
\{{GroovyCastException}} on both paths.

Removing the fast path also removes a latent hazard: the cached handle \{{new 
HashSet(collection)}} is not null-safe, and since GROOVY-12557 a cast from a 
final static type is no longer guarded against a later \{{null}} value.

{\{IndyGuardsFiltersAndSignatures.HASHSET_CONSTRUCTOR}} and 
\{{ARRAYLIST_CONSTRUCTOR}} are public. Keep them, deprecated, on master; they 
would then have no in-tree users.

A test should cover both abstract-subclass casts expecting 
\{{GroovyCastException}} under indy, plus the 
\{{Set}}/\{{AbstractSet}}/\{{Collection}} shapes to confirm the result types 
don't change.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to