From 2ae9f5367a99342d1603bc98e60b0f31a27831a6 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:31:24 +0000 Subject: [PATCH] fix: don't drop clang-tidy diagnostics when colors are on If `.clang-tidy` sets `UseColor: true`, clang-tidy prints ANSI color codes even when piped, so no line matched NOTE_HEADER and every diagnostic was dropped. Strip the codes from each line before parsing. Fixes #560 --- cpp-linter/src/clang_tools/clang_tidy/mod.rs | 11 ++++-- cpp-linter/src/clang_tools/clang_tidy/test.rs | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/cpp-linter/src/clang_tools/clang_tidy/mod.rs b/cpp-linter/src/clang_tools/clang_tidy/mod.rs index cc49ae54..ec6c21a7 100644 --- a/cpp-linter/src/clang_tools/clang_tidy/mod.rs +++ b/cpp-linter/src/clang_tools/clang_tidy/mod.rs @@ -182,6 +182,11 @@ impl TidyAdvice { /// A regex pattern to capture the clang-tidy note header. const NOTE_HEADER: &str = r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+),?[^\]]*\]$"; +/// A regex pattern to match ANSI color codes. +/// +/// clang-tidy emits them even when piped if `.clang-tidy` sets `UseColor: true`. +const ANSI_ESCAPE: &str = r"\x1b\[[0-9;]*m"; + /// Parses clang-tidy stdout. /// /// Here it helps to have the JSON database deserialized for normalizing paths present @@ -192,6 +197,7 @@ fn parse_tidy_output( repo_root: &Path, ) -> Result, ClangCaptureError> { let note_header = Regex::new(NOTE_HEADER)?; + let ansi_escape = Regex::new(ANSI_ESCAPE)?; let mut notification = None; let mut result = Vec::new(); for line in String::from_utf8(tidy_stdout.to_vec()) @@ -201,7 +207,8 @@ fn parse_tidy_output( })? .lines() { - if let Some(captured) = note_header.captures(line) { + let line = ansi_escape.replace_all(line, ""); + if let Some(captured) = note_header.captures(&line) { // First check that the diagnostic name is a actual diagnostic name. // Sometimes clang-tidy uses square brackets to enclose additional context // about the diagnostic rationale. For example: '[with auto = typename ...]' @@ -267,7 +274,7 @@ fn parse_tidy_output( } else if let Some(note) = &mut notification { // append lines of code that are part of // the previous line's notification - note.suggestion.push(line.to_string()); + note.suggestion.push(line.into_owned()); } } if let Some(note) = notification { diff --git a/cpp-linter/src/clang_tools/clang_tidy/test.rs b/cpp-linter/src/clang_tools/clang_tidy/test.rs index 32bd57e0..4aa71e22 100644 --- a/cpp-linter/src/clang_tools/clang_tidy/test.rs +++ b/cpp-linter/src/clang_tools/clang_tidy/test.rs @@ -223,6 +223,42 @@ TrenchBroom/TrenchBroom/common/test/src/mdl/tst_ReadFreeImageTexture.cpp:44:48: } } +#[test] +fn parse_colored_tidy_output() { + // clang-tidy 18 output when `.clang-tidy` sets `UseColor: true` + // cspell:ignore mwarning mvariable mnote deadcode + let tidy_out = concat!( + "\x1b[1ma.cpp:1:17: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1mvariable name 'x' is too short, expected at least 3 characters [readability-identifier-length]\x1b[0m\n", + " 1 | int main(){ int x = 0; x = 1; return 0; }\x1b[0m\n", + " | \x1b[0;1;32m ^\n", + "\x1b[0m\x1b[1ma.cpp:1:24: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1mValue stored to 'x' is never read [clang-analyzer-deadcode.DeadStores]\x1b[0m\n", + " 1 | int main(){ int x = 0; x = 1; return 0; }\x1b[0m\n", + " | \x1b[0;1;32m ^ ~\n", + "\x1b[0m\x1b[1ma.cpp:1:24: \x1b[0m\x1b[0;1;36mnote: \x1b[0mValue stored to 'x' is never read\x1b[0m\n", + " 1 | int main(){ int x = 0; x = 1; return 0; }\x1b[0m\n", + " | \x1b[0;1;32m ^ ~\n", + "\x1b[0m\n", + ); + let notes = parse_tidy_output(tidy_out.as_bytes(), &None, &PathBuf::from(".")).unwrap(); + let diagnostics = notes + .iter() + .map(|n| n.diagnostic.as_str()) + .collect::>(); + assert_eq!( + diagnostics, + [ + "readability-identifier-length", + "clang-analyzer-deadcode.DeadStores" + ] + ); + assert_eq!(notes[0].filename, "a.cpp"); + assert_eq!((notes[1].line, notes[1].cols), (1, 24)); + for note in ¬es { + assert!(!note.rationale.contains('\x1b')); + assert!(note.suggestion.iter().all(|l| !l.contains('\x1b'))); + } +} + fn dummy_note(filename: &str, fixed_lines: Vec) -> TidyNotification { TidyNotification { filename: filename.to_string(),