Merge pull request #4925 from oowekyala/typeres-explicitly-typed-lambdas

[java] Support explicitly typed lambdas
This commit is contained in:
Juan Martín Sotuyo Dodero
2024-04-04 23:18:16 -03:00
committed by GitHub
6 changed files with 171 additions and 17 deletions

View File

@ -292,7 +292,7 @@ public interface JClassType extends JTypeMirror {
/**
* Returns another class type which has the same erasure, but new
* type arguments.
* type arguments. Note that the bounds on the type arguments are not checked.
*
* @param args Type arguments of the returned type. If empty, and
* this type is generic, returns a raw type.

View File

@ -16,6 +16,7 @@ import static net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.Bo
import static net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.BoundKind.LOWER;
import static net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.BoundKind.UPPER;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@ -25,6 +26,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import net.sourceforge.pmd.lang.java.types.JClassType;
import net.sourceforge.pmd.lang.java.types.JMethodSig;
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
import net.sourceforge.pmd.lang.java.types.JTypeVar;
import net.sourceforge.pmd.lang.java.types.JWildcardType;
import net.sourceforge.pmd.lang.java.types.Substitution;
import net.sourceforge.pmd.lang.java.types.TypeOps;
@ -583,14 +585,72 @@ final class ExprCheckHelper {
}
if (lambda.isExplicitlyTyped() && lambda.getParamCount() > 0) {
// TODO infer, normally also for lambdas with no param, i'm just lazy
// https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.5.3
return null;
return inferGroundTargetTypeForExplicitlyTypedLambda(type, lambda);
} else {
return nonWildcardParameterization(type);
}
}
private @Nullable JClassType inferGroundTargetTypeForExplicitlyTypedLambda(JClassType targetType, LambdaExprMirror lambda) {
List<JTypeMirror> explicitParamTypes = lambda.getExplicitParameterTypes();
assert explicitParamTypes != null : "Expecting explicitly typed lambda";
// https://docs.oracle.com/javase/specs/jls/se22/html/jls-18.html#jls-18.5.3
// > For example:
// > Predicate<? super Integer> p = (Number n) -> n.equals(23);
// > The lambda expression is a Predicate<Number>, which is a subtype of Predicate<? super Integer> but not
// > Predicate<Integer>. The analysis in this section is used to infer that Number is an appropriate choice
// > for the type argument to Predicate.
// Let `targetType = F<A1, ..., Am>`
// Let `'a1, ..., 'am` be fresh inference variables.
JClassType targetGTD = targetType.getGenericTypeDeclaration();
List<JTypeVar> formalTypeParams = targetGTD.getFormalTypeParams();
InferenceContext ctx = infer.newContextFor(formalTypeParams, false);
// let `inferenceTarget = F<'a1, ..., 'am>`
JClassType inferenceTarget = (JClassType) ctx.mapToIVars(targetGTD);
JMethodSig msig = findFunctionalInterfaceMethod(inferenceTarget);
if (msig == null) {
return null;
}
List<JTypeMirror> formals = msig.getFormalParameters();
// Now match formal params of the lambda with those of the signature (which contains ivars)
// this adds constraints
if (!TypeOps.areSameTypesInInference(formals, explicitParamTypes)) {
return null;
}
ctx.solve(true); // may throw ResolutionFailedException
// If we are here then solving succeeded.
// Build type arguments with instantiated vars. Vars that were not bound (meaning,
// they don't depend on the parameter types of the lambda) are just taken from the
// provided type args.
int numTyArgs = formalTypeParams.size();
List<JTypeMirror> typeArgs = targetType.getTypeArgs();
List<JTypeMirror> newTyArgs = new ArrayList<>(numTyArgs);
for (int i = 0; i < numTyArgs; i++) {
InferenceVar ivarI = (InferenceVar) ctx.mapToIVars(formalTypeParams.get(i));
if (ivarI.getInst() != null) {
newTyArgs.add(ivarI.getInst());
} else {
newTyArgs.add(typeArgs.get(i));
}
}
// Now check that the primary bounds are valid.
ctx.addPrimaryBounds();
ctx.solve(); // may throw ResolutionFailedException
// This is our type
JClassType inferredTy = targetGTD.withTypeArguments(newTyArgs);
return nonWildcardParameterization(inferredTy);
}
@FunctionalInterface
interface ExprChecker {

View File

@ -121,7 +121,11 @@ public final class Infer {
}
InferenceContext newContextFor(List<JTypeVar> tvars) {
return new InferenceContext(ts, supertypeCheckCache, tvars, LOG);
return newContextFor(tvars, true);
}
InferenceContext newContextFor(List<JTypeVar> tvars, boolean addPrimaryBound) {
return new InferenceContext(ts, supertypeCheckCache, tvars, LOG, addPrimaryBound);
}
/**

View File

@ -80,8 +80,29 @@ final class InferenceContext {
* into ivars
* @param logger Logger for events related to ivar bounds
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic") // ctxId
InferenceContext(TypeSystem ts, SupertypeCheckCache supertypeCheckCache, List<JTypeVar> tvars, TypeInferenceLogger logger) {
this(ts, supertypeCheckCache, tvars, logger, true);
}
/**
* Create an inference context from a set of type variables to instantiate.
* This creates inference vars and may add the initial bounds as described in
*
* https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.1.3
*
* under the purple rectangle.
*
* @param ts The global type system
* @param supertypeCheckCache Super type check cache, shared by all
* inference runs in the same compilation unit
* (stored in {@link Infer}).
* @param tvars Initial tvars which will be turned
* into ivars
* @param logger Logger for events related to ivar bounds
* @param addPrimaryBound Whether to add the primary bound of the vars.
*/
@SuppressWarnings("PMD.AssignmentToNonFinalStatic") // ctxId
InferenceContext(TypeSystem ts, SupertypeCheckCache supertypeCheckCache, List<JTypeVar> tvars, TypeInferenceLogger logger, boolean addPrimaryBound) {
this.ts = ts;
this.supertypeCheckCache = supertypeCheckCache;
this.logger = logger;
@ -91,6 +112,16 @@ final class InferenceContext {
addVarImpl(p);
}
if (addPrimaryBound) {
addPrimaryBounds();
}
}
/**
* Add the primary bounds for the ivars of this context. This is usually done upon construction but may be deferred
* in some scenarios (inference of ground target type of an explicitly typed lambda).
*/
void addPrimaryBounds() {
for (InferenceVar ivar : inferenceVars) {
addPrimaryBound(ivar);
}
@ -121,7 +152,9 @@ final class InferenceContext {
}
}
/** Add a variable to this context. */
/**
* Add a variable to this context.
*/
InferenceVar addVar(JTypeVar tvar) {
InferenceVar ivar = addVarImpl(tvar);
addPrimaryBound(ivar);
@ -133,7 +166,9 @@ final class InferenceContext {
return ivar;
}
/** Add a variable to this context. */
/**
* Add a variable to this context.
*/
private InferenceVar addVarImpl(@NonNull JTypeVar tvar) {
InferenceVar ivar = new InferenceVar(this, tvar, varId++);
freeVars.add(ivar);

View File

@ -74,8 +74,4 @@ class O {
*/
/* TODO test explicitly typed lambda (in ExplicitTypesTest)
wildcard parameterization inference is not implemented yet.
*/

View File

@ -4,19 +4,17 @@
package net.sourceforge.pmd.lang.java.types.internal.infer
import io.kotest.matchers.shouldBe
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression
import net.sourceforge.pmd.lang.test.ast.shouldBeA
import net.sourceforge.pmd.lang.java.ast.ASTMethodCall
import net.sourceforge.pmd.lang.java.ast.ProcessorTestSpec
import net.sourceforge.pmd.lang.java.types.firstMethodCall
import net.sourceforge.pmd.lang.java.types.parseWithTypeInferenceSpy
import net.sourceforge.pmd.lang.java.types.shouldHaveType
import net.sourceforge.pmd.lang.java.types.shouldMatchMethod
import net.sourceforge.pmd.lang.java.types.*
import java.util.*
class ExplicitTypesTest : ProcessorTestSpec({
// todo test explicitly typed lambda
// todo test explicit type args on ctor call
@ -54,4 +52,65 @@ class ExplicitTypesTest : ProcessorTestSpec({
}
}
parserTest("Explicitly typed lambda") {
val (acu, spy) = parser.parseWithTypeInferenceSpy("""
interface Function<U,V> {
V apply(U u);
}
interface Comparable {}
interface Comparator<T> {
static <U, X extends Comparable> Comparator<U> comparing(Function<? super U, ? extends X> fun) {}
}
interface Foo extends Comparable { Foo foo(); }
class NodeStream {
static {
Comparator<Foo> cmp = Comparator.comparing((Foo s) -> s.foo());
}
}
""")
val (t_Function, _, _, t_Foo) = acu.declaredTypeSignatures()
val (lambda) = acu.descendants(ASTLambdaExpression::class.java).crossFindBoundaries().toList()
val call = acu.firstMethodCall()
spy.shouldBeOk {
lambda shouldHaveType t_Function[t_Foo, t_Foo]
call.overloadSelectionInfo.isFailed shouldBe false
}
}
parserTest("Explicitly typed lambda with wildcard") {
val (acu, spy) = parser.parseWithTypeInferenceSpy("""
interface Number {}
interface Integer extends Number {}
interface Predicate<T> { boolean test(T t); }
class NodeStream {
static {
// note that the lambda is inferred to Predicate<Number> not Predicate<? super Integer> or something
Predicate<? super Integer> p = (Number n) -> n.equals(23);
}
}
""")
val (t_Number, _, t_Predicate) = acu.declaredTypeSignatures()
val (lambda) = acu.descendants(ASTLambdaExpression::class.java).crossFindBoundaries().toList()
spy.shouldBeOk {
lambda shouldHaveType t_Predicate[t_Number]
}
}
})