Skip to content

Commit dd1bf07

Browse files
committed
fix(cli): propagate verbose compiler diagnostics
1 parent ecd1f06 commit dd1bf07

8 files changed

Lines changed: 177 additions & 28 deletions

File tree

‎include/vix/cli/ErrorHandler.hpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ namespace vix::cli
2323
class ErrorHandler
2424
{
2525
public:
26+
static bool printBuildErrors(
27+
const std::string &buildLog,
28+
const fs::path &sourceFile,
29+
const std::string &contextMessage,
30+
bool verbose);
31+
32+
// Compatibility overload for callers that do not expose a verbose mode.
2633
static bool printBuildErrors(
2734
const std::string &buildLog,
2835
const fs::path &sourceFile,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @file BuildFailureClassification.hpp
3+
* @brief Small, log-only guards used before selecting a build failure class.
4+
*/
5+
#ifndef VIX_BUILD_FAILURE_CLASSIFICATION_HPP
6+
#define VIX_BUILD_FAILURE_CLASSIFICATION_HPP
7+
8+
#include <cctype>
9+
#include <string_view>
10+
11+
namespace vix::cli::errors
12+
{
13+
inline bool contains_source_compiler_diagnostic(std::string_view log) noexcept
14+
{
15+
std::size_t lineStart = 0;
16+
17+
while (lineStart < log.size())
18+
{
19+
const std::size_t lineEnd = log.find('\n', lineStart);
20+
const std::string_view line = log.substr(
21+
lineStart,
22+
(lineEnd == std::string_view::npos ? log.size() : lineEnd) - lineStart);
23+
const std::size_t error = line.find(": error:");
24+
const std::size_t fatal = line.find(": fatal error:");
25+
const std::size_t marker = error != std::string_view::npos ? error : fatal;
26+
27+
if (marker != std::string_view::npos)
28+
{
29+
const std::string_view location = line.substr(0, marker);
30+
const std::size_t columnColon = location.rfind(':');
31+
const std::size_t lineColon =
32+
columnColon == std::string_view::npos
33+
? std::string_view::npos
34+
: location.rfind(':', columnColon == 0 ? 0 : columnColon - 1);
35+
36+
const auto is_number = [](std::string_view value)
37+
{
38+
return !value.empty() &&
39+
value.find_first_not_of("0123456789") == std::string_view::npos;
40+
};
41+
42+
if (columnColon != std::string_view::npos &&
43+
lineColon != std::string_view::npos &&
44+
is_number(location.substr(columnColon + 1)) &&
45+
is_number(location.substr(lineColon + 1, columnColon - lineColon - 1)))
46+
{
47+
return true;
48+
}
49+
}
50+
51+
if (lineEnd == std::string_view::npos)
52+
break;
53+
lineStart = lineEnd + 1;
54+
}
55+
56+
return false;
57+
}
58+
} // namespace vix::cli::errors
59+
60+
#endif
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @file CompilerDiagnosticPresentation.hpp
3+
* @brief Presentation limits for parsed compiler diagnostics.
4+
*/
5+
#ifndef VIX_COMPILER_DIAGNOSTIC_PRESENTATION_HPP
6+
#define VIX_COMPILER_DIAGNOSTIC_PRESENTATION_HPP
7+
8+
#include <cstddef>
9+
10+
namespace vix::cli::errors
11+
{
12+
struct CompilerDiagnosticPresentation
13+
{
14+
std::size_t shown = 0;
15+
std::size_t hidden = 0;
16+
17+
[[nodiscard]] bool show_verbose_hint() const noexcept
18+
{
19+
return hidden != 0;
20+
}
21+
};
22+
23+
inline CompilerDiagnosticPresentation compiler_diagnostic_presentation(
24+
std::size_t total,
25+
bool verbose) noexcept
26+
{
27+
const std::size_t shown = verbose ? total : (total == 0 ? 0 : 1);
28+
return {shown, total - shown};
29+
}
30+
} // namespace vix::cli::errors
31+
32+
#endif

‎src/ErrorHandler.cpp‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <vix/cli/errors/ClangGccParser.hpp>
1717
#include <vix/cli/errors/CodeFrame.hpp>
1818
#include <vix/cli/errors/CompilerError.hpp>
19+
#include <vix/cli/errors/CompilerDiagnosticPresentation.hpp>
1920
#include <vix/cli/errors/ErrorContext.hpp>
2021
#include <vix/cli/errors/ErrorPipeline.hpp>
2122
#include <vix/cli/errors/RawLogDetectors.hpp>
@@ -259,6 +260,19 @@ namespace vix::cli
259260
const std::string &buildLog,
260261
const fs::path &sourceFile,
261262
const std::string &contextMessage)
263+
{
264+
return printBuildErrors(
265+
buildLog,
266+
sourceFile,
267+
contextMessage,
268+
false);
269+
}
270+
271+
bool ErrorHandler::printBuildErrors(
272+
const std::string &buildLog,
273+
const fs::path &sourceFile,
274+
const std::string &contextMessage,
275+
bool verbose)
262276
{
263277
using namespace vix::cli::errors;
264278

@@ -333,7 +347,10 @@ namespace vix::cli
333347
ErrorContext ctx{sourceFile, contextMessage, buildLog};
334348
ErrorPipeline pipeline;
335349

336-
if (pipeline.tryHandle(errors, ctx))
350+
// Focused rules are intentionally concise. For a diagnostic batch they
351+
// can select a secondary cascading error; render the first parsed error
352+
// instead. Verbose mode always renders the complete parsed set.
353+
if (!verbose && errors.size() == 1 && pipeline.tryHandle(errors, ctx))
337354
return true;
338355

339356
std::vector<CompilerError> unique;
@@ -374,15 +391,8 @@ namespace vix::cli
374391
return false;
375392
}
376393

377-
/*
378-
* Show the first compiler error by default.
379-
*
380-
* Compiler errors frequently cascade: once the parser cannot
381-
* understand one declaration, it may report several secondary
382-
* errors. Showing the first error keeps the output focused for
383-
* beginners.
384-
*/
385-
const std::size_t maxToShow = 1;
394+
const CompilerDiagnosticPresentation presentation =
395+
compiler_diagnostic_presentation(unique.size(), verbose);
386396

387397
CodeFrameOptions codeFrameOptions;
388398
codeFrameOptions.contextLines = 1;
@@ -391,7 +401,7 @@ namespace vix::cli
391401
codeFrameOptions.leadingBlankLine = false;
392402

393403
for (std::size_t i = 0;
394-
i < unique.size() && i < maxToShow;
404+
i < presentation.shown;
395405
++i)
396406
{
397407
const CompilerError &err = unique[i];
@@ -420,17 +430,12 @@ namespace vix::cli
420430
print_hint(hint);
421431
}
422432

423-
const std::size_t hiddenCount =
424-
unique.size() > maxToShow
425-
? unique.size() - maxToShow
426-
: 0;
427-
428-
if (hiddenCount > 0)
433+
if (presentation.show_verbose_hint())
429434
{
430435
std::cerr << MUTED
431-
<< hiddenCount
436+
<< presentation.hidden
432437
<< " more compiler error"
433-
<< (hiddenCount == 1 ? "" : "s")
438+
<< (presentation.hidden == 1 ? "" : "s")
434439
<< " hidden. Run with --verbose to see them."
435440
<< RESET
436441
<< "\n";

‎src/commands/BuildCommand.cpp‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3721,17 +3721,17 @@ namespace vix::commands::BuildCommand
37213721
return value == "debug" || value == "trace";
37223722
}
37233723

3724-
static void print_debug_command_if_enabled(
3724+
static void print_failure_command_if_requested(
37253725
const process::Options &opt,
37263726
const process::ExecResult &result)
37273727
{
3728-
if (!debug_build_details_enabled(opt))
3728+
if (!opt.verbose && !debug_build_details_enabled(opt))
37293729
return;
37303730

37313731
if (result.displayCommand.empty())
37323732
return;
37333733

3734-
std::cerr << GRAY
3734+
std::cerr << LABEL
37353735
<< "command: "
37363736
<< RESET
37373737
<< result.displayCommand
@@ -5836,7 +5836,8 @@ namespace vix::commands::BuildCommand
58365836
vix::cli::ErrorHandler::printBuildErrors(
58375837
log,
58385838
plan_.cmakeSourceDir / "CMakeLists.txt",
5839-
"CMake configure failed");
5839+
"CMake configure failed",
5840+
opt_.verbose);
58405841

58415842
if (!handled && opt_.verbose && !log.empty())
58425843
{
@@ -5849,7 +5850,7 @@ namespace vix::commands::BuildCommand
58495850
if (!handled)
58505851
hint("run `vix build --log configure` for the captured configure output");
58515852

5852-
print_debug_command_if_enabled(opt_, r);
5853+
print_failure_command_if_requested(opt_, r);
58535854
}
58545855

58555856
return exitCode;
@@ -6197,8 +6198,7 @@ namespace vix::commands::BuildCommand
61976198

61986199
const bool legacyBuildQuiet =
61996200
opt_.quiet ||
6200-
(liveBuild &&
6201-
!showRawBuildOutput);
6201+
!showRawBuildOutput;
62026202

62036203
const process::ExecResult r =
62046204
measurePhase(
@@ -6245,14 +6245,15 @@ namespace vix::commands::BuildCommand
62456245
vix::cli::ErrorHandler::printBuildErrors(
62466246
log,
62476247
plan_.cmakeSourceDir / "CMakeLists.txt",
6248-
"Build failed");
6248+
"Build failed",
6249+
opt_.verbose);
62496250

62506251
if (!opt_.quiet)
62516252
{
62526253
if (!handled)
62536254
hint("run `vix build --log build` for the captured build output");
62546255

6255-
print_debug_command_if_enabled(opt_, r);
6256+
print_failure_command_if_requested(opt_, r);
62566257
}
62576258
return exitCode;
62586259
}

‎src/errors/RawLogDetectors.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*
1313
*/
1414
#include <vix/cli/errors/RawLogDetectors.hpp>
15+
#include <vix/cli/errors/BuildFailureClassification.hpp>
1516
#include <vix/cli/errors/CodeFrame.hpp>
1617
#include <vix/cli/errors/CompilerError.hpp>
1718
#include <vix/cli/errors/runtime/IRuntimeErrorRule.hpp>
@@ -885,6 +886,12 @@ namespace vix::cli::errors
885886
const std::filesystem::path &sourceFile,
886887
[[maybe_unused]] const std::string &contextMessage)
887888
{
889+
// A failed incremental Ninja invocation can still contain a stale linker
890+
// line. A source-location compiler diagnostic is stronger evidence and
891+
// must not be presented as a link failure.
892+
if (contains_source_compiler_diagnostic(buildLog))
893+
return false;
894+
888895
if (handleLinkerErrors(buildLog, sourceFile))
889896
return true;
890897

‎tests/CMakeLists.txt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ target_include_directories(vix_cli_clang_gcc_parser_tests PRIVATE
6363
)
6464
add_test(NAME vix_cli_clang_gcc_parser_tests COMMAND vix_cli_clang_gcc_parser_tests)
6565

66+
add_executable(vix_cli_compiler_diagnostic_presentation_tests
67+
CompilerDiagnosticPresentationTests.cpp
68+
)
69+
target_include_directories(vix_cli_compiler_diagnostic_presentation_tests PRIVATE
70+
${CMAKE_CURRENT_SOURCE_DIR}/../include
71+
)
72+
add_test(
73+
NAME vix_cli_compiler_diagnostic_presentation_tests
74+
COMMAND vix_cli_compiler_diagnostic_presentation_tests
75+
)
76+
6677
add_executable(vix_cli_git_progress_parser_tests GitProgressParserTests.cpp)
6778
target_include_directories(vix_cli_git_progress_parser_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
6879
add_test(NAME vix_cli_git_progress_parser_tests COMMAND vix_cli_git_progress_parser_tests)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include <vix/cli/errors/BuildFailureClassification.hpp>
2+
#include <vix/cli/errors/CompilerDiagnosticPresentation.hpp>
3+
4+
#include <stdexcept>
5+
6+
int main()
7+
{
8+
using namespace vix::cli::errors;
9+
10+
const auto concise = compiler_diagnostic_presentation(8, false);
11+
if (concise.shown != 1 || concise.hidden != 7 || !concise.show_verbose_hint())
12+
throw std::runtime_error("default diagnostics must remain concise");
13+
14+
const auto verbose = compiler_diagnostic_presentation(8, true);
15+
if (verbose.shown != 8 || verbose.hidden != 0 || verbose.show_verbose_hint())
16+
throw std::runtime_error("verbose diagnostics must show every parsed error");
17+
18+
const char *compilerThenStaleLinker =
19+
"/tmp/project/main.cpp:12:9: error: invalid expression\n"
20+
"collect2: error: ld returned 1 exit status\n";
21+
if (!contains_source_compiler_diagnostic(compilerThenStaleLinker))
22+
throw std::runtime_error("source compiler diagnostics must win over stale linker output");
23+
24+
if (contains_source_compiler_diagnostic("mold: error: undefined symbol: main\n"))
25+
throw std::runtime_error("a linker-only failure is not a compiler diagnostic");
26+
}

0 commit comments

Comments
 (0)