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

pmd / pmd / 415

27 Feb 2026 12:39PM UTC coverage: 79.038% (+0.03%) from 79.004%
415

push

github

adangel
[release] Prepare next development version

18604 of 24437 branches covered (76.13%)

Branch coverage included in aggregate %.

40598 of 50466 relevant lines covered (80.45%)

0.81 hits per line

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

94.74
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/ast/AstDisambiguationPass.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.ast;
7

8
import static net.sourceforge.pmd.lang.java.symbols.table.internal.JavaSemanticErrors.CANNOT_RESOLVE_AMBIGUOUS_NAME;
9

10
import java.util.Iterator;
11

12
import org.checkerframework.checker.nullness.qual.NonNull;
13
import org.checkerframework.checker.nullness.qual.Nullable;
14

15
import net.sourceforge.pmd.lang.ast.Node;
16
import net.sourceforge.pmd.lang.ast.NodeStream;
17
import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken;
18
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
19
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
20
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
21
import net.sourceforge.pmd.lang.java.symbols.table.JSymbolTable;
22
import net.sourceforge.pmd.lang.java.symbols.table.internal.JavaSemanticErrors;
23
import net.sourceforge.pmd.lang.java.symbols.table.internal.ReferenceCtx;
24
import net.sourceforge.pmd.lang.java.types.JClassType;
25
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
26
import net.sourceforge.pmd.lang.java.types.JVariableSig;
27
import net.sourceforge.pmd.lang.java.types.JVariableSig.FieldSig;
28
import net.sourceforge.pmd.lang.java.types.ast.internal.LazyTypeResolver;
29

30
/**
31
 * This implements name disambiguation following <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.2">JLS§6.5.2</a>.
32
 * (see also <a href="https://docs.oracle.com/javase/specs/jls/se13/html/jls-6.html#jls-6.4.2">JLS§6.4.2 - Obscuring</a>)
33
 */
