From ddd414842201fc4f4d0332bc1d4b6c7183f5900d Mon Sep 17 00:00:00 2001 From: Claudiu Soroiu Date: Tue, 21 Sep 2021 09:38:47 +0300 Subject: [PATCH] 1. Support method references for methods returning a primitive value 2. Fix Object class method and constructor reference detection 3. Fixed resolving lambdas when running with retrolambda & jacoco 4. Updated README.md --- README.md | 8 +- .../typetools/ReifiedParameterizedType.java | 22 +++- .../net/jodah/typetools/TypeResolver.java | 120 +++++++++++++++--- .../typetools/functional/LambdaTest.java | 31 +++++ 4 files changed, 154 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 62037cb..f66e0eb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License](http://img.shields.io/:license-apache-brightgreen.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) [![JavaDoc](https://img.shields.io/maven-central/v/net.jodah/typetools.svg?maxAge=60&label=javadoc&color=blue)](https://jodah.net/typetools/javadoc/) -A simple, zero-dependency library for working with types. Supports Java 1.6+ and Android. +A simple, zero-dependency library for working with types. Supports Java 8+ and Android. ## Introduction @@ -144,8 +144,8 @@ TypeResolver.disableCache(); Lambda type argument resolution is currently supported for: -* Oracle JDK 8, 9 -* Open JDK 8, 9 +* Oracle JDK 8 up to 17 +* Open JDK 8 up to 17 #### On Unresolvable Lambda Type Arguments @@ -154,7 +154,7 @@ When resolving type arguments with lambda expressions, only type parameters used ```java interface ExtraFunction extends Function{} ExtraFunction strToInt = s -> Integer.valueOf(s); -Class[] typeArgs = TypeResolver.resolveRawArguments(Function.class, strToInt.getClass()); +Class[] typeArgs = TypeResolver.resolveRawArguments(ExtraFunction.class, strToInt.getClass()); assert typeArgs[0] == String.class; assert typeArgs[1] == Integer.class; diff --git a/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java b/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java index f21fbbf..38bed1a 100644 --- a/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java +++ b/src/main/java/net/jodah/typetools/ReifiedParameterizedType.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package net.jodah.typetools; import java.lang.reflect.ParameterizedType; @@ -61,9 +77,9 @@ public String toString() { if (ownerType != null) { if (ownerType instanceof Class) { - sb.append(((Class) ownerType).getName()); + sb.append(((Class) ownerType).getName()); } else { - sb.append(ownerType.toString()); + sb.append(ownerType); } sb.append("$"); @@ -74,7 +90,7 @@ public String toString() { sb.append(rawType.getTypeName() .replace(((ParameterizedType) ownerType).getRawType().getTypeName() + "$", "")); } else if (rawType instanceof Class){ - sb.append(((Class) rawType).getSimpleName()); + sb.append(((Class) rawType).getSimpleName()); } else { sb.append(rawType.getTypeName()); } diff --git a/src/main/java/net/jodah/typetools/TypeResolver.java b/src/main/java/net/jodah/typetools/TypeResolver.java index 10767e1..304cefd 100644 --- a/src/main/java/net/jodah/typetools/TypeResolver.java +++ b/src/main/java/net/jodah/typetools/TypeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,13 @@ import java.lang.reflect.WildcardType; import java.security.AccessController; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; import sun.misc.Unsafe; @@ -58,7 +61,8 @@ public final class TypeResolver { private static Method GET_CONSTANT_POOL_SIZE; private static Method GET_CONSTANT_POOL_METHOD_AT; private static final Map OBJECT_METHODS = new HashMap(); - private static final Map, Class> PRIMITIVE_WRAPPERS; + private static final Map, Class> PRIMITIVE_WRAPPERS_MAP; + private static final Set> PRIMITIVE_WRAPPERS; private static final Double JAVA_VERSION; static { @@ -155,13 +159,14 @@ public void makeAccessible(AccessibleObject object) throws Throwable { types.put(long.class, Long.class); types.put(short.class, Short.class); types.put(void.class, Void.class); - PRIMITIVE_WRAPPERS = Collections.unmodifiableMap(types); + PRIMITIVE_WRAPPERS_MAP = Collections.unmodifiableMap(types); + PRIMITIVE_WRAPPERS = Collections.unmodifiableSet(new HashSet<>(types.values())); } - + private interface AccessMaker { void makeAccessible(AccessibleObject object) throws Throwable; } - + /** An unknown type. */ public static final class Unknown { private Unknown() { @@ -719,41 +724,116 @@ private static boolean isDefaultMethod(Method m) { } private static Member getMemberRef(Class type) { - Object constantPool; - try { - constantPool = GET_CONSTANT_POOL.invoke(JAVA_LANG_ACCESS, type); - } catch (Exception ignore) { - return null; + Member[] constantPoolMethods = extractConstantPoolMethods(type); + //skip jacoco synthetic methods + int start = constantPoolMethods.length; + if (isInstrumentedByJacoco(type)) { + for (int i = constantPoolMethods.length - 1; i >= 0; i--) { + if (constantPoolMethods[i].getName().startsWith("$jacoco")) { + start = i; + } + } } Member result = null; - for (int i = getConstantPoolSize(constantPool) - 1; i >= 0; i--) { - Member member = getConstantPoolMethodAt(constantPool, i); + Method prev = null; + for (int i = start - 1; i >= 0; i--) { + Member member = constantPoolMethods[i]; // Skip SerializedLambda constructors and members of the "type" class - if (member == null - || (member instanceof Constructor - && member.getDeclaringClass().getName().equals("java.lang.invoke.SerializedLambda")) - || member.getDeclaringClass().isAssignableFrom(type)) + if ((member instanceof Constructor + && member.getDeclaringClass().getName().equals("java.lang.invoke.SerializedLambda")) + || member.getDeclaringClass().equals(type)) continue; result = member; // Return if not valueOf method - if (!(member instanceof Method) || !isAutoBoxingMethod((Method) member)) - break; + if (isBoxingOrUnboxingMethod(member) && prev == null) { + prev = (Method) member; //retain the last member if it is boxing or unboxing + continue; + } + + break; + } + + if (prev != null) { + // depending on the result, if the constantpool ends with a boxing method, + // if result is a constructor, then special care is needed + // | constructor type | prev type | end result | + // | any | boxing method | boxing method | + // | primitive wrapper | unboxing method | constructor | + // | ! primitive wrapper | unboxing method | unboxing method | + if (result instanceof Constructor) { + if (isBoxingMethod(prev)) { + return prev; + } else if (isUnboxingMethod(prev) && !PRIMITIVE_WRAPPERS.contains(((Constructor) result).getDeclaringClass())) { + return prev; + } + } } return result; } - private static boolean isAutoBoxingMethod(Method method) { + private static Member[] extractConstantPoolMethods(Class type) { + Object constantPool; + try { + constantPool = GET_CONSTANT_POOL.invoke(JAVA_LANG_ACCESS, type); + } catch (Exception ignore) { + return new Member[0]; + } + ArrayList methods = new ArrayList<>(); + int constantPoolSize = getConstantPoolSize(constantPool); + for (int i = 0; i < constantPoolSize; i++) { + Member method = getConstantPoolMethodAt(constantPool, i); + if (method != null) + methods.add(method); + } + return methods.toArray(new Member[methods.size()]); + } + + static boolean isInstrumentedByJacoco(Class type) { + // http://www.eclemma.org/jacoco/trunk/doc/faq.html + // JaCoCo [...] adds two members to the classes: A private static field $jacocoData and + // a private static method $jacocoInit(). Both members are marked as synthetic. + boolean result = false; + try { + type.getDeclaredMethod("$jacocoInit"); + result = true; + } catch (NoSuchMethodException ignore) { + } + try { + type.getDeclaredField("$jacocoData"); + result = true; + } catch (NoSuchFieldException ignore) { + } + return result; + } + + private static boolean isBoxingOrUnboxingMethod(Member member) { + return member instanceof Method && (isBoxingMethod((Method) member) || isUnboxingMethod((Method) member)); + } + + private static boolean isBoxingMethod(Method method) { Class[] parameters = method.getParameterTypes(); return method.getName().equals("valueOf") && parameters.length == 1 && parameters[0].isPrimitive() && wrapPrimitives(parameters[0]).equals(method.getDeclaringClass()); } + private static boolean isUnboxingMethod(Method method) { + String methodName = method.getName(); + String returnType = method.getReturnType().getSimpleName(); + Class[] parameters = method.getParameterTypes(); + + return method.getReturnType().isPrimitive() && parameters.length == 0 + //booleanValue, byteValue, charValue, doubleValue, floatValue, intValue, longValue, shortValue + && methodName.startsWith(returnType) && methodName.endsWith("Value") + && (wrapPrimitives(method.getReturnType()).equals(method.getDeclaringClass()) + || method.getDeclaringClass().equals(Number.class)); + } + private static Class wrapPrimitives(Class clazz) { - return clazz.isPrimitive() ? PRIMITIVE_WRAPPERS.get(clazz) : clazz; + return clazz.isPrimitive() ? PRIMITIVE_WRAPPERS_MAP.get(clazz) : clazz; } private static int getConstantPoolSize(Object constantPool) { diff --git a/src/test/java/net/jodah/typetools/functional/LambdaTest.java b/src/test/java/net/jodah/typetools/functional/LambdaTest.java index 3f617fb..cc864b4 100644 --- a/src/test/java/net/jodah/typetools/functional/LambdaTest.java +++ b/src/test/java/net/jodah/typetools/functional/LambdaTest.java @@ -13,6 +13,7 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; import org.testng.annotations.Factory; import org.testng.annotations.Test; @@ -263,6 +264,36 @@ public void shouldResolveSubclassArgumentsForConstructorRef() { new Class[] { String.class, Integer.class }); } + public void shouldResolveObjectClassMethodRef() { + Function fn = Object::toString; + assertEquals(TypeResolver.resolveRawArguments(Function.class, fn.getClass()), + new Class[] { Object.class, String.class }); + } + + public void shouldResolveObjectClassConstructorRef() { + Supplier fn = Object::new; + assertEquals(TypeResolver.resolveRawArguments(Supplier.class, fn.getClass()), + new Class[] { Object.class }); + } + + public void shouldResolvePrimitiveReturnValue() { + ToDoubleFunction fn = Double::new; //this method returns Double, thus unboxing takes place + assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()), + new Class[] { String.class }); + } + + public void shouldResolvePrimitiveReturnValue2() { + ToDoubleFunction fn = Double::valueOf; //this method returns Double, thus unboxing takes place + assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()), + new Class[] { String.class }); + } + + public void shouldResolvePrimitiveReturnValue3() { + ToDoubleFunction fn = Double::doubleValue; + assertEquals(TypeResolver.resolveRawArguments(ToDoubleFunction.class, fn.getClass()), + new Class[] { Double.class }); + } + public void shouldResolveTransposedSubclassArguments() { SelectingFn fn = (String str) -> Integer.valueOf(str); assertEquals(TypeResolver.resolveRawArguments(SelectingFn.class, fn.getClass()),