[llvm] [llvm-cov] Migrate gcov to OptTable (PR #224955)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 21 02:04:37 PDT 2026


https://github.com/MaskRay updated https://github.com/llvm/llvm-project/pull/224955

>From 5ad0307b7dbece4836ca2b44b558e782bb41dff1 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Sun, 20 Sep 2026 11:59:47 -0700
Subject: [PATCH] [llvm-cov] Migrate gcov to OptTable

llvm-cov gcov is the last user of cl::Grouping. Parse its options with
an OptTable that enables grouped short options, as the other
binutils-style tools do, so that cl::Grouping can be removed.

Spellings follow the other migrated tools: long options take `--` only,
so `-gcno` and `-gcda=` become `--gcno` and `--gcda=`. `--help` now
lists only gcov's options rather than every cl:: option linked into
llvm-cov, and an unknown option or a missing source file is reported
as `llvm-cov gcov: error: ...`.

LLM-aided
---
 llvm/docs/CommandGuide/llvm-cov.md            |   4 +-
 llvm/test/tools/llvm-cov/gcov/basic.test      |  14 +-
 llvm/tools/llvm-cov/CMakeLists.txt            |   7 +
 llvm/tools/llvm-cov/GcovOpts.td               |  51 ++++++
 llvm/tools/llvm-cov/gcov.cpp                  | 168 ++++++++----------
 .../gn/secondary/llvm/tools/llvm-cov/BUILD.gn |   9 +
 .../llvm-project-overlay/llvm/BUILD.bazel     |  11 ++
 7 files changed, 157 insertions(+), 107 deletions(-)
 create mode 100644 llvm/tools/llvm-cov/GcovOpts.td

diff --git a/llvm/docs/CommandGuide/llvm-cov.md b/llvm/docs/CommandGuide/llvm-cov.md
index 11508c00d2598..54ac3e05c8b36 100644
--- a/llvm/docs/CommandGuide/llvm-cov.md
+++ b/llvm/docs/CommandGuide/llvm-cov.md
@@ -107,7 +107,7 @@ an entire source file.
 :::
 
 :::{option} --help
-Display available options (--help-hidden for more).
+Display available options.
 :::
 
 :::{option} -l, --long-file-names
@@ -157,7 +157,7 @@ Include unconditional branches in the output for the --branch-probabilities
 option.
 :::
 
-:::{option} -version
+:::{option} --version
 Display the version of llvm-cov.
 :::
 
diff --git a/llvm/test/tools/llvm-cov/gcov/basic.test b/llvm/test/tools/llvm-cov/gcov/basic.test
index 7557739add8ba..9c849d4824a8b 100644
--- a/llvm/test/tools/llvm-cov/gcov/basic.test
+++ b/llvm/test/tools/llvm-cov/gcov/basic.test
@@ -57,11 +57,11 @@ RUN: llvm-cov gcov -l test_paths.cpp
 RUN: ls test_paths.cpp##a.c.gcov
 
 # Long file names and preserve paths.
-RUN: mkdir -p src && llvm-cov gcov -lp -gcno test_paths.gcno -gcda test_paths.gcda src/../test_paths.cpp
+RUN: mkdir -p src && llvm-cov gcov -lp --gcno test_paths.gcno --gcda test_paths.gcda src/../test_paths.cpp
 RUN: ls src#^#test_paths.cpp##src#a.c.gcov
 
 # Hash pathnames.
-RUN: llvm-cov gcov -x -gcno test_paths.gcno -gcda test_paths.gcda src/../test_paths.cpp
+RUN: llvm-cov gcov -x --gcno test_paths.gcno --gcda test_paths.gcda src/../test_paths.cpp
 RUN: ls a.c##0c546a4dd99c1774b7b06e4fad16158c.gcov
 
 # Function summaries. This changes stdout, but not the gcov files.
@@ -153,7 +153,7 @@ RUN: FileCheck --input-file=test.h.gcov %s --check-prefix=H-C
 H-C: unconditional  0 taken 1
 
 # Missing gcda file just gives 0 counts.
-RUN: llvm-cov gcov test.c -gcda=no_such_gcda_file | FileCheck %s --check-prefix=NO-GCDA
+RUN: llvm-cov gcov test.c --gcda=no_such_gcda_file | FileCheck %s --check-prefix=NO-GCDA
 RUN: diff -ub test_no_gcda.cpp.gcov test.cpp.gcov
 RUN: diff -ub test_no_gcda.h.gcov test.h.gcov
 NO-GCDA:       File 'test.cpp'
@@ -165,19 +165,19 @@ NO-GCDA-NEXT:  Lines executed:0.00% of 1
 NO-GCDA-NEXT:  Creating 'test.h.gcov'
 
 # Invalid gcno file.
-RUN: llvm-cov gcov test.c -gcno=test_read_fail.gcno
+RUN: llvm-cov gcov test.c --gcno=test_read_fail.gcno
 
 # Not a .gcda file. Error but keep the .gcov output.
 RUN: echo invalid > not.gcda
-RUN: llvm-cov gcov test.c -gcda=not.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
+RUN: llvm-cov gcov test.c --gcda=not.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
 RUN: FileCheck %s --check-prefix=NOT-GCDA < %t.err
 NOT-GCDA: not.gcda:not a gcov data file
 
 # Bad file checksum on gcda.
-RUN: llvm-cov gcov test.c -gcda=test_file_checksum_fail.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
+RUN: llvm-cov gcov test.c --gcda=test_file_checksum_fail.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
 
 # Bad function checksum on gcda
-RUN: llvm-cov gcov test.c -gcda=test_func_checksum_fail.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
+RUN: llvm-cov gcov test.c --gcda=test_func_checksum_fail.gcda 2> %t.err | FileCheck %s --check-prefix=NO-GCDA
 
 # Has arcs from exit blocks
 RUN-DISABLED: llvm-cov gcov test_exit_block_arcs.c 2>&1 | FileCheck %s -check-prefix=EXIT_BLOCK_ARCS
diff --git a/llvm/tools/llvm-cov/CMakeLists.txt b/llvm/tools/llvm-cov/CMakeLists.txt
index 6602a2319cb57..41e826ac88a46 100644
--- a/llvm/tools/llvm-cov/CMakeLists.txt
+++ b/llvm/tools/llvm-cov/CMakeLists.txt
@@ -2,11 +2,16 @@ set(LLVM_LINK_COMPONENTS
   Core
   Support
   Object
+  Option
   Coverage
   ProfileData
   TargetParser
   )
 
+set(LLVM_TARGET_DEFINITIONS GcovOpts.td)
+tablegen(LLVM GcovOpts.inc -gen-opt-parser-defs)
+add_public_tablegen_target(CovGcovOptsTableGen)
+
 add_llvm_tool(llvm-cov
   llvm-cov.cpp
   gcov.cpp
@@ -20,6 +25,8 @@ add_llvm_tool(llvm-cov
   SourceCoverageViewHTML.cpp
   SourceCoverageViewText.cpp
   TestingSupport.cpp
+  DEPENDS
+  CovGcovOptsTableGen
   )
 
 target_link_libraries(llvm-cov PRIVATE LLVMHTTP LLVMDebuginfod)
diff --git a/llvm/tools/llvm-cov/GcovOpts.td b/llvm/tools/llvm-cov/GcovOpts.td
new file mode 100644
index 0000000000000..fbf1123bb424d
--- /dev/null
+++ b/llvm/tools/llvm-cov/GcovOpts.td
@@ -0,0 +1,51 @@
+include "llvm/Option/OptParser.td"
+
+class F<string letter, string help> : Flag<["-"], letter>, HelpText<help>;
+class FF<string name, string help> : Flag<["--"], name>, HelpText<help>;
+
+multiclass Eq<string name, string help> {
+  def NAME #_EQ : Joined<["--"], name #"=">, HelpText<help>;
+  def : Separate<["--"], name>, Alias<!cast<Joined>(NAME #_EQ)>;
+}
+
+def all_blocks : FF<"all-blocks", "Display all basic blocks">;
+def branch_counts : FF<"branch-counts", "Display branch counts instead of percentages (requires -b)">;
+def branch_probabilities : FF<"branch-probabilities", "Display branch probabilities">;
+def demangled_names : FF<"demangled-names", "Demangle function names">;
+def dump_gcov : FF<"dump", "Dump the gcov file to stderr">;
+def function_summaries : FF<"function-summaries", "Show coverage for each function">;
+defm gcda : Eq<"gcda", "Override inferred gcda file">, MetaVarName<"<file>">;
+defm gcno : Eq<"gcno", "Override inferred gcno file">, MetaVarName<"<file>">;
+def hash_filenames : FF<"hash-filenames", "Hash long pathnames">;
+def help : FF<"help", "Display this help">;
+// Supported by gcov 4.9~8. gcov 9 (GCC r265587) removed --intermediate-format
+// and -i was changed to mean --json-format. We consider this format still
+// useful and support -i.
+def intermediate_format : FF<"intermediate-format", "Output .gcov in intermediate text format">;
+def long_file_names : FF<"long-file-names", "Prefix filenames with the main file">;
+def no_output : FF<"no-output", "Do not output any .gcov files">;
+defm object_directory : Eq<"object-directory", "Find objects in DIR or based on FILE's path">, MetaVarName<"<DIR>">;
+def : Joined<["--"], "object-file=">, Alias<object_directory_EQ>, HelpText<"Alias for --object-directory">, MetaVarName<"<FILE>">;
+def : Separate<["--"], "object-file">, Alias<object_directory_EQ>;
+def preserve_paths : FF<"preserve-paths", "Preserve path components">;
+def relative_only : FF<"relative-only", "Only dump files with relative paths or absolute paths with the prefix specified by -s">;
+defm source_prefix : Eq<"source-prefix", "Source prefix to elide">, MetaVarName<"<prefix>">;
+def stdout : FF<"stdout", "Print to stdout">;
+def unconditional_branches : FF<"unconditional-branches", "Display unconditional branch info (requires -b)">;
+def version : FF<"version", "Display the version">;
+
+def : F<"a", "Alias for --all-blocks">, Alias<all_blocks>;
+def : F<"b", "Alias for --branch-probabilities">, Alias<branch_probabilities>;
+def : F<"c", "Alias for --branch-counts">, Alias<branch_counts>;
+def : F<"f", "Alias for --function-summaries">, Alias<function_summaries>;
+def : F<"i", "Alias for --intermediate-format">, Alias<intermediate_format>;
+def : F<"l", "Alias for --long-file-names">, Alias<long_file_names>;
+def : F<"m", "Alias for --demangled-names">, Alias<demangled_names>;
+def : F<"n", "Alias for --no-output">, Alias<no_output>;
+def : JoinedOrSeparate<["-"], "o">, Alias<object_directory_EQ>, HelpText<"Alias for --object-directory">, MetaVarName<"<DIR|FILE>">;
+def : F<"p", "Alias for --preserve-paths">, Alias<preserve_paths>;
+def : F<"r", "Alias for --relative-only">, Alias<relative_only>;
+def : JoinedOrSeparate<["-"], "s">, Alias<source_prefix_EQ>, HelpText<"Alias for --source-prefix">, MetaVarName<"<prefix>">;
+def : F<"t", "Alias for --stdout">, Alias<stdout>;
+def : F<"u", "Alias for --unconditional-branches">, Alias<unconditional_branches>;
+def : F<"x", "Alias for --hash-filenames">, Alias<hash_filenames>;
diff --git a/llvm/tools/llvm-cov/gcov.cpp b/llvm/tools/llvm-cov/gcov.cpp
index 00ea12415b220..f500d2a3e8d5e 100644
--- a/llvm/tools/llvm-cov/gcov.cpp
+++ b/llvm/tools/llvm-cov/gcov.cpp
@@ -12,17 +12,42 @@
 
 #include "llvm/ProfileData/GCOV.h"
 #include "llvm/ADT/SmallString.h"
+#include "llvm/Option/ArgList.h"
+#include "llvm/Option/OptTable.h"
+#include "llvm/Option/Option.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/Errc.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/StringSaver.h"
+#include "llvm/Support/WithColor.h"
 #include <system_error>
 using namespace llvm;
 
+namespace {
+enum ID {
+  OPT_INVALID = 0, // This is not an option ID.
+#define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__),
+#include "GcovOpts.inc"
+#undef OPTION
+};
+
+using namespace llvm::opt;
+#define OPTTABLE_CODE
+#include "GcovOpts.inc"
+
+class GcovOptTable : public opt::OptTable {
+public:
+  GcovOptTable() : OptTable(optionTables()) {
+    setGroupedShortOptions(true);
+    setDashDashParsing(true);
+  }
+};
+} // namespace
+
 static void reportCoverage(StringRef SourceFile, StringRef ObjectDir,
-                           const std::string &InputGCNO,
-                           const std::string &InputGCDA, bool DumpGCOV,
-                           const GCOV::Options &Options) {
+                           StringRef InputGCNO, StringRef InputGCDA,
+                           bool DumpGCOV, const GCOV::Options &Options) {
   SmallString<128> CoverageFileStem(ObjectDir);
   if (CoverageFileStem.empty()) {
     // If no directory was specified with -o, look next to the source file.
@@ -35,10 +60,10 @@ static void reportCoverage(StringRef SourceFile, StringRef ObjectDir,
     // A file was given. Ignore the source file and look next to this file.
     sys::path::replace_extension(CoverageFileStem, "");
 
-  std::string GCNO =
-      InputGCNO.empty() ? std::string(CoverageFileStem) + ".gcno" : InputGCNO;
-  std::string GCDA =
-      InputGCDA.empty() ? std::string(CoverageFileStem) + ".gcda" : InputGCDA;
+  std::string GCNO = InputGCNO.empty() ? std::string(CoverageFileStem) + ".gcno"
+                                       : InputGCNO.str();
+  std::string GCDA = InputGCDA.empty() ? std::string(CoverageFileStem) + ".gcda"
+                                       : InputGCDA.str();
   GCOVFile GF;
 
   // Open .gcda and .gcda without requiring a NUL terminator. The concurrent
@@ -81,97 +106,44 @@ static void reportCoverage(StringRef SourceFile, StringRef ObjectDir,
 }
 
 int gcovMain(int argc, const char *argv[]) {
-  cl::list<std::string> SourceFiles(cl::Positional, cl::OneOrMore,
-                                    cl::desc("SOURCEFILE"));
-
-  cl::opt<bool> AllBlocks("a", cl::Grouping, cl::init(false),
-                          cl::desc("Display all basic blocks"));
-  cl::alias AllBlocksA("all-blocks", cl::aliasopt(AllBlocks));
-
-  cl::opt<bool> BranchProb("b", cl::Grouping, cl::init(false),
-                           cl::desc("Display branch probabilities"));
-  cl::alias BranchProbA("branch-probabilities", cl::aliasopt(BranchProb));
-
-  cl::opt<bool> BranchCount("c", cl::Grouping, cl::init(false),
-                            cl::desc("Display branch counts instead "
-                                     "of percentages (requires -b)"));
-  cl::alias BranchCountA("branch-counts", cl::aliasopt(BranchCount));
-
-  cl::opt<bool> LongNames("l", cl::Grouping, cl::init(false),
-                          cl::desc("Prefix filenames with the main file"));
-  cl::alias LongNamesA("long-file-names", cl::aliasopt(LongNames));
-
-  cl::opt<bool> FuncSummary("f", cl::Grouping, cl::init(false),
-                            cl::desc("Show coverage for each function"));
-  cl::alias FuncSummaryA("function-summaries", cl::aliasopt(FuncSummary));
-
-  // Supported by gcov 4.9~8. gcov 9 (GCC r265587) removed --intermediate-format
-  // and -i was changed to mean --json-format. We consider this format still
-  // useful and support -i.
-  cl::opt<bool> Intermediate(
-      "intermediate-format", cl::init(false),
-      cl::desc("Output .gcov in intermediate text format"));
-  cl::alias IntermediateA("i", cl::desc("Alias for --intermediate-format"),
-                          cl::Grouping, cl::NotHidden,
-                          cl::aliasopt(Intermediate));
-
-  cl::opt<bool> Demangle("demangled-names", cl::init(false),
-                         cl::desc("Demangle function names"));
-  cl::alias DemangleA("m", cl::desc("Alias for --demangled-names"),
-                      cl::Grouping, cl::NotHidden, cl::aliasopt(Demangle));
-
-  cl::opt<bool> NoOutput("n", cl::Grouping, cl::init(false),
-                         cl::desc("Do not output any .gcov files"));
-  cl::alias NoOutputA("no-output", cl::aliasopt(NoOutput));
-
-  cl::opt<std::string> ObjectDir(
-      "o", cl::value_desc("DIR|FILE"), cl::init(""),
-      cl::desc("Find objects in DIR or based on FILE's path"));
-  cl::alias ObjectDirA("object-directory", cl::aliasopt(ObjectDir));
-  cl::alias ObjectDirB("object-file", cl::aliasopt(ObjectDir));
-
-  cl::opt<bool> PreservePaths("p", cl::Grouping, cl::init(false),
-                              cl::desc("Preserve path components"));
-  cl::alias PreservePathsA("preserve-paths", cl::aliasopt(PreservePaths));
-
-  cl::opt<bool> RelativeOnly(
-      "r", cl::Grouping,
-      cl::desc("Only dump files with relative paths or absolute paths with the "
-               "prefix specified by -s"));
-  cl::alias RelativeOnlyA("relative-only", cl::aliasopt(RelativeOnly));
-  cl::opt<std::string> SourcePrefix("s", cl::desc("Source prefix to elide"));
-  cl::alias SourcePrefixA("source-prefix", cl::aliasopt(SourcePrefix));
-
-  cl::opt<bool> UseStdout("t", cl::Grouping, cl::init(false),
-                          cl::desc("Print to stdout"));
-  cl::alias UseStdoutA("stdout", cl::aliasopt(UseStdout));
-
-  cl::opt<bool> UncondBranch("u", cl::Grouping, cl::init(false),
-                             cl::desc("Display unconditional branch info "
-                                      "(requires -b)"));
-  cl::alias UncondBranchA("unconditional-branches", cl::aliasopt(UncondBranch));
-
-  cl::opt<bool> HashFilenames("x", cl::Grouping, cl::init(false),
-                              cl::desc("Hash long pathnames"));
-  cl::alias HashFilenamesA("hash-filenames", cl::aliasopt(HashFilenames));
-
-
-  cl::OptionCategory DebugCat("Internal and debugging options");
-  cl::opt<bool> DumpGCOV("dump", cl::init(false), cl::cat(DebugCat),
-                         cl::desc("Dump the gcov file to stderr"));
-  cl::opt<std::string> InputGCNO("gcno", cl::cat(DebugCat), cl::init(""),
-                                 cl::desc("Override inferred gcno file"));
-  cl::opt<std::string> InputGCDA("gcda", cl::cat(DebugCat), cl::init(""),
-                                 cl::desc("Override inferred gcda file"));
-
-  cl::ParseCommandLineOptions(argc, argv, "LLVM code coverage tool\n");
-
-  GCOV::Options Options(AllBlocks, BranchProb, BranchCount, FuncSummary,
-                        PreservePaths, UncondBranch, Intermediate, LongNames,
-                        Demangle, NoOutput, RelativeOnly, UseStdout,
-                        HashFilenames, SourcePrefix);
-
-  for (const auto &SourceFile : SourceFiles)
+  StringRef ToolName = sys::path::filename(argv[0]);
+  auto Error = [&](const Twine &Msg) {
+    WithColor::error(errs(), ToolName) << Msg << '\n';
+    exit(1);
+  };
+  BumpPtrAllocator A;
+  StringSaver Saver(A);
+  GcovOptTable Tbl;
+  opt::InputArgList Args =
+      Tbl.parseArgs(argc, const_cast<char **>(argv), OPT_UNKNOWN, Saver, Error);
+  if (Args.hasArg(OPT_help)) {
+    Tbl.printHelp(outs(), (ToolName + " [options] SOURCEFILE").str().c_str(),
+                  "LLVM code coverage tool");
+    return 0;
+  }
+  if (Args.hasArg(OPT_version)) {
+    cl::PrintVersionMessage();
+    return 0;
+  }
+  std::vector<std::string> SourceFiles = Args.getAllArgValues(OPT_INPUT);
+  if (SourceFiles.empty())
+    Error("no source file specified");
+
+  GCOV::Options Options(
+      Args.hasArg(OPT_all_blocks), Args.hasArg(OPT_branch_probabilities),
+      Args.hasArg(OPT_branch_counts), Args.hasArg(OPT_function_summaries),
+      Args.hasArg(OPT_preserve_paths), Args.hasArg(OPT_unconditional_branches),
+      Args.hasArg(OPT_intermediate_format), Args.hasArg(OPT_long_file_names),
+      Args.hasArg(OPT_demangled_names), Args.hasArg(OPT_no_output),
+      Args.hasArg(OPT_relative_only), Args.hasArg(OPT_stdout),
+      Args.hasArg(OPT_hash_filenames),
+      Args.getLastArgValue(OPT_source_prefix_EQ).str());
+
+  StringRef ObjectDir = Args.getLastArgValue(OPT_object_directory_EQ);
+  StringRef InputGCNO = Args.getLastArgValue(OPT_gcno_EQ);
+  StringRef InputGCDA = Args.getLastArgValue(OPT_gcda_EQ);
+  bool DumpGCOV = Args.hasArg(OPT_dump_gcov);
+  for (const std::string &SourceFile : SourceFiles)
     reportCoverage(SourceFile, ObjectDir, InputGCNO, InputGCDA, DumpGCOV,
                    Options);
   return 0;
diff --git a/llvm/utils/gn/secondary/llvm/tools/llvm-cov/BUILD.gn b/llvm/utils/gn/secondary/llvm/tools/llvm-cov/BUILD.gn
index bf2493c1820a9..3fa2f04df93d4 100644
--- a/llvm/utils/gn/secondary/llvm/tools/llvm-cov/BUILD.gn
+++ b/llvm/utils/gn/secondary/llvm/tools/llvm-cov/BUILD.gn
@@ -1,10 +1,19 @@
+import("//llvm/utils/TableGen/tablegen.gni")
+
+tablegen("GcovOpts") {
+  visibility = [ ":llvm-cov" ]
+  args = [ "-gen-opt-parser-defs" ]
+}
+
 executable("llvm-cov") {
   deps = [
+    ":GcovOpts",
     "//llvm/include/llvm/Config:llvm-config",
     "//llvm/lib/Debuginfod",
     "//llvm/lib/HTTP",
     "//llvm/lib/IR",
     "//llvm/lib/Object",
+    "//llvm/lib/Option",
     "//llvm/lib/ProfileData",
     "//llvm/lib/ProfileData/Coverage",
     "//llvm/lib/Support",
diff --git a/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel b/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
index b7765075c72fb..fe23e55ade755 100644
--- a/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
@@ -5244,6 +5244,15 @@ cc_binary(
     ],
 )
 
+gentbl_cc_library(
+    name = "CovGcovOptsTableGen",
+    strip_include_prefix = "tools/llvm-cov",
+    tbl_outs = {"tools/llvm-cov/GcovOpts.inc": ["-gen-opt-parser-defs"]},
+    tblgen = ":llvm-tblgen",
+    td_file = "tools/llvm-cov/GcovOpts.td",
+    deps = [":OptParserTdFiles"],
+)
+
 cc_binary(
     name = "llvm-cov",
     srcs = glob([
@@ -5253,11 +5262,13 @@ cc_binary(
     copts = llvm_copts,
     stamp = 0,
     deps = [
+        ":CovGcovOptsTableGen",
         ":Coverage",
         ":Debuginfod",
         ":HTTP",
         ":Instrumentation",
         ":Object",
+        ":Option",
         ":ProfileData",
         ":Support",
         ":TargetParser",



More information about the llvm-commits mailing list