34
final class AstDisambiguationPass {
1✔
35

36
    private AstDisambiguationPass() {
37
        // façade
38
    }
39

40
    /**
41
     * Disambiguate the subtrees rooted at the given nodes. After this:
42
     * <ul>
43
     * <li>All ClassOrInterfaceTypes either see their ambiguous LHS
44
     * promoted to a ClassOrInterfaceType, or demoted to a package
45
     * name (removed from the tree)
46
     * <li>All ClassOrInterfaceTypes have a non-null symbol, even if
47
     * it is unresolved EXCEPT the ones of a qualified constructor call.
48
     * Those references are resolved lazily by {@link LazyTypeResolver},
49
     * because they depend on the full type resolution of the qualifier
50
     * expression, and that resolution may use things that are not yet
51
     * disambiguated
52
     * <li>There may still be AmbiguousNames, but only in expressions,
53
     * for the worst kind of ambiguity
54
     * </ul>
55
     */
56
    public static void disambigWithCtx(NodeStream<? extends JavaNode> nodes, ReferenceCtx ctx) {
57
        assert ctx != null : "Null context";
1!
58
        nodes.forEach(it -> it.acceptVisitor(DisambigVisitor.INSTANCE, ctx));
1✔
59
    }
1✔
60

61
    static void retryDisambigWithCtx(NodeStream<? extends ASTAmbiguousName> nodes, ReferenceCtx ctx, JSymbolTable symbolTable) {
62
        assert ctx != null : "Null context";
1!
63
        nodes.forEach(it -> {
1✔
64
            it.allowReprocessing();
1✔
65
            it.setSymbolTable(symbolTable);
1✔
66
            it.acceptVisitor(DisambigVisitor.INSTANCE, ctx);
1✔
67
        });
1✔
68
    }
1✔
69

70
    // those ignore JTypeParameterSymbol, for error handling logic to be uniform
71

72

73
    private static void checkParentIsMember(ReferenceCtx ctx, ASTClassType resolvedType, ASTClassType parent) {
74
        JTypeDeclSymbol sym = resolvedType.getReferencedSym();
1✔
75
        JClassSymbol parentClass = ctx.findTypeMember(sym, parent.getSimpleName(), parent);
1✔
76
        if (parentClass == null) {
1✔
77
            ctx.reportUnresolvedMember(parent, ReferenceCtx.Fallback.TYPE, parent.getSimpleName(), sym);
1✔
78
            int numTypeArgs = ASTList.sizeOrZero(parent.getTypeArguments());
1✔
79
            parentClass = ctx.makeUnresolvedReference(sym, parent.getSimpleName(), numTypeArgs);
1✔
80
        }
81
        parent.setSymbol(parentClass);
1✔
82
    }
1✔
83

84
    private static @Nullable JClassType enclosingType(JTypeMirror typeResult) {
85
        return typeResult instanceof JClassType ? ((JClassType) typeResult).getEnclosingType() : null;
1✔
86
    }
87

88
    private static final class DisambigVisitor extends JavaVisitorBase<ReferenceCtx, Void> {
89

90
        public static final DisambigVisitor INSTANCE = new DisambigVisitor();
1✔
91

92

93
        @Override
94
        protected Void visitChildren(Node node, ReferenceCtx data) {
95
            // note that this differs from the default impl, because
96
            // the default declares last = node.getNumChildren()
97
            // at the beginning of the loop, but in this visitor the
98
            // number of children may change.
99
            for (int i = 0; i < node.getNumChildren(); i++) {
1✔
100
                node.getChild(i).acceptVisitor(this, data);
1✔
101
            }
102
            return null;
1✔
103
        }
104

105
        @Override
106
        public Void visitTypeDecl(ASTTypeDeclaration node, ReferenceCtx data) {
107
            // since type headers are disambiguated early it doesn't matter
108
            // if the context is inaccurate in type headers
109
            return visitChildren(node, data.scopeDownToNested(node.getSymbol()));
1✔
110
        }
111

112
        @Override
113
        public Void visit(ASTAmbiguousName name, ReferenceCtx processor) {
114
            if (name.wasProcessed()) {
1✔
115
                // don't redo analysis
116
                return null;
1✔
117
            }
118

119
            JSymbolTable symbolTable = name.getSymbolTable();
1✔
120
            assert symbolTable != null : "Symbol tables haven't been set yet??";
1!
121

122
            boolean isPackageOrTypeOnly;
123
            if (name.getParent() instanceof ASTClassType) {
1✔
124
                isPackageOrTypeOnly = true;
1✔
125
            } else if (name.getParent() instanceof ASTExpression) {
1!
126
                isPackageOrTypeOnly = false;
1✔
127
            } else {
128
                throw new AssertionError("Unrecognised context for ambiguous name: " + name.getParent());
×
129
            }
130

131
            // do resolve
132
            JavaNode resolved = startResolve(name, processor, isPackageOrTypeOnly);
1✔
133

134
            // finish
135
            assert !isPackageOrTypeOnly
1!
136
                || resolved instanceof ASTTypeExpression
137
                || resolved instanceof ASTAmbiguousName
138
                : "Unexpected result " + resolved + " for PackageOrTypeName resolution";
139

140
            if (isPackageOrTypeOnly && resolved instanceof ASTTypeExpression) {
1✔
141
                // unambiguous, we just have to check that the parent is a member of the enclosing type
142

143
                ASTClassType resolvedType = (ASTClassType) ((ASTTypeExpression) resolved).getTypeNode();
1✔
144
                resolved = resolvedType;
1✔
145
                ASTClassType parent = (ASTClassType) name.getParent();
1✔
146

147
                checkParentIsMember(processor, resolvedType, parent);
1✔
148
            }
149

150
            if (resolved != name) {
1✔
151
                InternalApiBridge.setSymbolTable(resolved, symbolTable);
1✔
152
                ((AbstractJavaNode) name.getParent()).setChild((AbstractJavaNode) resolved, name.getIndexInParent());
1✔
153
            }
154

155
            return null;
1✔
156
        }
157

158
        @Override
159
        public Void visit(ASTClassType type, ReferenceCtx ctx) {
160

161
            if (type.getReferencedSym() != null) {
1✔
162
                return null;
1✔
163
            }
164

165
            if (type.getFirstChild() instanceof ASTAmbiguousName) {
1✔
166
                type.getFirstChild().acceptVisitor(this, ctx);
1✔
167
            }
168

169
            // revisit children, which may have changed
170
            visitChildren(type, ctx);
1✔
171

172
            if (type.getReferencedSym() != null) {
1✔
173
                postProcess(type, ctx);
1✔
174
                return null;
1✔
175
            }
176

177
            ASTClassType lhsType = type.getQualifier();
1✔
178
            if (lhsType != null) {
1✔
179
                JTypeDeclSymbol lhsSym = lhsType.getReferencedSym();
1✔
180
                assert lhsSym != null : "Unresolved LHS for " + type;
1!
181
                checkParentIsMember(ctx, lhsType, type);
1✔
182
            } else {
1✔
183
                if (type.getParent() instanceof ASTConstructorCall
1✔
184
                    && ((ASTConstructorCall) type.getParent()).isQualifiedInstanceCreation()) {
1✔
185
                    // Leave the reference null, this is handled lazily,
186
                    // because the interaction it depends on the type of
187
                    // the qualifier
188
                    return null;
1✔
189
                }
190

191
                if (type.getReferencedSym() == null) {
1!
192
                    setClassSymbolIfNoQualifier(type, ctx);
1✔
193
                }
194
            }
195

196
            assert type.getReferencedSym() != null : "Null symbol for " + type;
1!
197

198
            postProcess(type, ctx);
1✔
199
            return null;
1✔
200
        }
201

202
        private static void setClassSymbolIfNoQualifier(ASTClassType type, ReferenceCtx ctx) {
203
            final JTypeMirror resolved = ctx.resolveSingleTypeName(type.getSymbolTable(), type.getSimpleName(), type);
1✔
204
            JTypeDeclSymbol sym;
205
            if (resolved == null) {
1✔
206
                ctx.reportCannotResolveSymbol(type, type.getSimpleName());
1✔
207
                sym = setArity(type, ctx, type.getSimpleName());
1✔
208
            } else {
209
                sym = resolved.getSymbol();
1✔
210
                if (sym.isUnresolved()) {
1✔
211
                    sym = setArity(type, ctx, ((JClassSymbol) sym).getCanonicalName());
1✔
212
                }
213
            }
214
            type.setSymbol(sym);
1✔
215
            type.setImplicitEnclosing(enclosingType(resolved));
1✔
216
        }
1✔
217

218
        private void postProcess(ASTClassType type, ReferenceCtx ctx) {
219
            JTypeDeclSymbol sym = type.getReferencedSym();
1✔
220
            if (type.getParent() instanceof ASTAnnotation) {
1✔
221
                if (!(sym instanceof JClassSymbol && (sym.isUnresolved() || ((JClassSymbol) sym).isAnnotation()))) {
1✔
222
                    ctx.getLogger().warning(type, JavaSemanticErrors.EXPECTED_ANNOTATION_TYPE);
1✔
223
                }
224
                return;
1✔
225
            }
226

227
            int actualArity = ASTList.sizeOrZero(type.getTypeArguments());
1✔
228
            int expectedArity = sym instanceof JClassSymbol ? ((JClassSymbol) sym).getTypeParameterCount() : 0;
1✔
229
            if (actualArity != 0 && actualArity != expectedArity) {
1✔
230
                ctx.getLogger().warning(type, JavaSemanticErrors.MALFORMED_GENERIC_TYPE, expectedArity, actualArity);
1✔
231
            }
232
        }
1✔
233

234
        private static @NonNull JTypeDeclSymbol setArity(ASTClassType type, ReferenceCtx ctx, String canonicalName) {
235
            int arity = ASTList.sizeOrZero(type.getTypeArguments());
1✔
236
            return ctx.makeUnresolvedReference(canonicalName, arity);
1✔
237
        }
238

239
        /*
240

241
           This is implemented as a set of mutually recursive methods
242
           that act as a kind of automaton. State transitions:
243

244
                        +-----+       +--+        +--+
245
                        |     |       |  |        |  |
246
           +-----+      +     v       +  v        +  v
247
           |START+----> PACKAGE +---> TYPE +----> EXPR
248
           +-----+                     ^           ^
249
             |                         |           |
250
             +-------------------------------------+
251

252
           Not pictured are the error transitions.
253
           Only Type & Expr are valid exit states.
254
         */
255

256
        /**
257
         * Resolve an ambiguous name occurring in an expression context.
258
         * Returns the expression to which the name was resolved. If the
259
         * name is a type, this is a {@link ASTTypeExpression}, otherwise
260
         * it could be a {@link ASTFieldAccess} or {@link ASTVariableAccess},
261
         * and in the worst case, the original {@link ASTAmbiguousName}.
262
         */
263
        private static ASTExpression startResolve(ASTAmbiguousName name, ReferenceCtx ctx, boolean isPackageOrTypeOnly) {
264
            Iterator<JavaccToken> tokens = name.tokens().iterator();
1✔
265
            JavaccToken firstIdent = tokens.next();
1✔
266
            TokenUtils.expectKind(firstIdent, JavaTokenKinds.IDENTIFIER);
1✔
267

268
            JSymbolTable symTable = name.getSymbolTable();
1✔
269

270
            String firstImage = firstIdent.getImage();
1✔
271

272
            if (!isPackageOrTypeOnly) {
1✔
273
                // first test if the leftmost segment is an expression
274
                JVariableSig varResult = symTable.variables().resolveFirst(firstImage);
1✔
275

276
                if (varResult != null) {
1✔
277
                    return resolveExpr(null, varResult, firstIdent, tokens, ctx);
1✔
278
                }
279
            }
280

281
            // otherwise, test if it is a type name
282

283
            JTypeMirror typeResult = ctx.resolveSingleTypeName(symTable, firstImage, name);
1✔
284

285
            if (typeResult != null) {
1✔
286
                JClassType enclosing = enclosingType(typeResult);
1✔
287
                return resolveType(null, enclosing, typeResult.getSymbol(), false, firstIdent, tokens, name, isPackageOrTypeOnly, ctx);
1✔
288
            }
289

290
            // otherwise, first is reclassified as package name.
291
            return resolvePackage(firstIdent, new StringBuilder(firstImage), tokens, name, isPackageOrTypeOnly, ctx);
1✔
292
        }
293

294

295
        /**
296
         * Classify the given [identifier] as an expression name. This
297
         * produces a FieldAccess/VariableAccess, depending on whether there is a qualifier.
298
         * The remaining token chain is reclassified as a sequence of
299
         * field accesses.
300
         *
301
         * TODO Check the field accesses are legal
302
         *  Also must filter by visibility
303
         */
304
        private static ASTExpression resolveExpr(@Nullable ASTExpression qualifier, // lhs
305
                                                 @Nullable JVariableSig varSym,     // signature, only set if this is the leftmost access
306
                                                 JavaccToken identifier,            // identifier for the field/var name
307
                                                 Iterator<JavaccToken> remaining,   // rest of tokens, starting with following '.'
308
                                                 ReferenceCtx ctx) {
309

310
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
1✔
311

312
            ASTNamedReferenceExpr var;
313
            if (qualifier == null) {
1✔
314
                ASTVariableAccess varAccess = new ASTVariableAccess(identifier);
1✔
315
                varAccess.setTypedSym(varSym);
1✔
316
                var = varAccess;
1✔
317
            } else {
1✔
318
                ASTFieldAccess fieldAccess = new ASTFieldAccess(qualifier, identifier);
1✔
319
                fieldAccess.setTypedSym((FieldSig) varSym);
1✔
320
                var = fieldAccess;
1✔
321
            }
322

323

324
            if (!remaining.hasNext()) { // done
1✔
325
                return var;
1✔
326
            }
327

328
            JavaccToken nextIdent = skipToNextIdent(remaining);
1✔
329

330
            // following must also be expressions (field accesses)
331
            // we can't assert that for now, as symbols lack type information
332

333
            return resolveExpr(var, null, nextIdent, remaining, ctx);
1✔
334
        }
335

336
        /**
337
         * Classify the given [identifier] as a reference to the [sym].
338
         * This produces a ClassOrInterfaceType with the given [image] (which
339
         * may be prepended by a package name, or otherwise is just a simple name).
340
         * We then lookup the following identifier, and take a decision:
341
         * <ul>
342
         * <li>If there is a field with the given name in [classSym],
343
         * then the remaining tokens are reclassified as expression names
344
         * <li>Otherwise, if there is a member type with the given name
345
         * in [classSym], then the remaining segment is classified as a
346
         * type name (recursive call to this procedure)
347
         * <li>Otherwise, normally a compile-time error occurs. We instead
348
         * log a warning and treat it as a field access.
349
         * </ul>
350
         *
351
         * @param isPackageOrTypeOnly If true, expressions are disallowed by the context, so we don't check fields
352
         */
353
        private static ASTExpression resolveType(final @Nullable ASTClassType qualifier, // lhs
354
                                                 final @Nullable JClassType implicitEnclosing,      // enclosing type, if it is implicitly inherited
355
                                                 final JTypeDeclSymbol sym,                         // symbol for the type
356
                                                 final boolean isFqcn,                              // whether this is a fully-qualified name
357
                                                 final JavaccToken identifier,                      // ident of the simple name of the symbol
358
                                                 final Iterator<JavaccToken> remaining,             // rest of tokens, starting with following '.'
359
                                                 final ASTAmbiguousName ambig,                      // original ambiguous name
360
                                                 final boolean isPackageOrTypeOnly,
361
                                                 final ReferenceCtx ctx) {
362

363
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
1✔
364

365
            final ASTClassType type = new ASTClassType(qualifier, isFqcn, ambig.getFirstToken(), identifier);
1✔
366
            type.setSymbol(sym);
1✔
367
            type.setImplicitEnclosing(implicitEnclosing);
1✔
368

369
            if (!remaining.hasNext()) { // done
1✔
370
                return new ASTTypeExpression(type);
1✔
371
            }
372

373
            final JavaccToken nextIdent = skipToNextIdent(remaining);
1✔
374
            final String nextSimpleName = nextIdent.getImage();
1✔
375

376
            if (!isPackageOrTypeOnly) {
1!
377
                @Nullable FieldSig field = ctx.findStaticField(sym, nextSimpleName);
1✔
378
                if (field != null) {
1✔
379
                    // todo check field is static
380
                    ASTTypeExpression typeExpr = new ASTTypeExpression(type);
1✔
381
                    return resolveExpr(typeExpr, field, nextIdent, remaining, ctx);
1✔
382
                }
383
            }
384

385
            JClassSymbol inner = ctx.findTypeMember(sym, nextSimpleName, ambig);
1✔
386

387
            if (inner == null && isPackageOrTypeOnly) {
1!
388
                // normally compile-time error, continue by considering it an unresolved inner type
389
                ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.TYPE, nextSimpleName, sym);
×
390
                inner = ctx.makeUnresolvedReference(sym, nextSimpleName, 0);
×
391
            }
392

393
            if (inner != null) {
1✔
394
                return resolveType(type, null, inner, false, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx);
1✔
395
            }
396

397
            // no inner type, yet we have a lhs that is a type...
398
            // this is normally a compile-time error
399
            // treat as unresolved field accesses, this is the smoothest for later type res
400

401
            // todo report on the specific token failing
402
            ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.FIELD_ACCESS, nextSimpleName, sym);
1✔
403
            ASTTypeExpression typeExpr = new ASTTypeExpression(type);
1✔
404
            return resolveExpr(typeExpr, null, nextIdent, remaining, ctx); // this will chain for the rest of the name
1✔
405
        }
