2
2
Parses compiler output with -fdiagnostics-format=json and checks that warnings
3
3
exist only in files that are expected to have warnings.
4
4
"""
5
+
5
6
import argparse
7
+ from collections import defaultdict
6
8
import json
7
9
import re
8
10
import sys
9
11
from pathlib import Path
10
12
11
13
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 ]:
13
17
"""
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
+ )
16
35
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 ]:
19
42
"""
43
+ Extracts warnings from the compiler output when using
44
+ -fdiagnostics-format=json.
20
45
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
+ """
21
50
# Regex to find json arrays at the top level of the file
22
51
# in the compiler output
23
52
json_arrays = re .findall (
24
- r"\[(?:[^\ [\]]|\[(?: [^\[\]]|\[[^\[\]]*\]) *\])*\]" , compiler_output
53
+ r"\[(?:[^[\]]|\[[^\]] *\])*\]" , compiler_output
25
54
)
26
55
compiler_warnings = []
27
56
for array in json_arrays :
28
57
try :
29
58
json_data = json .loads (array )
30
59
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
+
38
86
except json .JSONDecodeError :
39
87
continue # Skip malformed JSON
40
88
@@ -46,27 +94,16 @@ def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]:
46
94
Returns a dictionary where the key is the file and the data is the warnings
47
95
in that file
48
96
"""
49
- warnings_by_file = {}
97
+ warnings_by_file = defaultdict ( list )
50
98
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 )
62
100
63
101
return warnings_by_file
64
102
65
103
66
104
def get_unexpected_warnings (
67
- warnings : list [dict ],
68
105
files_with_expected_warnings : set [str ],
69
- files_with_warnings : set [str ],
106
+ files_with_warnings : dict [str , list [ dict ] ],
70
107
) -> int :
71
108
"""
72
109
Returns failure status if warnings discovered in list of warnings
@@ -88,13 +125,12 @@ def get_unexpected_warnings(
88
125
89
126
90
127
def get_unexpected_improvements (
91
- warnings : list [dict ],
92
128
files_with_expected_warnings : set [str ],
93
- files_with_warnings : set [str ],
129
+ files_with_warnings : dict [str , list [ dict ] ],
94
130
) -> int :
95
131
"""
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
98
134
"""
99
135
unexpected_improvements = []
100
136
for file in files_with_expected_warnings :
@@ -123,7 +159,6 @@ def main(argv: list[str] | None = None) -> int:
123
159
"-i" ,
124
160
"--warning-ignore-file-path" ,
125
161
type = str ,
126
- required = True ,
127
162
help = "Path to the warning ignore file" ,
128
163
)
129
164
parser .add_argument (
@@ -141,6 +176,14 @@ def main(argv: list[str] | None = None) -> int:
141
176
help = "Flag to fail if files that were expected "
142
177
"to have warnings have no warnings" ,
143
178
)
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
+ )
144
187
145
188
args = parser .parse_args (argv )
146
189
@@ -149,44 +192,56 @@ def main(argv: list[str] | None = None) -> int:
149
192
# Check that the compiler output file is a valid path
150
193
if not Path (args .compiler_output_file_path ).is_file ():
151
194
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 } "
154
197
)
155
198
return 1
156
199
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 :
159
202
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). "
162
205
)
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
+ }
164
222
165
223
with Path (args .compiler_output_file_path ).open (encoding = "UTF-8" ) as f :
166
224
compiler_output_file_contents = f .read ()
167
225
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
+
180
235
files_with_warnings = get_warnings_by_file (warnings )
181
236
182
237
status = get_unexpected_warnings (
183
- warnings , files_with_expected_warnings , files_with_warnings
238
+ files_with_expected_warnings , files_with_warnings
184
239
)
185
240
if args .fail_on_regression :
186
241
exit_code |= status
187
242
188
243
status = get_unexpected_improvements (
189
- warnings , files_with_expected_warnings , files_with_warnings
244
+ files_with_expected_warnings , files_with_warnings
190
245
)
191
246
if args .fail_on_improvement :
192
247
exit_code |= status
0 commit comments