• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 725

14 Aug 2026 02:59PM UTC coverage: 79.308% (-0.003%) from 79.311%
725

push

github

web-flow
[java] Fix #5441: Resolve interdependent inference variables simultaneously (#6934)

* [java] Fix #5441: Resolve interdependent inference variables simultaneously

UseDiamondOperator reported a false positive on code where javac cannot
infer the type arguments, so following the suggestion did not compile:

    Cache<String, List<Integer>> cache =
        provider.<String, List<Integer>>newCacheBuilder("test")
                .buildAsync(new ConcreteCacheLoader<String, List<Integer>>() { ... })
                .synchronous();

The rule itself was fine; the underlying type inference was wrong. PMD
inferred the diamond as ConcreteCacheLoader<String, List<Integer>>, which
is identical to the explicit type arguments, so the rule concluded they
were redundant. javac infers ConcreteCacheLoader<Object, List<Integer>>
instead, which is why replacing the type arguments with a diamond breaks
compilation ("does not override abstract method fetch(Object)").

Cause: VarWalkStrategy merges strongly connected components of the ivar
dependency graph into a single batch, precisely so that mutually dependent
variables are solved together. But solveBatchProgressed instantiated only
the first acceptable variable of the batch and returned, so incorporation
ran again before the rest of the batch was solved.

That violates JLS 18.4, which requires the instantiations of such a batch
to all be *computed* from the same bound set and only then be incorporated.
The distinction matters because the resolution rules may only use proper
bounds, i.e. bounds that do not mention an inference variable. For the
snippet above the relevant context is

    'a { 'a <: String, 'a <: 'e }     # K1 of buildAsync
    'e { 'e <: Object, 'e >: 'a }     # K of the diamond

Solved sequentially, 'a := String by rule UPPER; incorporation then turns
'e >: 'a into the proper bound 'e >: String, rule LOWER applies and
'e := String. Solved simultaneously, 'e has no proper lower bound (its only
lower bound ... (continued)

19521 of 25584 branches covered (76.3%)

Branch coverage included in aggregate %.

12 of 12 new or added lines in 2 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

42287 of 52350 relevant lines covered (80.78%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.74
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/JIntersectionType.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5

6
package net.sourceforge.pmd.lang.java.types;
7

8
import java.util.Collection;
9
import java.util.Collections;
10
import java.util.List;
11
import java.util.Objects;
12
import java.util.function.Function;
13
import java.util.function.Predicate;
14
import java.util.stream.Collectors;
15
import java.util.stream.Stream;
16

17
import org.checkerframework.checker.nullness.qual.NonNull;
18
import org.checkerframework.checker.nullness.qual.Nullable;
19
import org.pcollections.HashTreePSet;
20
import org.pcollections.PSet;
21

22
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
23
import net.sourceforge.pmd.lang.java.symbols.JMethodSymbol;
24
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
25
import net.sourceforge.pmd.lang.java.symbols.SymbolicValue.SymAnnot;
26
import net.sourceforge.pmd.util.CollectionUtil;
27

28
/**
29
 * An intersection type. Intersections type act as the
30
 * {@linkplain TypeSystem#glb(Collection) greatest lower bound}
31
 * for a set of types.
32
 *
33
 * <p>https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.9
34
 */
35
@SuppressWarnings("PMD.CompareObjectsWithEquals")
1✔
36
public final class JIntersectionType implements JTypeMirror {
37

38

39
    private final TypeSystem ts;
40
    private final JTypeMirror primaryBound;
41
    private final List<JTypeMirror> components;
42
    private JClassType induced;
43

44
    /**
45
     * @param primaryBound may be Object if every bound is an interface
46
     * @param allBounds    including the superclass, unless Object
47
     */
48
    JIntersectionType(TypeSystem ts,
49
                      JTypeMirror primaryBound,
50
                      List<? extends JTypeMirror> allBounds) {
1✔
51
        this.primaryBound = primaryBound;
1✔
52
        this.components = Collections.unmodifiableList(allBounds);
1✔
53
        this.ts = ts;
1✔
54

55
        assert Lub.isExclusiveIntersectionBound(primaryBound)
1!
56
            : "Wrong primary intersection bound: " + toString(primaryBound, allBounds);
×
57
        assert primaryBound != ts.OBJECT || allBounds.size() > 1
1!
58
            : "Intersection of a single bound: " + toString(primaryBound, allBounds); // should be caught by GLB
×
59

60
        checkWellFormed(primaryBound, allBounds);
1✔
61

62
    }
1✔
63

64
    @Override
65
    public PSet<SymAnnot> getTypeAnnotations() {
66
        return HashTreePSet.empty();
1✔
67
    }
68

69
    @Override
70
    public JTypeMirror withAnnotations(PSet<SymAnnot> newTypeAnnots) {
71
        return new JIntersectionType(
×
72
            ts,
73
            primaryBound.withAnnotations(newTypeAnnots),
×
74
            CollectionUtil.map(components, c -> c.withAnnotations(newTypeAnnots))
×
75
        );
76
    }
77

78
    /**
79
     * Returns the list of components. Their erasure must be pairwise disjoint.
80
     * If the intersection's superclass is {@link TypeSystem#OBJECT},
81
     * then it is excluded from this set.
82
     */
83
    public List<JTypeMirror> getComponents() {
84
        return components;
1✔
85
    }
86

87

88
    /**
89
     * The primary bound of this intersection, which may be a type variable,
90
     * array type, or class type (not an interface). If all bounds are interfaces,
91
     * then this returns {@link TypeSystem#OBJECT}.
92
     */
93
    public @NonNull JTypeMirror getPrimaryBound() {
94
        return primaryBound;
1✔
95
    }
96

97

98
    /**
99
     * Returns all additional bounds on the primary bound, which are
100
     * necessarily interface types.
101
     */
102
    @SuppressWarnings({"unchecked", "rawtypes"}) // safe because of checkWellFormed
103
    public @NonNull List<JClassType> getInterfaces() {
104
        return (List) (primaryBound == ts.OBJECT ? components
1✔
105
                                                 : components.subList(1, components.size()));
1✔
106
    }
107

108
    /**
109
     * Every intersection type induces a notional class or interface
110
     * for the purpose of identifying its members. This may be a functional
111
     * interface. This returns null for the non-implemented cases.
112
     *
113
     * <p>This is only relevant to check for functional
114
     * interface parameterization, eg {@code Runnable & Serializable}. Do
115
     * not use this to find out the members of this type, rather, use {@link #streamMethods(Predicate)}
116
     * or so.
117
     */
118
    public @Nullable JClassType getInducedClassType() {
119
        JTypeMirror primary = getPrimaryBound();
1✔
120
        if (primary instanceof JTypeVar || primary instanceof JArrayType) {
1!
121
            // Normally, should generate an interface which has all the members of Ti
122
            // But as per the experimental notice, this case may be ignored until needed
123
            return null;
1✔
124
        }
125

126
        if (induced == null) {
1!
127
            JClassSymbol sym = new FakeIntersectionSymbol("", (JClassType) primary, getInterfaces());
1✔
128
            this.induced = (JClassType) ts.declaration(sym);
1✔
129
        }
130
        return induced;
1✔
131
    }
132

133
    @Override
134
    public <T, P> T acceptVisitor(JTypeVisitor<T, P> visitor, P p) {
135
        return visitor.visitIntersection(this, p);
1✔
136
    }
137

138

139
    @Override
140
    public Stream<JMethodSig> streamMethods(Predicate<? super JMethodSymbol> prefilter) {
141
        return getComponents().stream().flatMap(it -> it.streamMethods(prefilter));
1✔
142
    }
143

144
    @Override
145
    public Stream<JTypeMirror> streamClasses() {
146
        return getComponents().stream().flatMap(it -> it.streamClasses());
×
147
    }
148

149
    @Override
150
    public JIntersectionType subst(Function<? super SubstVar, ? extends @NonNull JTypeMirror> subst) {
151
        JTypeMirror newPrimary = primaryBound.subst(subst);
1✔
152
        List<JClassType> myItfs = getInterfaces();
1✔
153
        List<JClassType> newBounds = TypeOps.substClasses(myItfs, subst);
1✔
154
        return newPrimary == getPrimaryBound() && newBounds == myItfs
1!
155
               ? this
1✔
156
               : new JIntersectionType(ts, newPrimary, newBounds);
×
157
    }
158

159
    @Override
160
    public @Nullable JTypeDeclSymbol getSymbol() {
UNCOV
161
        return null; // the induced type may have a symbol though
×
162
    }
163

164
    @Override
165
    public TypeSystem getTypeSystem() {
166
        return ts;
1✔
167
    }
168

169

170
    @Override
171
    public JTypeMirror getErasure() {
172
        return getPrimaryBound().getErasure();
1✔
173
    }
174

175
    @Override
176
    public String toString() {
177
        return TypePrettyPrint.prettyPrint(this);
1✔
178
    }
179

180
    @Override
181
    public boolean equals(Object o) {
182
        if (this == o) {
1!
183
            return true;
×
184
        }
185
        if (!(o instanceof JIntersectionType)) {
1✔
186
            return false;
1✔
187
        }
188
        JIntersectionType that = (JIntersectionType) o;
1✔
189
        return TypeOps.isSameType(this, that);
1✔
190
    }
191

192
    @Override
193
    public int hashCode() {
194
        return Objects.hash(components);
1✔
195
    }
196

197
    private static void checkWellFormed(JTypeMirror primary, List<? extends JTypeMirror> flattened) {
198
        assert flattened.get(0) == primary || primary == primary.getTypeSystem().OBJECT
1!
199
            : "Not a well-formed intersection " + flattened;
200
        for (int i = 0; i < flattened.size(); i++) {
1✔
201
            JTypeMirror ci = flattened.get(i);
1✔
202
            Objects.requireNonNull(ci, "Null intersection component");
1✔
203
            if (Lub.isExclusiveIntersectionBound(ci)) {
1✔
204
                if (i != 0) {
1!
205
                    throw malformedIntersection(primary, flattened);
×
206
                }
207
            } else if (ci instanceof JClassType) {
1!
208
                // must be an interface, as per isExclusiveBlabla
209
                assert ci.isInterface() || TypeOps.hasUnresolvedSymbol(ci);
1!
210
            } else {
211
                throw malformedIntersection(primary, flattened);
×
212
            }
213
        }
214
    }
1✔
215

216
    private static RuntimeException malformedIntersection(JTypeMirror primary, List<? extends JTypeMirror> flattened) {
217
        return new IllegalArgumentException(
×
218
            "Malformed intersection: " + toString(primary, flattened)
×
219
        );
220
    }
221

222
    private static String toString(JTypeMirror primary, List<? extends JTypeMirror> flattened) {
223
        return flattened.stream().map(JTypeMirror::toString).collect(Collectors.joining(" & ",
×
224
                                                                                        primary.toString() + " & ",
×
225
                                                                                        ""));
226
    }
227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc