diff --git a/tools/report-converter/codechecker_report_converter/report/__init__.py b/tools/report-converter/codechecker_report_converter/report/__init__.py index 525ced65be..72ec311f42 100644 --- a/tools/report-converter/codechecker_report_converter/report/__init__.py +++ b/tools/report-converter/codechecker_report_converter/report/__init__.py @@ -13,7 +13,9 @@ import json import logging import os +import re +from codechecker_report_converter.util import trim_path_prefixes from typing import Callable, Dict, List, Optional, Protocol, Set, Tuple from .. import util @@ -395,6 +397,45 @@ def trim_path_prefixes(self, path_prefixes: Optional[List[str]] = None): ): event.file.trim(path_prefixes) + # Also trim file paths in the content of Reports + if path_prefixes: + self.message = self._trim_path_in_text(self.message, path_prefixes) + + if self.static_message: + self.static_message = self._trim_path_in_text( + self.static_message, path_prefixes + ) + + for event in self.bug_path_events: + event.message = self._trim_path_in_text( + event.message, path_prefixes + ) + + for note in self.notes: + note.message = self._trim_path_in_text( + note.message, path_prefixes + ) + + for macro in self.macro_expansions: + macro.message = self._trim_path_in_text( + macro.message, path_prefixes + ) + + def _trim_path_in_text(self, text: str, path_prefixes: List[str]) -> str: + """ + Finds file paths in text and trims their prefixes using the same logic + as trim_path_prefixes from util.py. + """ + if not path_prefixes: + return text + + result = text + for path in util.find_paths_in_text(text): + trimmed_path = util.trim_path_prefixes(path, path_prefixes) + result = result.replace(path, trimmed_path) + + return result + @property def files(self) -> Set[File]: """ Returns all referenced file paths. """ diff --git a/tools/report-converter/codechecker_report_converter/util.py b/tools/report-converter/codechecker_report_converter/util.py index 789f8001f5..214aa2798c 100644 --- a/tools/report-converter/codechecker_report_converter/util.py +++ b/tools/report-converter/codechecker_report_converter/util.py @@ -18,6 +18,22 @@ LOG = logging.getLogger('report-converter') +# Regular expression patterns for path matching +DRIVE_LETTER = r'[A-Za-z]:' +PATH_SEPARATOR = r'[/\\]' +PATH_COMPONENT = r'[^/\s\\]+' +FILE_EXTENSION = r'\.[a-zA-Z0-9]+' +PATH_PATTERN = f'(?:{DRIVE_LETTER})?{PATH_SEPARATOR}{PATH_COMPONENT}' \ + f'(?:{PATH_SEPARATOR}{PATH_COMPONENT})*(?:{FILE_EXTENSION})?' + + +def find_paths_in_text(text: str) -> List[str]: + """ + Find all potential file paths in the given text. + Handles both Unix and Windows paths. + """ + return [match.group() for match in re.finditer(PATH_PATTERN, text)] + def get_last_mod_time(file_path: str) -> Optional[float]: """ Return the last modification time of a file. """ diff --git a/tools/report-converter/tests/unit/util/test_trim_path_prefix.py b/tools/report-converter/tests/unit/util/test_trim_path_prefix.py index 0a6cf3de30..21b8421b08 100644 --- a/tools/report-converter/tests/unit/util/test_trim_path_prefix.py +++ b/tools/report-converter/tests/unit/util/test_trim_path_prefix.py @@ -12,6 +12,12 @@ import unittest from codechecker_report_converter.util import trim_path_prefixes +from codechecker_report_converter.report import ( + BugPathEvent, + File, + MacroExpansion, + Report, +) class TrimPathPrefixTestCase(unittest.TestCase): @@ -70,3 +76,270 @@ def test_prefix_blob(self): self.assertEqual('my_proj/x.cpp', trim_path_prefixes('/home/jsmith/my_proj/x.cpp', ['/home/jsmith/'])) + + def test_remove_only_root_prefix(self): + """Test removing only the root '/' prefix from paths.""" + + test_paths = ["/a/b/c", "/foo.txt", "/dir/subdir/file.cpp", "/"] + + expected_paths = ["/a/b/c", "/foo.txt", "/dir/subdir/file.cpp", "/"] + + for test_path, expected in zip(test_paths, expected_paths): + self.assertEqual( + expected, + trim_path_prefixes(test_path, ["/"]), + f"Failed to handle root prefix in {test_path}", + ) + + def test_trim_path_in_message(self): + """ + Test trimming path prefixes in report messages and bug path events. + """ + + test_file = File("/path/to/workspace/src/example.cpp") + + test_cases = [ + # Simple message with one path + { + "name": "simple_message", + "message": ("Error in file " + "/path/to/workspace/src/example.cpp:10:20"), + "bug_path_events": [ + BugPathEvent( + "Found issue at " + "/path/to/workspace/include/header.h:5:10", + File("/path/to/workspace/include/header.h"), + 5, + 10, + ) + ], + "expected_message": "Error in file src/example.cpp:10:20", + "expected_bug_path_messages": [ + "Found issue at include/header.h:5:10" + ], + }, + # Complex message with multiple paths + { + "name": "complex_message", + "message": ( + "Multiple errors: " + "/path/to/workspace/src/example.cpp:10:20 and " + "/path/to/workspace/include/header.h:5:10" + ), + "bug_path_events": [], + "expected_message": ( + "Multiple errors: src/example.cpp:10:20 and " + "include/header.h:5:10" + ), + "expected_bug_path_messages": [], + }, + ] + + for case in test_cases: + report = Report( + file=test_file, + line=10, + column=20, + message=case["message"], + checker_name="test-checker", + severity="HIGH", + ) + + for event in case["bug_path_events"]: + report.bug_path_events.append(event) + + report.trim_path_prefixes(["/path/to/workspace"]) + + self.assertEqual( + case["expected_message"], + report.message, + f"Failed to trim message in {case['name']} case", + ) + + for i, expected_msg in enumerate( + case["expected_bug_path_messages"] + ): + # Note: Bug path events are 1-indexed in the report + self.assertEqual( + expected_msg, + report.bug_path_events[i + 1].message, + "Failed to trim bug path event message " + "in {case['name']} case", + ) + + def test_trim_path_in_complex_message(self): + + test_file = File("/path/to/workspace/src/example.cpp") + + message = ( + "Multiple errors: " + "/path/to/workspace/src/example.cpp:10:20 and " + "/path/to/workspace/include/header.h:5:10" + ) + + report = Report( + file=test_file, + line=10, + column=20, + message=message, + checker_name="test-checker", + severity="HIGH", + ) + + report.trim_path_prefixes(["/path/to/workspace"]) + + expected = ( + "Multiple errors: src/example.cpp:10:20 and " + "include/header.h:5:10" + ) + self.assertEqual(expected, report.message) + + def test_trim_path_in_macro_expansions(self): + """ + Test trimming path prefixes in macro expansions with various scenarios. + """ + + test_file = File("/path/to/workspace/src/example.cpp") + + test_cases = [ + # Single macro with simple path + { + "name": "single_macro", + "files": [File("/path/to/workspace/include/macro.h")], + "messages": [ + "Macro expanded from " + "/path/to/workspace/include/macro.h:5:10" + ], + "names": ["TEST_MACRO"], + "lines": [5], + "columns": [10], + "expected_messages": [ + "Macro expanded from include/macro.h:5:10" + ], + "expected_paths": ["include/macro.h"], + }, + # Multiple macros + { + "name": "multiple_macros", + "files": [ + File("/path/to/workspace/include/macro1.h"), + File("/path/to/workspace/include/macro2.h"), + ], + "messages": [ + "First macro from " + "/path/to/workspace/include/macro1.h:5:10", + "Second macro from " + "/path/to/workspace/include/macro2.h:15:20", + ], + "names": ["MACRO1", "MACRO2"], + "lines": [5, 15], + "columns": [10, 20], + "expected_messages": [ + "First macro from include/macro1.h:5:10", + "Second macro from include/macro2.h:15:20", + ], + "expected_paths": ["include/macro1.h", "include/macro2.h"], + }, + # Macro with multiple path references + { + "name": "multiple_paths", + "files": [File("/path/to/workspace/include/macro.h")], + "messages": [ + "Macro expanded from " + "/path/to/workspace/include/macro.h:5:10 " + "includes /path/to/workspace/include/header.h:3:4" + ], + "names": ["TEST_MACRO"], + "lines": [5], + "columns": [10], + "expected_messages": [ + "Macro expanded from include/macro.h:5:10 " + "includes include/header.h:3:4" + ], + "expected_paths": ["include/macro.h"], + }, + ] + + for case in test_cases: + macros = [] + for i in range(len(case["files"])): + macros.append( + MacroExpansion( + message=case["messages"][i], + name=case["names"][i], + file=case["files"][i], + line=case["lines"][i], + column=case["columns"][i], + ) + ) + + report = Report( + file=test_file, + line=10, + column=20, + message=f"Error in macro expansion - {case['name']}", + checker_name="test-checker", + severity="HIGH", + macro_expansions=macros, + ) + + report.trim_path_prefixes(["/path/to/workspace"]) + + for i, (expected_msg, expected_path) in enumerate( + zip(case["expected_messages"], case["expected_paths"]) + ): + self.assertEqual( + expected_msg, + report.macro_expansions[i].message, + f"Failed to trim message in {case['name']} case", + ) + self.assertEqual( + expected_path, + report.macro_expansions[i].file.path, + f"Failed to trim file path in {case['name']} case", + ) + + def test_macro_expansion_edge_cases(self): + """Test trimming path prefixes in macro expansions with edge cases.""" + + test_file = File("/path/to/workspace/src/example.cpp") + + edge_cases = [ + (File(""), "Empty file path"), + (File("/"), "Root path only"), + (File("/path/to/workspace/"), "Directory path ending with slash"), + (File("relative/path.h"), "Relative path"), + ] + + for file, desc in edge_cases: + macro = MacroExpansion( + message=f"Macro with {desc}: {file.path}", + name="TEST_MACRO", + file=file, + line=1, + column=1, + ) + + report = Report( + file=test_file, + line=10, + column=20, + message=f"Error in macro expansion - {desc}", + checker_name="test-checker", + severity="HIGH", + macro_expansions=[macro], + ) + + report.trim_path_prefixes(["/path/to/workspace"]) + + if desc in ["Empty file path", "Root path only"]: + self.assertEqual( + file.path, report.macro_expansions[0].file.path + ) + elif desc == "Directory path ending with slash": + self.assertEqual("", report.macro_expansions[0].file.path) + else: + # Relative path unchanged + self.assertEqual( + file.path, report.macro_expansions[0].file.path + )