• 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

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

5
package net.sourceforge.pmd.lang.java.types.internal.infer;
6

7

8
import static net.sourceforge.pmd.lang.java.types.TypeOps.asList;
9
import static net.sourceforge.pmd.util.CollectionUtil.intersect;
10

11
import java.util.ArrayDeque;
12
import java.util.Collection;
13
import java.util.Collections;
14
import java.util.Deque;
15
import java.util.HashMap;
16
import java.util.HashSet;
17
import java.util.LinkedHashMap;
18
import java.util.LinkedHashSet;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Map.Entry;
22
import java.util.Set;
23
import java.util.function.Function;
24
import java.util.function.Supplier;
25

26
import org.checkerframework.checker.nullness.qual.NonNull;
27
import org.checkerframework.checker.nullness.qual.Nullable;
28

29
import net.sourceforge.pmd.lang.java.types.JClassType;
30
import net.sourceforge.pmd.lang.java.types.JMethodSig;
31
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
32
import net.sourceforge.pmd.lang.java.types.JTypeVar;
33
import net.sourceforge.pmd.lang.java.types.JTypeVisitable;
34
import net.sourceforge.pmd.lang.java.types.SubstVar;
35
import net.sourceforge.pmd.lang.java.types.Substitution;
36
import net.sourceforge.pmd.lang.java.types.TypeOps;
37
import net.sourceforge.pmd.lang.java.types.TypeSystem;
38
import net.sourceforge.pmd.lang.java.types.internal.infer.ExprMirror.InvocationMirror.MethodCtDecl;
39
import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.CheckBound;
40
import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.PropagateAllBounds;
41
import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.PropagateBounds;
42
import net.sourceforge.pmd.lang.java.types.internal.infer.IncorporationAction.SubstituteInst;
43
import net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar.BoundKind;
44
import net.sourceforge.pmd.lang.java.types.internal.infer.VarWalkStrategy.GraphWalk;
45
import net.sourceforge.pmd.util.CollectionUtil;
46

47
/**
48
 * Context of a type inference process. This object maintains a set of
49
 * unique inference variables. Inference variables maintain the set of
50
 * bounds that apply to them.
51
 */
