Skip to content

Commit 8ac5565

Browse files
nohlsonblurb-it[bot]hugovk
authored
gh-112301: Compiler warning management tooling (#121730)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent bb09ba6 commit 8ac5565

File tree

5 files changed

+205
-2
lines changed

5 files changed

+205
-2
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ jobs:
348348
with:
349349
save: false
350350
- name: Configure CPython
351-
run: ./configure --config-cache --enable-slower-safety --with-pydebug --with-openssl=$OPENSSL_DIR
351+
run: ./configure CFLAGS="-fdiagnostics-format=json" --config-cache --enable-slower-safety --with-pydebug --with-openssl=$OPENSSL_DIR
352352
- name: Build CPython
353353
run: make -j4
354354
- name: Display build info

.github/workflows/reusable-ubuntu.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,20 @@ jobs:
6767
working-directory: ${{ env.CPYTHON_BUILDDIR }}
6868
run: >-
6969
../cpython-ro-srcdir/configure
70+
CFLAGS="-fdiagnostics-format=json"
7071
--config-cache
7172
--with-pydebug
7273
--enable-slower-safety
7374
--with-openssl=$OPENSSL_DIR
7475
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
7576
- name: Build CPython out-of-tree
7677
working-directory: ${{ env.CPYTHON_BUILDDIR }}
77-
run: make -j4
78+
run: make -j4 &> compiler_output.txt
7879
- name: Display build info
7980
working-directory: ${{ env.CPYTHON_BUILDDIR }}
8081
run: make pythoninfo
82+
- name: Check compiler warnings
83+
run: python Tools/build/check_warnings.py --compiler-output-file-path=${{ env.CPYTHON_BUILDDIR }}/compiler_output.txt --warning-ignore-file-path ${GITHUB_WORKSPACE}/Tools/build/.warningignore_ubuntu
8184
- name: Remount sources writable for tests
8285
# some tests write to srcdir, lack of pyc files slows down testing
8386
run: sudo mount $CPYTHON_RO_SRCDIR -oremount,rw
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add tooling to check for changes in compiler warnings.
2+
Patch by Nate Ohlson.

Tools/build/.warningignore_ubuntu

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Files listed will be ignored by the compiler warning checker
2+
# for the Ubuntu/build and test job.
3+
# Keep lines sorted lexicographically to help avoid merge conflicts.

Tools/build/check_warnings.py

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Parses compiler output with -fdiagnostics-format=json and checks that warnings
4+
exist only in files that are expected to have warnings.
5+
"""
6+
import argparse
7+
import json
8+
import re
9+
import sys
10+
from pathlib import Path
11+
12+
13+
def extract_warnings_from_compiler_output(compiler_output: str) -> list[dict]:
14+
"""
15+
Extracts warnings from the compiler output when using
16+
-fdiagnostics-format=json
17+
18+
Compiler output as a whole is not a valid json document, but includes many
19+
json objects and may include other output that is not json.
20+
"""
21+
22+
# Regex to find json arrays at the top level of the file
23+
# in the compiler output
24+
json_arrays = re.findall(
25+
r"\[(?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*\]", compiler_output
26+
)
27+
compiler_warnings = []
28+
for array in json_arrays:
29+
try:
30+
json_data = json.loads(array)
31+
json_objects_in_array = [entry for entry in json_data]
32+
compiler_warnings.extend(
33+
[
34+
entry
35+
for entry in json_objects_in_array
36+
if entry.get("kind") == "warning"
37+
]
38+
)
39+
except json.JSONDecodeError:
40+
continue # Skip malformed JSON
41+
42+
return compiler_warnings
43+
44+
45+
def get_warnings_by_file(warnings: list[dict]) -> dict[str, list[dict]]:
46+
"""
47+
Returns a dictionary where the key is the file and the data is the warnings
48+
in that file
49+
"""
50+
warnings_by_file = {}
51+
for warning in warnings:
52+
locations = warning["locations"]
53+
for location in locations:
54+
for key in ["caret", "start", "end"]:
55+
if key in location:
56+
file = location[key]["file"]
57+
file = file.lstrip(
58+
"./"
59+
) # Remove leading current directory if present
60+
if file not in warnings_by_file:
61+
warnings_by_file[file] = []
62+
warnings_by_file[file].append(warning)
63+
64+
return warnings_by_file
65+
66+
67+
def get_unexpected_warnings(
68+
warnings: list[dict],
69+
files_with_expected_warnings: set[str],
70+
files_with_warnings: set[str],
71+
) -> int:
72+
"""
73+
Returns failure status if warnings discovered in list of warnings
74+
are associated with a file that is not found in the list of files
75+
with expected warnings
76+
"""
77+
unexpected_warnings = []
78+
for file in files_with_warnings.keys():
79+
if file not in files_with_expected_warnings:
80+
unexpected_warnings.extend(files_with_warnings[file])
81+
82+
if unexpected_warnings:
83+
print("Unexpected warnings:")
84+
for warning in unexpected_warnings:
85+
print(warning)
86+
return 1
87+
88+
return 0
89+
90+
91+
def get_unexpected_improvements(
92+
warnings: list[dict],
93+
files_with_expected_warnings: set[str],
94+
files_with_warnings: set[str],
95+
) -> int:
96+
"""
97+
Returns failure status if there are no warnings in the list of warnings for
98+
a file that is in the list of files with expected warnings
99+
"""
100+
unexpected_improvements = []
101+
for file in files_with_expected_warnings:
102+
if file not in files_with_warnings.keys():
103+
unexpected_improvements.append(file)
104+
105+
if unexpected_improvements:
106+
print("Unexpected improvements:")
107+
for file in unexpected_improvements:
108+
print(file)
109+
return 1
110+
111+
return 0
112+
113+
114+
def main(argv: list[str] | None = None) -> int:
115+
parser = argparse.ArgumentParser()
116+
parser.add_argument(
117+
"--compiler-output-file-path",
118+
type=str,
119+
required=True,
120+
help="Path to the compiler output file",
121+
)
122+
parser.add_argument(
123+
"--warning-ignore-file-path",
124+
type=str,
125+
required=True,
126+
help="Path to the warning ignore file",
127+
)
128+
parser.add_argument(
129+
"--fail-on-regression",
130+
action="store_true",
131+
default=False,
132+
help="Flag to fail if new warnings are found",
133+
)
134+
parser.add_argument(
135+
"--fail-on-improvement",
136+
action="store_true",
137+
default=False,
138+
help="Flag to fail if files that were expected "
139+
"to have warnings have no warnings",
140+
)
141+
142+
args = parser.parse_args(argv)
143+
144+
exit_code = 0
145+
146+
# Check that the compiler output file is a valid path
147+
if not Path(args.compiler_output_file_path).is_file():
148+
print(
149+
"Compiler output file does not exist: "
150+
f"{args.compiler_output_file_path}"
151+
)
152+
return 1
153+
154+
# Check that the warning ignore file is a valid path
155+
if not Path(args.warning_ignore_file_path).is_file():
156+
print(
157+
"Warning ignore file does not exist: "
158+
f"{args.warning_ignore_file_path}"
159+
)
160+
return 1
161+
162+
with Path(args.compiler_output_file_path).open(encoding="UTF-8") as f:
163+
compiler_output_file_contents = f.read()
164+
165+
with Path(args.warning_ignore_file_path).open(
166+
encoding="UTF-8"
167+
) as clean_files:
168+
files_with_expected_warnings = {
169+
file.strip()
170+
for file in clean_files
171+
if file.strip() and not file.startswith("#")
172+
}
173+
174+
warnings = extract_warnings_from_compiler_output(
175+
compiler_output_file_contents
176+
)
177+
files_with_warnings = get_warnings_by_file(warnings)
178+
179+
status = get_unexpected_warnings(
180+
warnings, files_with_expected_warnings, files_with_warnings
181+
)
182+
if args.fail_on_regression:
183+
exit_code |= status
184+
185+
status = get_unexpected_improvements(
186+
warnings, files_with_expected_warnings, files_with_warnings
187+
)
188+
if args.fail_on_improvement:
189+
exit_code |= status
190+
191+
return exit_code
192+
193+
194+
if __name__ == "__main__":
195+
sys.exit(main())

0 commit comments

Comments
 (0)