406

407
        /**
408
         * Classify the given [identifier] as a package name. This means, that
409
         * we look ahead into the [remaining] tokens, and try to find a class
410
         * by that name in the given package. Then:
411
         * <ul>
412
         * <li>If such a class exists, continue the classification with resolveType
413
         * <li>Otherwise, the looked ahead segment is itself reclassified as a package name
414
         * </ul>
415
         *
416
         * <p>If we consumed the entire name without finding a suitable
417
         * class, then we report it and return the original ambiguous name.
418
         */
419
        private static ASTExpression resolvePackage(JavaccToken identifier,
420
                                                    StringBuilder packageImage,
421
                                                    Iterator<JavaccToken> remaining,
422
                                                    ASTAmbiguousName ambig,
423
                                                    boolean isPackageOrTypeOnly,
424
                                                    ReferenceCtx ctx) {
425

426
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
1✔
427

428
            if (!remaining.hasNext()) {
1✔
429
                if (isPackageOrTypeOnly) {
1✔
430
                    // There's one last segment to try, the parent of the ambiguous name
431
                    // This may only be because this ambiguous name is the package qualification of the parent type
432
                    forceResolveAsFullPackageNameOfParent(packageImage, ambig, ctx);
1✔
433
                    return ambig; // returning ambig makes the outer routine not replace
1✔
434
                }
435

436
                // then this name is unresolved, leave the ambiguous name in the tree
437
                // this only happens inside expressions
438
                ctx.getLogger().warning(ambig, CANNOT_RESOLVE_AMBIGUOUS_NAME, packageImage, ReferenceCtx.Fallback.AMBIGUOUS);
1✔
439
                ambig.setProcessed(); // signal that we don't want to retry resolving this
1✔
440
                return ambig;
1✔
441
            }
442

443
            JavaccToken nextIdent = skipToNextIdent(remaining);
1✔
444

445

446
            packageImage.append('.').append(nextIdent.getImage());
1✔
447
            String canonical = packageImage.toString();
1✔
448

449
            // Don't interpret periods as nested class separators (this will be handled by resolveType).
450
            // Otherwise lookup of a fully qualified name would be quadratic
451
            JClassSymbol nextClass = ctx.resolveClassFromBinaryName(canonical);
1✔
452

453
            if (nextClass != null) {
1✔
454
                return resolveType(null, null, nextClass, true, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx);
1✔
455
            } else {
456
                return resolvePackage(nextIdent, packageImage, remaining, ambig, isPackageOrTypeOnly, ctx);
1✔
457
            }
458
        }
