Index: include/clang/StaticAnalyzer/Core/AnalyzerOptions.h =================================================================== --- include/clang/StaticAnalyzer/Core/AnalyzerOptions.h +++ include/clang/StaticAnalyzer/Core/AnalyzerOptions.h @@ -269,6 +269,9 @@ /// \sa shouldDisplayNotesAsEvents Optional DisplayNotesAsEvents; + /// \sa shouldRecordCoverage + Optional CoverageExportDir; + /// A helper function that retrieves option for a given full-qualified /// checker name. /// Options for checkers can be specified via 'analyzer-config' command-line @@ -545,6 +548,10 @@ /// to false when unset. bool shouldDisplayNotesAsEvents(); + /// Determines where the coverage info should be dumped to. The coverage + /// information is recorded on the basic block level granularity. + StringRef coverageExportDir(); + public: AnalyzerOptions() : AnalysisStoreOpt(RegionStoreModel), Index: lib/StaticAnalyzer/Core/AnalyzerOptions.cpp =================================================================== --- lib/StaticAnalyzer/Core/AnalyzerOptions.cpp +++ lib/StaticAnalyzer/Core/AnalyzerOptions.cpp @@ -351,3 +351,10 @@ getBooleanOption("notes-as-events", /*Default=*/false); return DisplayNotesAsEvents.getValue(); } + +StringRef AnalyzerOptions::coverageExportDir() { + if (!CoverageExportDir.hasValue()) + CoverageExportDir = getOptionAsString("record-coverage", /*Default=*/""); + return CoverageExportDir.getValue(); +} + Index: lib/StaticAnalyzer/Core/ExprEngine.cpp =================================================================== --- lib/StaticAnalyzer/Core/ExprEngine.cpp +++ lib/StaticAnalyzer/Core/ExprEngine.cpp @@ -28,8 +28,13 @@ #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" #include "clang/StaticAnalyzer/Core/PathSensitive/LoopWidening.h" #include "llvm/ADT/Statistic.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/LineIterator.h" +#include "llvm/Support/Path.h" #include "llvm/Support/SaveAndRestore.h" #include "llvm/Support/raw_ostream.h" +#include "llvm/Support/raw_os_ostream.h" +#include #ifndef NDEBUG #include "llvm/Support/GraphWriter.h" @@ -251,6 +256,45 @@ return State; } +// Mapping from file to line indexed hit count vector. +static llvm::DenseMap> CoverageInfo; + +static void dumpCoverageInfo(llvm::SmallVectorImpl &Path, + SourceManager &SM) { + for (auto &Entry : CoverageInfo) { + SmallString<128> FilePath; + const FileEntry *FE = Entry.getFirst(); + llvm::sys::path::append(FilePath, Path, FE->getName() + ".gcov"); + SmallString<128> DirPath = FilePath; + llvm::sys::path::remove_filename(DirPath); + llvm::sys::fs::create_directories(DirPath); + bool Invalid = false; + llvm::MemoryBuffer *Buf = SM.getMemoryBufferForFile(FE, &Invalid); + if (Invalid) + continue; + std::ofstream OutFile(FilePath.c_str()); + if (!OutFile) { + llvm::errs() << FilePath << " Fuck!\n"; + continue; + } + llvm::raw_os_ostream Out(OutFile); + Out << "-:0:Source:" << FE->getName() << '\n'; + Out << "-:0:Runs:1\n"; + Out << "-:0:Programs:1\n"; + for (llvm::line_iterator LI(*Buf, false); !LI.is_at_eof(); ++LI) { + int Count = Entry.getSecond()[LI.line_number() - 1]; + if (Count > 0) { + Out << Count; + } else if (Count < 0) { + Out << "#####"; + } else { + Out << '-'; + } + Out << ':' << LI.line_number() << ':' << *LI << '\n'; + } + } +} + //===----------------------------------------------------------------------===// // Top-level transfer function logic (Dispatcher). //===----------------------------------------------------------------------===// @@ -282,6 +326,12 @@ } void ExprEngine::processEndWorklist(bool hasWorkRemaining) { + if (!AMgr.options.coverageExportDir().empty()) { + SmallString<128> Path = AMgr.options.coverageExportDir(); + SourceManager &SM = getContext().getSourceManager(); + SM.getFileManager().makeAbsolutePath(Path); + dumpCoverageInfo(Path, SM); + } getCheckerManager().runCheckersForEndAnalysis(G, BR, *this); } @@ -1409,12 +1459,84 @@ return true; } +// Add the line range of the CFGBlock to a file entry indexed map. +static void processCoverageInfo(const CFGBlock &Block, SourceManager &SM, + bool Unexecuted = false) { + llvm::SmallVector LinesInBlock; + const FileEntry *FE = nullptr; + for (unsigned I = 0; I < Block.size(); ++I) { + const Stmt *S = nullptr; + switch (Block[I].getKind()) { + case CFGElement::Statement: + S = Block[I].castAs().getStmt(); + break; + case CFGElement::Initializer: + S = Block[I].castAs().getInitializer()->getInit(); + if (!S) + continue; + break; + case CFGElement::NewAllocator: + S = Block[I].castAs().getAllocatorExpr(); + break; + default: + continue; + } + assert(S); + SourceLocation SpellingStartLoc = SM.getSpellingLoc(S->getLocStart()); + if (SM.isInSystemHeader(SpellingStartLoc)) + return; + FileID FID = SM.getFileID(SpellingStartLoc); + if (FE) { + if (FE != SM.getFileEntryForID(FID)) + continue; + } else { + FE = SM.getFileEntryForID(FID); + if (CoverageInfo.find(FE) == CoverageInfo.end()) { + unsigned Lines = SM.getSpellingLineNumber(SM.getLocForEndOfFile(FID)); + CoverageInfo.insert(std::make_pair(FE, std::vector(Lines, 0))); + } + } + bool Invalid = false; + unsigned LineBegin = SM.getSpellingLineNumber(S->getLocStart(), &Invalid); + if (Invalid) + continue; + unsigned LineEnd = SM.getSpellingLineNumber(S->getLocEnd(), &Invalid); + if (Invalid) + continue; + for (unsigned Line = LineBegin; Line <= LineEnd; ++Line) { + LinesInBlock.push_back(Line); + } + } + if (!FE) + return; + std::sort(LinesInBlock.begin(), LinesInBlock.end()); + LinesInBlock.erase(std::unique(LinesInBlock.begin(), LinesInBlock.end()), + LinesInBlock.end()); + std::vector &FileCov = CoverageInfo[FE]; + if (Unexecuted) { + for (unsigned Line : LinesInBlock) { + if (FileCov[Line - 1] == 0) + FileCov[Line - 1] = -1; + } + return; + } + for (unsigned Line : LinesInBlock) { + if (FileCov[Line - 1] < 0) + FileCov[Line - 1] = 1; + else + ++FileCov[Line - 1]; + } +} + /// Block entrance. (Update counters). void ExprEngine::processCFGBlockEntrance(const BlockEdge &L, NodeBuilderWithSinks &nodeBuilder, ExplodedNode *Pred) { PrettyStackTraceLocationContext CrashInfo(Pred->getLocationContext()); + if (!AMgr.options.coverageExportDir().empty()) + processCoverageInfo(*L.getDst(), getContext().getSourceManager()); + // If this block is terminated by a loop and it has already been visited the // maximum number of times, widen the loop. unsigned int BlockCount = nodeBuilder.getContext().blockCount(); @@ -1760,6 +1882,10 @@ ExplodedNode *Pred, ExplodedNodeSet &Dst, const BlockEdge &L) { + if (!AMgr.options.coverageExportDir().empty()) { + for (auto BlockIT : *L.getLocationContext()->getCFG()) + processCoverageInfo(*BlockIT, getContext().getSourceManager(), true); + } SaveAndRestore NodeContextRAII(currBldrCtx, &BC); getCheckerManager().runCheckersForBeginFunction(Dst, L, Pred, *this); } Index: test/Analysis/analyzer-config.c =================================================================== --- test/Analysis/analyzer-config.c +++ test/Analysis/analyzer-config.c @@ -24,8 +24,9 @@ // CHECK-NEXT: max-times-inline-large = 32 // CHECK-NEXT: min-cfg-size-treat-functions-as-large = 14 // CHECK-NEXT: mode = deep +// CHECK-NEXT: record-coverage = // CHECK-NEXT: region-store-small-struct-limit = 2 // CHECK-NEXT: widen-loops = false // CHECK-NEXT: [stats] -// CHECK-NEXT: num-entries = 15 +// CHECK-NEXT: num-entries = 16 Index: test/Analysis/analyzer-config.cpp =================================================================== --- test/Analysis/analyzer-config.cpp +++ test/Analysis/analyzer-config.cpp @@ -35,7 +35,8 @@ // CHECK-NEXT: max-times-inline-large = 32 // CHECK-NEXT: min-cfg-size-treat-functions-as-large = 14 // CHECK-NEXT: mode = deep +// CHECK-NEXT: record-coverage = // CHECK-NEXT: region-store-small-struct-limit = 2 // CHECK-NEXT: widen-loops = false // CHECK-NEXT: [stats] -// CHECK-NEXT: num-entries = 20 +// CHECK-NEXT: num-entries = 21 Index: test/Analysis/record-coverage.cpp =================================================================== --- /dev/null +++ test/Analysis/record-coverage.cpp @@ -0,0 +1,12 @@ +// RUN: %clang_cc1 -analyze -analyzer-checker=core -analyzer-config record-coverage=%T %s +// RUN: FileCheck -input-file %T/%s.gcov %s.expected + +int main() { + int i = 2; + ++i; + if (i != 0) { + ++i; + } else { + --i; + } +} Index: test/Analysis/record-coverage.cpp.expected =================================================================== --- /dev/null +++ test/Analysis/record-coverage.cpp.expected @@ -0,0 +1,9 @@ +// CHECK: -:4:int main() { +// CHECK-NEXT: 1:5: int i = 2; +// CHECK-NEXT: 1:6: ++i; +// CHECK-NEXT: 1:7: if (i != 0) { +// CHECK-NEXT: 1:8: ++i; +// CHECK-NEXT: -:9: } else { +// CHECK-NEXT: #####:10: --i; +// CHECK-NEXT: -:11: } +// CHECK-NEXT: -:12:} Index: utils/analyzer/MergeCoverage.py =================================================================== --- /dev/null +++ utils/analyzer/MergeCoverage.py @@ -0,0 +1,127 @@ +''' + Script to merge gcov files produced by the static analyzer. + So coverage information of header files from multiple translation units are + merged together. The output can be pocessed by gcovr format like: + gcovr -g outputdir --html --html-details -r sourceroot -o example.html + The expected layout of input (the input should be the gcovdir): + gcovdir/TranslationUnit1/file1.gcov + gcovdir/TranslationUnit1/subdir/file2.gcov + ... + gcovdir/TranslationUnit1/fileN.gcov + ... + gcovdir/TranslationUnitK/fileM.gcov + The output: + outputdir/file1.gcov + outputdir/subdir/file2.gcov + ... + outputdir/fileM.gcov +''' + +import argparse +import os +import sys +import shutil + +def is_num(val): + '''Check if val can be converted to int.''' + try: + int(val) + return True + except ValueError: + return False + + +def is_valid(line): + '''Check whether a list is a valid gcov line after join on colon.''' + if len(line) == 4: + return line[2].lower() in {"graph", "data", "runs", "programs", + "source"} + else: + return len(line) == 3 + + +def merge_gcov(from_gcov, to_gcov): + '''Merge to existing gcov file, modify the second one.''' + with open(from_gcov) as from_file, open(to_gcov) as to_file: + from_lines = from_file.readlines() + to_lines = to_file.readlines() + + if len(from_lines) != len(to_lines): + print("Fatal error: failed to match gcov files," + " different line count: (%s, %s)" % + (from_gcov, to_gcov)) + sys.exit(1) + + for i in range(len(from_lines)): + from_split = from_lines[i].split(":") + to_split = to_lines[i].split(":") + + if not is_valid(from_split) or not is_valid(to_split): + print("Fatal error: invalid gcov format (%s, %s)" % + (from_gcov, to_gcov)) + print("%s, %s" % (from_split, to_split)) + sys.exit(1) + + for j in range(2): + if from_split[j+1] != to_split[j+1]: + print("Fatal error: failed to match gcov files: (%s, %s)" % + (from_gcov, to_gcov)) + print("%s != %s" % (from_split[j+1], to_split[j+1])) + sys.exit(1) + + if to_split[0] == '#####': + to_split[0] = from_split[0] + elif to_split[0] == '-': + assert from_split[0] == '-' + elif is_num(to_split[0]): + assert is_num(from_split[0]) or from_split[0] == '#####' + if is_num(from_split[0]): + to_split[0] = str(int(to_split[0]) + int(from_split[0])) + + to_lines[i] = ":".join(to_split) + + with open(to_gcov, 'w') as to_file: + to_file.writelines(to_lines) + + +def process_tu(tu_path, output): + '''Process a directory containing files originated from checking a tu.''' + for root, _, files in os.walk(tu_path): + for gcovfile in files: + _, ext = os.path.splitext(gcovfile) + if ext != ".gcov": + continue + gcov_in_path = os.path.join(root, gcovfile) + gcov_out_path = os.path.join( + output, os.path.relpath(gcov_in_path, tu_path)) + if os.path.exists(gcov_out_path): + merge_gcov(gcov_in_path, gcov_out_path) + else: + # No merging needed. + shutil.copyfile(gcov_in_path, gcov_out_path) + + +def main(): + '''Parsing arguments, process each tu dir.''' + parser = argparse.ArgumentParser(description="Merge gcov files from " + "different translation units") + parser.add_argument("--input", "-i", help="Directory containing the input" + " gcov files", required=True) + parser.add_argument("--output", "-o", help="Output directory for gcov" + " files. Warning! Output tree will be cleared!", required=True) + args = parser.parse_args() + + if os.path.exists(args.output): + shutil.rmtree(args.output) + os.mkdir(args.output) + + for tu_dir in os.listdir(args.input): + tu_path = os.path.join(args.input, tu_dir) + if not os.path.isdir(tu_path): + continue + + process_tu(tu_path, args.output) + + +if __name__ == '__main__': + main()