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

rieske / trans / 28460720020

30 Jun 2026 04:42PM UTC coverage: 90.232% (-0.3%) from 90.53%
28460720020

Pull #44

github

rieske
Support pointer members in structs including self-reference

Apply declarator pointer levels when building field types so `int *p`
is not stored as `int`. Use a shared StructBody for struct tags so
`struct Node *next` can refer to an incomplete Node that is completed
when the definition finishes. Tests for int* members and linked nodes.
Pull Request #44: Implement C structs with member access

245 of 306 new or added lines in 12 files covered. (80.07%)

29 existing lines in 3 files now uncovered.

5358 of 5938 relevant lines covered (90.23%)

236113.43 hits per line

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

87.54
/src/semantic_analyzer/SemanticAnalysisVisitor.cpp
1
#include "SemanticAnalysisVisitor.h"
2

3
#include <algorithm>
4
#include <stdexcept>
5

6
#include "translation_unit/Context.h"
7
#include "types/Type.h"
8
#include "util/Logger.h"
9
#include "util/LogManager.h"
10

11
namespace semantic_analyzer {
12

13
static const translation_unit::Context EXTERNAL_CONTEXT {"external", 0};
14

15
static Logger& err = LogManager::getErrorLogger();
16

17
SemanticAnalysisVisitor::SemanticAnalysisVisitor() {
386✔
18
    type::Type functionType = type::function(type::signedInteger());
386✔
19
    symbolTable.insertFunction("printf", functionType.getFunction(), EXTERNAL_CONTEXT);
1,158✔
20
    symbolTable.insertFunction("scanf", functionType.getFunction(), EXTERNAL_CONTEXT);
1,158✔
21
}
386✔
22

23
SemanticAnalysisVisitor::~SemanticAnalysisVisitor() {
386✔
24
}
386✔
25

26
void SemanticAnalysisVisitor::printSymbolTable() const {
374✔
27
    symbolTable.printTable();
374✔
28
}
374✔
29

30
void SemanticAnalysisVisitor::visit(ast::DeclarationSpecifiers& declarationSpecifiers) {
1,162✔
31
    // FIXME: this would look so much better
32
    /*for (std::string error : declarationSpecifiers.getSemanticErrors()) {
33
     semanticError(error, globalContext);
34
     }*/
35
    if (declarationSpecifiers.getStorageSpecifiers().size() > 1) {
1,162✔
36
        semanticError("multiple storage classes in declaration specifiers",
×
37
                declarationSpecifiers.getStorageSpecifiers().at(1).getContext());
×
38
    }
39
}
1,162✔
40

41
void SemanticAnalysisVisitor::visit(ast::Declaration& declaration) {
516✔
42
    declaration.visitChildren(*this);
516✔
43

44
    auto baseType = declaration.getDeclarationSpecifiers().getTypeSpecifiers().at(0).getType();
516✔
45
    for (const auto& declarator : declaration.getDeclarators()) {
1,144✔
46
        auto type = declarator->getFundamentalType(baseType);
628✔
47
        if (type.isVoid()) {
628✔
48
            semanticError("variable `" + declarator->getName() + "` declared void", declarator->getContext());
2✔
49
        } else if (symbolTable.isAtFileScope() && symbolTable.hasFunction(declarator->getName())) {
626✔
50
            semanticError("symbol `" + declarator->getName() + "` declaration conflicts with function of the same name",
×
51
                    declarator->getContext());
×
52
        } else if (symbolTable.insertSymbol(declarator->getName(), type, declarator->getContext())) {
626✔
53
            declarator->setHolder(symbolTable.lookup(declarator->getName()));
624✔
54
            // TODO: type check initializers
55
            if (declarator->hasInitializer() && symbolTable.isAtFileScope()) {
624✔
56
                long initValue = 0;
28✔
57
                if (declarator->getInitializer()->evaluateConstant(initValue)) {
28✔
58
                    symbolTable.setGlobalInitializer(declarator->getName(), initValue);
28✔
59
                } else {
60
                    semanticError("global initializer is not a constant expression", declarator->getContext());
×
61
                }
62
            }
63
        } else {
64
            semanticError(
2✔
65
                    "symbol `" + declarator->getName() +
4✔
66
                            "` declaration conflicts with previous declaration on " +
4✔
67
                            to_string(symbolTable.lookup(declarator->getName()).getContext()),
4✔
68
                    declarator->getContext());
4✔
69
        }
70
    }
628✔
71
}
516✔
72

73
void SemanticAnalysisVisitor::visit(ast::Declarator& declarator) {
1,274✔
74
    declarator.visitChildren(*this);
1,274✔
75
}
1,274✔
76

77
void SemanticAnalysisVisitor::visit(ast::InitializedDeclarator& declarator) {
628✔
78
    declarator.visitChildren(*this);
628✔
79
}
628✔
80

81
void SemanticAnalysisVisitor::visit(ast::ArrayAccess& arrayAccess) {
×
82
    arrayAccess.visitLeftOperand(*this);
×
83
    arrayAccess.visitRightOperand(*this);
×
84

85
    auto type = arrayAccess.leftOperandType();
×
86
    if (type.isPointer()) {
×
87
        arrayAccess.setLvalue(symbolTable.createTemporarySymbol(type.dereference()));
×
88
        arrayAccess.setResultSymbol(symbolTable.createTemporarySymbol(type.dereference()));
×
89
    } else {
90
        semanticError("invalid type for operator[]\n", arrayAccess.getContext());
×
91
    }
92
}
×
93

94
void SemanticAnalysisVisitor::visit(ast::FunctionCall& functionCall) {
916✔
95
    functionCall.visitOperand(*this);
916✔
96
    functionCall.visitArguments(*this);
916✔
97

98
    auto functionSymbol = symbolTable.findFunction(functionCall.operandSymbol()->getName());
916✔
99

100
    functionCall.setSymbol(functionSymbol);
916✔
101

102
    auto& arguments = functionCall.getArgumentList();
916✔
103
    if (arguments.size() == functionSymbol.argumentCount()) {
916✔
104
        auto declaredArguments = functionSymbol.arguments();
110✔
105
        for (std::size_t i { 0 }; i < arguments.size(); ++i) {
300✔
106
            const auto& declaredArgument = declaredArguments.at(i);
190✔
107
            const auto& actualArgument = arguments.at(i)->getResultSymbol();
190✔
108
            typeCheck(actualArgument->getType(), declaredArgument, functionCall.getContext());
190✔
109
        }
110

111
        auto returnType = functionSymbol.returnType();
110✔
112
        if (!returnType.isVoid()) {
110✔
113
            functionCall.setResultSymbol(symbolTable.createTemporarySymbol(returnType));
110✔
114
        }
115
    } else if (functionSymbol.getContext() == EXTERNAL_CONTEXT) {
916✔
116
    // FIXME: using EXTERNAL_CONTEXT as a workaround for printf/scanf external functions until varargs are properly implemented
117
        auto returnType = functionSymbol.returnType();
806✔
118
        if (!returnType.isVoid()) {
806✔
119
            functionCall.setResultSymbol(symbolTable.createTemporarySymbol(returnType));
806✔
120
        }
121
    } else {
806✔
122
        semanticError("no match for function " + functionSymbol.getType().to_string(), functionCall.getContext());
×
123
    }
124
}
916✔
125

126
void SemanticAnalysisVisitor::visit(ast::IdentifierExpression& identifier) {
2,738✔
127
    if (symbolTable.hasSymbol(identifier.getIdentifier())) {
2,738✔
128
        identifier.setResultSymbol(symbolTable.lookup(identifier.getIdentifier()));
2,736✔
129
    } else {
130
        semanticError("symbol `" + identifier.getIdentifier() + "` is not defined", identifier.getContext());
2✔
131
    }
132
}
2,738✔
133

134
void SemanticAnalysisVisitor::visit(ast::ConstantExpression& constant) {
1,268✔
135
    constant.setResultSymbol(symbolTable.createTemporarySymbol(constant.getType()));
1,268✔
136
}
1,268✔
137

138
void SemanticAnalysisVisitor::visit(ast::StringLiteralExpression& stringLiteral) {
806✔
139
    std::string constantSymbol = symbolTable.newConstant(stringLiteral.getValue());
806✔
140
    stringLiteral.setConstantSymbol(constantSymbol);
806✔
141
    stringLiteral.setResultSymbol(symbolTable.createTemporarySymbol(stringLiteral.getType()));
806✔
142
}
806✔
143

144
void SemanticAnalysisVisitor::visit(ast::PostfixExpression& expression) {
24✔
145
    expression.visitOperand(*this);
24✔
146

147
    expression.setType(expression.operandType());
24✔
148
    auto operandSymbol = *expression.operandSymbol();
24✔
149
    expression.setResultSymbol(operandSymbol);
24✔
150

151
    auto preOperationSymbolName = operandSymbol.getName() + "_pre";
24✔
152
    symbolTable.insertSymbol(preOperationSymbolName, operandSymbol.getType(), operandSymbol.getContext());
24✔
153
    expression.setPreOperationSymbol(symbolTable.lookup(preOperationSymbolName));
24✔
154

155
    if (!expression.isLval()) {
24✔
156
        semanticError("lvalue required as increment operand", expression.getContext());
×
157
    }
158
}
24✔
159

160
void SemanticAnalysisVisitor::visit(ast::PrefixExpression& expression) {
28✔
161
    expression.visitOperand(*this);
28✔
162

163
    expression.setType(expression.operandType());
28✔
164
    expression.setResultSymbol(*expression.operandSymbol());
28✔
165

166
    if (!expression.isLval()) {
28✔
167
        semanticError("lvalue required as increment operand", expression.getContext());
×
168
    }
169
}
28✔
170

171
void SemanticAnalysisVisitor::visit(ast::UnaryExpression& expression) {
546✔
172
    expression.visitOperand(*this);
546✔
173

174
    switch (expression.getOperator()->getLexeme().front()) {
546✔
175
    case '&':
334✔
176
        expression.setResultSymbol(symbolTable.createTemporarySymbol(type::pointer(expression.operandType())));
334✔
177
        break;
334✔
178
    case '*':
90✔
179
        if (expression.operandType().isPointer()) {
90✔
180
            expression.setResultSymbol(symbolTable.createTemporarySymbol(expression.operandType().dereference()));
90✔
181
            expression.setLvalueSymbol(symbolTable.createTemporarySymbol(expression.operandType()));
90✔
182
        } else {
183
            semanticError("invalid type argument of ‘unary *’ :" + expression.operandType().to_string(), expression.getContext());
×
184
        }
185
        break;
90✔
186
    case '+':
4✔
187
        expression.setResultSymbol(*expression.operandSymbol());
4✔
188
        break;
4✔
189
    case '-':
88✔
190
        expression.setResultSymbol(symbolTable.createTemporarySymbol(expression.operandType()));
88✔
191
        break;
88✔
192
    case '!':
30✔
193
        expression.setResultSymbol(symbolTable.createTemporarySymbol(type::signedInteger()));
30✔
194
        expression.setTruthyLabel(symbolTable.newLabel());
30✔
195
        expression.setFalsyLabel(symbolTable.newLabel());
30✔
196
        break;
30✔
197
    default:
×
198
        throw std::runtime_error { "Unidentified increment operator: " + expression.getOperator()->getLexeme() };
×
199
    }
200
}
546✔
201

202
void SemanticAnalysisVisitor::visit(ast::TypeCast& expression) {
×
203
    expression.visitOperand(*this);
×
204

205
    expression.setResultSymbol(symbolTable.createTemporarySymbol(expression.getType().getType()));
×
206
}
×
207

208
void SemanticAnalysisVisitor::visit(ast::ArithmeticExpression& expression) {
114✔
209
    expression.visitLeftOperand(*this);
114✔
210
    expression.visitRightOperand(*this);
114✔
211

212
    typeCheck(
114✔
213
            expression.leftOperandType(),
228✔
214
            expression.rightOperandType(),
228✔
215
            expression.getContext());
228✔
216
    // FIXME: type conversion
217
    expression.setResultSymbol(symbolTable.createTemporarySymbol(expression.leftOperandType()));
114✔
218
}
114✔
219

220
void SemanticAnalysisVisitor::visit(ast::ShiftExpression& expression) {
20✔
221
    expression.visitLeftOperand(*this);
20✔
222
    expression.visitRightOperand(*this);
20✔
223

224
    if (expression.rightOperandType().isPrimitive() && !expression.rightOperandType().getPrimitive().isFloating()) {
20✔
225
        expression.setResultSymbol(symbolTable.createTemporarySymbol(expression.leftOperandType()));
20✔
226
    } else {
227
        semanticError("argument of type int required for shift expression", expression.getContext());
×
228
    }
229
}
20✔
230

231
void SemanticAnalysisVisitor::visit(ast::ComparisonExpression& expression) {
180✔
232
    expression.visitLeftOperand(*this);
180✔
233
    expression.visitRightOperand(*this);
180✔
234

235
    typeCheck(
180✔
236
            expression.leftOperandType(),
360✔
237
            expression.rightOperandType(),
360✔
238
            expression.getContext());
360✔
239

240
    expression.setResultSymbol(symbolTable.createTemporarySymbol(type::signedInteger()));
180✔
241
    expression.setTruthyLabel(symbolTable.newLabel());
180✔
242
    expression.setFalsyLabel(symbolTable.newLabel());
180✔
243
}
180✔
244

245
void SemanticAnalysisVisitor::visit(ast::BitwiseExpression& expression) {
16✔
246
    expression.visitLeftOperand(*this);
16✔
247
    expression.visitRightOperand(*this);
16✔
248
    expression.setType(expression.leftOperandType());
16✔
249

250
    typeCheck(
16✔
251
            expression.leftOperandType(),
32✔
252
            expression.rightOperandType(),
32✔
253
            expression.getContext());
32✔
254

255
    expression.setResultSymbol(
16✔
256
            symbolTable.createTemporarySymbol(expression.getType()));
32✔
257
}
16✔
258

259
void SemanticAnalysisVisitor::visit(ast::LogicalAndExpression& expression) {
12✔
260
    expression.visitLeftOperand(*this);
12✔
261
    expression.visitRightOperand(*this);
12✔
262

263
    typeCheck(
12✔
264
            expression.leftOperandType(),
24✔
265
            expression.rightOperandType(),
24✔
266
            expression.getContext());
24✔
267

268
    expression.setResultSymbol(symbolTable.createTemporarySymbol(type::signedInteger()));
12✔
269
    expression.setExitLabel(symbolTable.newLabel());
12✔
270
}
12✔
271

272
void SemanticAnalysisVisitor::visit(ast::LogicalOrExpression& expression) {
12✔
273
    expression.visitLeftOperand(*this);
12✔
274
    expression.visitRightOperand(*this);
12✔
275

276
    typeCheck(
12✔
277
            expression.leftOperandType(),
24✔
278
            expression.rightOperandType(),
24✔
279
            expression.getContext());
24✔
280

281
    expression.setResultSymbol(symbolTable.createTemporarySymbol(type::signedInteger()));
12✔
282
    expression.setExitLabel(symbolTable.newLabel());
12✔
283
}
12✔
284

285
void SemanticAnalysisVisitor::visit(ast::AssignmentExpression& expression) {
486✔
286
    expression.visitLeftOperand(*this);
486✔
287
    expression.visitRightOperand(*this);
486✔
288

289
    if (expression.isLval()) {
486✔
290
        typeCheck(
482✔
291
                expression.leftOperandType(),
964✔
292
                expression.rightOperandType(),
964✔
293
                expression.getContext());
964✔
294

295
        expression.setResultSymbol(*expression.leftOperandSymbol());
482✔
296
    } else {
297
        semanticError("lvalue required on the left side of assignment", expression.getContext());
12✔
298
    }
299
}
486✔
300

301
void SemanticAnalysisVisitor::visit(ast::ExpressionList& expression) {
2✔
302
    expression.visitLeftOperand(*this);
2✔
303
    expression.visitRightOperand(*this);
2✔
304
    // Comma operator: value and type of the right operand
305
    expression.setType(expression.rightOperandType());
2✔
306
    expression.setResultSymbol(*expression.rightOperandSymbol());
2✔
307
}
2✔
308

309
void SemanticAnalysisVisitor::visit(ast::Operator&) {
×
310
}
×
311

312
void SemanticAnalysisVisitor::visit(ast::JumpStatement& statement) {
×
313
    throw std::runtime_error { "not implemented" };
×
314
}
315

316
void SemanticAnalysisVisitor::visit(ast::ReturnStatement& statement) {
458✔
317
    statement.returnExpression->accept(*this);
458✔
318
}
458✔
319

320
void SemanticAnalysisVisitor::visit(ast::VoidReturnStatement& statement) {
22✔
321
}
22✔
322

323
void SemanticAnalysisVisitor::visit(ast::IfStatement& statement) {
20✔
324
    statement.testExpression->accept(*this);
20✔
325
    statement.body->accept(*this);
20✔
326

327
    statement.setFalsyLabel(symbolTable.newLabel());
20✔
328
}
20✔
329

330
void SemanticAnalysisVisitor::visit(ast::IfElseStatement& statement) {
16✔
331
    statement.testExpression->accept(*this);
16✔
332
    statement.truthyBody->accept(*this);
16✔
333
    statement.falsyBody->accept(*this);
16✔
334

335
    statement.setFalsyLabel(symbolTable.newLabel());
16✔
336
    statement.setExitLabel(symbolTable.newLabel());
16✔
337
}
16✔
338

339
void SemanticAnalysisVisitor::visit(ast::LoopStatement& loop) {
50✔
340
    loop.header->accept(*this);
50✔
341
    loop.body->accept(*this);
50✔
342
}
50✔
343

344
void SemanticAnalysisVisitor::visit(ast::ForLoopHeader& loopHeader) {
30✔
345
    if (loopHeader.initialization) {
30✔
346
        loopHeader.initialization->accept(*this);
20✔
347
    }
348
    if (loopHeader.clause) {
30✔
349
        loopHeader.clause->accept(*this);
22✔
350
    }
351
    if (loopHeader.increment) {
30✔
352
        loopHeader.increment->accept(*this);
22✔
353
    }
354

355
    loopHeader.setLoopEntry(symbolTable.newLabel());
30✔
356
    loopHeader.setLoopExit(symbolTable.newLabel());
30✔
357
}
30✔
358

359
void SemanticAnalysisVisitor::visit(ast::WhileLoopHeader& loopHeader) {
20✔
360
    loopHeader.clause->accept(*this);
20✔
361

362
    loopHeader.setLoopEntry(symbolTable.newLabel());
20✔
363
    loopHeader.setLoopExit(symbolTable.newLabel());
20✔
364
}
20✔
365

366
void SemanticAnalysisVisitor::visit(ast::Pointer&) {
68✔
367
}
68✔
368

369
void SemanticAnalysisVisitor::visit(ast::Identifier&) {
796✔
370
}
796✔
371

372
void SemanticAnalysisVisitor::visit(ast::ArrayDeclarator& declaration) {
×
373
    declaration.subscriptExpression->accept(*this);
×
374
    throw std::runtime_error { "not implemented" };
×
375
}
376

377
void SemanticAnalysisVisitor::visit(ast::FunctionDeclarator& declarator) {
478✔
378
    declarator.visitFormalArguments(*this);
478✔
379

380
    argumentNames.clear();
478✔
381
    std::vector<type::Type> arguments;
478✔
382
    for (auto& argumentDeclaration : declarator.getFormalArguments()) {
646✔
383
        arguments.push_back(argumentDeclaration.getType());
168✔
384
        argumentNames.push_back(argumentDeclaration.getName());
168✔
385
    }
386

387
    // FIXME: return type is not known at this point!
388
    type::Type functionType = type::function(type::signedInteger(), arguments);
478✔
389
    if (symbolTable.hasGlobalVariable(declarator.getName())) {
478✔
390
        semanticError("function `" + declarator.getName() + "` conflicts with global variable of the same name",
2✔
391
                declarator.getContext());
4✔
392
        return;
2✔
393
    }
394
    FunctionEntry functionEntry = symbolTable.insertFunction(
395
            declarator.getName(),
952✔
396
            functionType.getFunction(),
952✔
397
            declarator.getContext());
1,428✔
398

399
    if (functionEntry.getContext() != declarator.getContext()) {
476✔
400
        semanticError("function `" + declarator.getName() + "` definition conflicts with previous one on "
×
401
                + to_string(functionEntry.getContext()), declarator.getContext());
×
402
    }
403
}
480✔
404

405
void SemanticAnalysisVisitor::visit(ast::FormalArgument& argument) {
168✔
406
    argument.visitSpecifiers(*this);
168✔
407
    argument.visitDeclarator(*this);
168✔
408
    if (argument.getType().isVoid()) {
168✔
409
        semanticError("function argument ‘" + argument.getName() + "’ declared void", argument.getDeclarationContext());
×
410
    }
411
}
168✔
412

413
void SemanticAnalysisVisitor::visit(ast::FunctionDefinition& function) {
478✔
414
    function.visitReturnType(*this);
478✔
415
    function.visitDeclarator(*this);
478✔
416

417
    if (!symbolTable.hasFunction(function.getName())) {
478✔
418
        return;
2✔
419
    }
420
    function.setSymbol(symbolTable.findFunction(function.getName()));
476✔
421
    symbolTable.startFunction(function.getName(), argumentNames);
476✔
422
    // Parameters and outermost body declarations share one scope (C); do not enterBlockScope.
423
    function.visitBodyChildren(*this);
476✔
424
    function.setArguments(symbolTable.getCurrentScopeArguments());
476✔
425
    function.setLocalVariables(symbolTable.getCurrentScopeSymbols());
476✔
426
    symbolTable.endFunction();
476✔
427
}
428

429
void SemanticAnalysisVisitor::visit(ast::Block& block) {
112✔
430
    symbolTable.enterBlockScope();
112✔
431
    block.visitChildren(*this);
112✔
432
    symbolTable.exitBlockScope();
112✔
433
}
112✔
434

435
void SemanticAnalysisVisitor::typeCheck(const type::Type& typeFrom, const type::Type& typeTo,
1,006✔
436
        const translation_unit::Context& context)
437
{
438
    if (!typeTo.canAssignFrom(typeFrom)) {
1,006✔
UNCOV
439
        semanticError("type mismatch: can't convert " + typeFrom.to_string() + " to " + typeTo.to_string(), context);
×
440
    }
441
}
1,006✔
442

443
void SemanticAnalysisVisitor::semanticError(std::string message, const translation_unit::Context& context) {
12✔
444
    containsSemanticErrors = true;
12✔
445
    err << context << ": error: " << message << "\n";
12✔
446
}
12✔
447

448
bool SemanticAnalysisVisitor::successfulSemanticAnalysis() const {
386✔
449
    return !containsSemanticErrors;
386✔
450
}
451

452
std::map<std::string, std::string> SemanticAnalysisVisitor::getConstants() const {
374✔
453
    return symbolTable.getConstants();
374✔
454
}
455

456
std::vector<ValueEntry> SemanticAnalysisVisitor::getGlobalVariables() const {
374✔
457
    return symbolTable.getGlobalVariables();
374✔
458
}
459

460

461
void SemanticAnalysisVisitor::visit(ast::MemberAccess& expression) {
48✔
462
    expression.getBase()->accept(*this);
48✔
463
    type::Type baseType = expression.getBase()->getType();
48✔
464
    type::Type structType = baseType;
48✔
465
    if (expression.isArrow()) {
48✔
466
        if (!baseType.isPointer()) {
8✔
NEW
467
            semanticError("base of '->' is not a pointer", expression.getContext());
×
NEW
468
            return;
×
469
        }
470
        structType = baseType.dereference();
8✔
471
    }
472
    if (!structType.isStructure()) {
48✔
NEW
473
        semanticError("request for member in non-struct", expression.getContext());
×
NEW
474
        return;
×
475
    }
476
    type::Type memberTy = type::signedInteger();
48✔
477
    int offset = 0;
48✔
478
    if (!structType.memberType(expression.getMemberName(), memberTy) ||
96✔
479
            !structType.memberOffset(expression.getMemberName(), offset)) {
48✔
NEW
480
        semanticError("struct has no member named `" + expression.getMemberName() + "`", expression.getContext());
×
NEW
481
        return;
×
482
    }
483
    expression.setMemberOffset(offset);
48✔
484
    expression.setBaseResultSymbol(*expression.getBase()->getResultSymbol());
48✔
485
    expression.setResultSymbol(symbolTable.createTemporarySymbol(memberTy));
48✔
486
    expression.setFieldAddressSymbol(symbolTable.createTemporarySymbol(type::pointer(memberTy)));
48✔
487
}
48✔
488

489
} // namespace semantic_analyzer
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