| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | """
 | 
					
						
							|  |  |  | Parses compiler output with -fdiagnostics-format=json and checks that warnings | 
					
						
							|  |  |  | exist only in files that are expected to have warnings. | 
					
						
							|  |  |  | """
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | import argparse | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  | from collections import defaultdict | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | import json | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | from pathlib import Path | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  | from typing import NamedTuple | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class FileWarnings(NamedTuple): | 
					
						
							|  |  |  |     name: str | 
					
						
							|  |  |  |     count: int | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  | def extract_warnings_from_compiler_output_clang( | 
					
						
							|  |  |  |     compiler_output: str, | 
					
						
							|  |  |  | ) -> list[dict]: | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     Extracts warnings from the compiler output when using clang | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     # Regex to find warnings in the compiler output | 
					
						
							|  |  |  |     clang_warning_regex = re.compile( | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |         r"(?P<file>.*):(?P<line>\d+):(?P<column>\d+): warning: " | 
					
						
							|  |  |  |         r"(?P<message>.*) (?P<option>\[-[^\]]+\])$" | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     ) | 
					
						
							|  |  |  |     compiler_warnings = [] | 
					
						
							|  |  |  |     for line in compiler_output.splitlines(): | 
					
						
							|  |  |  |         if match := clang_warning_regex.match(line): | 
					
						
							|  |  |  |             compiler_warnings.append( | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     "file": match.group("file"), | 
					
						
							|  |  |  |                     "line": match.group("line"), | 
					
						
							|  |  |  |                     "column": match.group("column"), | 
					
						
							|  |  |  |                     "message": match.group("message"), | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |                     "option": match.group("option").lstrip("[").rstrip("]"), | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |                 } | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     return compiler_warnings | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def extract_warnings_from_compiler_output_json( | 
					
						
							|  |  |  |     compiler_output: str, | 
					
						
							|  |  |  | ) -> list[dict]: | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     Extracts warnings from the compiler output when using | 
					
						
							|  |  |  |     -fdiagnostics-format=json. | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     Compiler output as a whole is not a valid json document, | 
					
						
							|  |  |  |     but includes many json objects and may include other output | 
					
						
							|  |  |  |     that is not json. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     # Regex to find json arrays at the top level of the file | 
					
						
							|  |  |  |     # in the compiler output | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |     json_arrays = re.findall(r"\[(?:[^[\]]|\[[^]]*])*]", compiler_output) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     compiler_warnings = [] | 
					
						
							|  |  |  |     for array in json_arrays: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             json_data = json.loads(array) | 
					
						
							|  |  |  |             json_objects_in_array = [entry for entry in json_data] | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |             warning_list = [ | 
					
						
							|  |  |  |                 entry | 
					
						
							|  |  |  |                 for entry in json_objects_in_array | 
					
						
							|  |  |  |                 if entry.get("kind") == "warning" | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             for warning in warning_list: | 
					
						
							|  |  |  |                 locations = warning["locations"] | 
					
						
							|  |  |  |                 for location in locations: | 
					
						
							|  |  |  |                     for key in ["caret", "start", "end"]: | 
					
						
							|  |  |  |                         if key in location: | 
					
						
							|  |  |  |                             compiler_warnings.append( | 
					
						
							|  |  |  |                                 { | 
					
						
							|  |  |  |                                     # Remove leading current directory if present | 
					
						
							|  |  |  |                                     "file": location[key]["file"].lstrip("./"), | 
					
						
							|  |  |  |                                     "line": location[key]["line"], | 
					
						
							|  |  |  |                                     "column": location[key]["column"], | 
					
						
							|  |  |  |                                     "message": warning["message"], | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |                                     "option": warning["option"], | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |                                 } | 
					
						
							|  |  |  |                             ) | 
					
						
							|  |  |  |                             # Found a caret, start, or end in location so | 
					
						
							|  |  |  |                             # break out completely to address next warning | 
					
						
							|  |  |  |                             break | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         continue | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         except json.JSONDecodeError: | 
					
						
							|  |  |  |             continue  # Skip malformed JSON | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return compiler_warnings | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Returns a dictionary where the key is the file and the data is the warnings | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |     in that file. Does not include duplicate warnings for a file from list of | 
					
						
							|  |  |  |     provided warnings. | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     warnings_by_file = defaultdict(list) | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |     warnings_added = set() | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     for warning in warnings: | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |         warning_key = ( | 
					
						
							|  |  |  |             f"{warning['file']}-{warning['line']}-" | 
					
						
							|  |  |  |             f"{warning['column']}-{warning['option']}" | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         if warning_key not in warnings_added: | 
					
						
							|  |  |  |             warnings_added.add(warning_key) | 
					
						
							|  |  |  |             warnings_by_file[warning["file"]].append(warning) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return warnings_by_file | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_unexpected_warnings( | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |     files_with_expected_warnings: set[FileWarnings], | 
					
						
							|  |  |  |     files_with_warnings: set[FileWarnings], | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | ) -> int: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Returns failure status if warnings discovered in list of warnings | 
					
						
							|  |  |  |     are associated with a file that is not found in the list of files | 
					
						
							|  |  |  |     with expected warnings | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     unexpected_warnings = [] | 
					
						
							|  |  |  |     for file in files_with_warnings.keys(): | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |         found_file_in_ignore_list = False | 
					
						
							|  |  |  |         for ignore_file in files_with_expected_warnings: | 
					
						
							|  |  |  |             if file == ignore_file.name: | 
					
						
							|  |  |  |                 if len(files_with_warnings[file]) > ignore_file.count: | 
					
						
							|  |  |  |                     unexpected_warnings.extend(files_with_warnings[file]) | 
					
						
							|  |  |  |                 found_file_in_ignore_list = True | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |         if not found_file_in_ignore_list: | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |             unexpected_warnings.extend(files_with_warnings[file]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if unexpected_warnings: | 
					
						
							|  |  |  |         print("Unexpected warnings:") | 
					
						
							|  |  |  |         for warning in unexpected_warnings: | 
					
						
							|  |  |  |             print(warning) | 
					
						
							|  |  |  |         return 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_unexpected_improvements( | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |     files_with_expected_warnings: set[FileWarnings], | 
					
						
							|  |  |  |     files_with_warnings: set[FileWarnings], | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | ) -> int: | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     Returns failure status if there are no warnings in the list of warnings | 
					
						
							|  |  |  |     for a file that is in the list of files with expected warnings | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     unexpected_improvements = [] | 
					
						
							|  |  |  |     for file in files_with_expected_warnings: | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |         if file.name not in files_with_warnings.keys(): | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |             unexpected_improvements.append(file) | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |         elif len(files_with_warnings[file.name]) < file.count: | 
					
						
							|  |  |  |                 unexpected_improvements.append(file) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if unexpected_improvements: | 
					
						
							|  |  |  |         print("Unexpected improvements:") | 
					
						
							|  |  |  |         for file in unexpected_improvements: | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |             print(file.name) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         return 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def main(argv: list[str] | None = None) -> int: | 
					
						
							|  |  |  |     parser = argparse.ArgumentParser() | 
					
						
							|  |  |  |     parser.add_argument( | 
					
						
							| 
									
										
										
										
											2024-07-30 14:49:15 -05:00
										 |  |  |         "-c", | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         "--compiler-output-file-path", | 
					
						
							|  |  |  |         type=str, | 
					
						
							|  |  |  |         required=True, | 
					
						
							|  |  |  |         help="Path to the compiler output file", | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     parser.add_argument( | 
					
						
							| 
									
										
										
										
											2024-07-30 14:49:15 -05:00
										 |  |  |         "-i", | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         "--warning-ignore-file-path", | 
					
						
							|  |  |  |         type=str, | 
					
						
							|  |  |  |         help="Path to the warning ignore file", | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     parser.add_argument( | 
					
						
							| 
									
										
										
										
											2024-07-30 14:49:15 -05:00
										 |  |  |         "-x", | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         "--fail-on-regression", | 
					
						
							|  |  |  |         action="store_true", | 
					
						
							|  |  |  |         default=False, | 
					
						
							|  |  |  |         help="Flag to fail if new warnings are found", | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     parser.add_argument( | 
					
						
							| 
									
										
										
										
											2024-07-30 14:49:15 -05:00
										 |  |  |         "-X", | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         "--fail-on-improvement", | 
					
						
							|  |  |  |         action="store_true", | 
					
						
							|  |  |  |         default=False, | 
					
						
							|  |  |  |         help="Flag to fail if files that were expected " | 
					
						
							|  |  |  |         "to have warnings have no warnings", | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     parser.add_argument( | 
					
						
							|  |  |  |         "-t", | 
					
						
							|  |  |  |         "--compiler-output-type", | 
					
						
							|  |  |  |         type=str, | 
					
						
							|  |  |  |         required=True, | 
					
						
							|  |  |  |         choices=["json", "clang"], | 
					
						
							|  |  |  |         help="Type of compiler output file (json or clang)", | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     args = parser.parse_args(argv) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     exit_code = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Check that the compiler output file is a valid path | 
					
						
							|  |  |  |     if not Path(args.compiler_output_file_path).is_file(): | 
					
						
							|  |  |  |         print( | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |             f"Compiler output file does not exist:" | 
					
						
							|  |  |  |             f" {args.compiler_output_file_path}" | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         ) | 
					
						
							|  |  |  |         return 1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     # Check that a warning ignore file was specified and if so is a valid path | 
					
						
							|  |  |  |     if not args.warning_ignore_file_path: | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         print( | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |             "Warning ignore file not specified." | 
					
						
							|  |  |  |             " Continuing without it (no warnings ignored)." | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |         ) | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |         files_with_expected_warnings = set() | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         if not Path(args.warning_ignore_file_path).is_file(): | 
					
						
							|  |  |  |             print( | 
					
						
							|  |  |  |                 f"Warning ignore file does not exist:" | 
					
						
							|  |  |  |                 f" {args.warning_ignore_file_path}" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             return 1 | 
					
						
							|  |  |  |         with Path(args.warning_ignore_file_path).open( | 
					
						
							|  |  |  |             encoding="UTF-8" | 
					
						
							|  |  |  |         ) as clean_files: | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |             # Files with expected warnings are stored as a set of tuples | 
					
						
							|  |  |  |             # where the first element is the file name and the second element | 
					
						
							|  |  |  |             # is the number of warnings expected in that file | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |             files_with_expected_warnings = { | 
					
						
							| 
									
										
										
										
											2024-08-14 16:03:53 -05:00
										 |  |  |                 FileWarnings(file.strip().split()[0], int(file.strip().split()[1])) | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |                 for file in clean_files | 
					
						
							|  |  |  |                 if file.strip() and not file.startswith("#") | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     with Path(args.compiler_output_file_path).open(encoding="UTF-8") as f: | 
					
						
							|  |  |  |         compiler_output_file_contents = f.read() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |     if args.compiler_output_type == "json": | 
					
						
							|  |  |  |         warnings = extract_warnings_from_compiler_output_json( | 
					
						
							|  |  |  |             compiler_output_file_contents | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     elif args.compiler_output_type == "clang": | 
					
						
							|  |  |  |         warnings = extract_warnings_from_compiler_output_clang( | 
					
						
							|  |  |  |             compiler_output_file_contents | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     files_with_warnings = get_warnings_by_file(warnings) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     status = get_unexpected_warnings( | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |         files_with_expected_warnings, files_with_warnings | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     ) | 
					
						
							|  |  |  |     if args.fail_on_regression: | 
					
						
							|  |  |  |         exit_code |= status | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     status = get_unexpected_improvements( | 
					
						
							| 
									
										
										
										
											2024-08-06 12:26:37 -05:00
										 |  |  |         files_with_expected_warnings, files_with_warnings | 
					
						
							| 
									
										
										
										
											2024-07-27 04:57:44 -05:00
										 |  |  |     ) | 
					
						
							|  |  |  |     if args.fail_on_improvement: | 
					
						
							|  |  |  |         exit_code |= status | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return exit_code | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     sys.exit(main()) |