52
final class InferenceContext {
53

54
    // ivar/ctx ids are globally unique, & repeatable in debug output if you do exactly the same run
55
    private static int varId = 0;
1✔
56
    private static int ctxId = 0;
1✔
57

58
    private final Map<InstantiationListener, Set<InferenceVar>> instantiationListeners = new HashMap<>();
1✔
59
    // explicit dependencies between variables for graph building
60
    private final Map<InferenceVar, Set<InferenceVar>> instantiationConstraints = new HashMap<>();
1✔
61
    // This flag is set to true when the explicit dependencies are changed,
62
    // or when this context adopted new ivars. This means we should interrupt
63
    // resolution and recompute the dependency graph between ivars, because
64
    // the new variables may have dependencies on existing variables, and vice versa.
65
    private boolean graphWasChanged = false;
1✔
66

67
    private final Set<InferenceVar> freeVars = new LinkedHashSet<>();
1✔
68
    private final Set<InferenceVar> inferenceVars = new LinkedHashSet<>();
1✔
69
    private final Deque<IncorporationAction> incorporationActions = new ArrayDeque<>();
1✔
70
    final TypeSystem ts;
71
    private final SupertypeCheckCache supertypeCheckCache;
72
    final TypeInferenceLogger logger;
73

74
    private Substitution mapping = Substitution.EMPTY;
1✔
75
    private @Nullable InferenceContext parent;
76
    private boolean needsUncheckedConversion;
77
    private final int id;
78

79
    /**
80
     * Create an inference context from a set of type variables to instantiate.
81
     * This creates inference vars and adds the initial bounds as described in
82
     *
83
     * https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.1.3
84
     *
85
     * under the purple rectangle.
86
     *
87
     * @param ts                  The global type system
88
     * @param supertypeCheckCache Super type check cache, shared by all
89
     *                            inference runs in the same compilation unit
90
     *                            (stored in {@link Infer}).
91
     * @param tvars               Initial tvars which will be turned
92
     *                            into ivars
93
     * @param logger              Logger for events related to ivar bounds
94
     */
95
    InferenceContext(TypeSystem ts, SupertypeCheckCache supertypeCheckCache, List<JTypeVar> tvars, TypeInferenceLogger logger) {
96
        this(ts, supertypeCheckCache, tvars, logger, true);
1✔
97
    }
1✔
98

99
    /**
100
     * Create an inference context from a set of type variables to instantiate.
101
     * This creates inference vars and may add the initial bounds as described in
102
     *
103
     * https://docs.oracle.com/javase/specs/jls/se9/html/jls-18.html#jls-18.1.3
104
     *
105
     * under the purple rectangle.
106
     *
107
     * @param ts                  The global type system
108
     * @param supertypeCheckCache Super type check cache, shared by all
109
     *                            inference runs in the same compilation unit
110
     *                            (stored in {@link Infer}).
111
     * @param tvars               Initial tvars which will be turned
112
     *                            into ivars
113
     * @param logger              Logger for events related to ivar bounds
114
     * @param addPrimaryBound     Whether to add the primary bound of the vars.
115
     */
116
    @SuppressWarnings("PMD.AssignmentToNonFinalStatic") // ctxId
117
    InferenceContext(TypeSystem ts, SupertypeCheckCache supertypeCheckCache, List<JTypeVar> tvars, TypeInferenceLogger logger, boolean addPrimaryBound) {
1✔
118
        this.ts = ts;
1✔
119
        this.supertypeCheckCache = supertypeCheckCache;
1✔
120
        this.logger = logger;
1✔
121
        this.id = ctxId++;
1✔
122

123
        for (JTypeVar p : tvars) {
1✔
124
            addVarImpl(p);
1✔
125
        }
1✔
126

127
        if (addPrimaryBound) {
1✔
128
            addPrimaryBounds();
1✔
129
        }
130
    }
1✔
131

132
    /**
133
     * Add the primary bounds for the ivars of this context. This is usually done upon construction but may be deferred
134
     * in some scenarios (inference of ground target type of an explicitly typed lambda).
135
     */
136
    void addPrimaryBounds() {
137
        for (InferenceVar ivar : inferenceVars) {
1✔
138
            addPrimaryBound(ivar);
1✔
139
        }
1✔
140
    }
1✔
141

142
    /**
143
     * Performs a shallow copy of this context, which would allow solving
144
     * the variables without executing listeners. Instantiation listeners
145
     * are not copied, and parent contexts are not copied.
146
     */
147
    public InferenceContext shallowCopy() {
148
        final InferenceContext copy = new InferenceContext(ts, supertypeCheckCache, Collections.emptyList(), logger);
1✔
149
        copy.freeVars.addAll(this.freeVars);
1✔
150
        copy.inferenceVars.addAll(this.inferenceVars);
1✔
151
        copy.incorporationActions.addAll(this.incorporationActions);
1✔
152
        copy.instantiationConstraints.putAll(this.instantiationConstraints);
1✔
153
        copy.mapping = mapping; // mapping is immutable, so we can share it safely
1✔
154

155
        return copy;
1✔
156
    }
157

158
    public int getId() {
159
        return id;
1✔
160
    }
161

162
    private void addPrimaryBound(InferenceVar ivar) {
163
        for (JTypeMirror ui : asList(ivar.getBaseVar().getUpperBound())) {
1✔
164
            ivar.addPrimaryBound(BoundKind.UPPER, mapToIVars(ui));
1✔
165
        }
1✔
166
    }
1✔
167

168
    /**
169
     * Add a variable to this context.
170
     */
171
    InferenceVar addVar(JTypeVar tvar) {
172
        InferenceVar ivar = addVarImpl(tvar);
1✔
173
        addPrimaryBound(ivar);
1✔
174

175
        for (InferenceVar otherIvar : inferenceVars) {
1✔
176
            // remove remaining occurrences of type params
177
            otherIvar.substBounds(this::mapToIVars);
1✔
178
        }
1✔
179
        return ivar;
1✔
180
    }
181

182
    /**
183
     * Add a variable to this context.
184
     */
185
    private InferenceVar addVarImpl(@NonNull JTypeVar tvar) {
186
        InferenceVar ivar = new InferenceVar(this, tvar, varId++);
1✔
187
        freeVars.add(ivar);
1✔
188
        inferenceVars.add(ivar);
1✔
189
        mapping = mapping.plus(tvar, ivar);
1✔
190
        return ivar;
1✔
191
    }
192

193
    /**
194
     * Replace all type variables in the given type with corresponding
195
     * inference vars.
196
     */
197
    JTypeMirror mapToIVars(JTypeMirror t) {
198
        return TypeOps.subst(t, mapping);
1✔
199
    }
200

201
    /**
202
     * Replace all type variables in the given type with corresponding
203
     * inference vars.
204
     */
205
    JMethodSig mapToIVars(JMethodSig t) {
206
        return t.subst(mapping);
1✔
207
    }
208

209
    /**
210
     * Returns true if the type mentions no free inference variables.
211
     * This is what the JLS calls a "proper type".
212
     */
213
    boolean isGround(JTypeVisitable t) {
214
        return !TypeOps.mentionsAny(t, freeVars);
1✔
215
    }
216

217
    /**
218
     * Returns true if the type mentions no free inference variables.
219
     */
220
    boolean areAllGround(Collection<? extends JTypeVisitable> ts) {
221
        for (JTypeVisitable t : ts) {
1✔
222
            if (!isGround(t)) {
1!
UNCOV
223
                return false;
×
224
            }
225
        }
1✔
226
        return true;
1✔
227
    }
228

229
    Set<InferenceVar> freeVarsIn(Iterable<? extends JTypeVisitable> types) {
230
        Set<InferenceVar> vars = new LinkedHashSet<>();
1✔
231
        for (InferenceVar ivar : freeVars) {
1✔
232
            for (JTypeVisitable t : types) {
1✔
233
                if (TypeOps.mentions(t, ivar)) {
1✔
234
                    vars.add(ivar);
1✔
235
                }
236
            }
1✔
237
        }
1✔
238
        return vars;
1✔
239
    }
240

241
    Set<InferenceVar> freeVarsIn(JTypeVisitable t) {
242
        return freeVarsIn(Collections.singleton(t));
1✔
243
    }
244

245
    /**
246
     * Replace instantiated inference vars with their instantiation in the given type.
247
     */
248
    JTypeMirror ground(JTypeMirror t) {
249
        return t.subst(InferenceContext::groundSubst);
1✔
250
    }
251

252
    JClassType ground(JClassType t) {
253
        return t.subst(InferenceContext::groundSubst);
1✔
254
    }
255

256
    /**
257
     * Replace instantiated inference vars with their instantiation in the given type.
258
     */
259
    JMethodSig ground(JMethodSig t) {
260
        return t.subst(InferenceContext::groundSubst);
1✔
261
    }
262

263
    void setNeedsUncheckedConversion() {
264
        this.needsUncheckedConversion = true;
1✔
265
    }
1✔
266

267
    /**
268
     * Whether incorporation/solving required an unchecked conversion.
269
     * This means the invocation type of the overload must be erased.
270
     *
271
     * @see MethodCtDecl#needsUncheckedConversion()
272
     */
273
    boolean needsUncheckedConversion() {
274
        return this.needsUncheckedConversion;
1✔
275
    }
276

277
    SupertypeCheckCache getSupertypeCheckCache() {
278
        return supertypeCheckCache;
1✔
279
    }
280

281
    private static JTypeMirror groundSubst(SubstVar var) {
282
        if (var instanceof InferenceVar) {
1✔
283
            JTypeMirror inst = ((InferenceVar) var).getInst();
1✔
284
            if (inst != null) {
1✔
285
                return inst;
1✔
286
            }
287
        }
288
        return var;
1✔
289
    }
290

291
    /**
292
     * Replace instantiated inference vars with their instantiation in the given type,
293
     * or else replace them with a failed type.
294
     */
295
    static JMethodSig finalGround(JMethodSig t) {
296
        return t.subst(finalGroundSubst());
1✔
297
    }
298

299
    public static Function<SubstVar, JTypeMirror> finalGroundSubst() {
300
        return s -> {
1✔
301
            if (!(s instanceof InferenceVar)) {
1✔
302
                return s;
1✔
303
            } else {
304
                InferenceVar ivar = (InferenceVar) s;
1✔
305
                return ivar.getInst() != null ? ivar.getInst() : s.getTypeSystem().ERROR;
1✔
306
            }
307
        };
308
    }
309

310

311
    /**
312
     * Replace instantiated inference vars with their instantiation in the given type,
313
     * or else replace them with a wildcard.
314
     */
315
    static JTypeMirror groundOrWildcard(JTypeMirror t) {
316
        return t.subst(s -> {
1✔
317
            if (!(s instanceof InferenceVar)) {
1!
318
                return s;
×
319
            } else {
320
                InferenceVar ivar = (InferenceVar) s;
1✔
321
                return ivar.getInst() != null ? ivar.getInst() : s.getTypeSystem().UNBOUNDED_WILD;
1✔
322
            }
323
        });
324
    }
325

326
    /**
327
     * Copy variable in this inference context to the given context
328
     */
329
    void duplicateInto(final InferenceContext that) {
330
        boolean changedGraph = !that.freeVars.containsAll(this.freeVars)
1!
331
            || !this.instantiationConstraints.isEmpty();
1!
332
        that.graphWasChanged |= changedGraph;
1✔
333
        that.inferenceVars.addAll(this.inferenceVars);
1✔
334
        that.freeVars.addAll(this.freeVars);
1✔
335
        that.incorporationActions.addAll(this.incorporationActions);
1✔
336
        that.instantiationListeners.putAll(this.instantiationListeners);
1✔
337
        CollectionUtil.mergeMaps(
1✔
338
            that.instantiationConstraints,
339
            this.instantiationConstraints,
340
            (set1, set2) -> {
341
                set1.addAll(set2);
×
342
                return set1;
×
343
            });
344

345
        this.parent = that;
1✔
346

347
        // propagate existing bounds into the new context
348
        for (InferenceVar freeVar : this.freeVars) {
1✔
349
            that.incorporationActions.add(new PropagateAllBounds(freeVar));
1✔
350
        }
1✔
351
    }
1✔
352

353

354
    // The `from` ivars depend on the `dependencies` ivars for resolution.
355
    void addInstantiationDependencies(Set<? extends InferenceVar> from, Set<? extends InferenceVar> dependencies) {
356
        if (from.isEmpty()) {
1✔
357
            return;
1✔
358
        }
359
        Set<InferenceVar> outputVars = new HashSet<>(dependencies);
1✔
360
        outputVars.removeAll(from);
1✔
361
        if (outputVars.isEmpty()) {
1✔
362
            return;
1✔
363
        }
364
        for (InferenceVar inputVar : from) {
1✔
365
            logger.ivarDependencyRegistered(this, inputVar, outputVars);
1✔
366
            instantiationConstraints.merge(inputVar, outputVars, (o1, o2) -> {
1✔
367
                o2 = new LinkedHashSet<>(o2);
1✔
368
                o2.addAll(o1);
1✔
369
                return o2;
1✔
370
            });
371
        }
1✔
372
    }
1✔
373

374
    Map<InferenceVar, Set<InferenceVar>> getInstantiationDependencies() {
375
        return instantiationConstraints;
1✔
376
    }
377

378
    void addInstantiationListener(Set<? extends JTypeMirror> relevantTypes, InstantiationListener listener) {
379
        Set<InferenceVar> free = freeVarsIn(relevantTypes);
1✔
380
        if (free.isEmpty()) {
1✔
381
            listener.onInstantiation(this);
1✔
382
            return;
1✔
383
        }
384
        instantiationListeners.put(listener, free);
1✔
385
    }
1✔
386

387
    /**
388
     * Call the listeners registered with {@link #addInstantiationListener(Set, InstantiationListener)}.
389
     * Listeners are used to perform deferred checks, like checking
390
     * compatibility of a formal parameter with an expression when the
391
     * formal parameter is not ground.
392
     */
393
    void callListeners() {
394
        if (instantiationListeners.isEmpty()) {
1✔
395
            return;
1✔
396
        }
397
        Set<InferenceVar> solved = new LinkedHashSet<>(inferenceVars);
1✔
398
        solved.removeAll(freeVars);
1✔
399

400

401
        for (Entry<InstantiationListener, Set<InferenceVar>> entry : new LinkedHashSet<>(instantiationListeners.entrySet())) {
1✔
402
            if (solved.containsAll(entry.getValue())) {
1✔
403
                try {
404
                    entry.getKey().onInstantiation(this);
1✔
405
                } catch (ResolutionFailedException ignored) {
1✔
406
                    // that is a compile-time error, but that
407
                    // shouldn't affect PMD
408

409
                    // This can happen eg when an assertion fails in a
410
                    // subcontext that depends on this one, which is waiting
411
                    // for more inference to happen
412

413
                    // TODO investigate
414
                } catch (Exception e) {
×
415
                    e.printStackTrace();
×
416
                } finally {
417
                    instantiationListeners.remove(entry.getKey());
1✔
418
                }
419
            }
420
        }
1✔
421
    }
1✔
422

423
    Set<InferenceVar> getFreeVars() {
424
        return Collections.unmodifiableSet(freeVars);
1✔
425
    }
426

427
    private void onVarInstantiated(InferenceVar ivar) {
428
        if (parent != null) {
1!
429
            parent.onVarInstantiated(ivar);
×
430
            return;
×
431
        }
432

433
        logger.ivarInstantiated(this, ivar, ivar.getInst());
1✔
434

435
        incorporationActions.addFirst(new SubstituteInst(ivar, ivar.getInst()) {
1✔
436
            @Override
437
            public void apply(InferenceContext ctx) {
438
                freeVars.removeIf(it -> it.getInst() != null);
1✔
439
                super.apply(ctx);
1✔
440
            }
1✔
441
        });
442
    }
1✔
443

444

445
    void onBoundAdded(InferenceVar ivar, BoundKind kind, JTypeMirror bound, boolean isPrimary) {
446
        // guard against α <: Object
447
        // all variables have it, it's useless to propagate it
448
        if (kind != BoundKind.UPPER || bound != ts.OBJECT) {
1✔
449
            if (parent != null) {
1✔
450
                parent.onBoundAdded(ivar, kind, bound, isPrimary);
1✔
451
                return;
1✔
452
            }
453

454
            logger.boundAdded(this, ivar, kind, bound, isPrimary);
1✔
455

456
            incorporationActions.add(new CheckBound(ivar, kind, bound));
1✔
457
            incorporationActions.add(new PropagateBounds(ivar, kind, bound));
1✔
458
        }
459
    }
1✔
460

461
    void onIvarMerged(InferenceVar prev, InferenceVar delegate) {
462
        if (parent != null) {
1✔
463
            parent.onIvarMerged(prev, delegate);
1✔
464
            return;
1✔
465
        }
466

467
        logger.ivarMerged(this, prev, delegate);
1✔
468

469
        mapping = mapping.plus(prev.getBaseVar(), delegate);
1✔
470
        incorporationActions.addFirst(new SubstituteInst(prev, delegate));
1✔
471
    }
1✔
472

473
    /**
474
     * Runs the incorporation hooks registered for the free vars.
475
     *
476
     * @throws ResolutionFailedException If some propagated bounds are incompatible
477
     */
478
    void incorporate() {
479
        if (incorporationActions.isEmpty()) {
1✔
480
            return;
1✔
481
        }
482

483
        IncorporationAction hook = incorporationActions.pollFirst();
1✔
484
        while (hook != null) {
1✔
485

486
            if (hook.doApplyToInstVar || hook.ivar.getInst() == null) {
1✔
487
                hook.apply(this);
1✔
488
            }
489

490
            hook = incorporationActions.pollFirst();
1✔
491
        }
492
    }
1✔
493

494
    /**
495
     * @throws ResolutionFailedException Because it calls {@link #incorporate()}
496
     */
497
    void solve() {
498
        solve(false);
1✔
499
    }
1✔
500

501
    boolean solve(boolean onlyBoundedVars) {
502
        return solve(() -> new GraphWalk(this, onlyBoundedVars));
1✔
503
    }
504

505
    /**
506
     * Solve a single var, this does not solve its dependencies, so that
507
     * if some bounds are not ground, instantiation will be wrong.
508
     */
509
    void solve(InferenceVar var) {
510
        solve(new GraphWalk(var));
×
511
    }
×
512

513

514
    private boolean solve(Supplier<VarWalkStrategy> newWalker) {
515
        VarWalkStrategy strategy = newWalker.get();
1✔
516
        while (strategy != null) {
1!
517
            if (solve(strategy)) {
1✔
518
                break;
1✔
519
            }
520
            strategy = newWalker.get();
1✔
521
        }
522
        return freeVars.isEmpty();
1✔
523
    }
524

525

526
    /**
527
     * This returns true if solving the VarWalkStrategy succeeded entirely.
528
     * Resolution can be interrupted early to account for new ivars and dependencies,
529
     * which may change the graph dependencies. In this case this method returns
530
     * false, we recompute the graph with the new ivars and dependencies, and
531
     * we try again to make progress.
532
     */
533
    private boolean solve(VarWalkStrategy walker) {
534
        graphWasChanged = false;
1✔
535
        incorporate();
1✔
536

537
        while (walker.hasNext()) {
1✔
538

539
            Set<InferenceVar> varsToSolve = walker.next();
1✔
540

541
            boolean progress = true;
1✔
542
            //repeat until all variables are solved
543
            outer:
544
            while (!intersect(freeVars, varsToSolve).isEmpty() && progress) {
1✔
545
                if (graphWasChanged) {
1✔
546
                    graphWasChanged = false;
1✔
547
                    logger.contextDependenciesChanged(this);
1✔
548
                    return false;
1✔
549
                }
550

551
                progress = false;
1✔
552
                for (List<ReductionStep> wave : ReductionStep.WAVES) {
1✔
553
                    if (solveBatchProgressed(varsToSolve, wave)) {
1✔
554
                        incorporate();
1✔
555
                        progress = true;
1✔
556
                        callListeners();
1✔
557
                        continue outer;
1✔
558
                    }
559
                }
1✔
560
            }
561
        }
1✔
562
        return true;
1✔
563
    }
564

565
    /**
566
     * Tries to solve as much of varsToSolve as possible using some reduction steps.
567
     * Returns true if at least one variable was instantiated.
568
     */
569
    private boolean solveBatchProgressed(Set<InferenceVar> varsToSolve, List<ReductionStep> wave) {
570
        // The variables of a batch are interdependent, so the JLS (18.4)
571
        // mandates that their instantiations all be *computed* from the same
572
        // bound set, and only then incorporated. Solving them one after the
573
        // other would be incorrect: instantiating the first variable may turn
574
        // an improper bound of the second one (a bound mentioning an inference
575
        // variable) into a proper one, and the resolution rules would then use
576
        // that bound instead of the one the JLS prescribes.
577
        Map<InferenceVar, JTypeMirror> instantiations = new LinkedHashMap<>();
1✔
578
        for (InferenceVar ivar : intersect(varsToSolve, freeVars)) {
1✔
579
            for (ReductionStep step : wave) {
1✔
580
                if (step.accepts(ivar, this)) {
1✔
581
                    instantiations.put(ivar, step.solve(ivar, this));
1✔
582
                    break;
1✔
583
                }
584
            }
1✔
585
        }
1✔
586

587
        if (instantiations.isEmpty()) {
1✔
588
            return false;
1✔
589
        }
590

591
        instantiations.forEach((ivar, inst) -> {
1✔
592
            ivar.setInst(inst);
1✔
593
            onVarInstantiated(ivar);
1✔
594
        });
1✔
595
        return true;
1✔
596
    }
597

598
    public boolean isEmpty() {
599
        return inferenceVars.isEmpty();
1✔
600
    }
601

602
    @Override
603
    public String toString() {
604
        StringBuilder sb = new StringBuilder("Inference context " + getId()).append('\n');
×
605
        for (InferenceVar ivar : inferenceVars) {
×
606
            sb.append(ivar);
×
607
            if (ivar.getInst() != null) {
×
608
                sb.append(" := ").append(ivar.getInst()).append('\n');
×
609
            } else {
610
                ivar.formatBounds(sb).append('\n');
×
611
            }
612
        }
×
613

614
        return sb.toString();
×
615
    }
616

617
    /** A callback called when a set of variables have been solved. */
618
    public interface InstantiationListener {
619

620
        /**
621
         * Called when the set of dependencies provided to {@link #addInstantiationListener(Set, InstantiationListener)}
622
         * have been solved. The parameter is not necessarily the context
623
         * on which this has been registered, because contexts adopt the
624
         * inference variables of their children in some cases, to solve
625
         * them together. Use {@link #ground(JClassType)} with the context
626
         * parameter, not the context on which the callback was registered.
627
         */
628
        void onInstantiation(InferenceContext solvedCtx);
629

630
    }
631
}
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