diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/JClassType.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/JClassType.java index 369b20e682..f5fe5d5124 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/JClassType.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/JClassType.java @@ -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. diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/ExprCheckHelper.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/ExprCheckHelper.java index 148a542403..c5c20caec9 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/ExprCheckHelper.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/ExprCheckHelper.java @@ -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 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 p = (Number n) -> n.equals(23); + // > The lambda expression is a Predicate, which is a subtype of Predicate but not + // > Predicate. The analysis in this section is used to infer that Number is an appropriate choice + // > for the type argument to Predicate. + + // Let `targetType = F` + // Let `'a1, ..., 'am` be fresh inference variables. + JClassType targetGTD = targetType.getGenericTypeDeclaration(); + List 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 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 typeArgs = targetType.getTypeArgs(); + List 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 { diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/Infer.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/Infer.java index 69498d0087..595bf77921 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/Infer.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/Infer.java @@ -121,7 +121,11 @@ public final class Infer { } InferenceContext newContextFor(List tvars) { - return new InferenceContext(ts, supertypeCheckCache, tvars, LOG); + return newContextFor(tvars, true); + } + + InferenceContext newContextFor(List tvars, boolean addPrimaryBound) { + return new InferenceContext(ts, supertypeCheckCache, tvars, LOG, addPrimaryBound); } /** diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceContext.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceContext.java index 432906d097..4a979dec0e 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceContext.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/internal/infer/InferenceContext.java @@ -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 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 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); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/package-info.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/package-info.java index 47f7cff1d4..86e48aaf16 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/package-info.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/package-info.java @@ -74,8 +74,4 @@ class O { */ -/* TODO test explicitly typed lambda (in ExplicitTypesTest) - wildcard parameterization inference is not implemented yet. - */ - diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/ExplicitTypesTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/ExplicitTypesTest.kt index 2abcd43a66..85e44cbd22 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/ExplicitTypesTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/internal/infer/ExplicitTypesTest.kt @@ -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 { + V apply(U u); +} +interface Comparable {} +interface Comparator { + static Comparator comparing(Function fun) {} +} +interface Foo extends Comparable { Foo foo(); } + +class NodeStream { + static { + Comparator 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 { boolean test(T t); } + +class NodeStream { + static { + // note that the lambda is inferred to Predicate not Predicate or something + Predicate 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] + } + } + + })