459

460
        /**
461
         * Force resolution of the ambiguous name as a package name.
462
         * The parent type's image is set to a package name + simple name.
463
         */
464
        private static void forceResolveAsFullPackageNameOfParent(StringBuilder packageImage, ASTAmbiguousName ambig, ReferenceCtx ctx) {
465
            ASTClassType parent = (ASTClassType) ambig.getParent();
1✔
466

467
            packageImage.append('.').append(parent.getSimpleName());
1✔
468
            String fullName = packageImage.toString();
1✔
469
            JClassSymbol parentClass = ctx.resolveClassFromBinaryName(fullName);
1✔
470
            if (parentClass == null) {
1✔
471
                ctx.getLogger().warning(parent, CANNOT_RESOLVE_AMBIGUOUS_NAME, fullName, ReferenceCtx.Fallback.TYPE);
1✔
472
                parentClass = ctx.makeUnresolvedReference(fullName, ASTList.sizeOrZero(parent.getTypeArguments()));
1✔
473
            }
474
            parent.setSymbol(parentClass);
1✔
475
            parent.setFullyQualified();
1✔
476
            ambig.deleteInParent();
1✔
477
        }
1✔
478

479
        private static JavaccToken skipToNextIdent(Iterator<JavaccToken> remaining) {
480
            JavaccToken dot = remaining.next();
1✔
481
            TokenUtils.expectKind(dot, JavaTokenKinds.DOT);
1✔
482
            assert remaining.hasNext() : "Ambiguous name must end with an identifier";
1!
483
            return remaining.next();
1✔
484
        }
485
    }
486

487
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc