22Parses compiler output with -fdiagnostics-format=json and checks that warnings
33exist only in files that are expected to have warnings.
44"""
5+
56import argparse
7+ from collections import defaultdict
68import json
79import re
810import sys
911from pathlib import Path
1012
1113
12- def extract_warnings_from_compiler_output (compiler_output : str ) -> list [dict ]:
14+ def extract_warnings_from_compiler_output_clang (
15+ compiler_output : str ,
16+ ) -> list [dict ]:
1317 """
14- Extracts warnings from the compiler output when using
15- -fdiagnostics-format=json
18+ Extracts warnings from the compiler output when using clang
19+ """
20+ # Regex to find warnings in the compiler output
21+ clang_warning_regex = re .compile (
22+ r"(?P<file>.*):(?P<line>\d+):(?P<column>\d+): warning: (?P<message>.*)"
23+ )
24+ compiler_warnings = []
25+ for line in compiler_output .splitlines ():
26+ if match := clang_warning_regex .match (line ):
27+ compiler_warnings .append (
28+ {
29+ "file" : match .group ("file" ),
30+ "line" : match .group ("line" ),
31+ "column" : match .group ("column" ),
32+ "message" : match .group ("message" ),
33+ }
34+ )
1635
17- Compiler output as a whole is not a valid json document, but includes many
18- json objects and may include other output that is not json.
36+ return compiler_warnings
37+
38+
39+ def extract_warnings_from_compiler_output_json (
40+ compiler_output : str ,
41+ ) -> list [dict ]:
1942 """
43+ Extracts warnings from the compiler output when using
44+ -fdiagnostics-format=json.
2045
46+ Compiler output as a whole is not a valid json document,
47+ but includes many json objects and may include other output
48+ that is not json.
49+ """
2150 # Regex to find json arrays at the top level of the file
2251 # in the compiler output
2352 json_arrays = re .findall (
24- r"\[(?:[^\ [\]]|\[(?: [^\[\]]|\[[^\[\]]*\]) *\])*\]" , compiler_output
53+ r"\[(?:[^[\]]|\[[^\]] *\])*\]" , compiler_output
2554 )
2655 compiler_warnings = []
2756 for array in json_arrays :
2857 try :
2958 json_data = json .loads (array )
3059 json_objects_in_array = [entry for entry in json_data ]
31- compiler_warnings .extend (
32- [
33- entry
34- for entry in json_objects_in_array
35- if entry .get ("kind" ) == "warning"
36- ]
37- )
60+ warning_list = [
61+ entry
62+ for entry in json_objects_in_array
63+ if entry .get ("kind" ) == "warning"
64+ ]
65+ for warning in warning_list :
66+ locations = warning ["locations" ]
67+ for location in locations :
68+ for key in ["caret" , "start" , "end" ]:
69+ if key in location :
70+ compiler_warnings .append (
71+ {
72+ # Remove leading current directory if present
73+ "file" : location [key ]["file" ].lstrip ("./" ),
74+ "line" : location [key ]["line" ],
75+ "column" : location [key ]["column" ],
76+ "message" : warning ["message" ],
77+ }
78+ )
79+ # Found a caret, start, or end in location so
80+ # break out completely to address next warning
81+ break
82+ else :
83+ continue
84+ break
85+
3886 except json .JSONDecodeError :
3987 continue # Skip malformed JSON
4088
@@ -46,27 +94,16 @@ def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]:
4694 Returns a dictionary where the key is the file and the data is the warnings
4795 in that file
4896 """
49- warnings_by_file = {}
97+ warnings_by_file = defaultdict ( list )
5098 for warning in warnings :
51- locations = warning ["locations" ]
52- for location in locations :
53- for key in ["caret" , "start" , "end" ]:
54- if key in location :
55- file = location [key ]["file" ]
56- file = file .lstrip (
57- "./"
58- ) # Remove leading current directory if present
59- if file not in warnings_by_file :
60- warnings_by_file [file ] = []
61- warnings_by_file [file ].append (warning )
99+ warnings_by_file [warning ["file" ]].append (warning )
62100
63101 return warnings_by_file
64102
65103
66104def get_unexpected_warnings (
67- warnings : list [dict ],
68105 files_with_expected_warnings : set [str ],
69- files_with_warnings : set [str ],
106+ files_with_warnings : dict [str , list [ dict ] ],
70107) -> int :
71108 """
72109 Returns failure status if warnings discovered in list of warnings
@@ -88,13 +125,12 @@ def get_unexpected_warnings(
88125
89126
90127def get_unexpected_improvements (
91- warnings : list [dict ],
92128 files_with_expected_warnings : set [str ],
93- files_with_warnings : set [str ],
129+ files_with_warnings : dict [str , list [ dict ] ],
94130) -> int :
95131 """
96- Returns failure status if there are no warnings in the list of warnings for
97- a file that is in the list of files with expected warnings
132+ Returns failure status if there are no warnings in the list of warnings
133+ for a file that is in the list of files with expected warnings
98134 """
99135 unexpected_improvements = []
100136 for file in files_with_expected_warnings :
@@ -123,7 +159,6 @@ def main(argv: list[str] | None = None) -> int:
123159 "-i" ,
124160 "--warning-ignore-file-path" ,
125161 type = str ,
126- required = True ,
127162 help = "Path to the warning ignore file" ,
128163 )
129164 parser .add_argument (
@@ -141,6 +176,14 @@ def main(argv: list[str] | None = None) -> int:
141176 help = "Flag to fail if files that were expected "
142177 "to have warnings have no warnings" ,
143178 )
179+ parser .add_argument (
180+ "-t" ,
181+ "--compiler-output-type" ,
182+ type = str ,
183+ required = True ,
184+ choices = ["json" , "clang" ],
185+ help = "Type of compiler output file (json or clang)" ,
186+ )
144187
145188 args = parser .parse_args (argv )
146189
@@ -149,44 +192,56 @@ def main(argv: list[str] | None = None) -> int:
149192 # Check that the compiler output file is a valid path
150193 if not Path (args .compiler_output_file_path ).is_file ():
151194 print (
152- "Compiler output file does not exist: "
153- f"{ args .compiler_output_file_path } "
195+ f "Compiler output file does not exist:"
196+ f" { args .compiler_output_file_path } "
154197 )
155198 return 1
156199
157- # Check that the warning ignore file is a valid path
158- if not Path ( args .warning_ignore_file_path ). is_file () :
200+ # Check that a warning ignore file was specified and if so is a valid path
201+ if not args .warning_ignore_file_path :
159202 print (
160- "Warning ignore file does not exist: "
161- f" { args . warning_ignore_file_path } "
203+ "Warning ignore file not specified. "
204+ " Continuing without it (no warnings ignored). "
162205 )
163- return 1
206+ files_with_expected_warnings = set ()
207+ else :
208+ if not Path (args .warning_ignore_file_path ).is_file ():
209+ print (
210+ f"Warning ignore file does not exist:"
211+ f" { args .warning_ignore_file_path } "
212+ )
213+ return 1
214+ with Path (args .warning_ignore_file_path ).open (
215+ encoding = "UTF-8"
216+ ) as clean_files :
217+ files_with_expected_warnings = {
218+ file .strip ()
219+ for file in clean_files
220+ if file .strip () and not file .startswith ("#" )
221+ }
164222
165223 with Path (args .compiler_output_file_path ).open (encoding = "UTF-8" ) as f :
166224 compiler_output_file_contents = f .read ()
167225
168- with Path (args .warning_ignore_file_path ).open (
169- encoding = "UTF-8"
170- ) as clean_files :
171- files_with_expected_warnings = {
172- file .strip ()
173- for file in clean_files
174- if file .strip () and not file .startswith ("#" )
175- }
176-
177- warnings = extract_warnings_from_compiler_output (
178- compiler_output_file_contents
179- )
226+ if args .compiler_output_type == "json" :
227+ warnings = extract_warnings_from_compiler_output_json (
228+ compiler_output_file_contents
229+ )
230+ elif args .compiler_output_type == "clang" :
231+ warnings = extract_warnings_from_compiler_output_clang (
232+ compiler_output_file_contents
233+ )
234+
180235 files_with_warnings = get_warnings_by_file (warnings )
181236
182237 status = get_unexpected_warnings (
183- warnings , files_with_expected_warnings , files_with_warnings
238+ files_with_expected_warnings , files_with_warnings
184239 )
185240 if args .fail_on_regression :
186241 exit_code |= status
187242
188243 status = get_unexpected_improvements (
189- warnings , files_with_expected_warnings , files_with_warnings
244+ files_with_expected_warnings , files_with_warnings
190245 )
191246 if args .fail_on_improvement :
192247 exit_code |= status
0 commit comments