-
-
Notifications
You must be signed in to change notification settings - Fork 32k
gh-112301: Compiler warning management tooling #121730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5148727
3bd5a10
6813003
ab4d754
615d228
fc0a60b
7793d80
99715d2
bc44ec2
e2ca75f
bd1634e
b07b1d6
e1954a5
5935143
c19d953
b9b9500
0c0fc31
e568d6c
9f6281e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add tooling to check for changes in compiler warnings. | ||
hugovk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Patch by Nate Ohlson. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Files listed will be ignored by the compiler warning checker | ||
# for the Ubuntu/build and test job. | ||
# Keep lines sorted lexicographically to help avoid merge conflicts. |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,195 @@ | ||||||||
#!/usr/bin/env python3 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant to comment this too: no, I don't think we do because the file does not have the executable bit and we're running it via |
||||||||
""" | ||||||||
Parses compiler output with -fdiagnostics-format=json and checks that warnings | ||||||||
exist only in files that are expected to have warnings. | ||||||||
""" | ||||||||
import argparse | ||||||||
import json | ||||||||
import re | ||||||||
import sys | ||||||||
from pathlib import Path | ||||||||
|
||||||||
|
||||||||
def extract_warnings_from_compiler_output(compiler_output: str) -> list[dict]: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks almost good to merge, just some style nits :) Technically we're outside the stdlib so strictly speaking PEP 8 doesn't apply, but let's follow it anyway. For example, please could you add extra newlines before these defs and wrap things to 79 chars? The easiest way is to run Black (which we don't run on the stdlib but again we're outside the stdlib :)
Then some comments and long strings will need manual wrapping.
Suggested change
Also, please don't force push in this repo, it makes it easier to review new commits, and we squash merge everything at the end anyway. I think force-pushing is sometimes to blame for pinging lots of unrelated CODEOWNERS too.
https://devguide.python.org/getting-started/pull-request-lifecycle/#quick-guide Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to adhere to PEP8. After a rebase there were several commits that came from branches from different forks. Not really sure how that happened but I had to revert to my last commit and force push to get rid of them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! I just pushed a commit to also wrap comments and strings to 79 chars as well. |
||||||||
""" | ||||||||
Extracts warnings from the compiler output when using | ||||||||
-fdiagnostics-format=json | ||||||||
|
||||||||
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. | ||||||||
""" | ||||||||
|
||||||||
# Regex to find json arrays at the top level of the file | ||||||||
# in the compiler output | ||||||||
json_arrays = re.findall( | ||||||||
r"\[(?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*\]", compiler_output | ||||||||
) | ||||||||
compiler_warnings = [] | ||||||||
for array in json_arrays: | ||||||||
try: | ||||||||
json_data = json.loads(array) | ||||||||
json_objects_in_array = [entry for entry in json_data] | ||||||||
compiler_warnings.extend( | ||||||||
[ | ||||||||
entry | ||||||||
for entry in json_objects_in_array | ||||||||
if entry.get("kind") == "warning" | ||||||||
] | ||||||||
) | ||||||||
except json.JSONDecodeError: | ||||||||
continue # Skip malformed JSON | ||||||||
sethmlarson marked this conversation as resolved.
Show resolved
Hide resolved
sethmlarson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
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 | ||||||||
in that file | ||||||||
""" | ||||||||
warnings_by_file = {} | ||||||||
for warning in warnings: | ||||||||
locations = warning["locations"] | ||||||||
for location in locations: | ||||||||
for key in ["caret", "start", "end"]: | ||||||||
if key in location: | ||||||||
file = location[key]["file"] | ||||||||
file = file.lstrip( | ||||||||
"./" | ||||||||
) # Remove leading current directory if present | ||||||||
if file not in warnings_by_file: | ||||||||
warnings_by_file[file] = [] | ||||||||
warnings_by_file[file].append(warning) | ||||||||
|
||||||||
return warnings_by_file | ||||||||
|
||||||||
|
||||||||
def get_unexpected_warnings( | ||||||||
sethmlarson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
warnings: list[dict], | ||||||||
files_with_expected_warnings: set[str], | ||||||||
files_with_warnings: set[str], | ||||||||
) -> 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(): | ||||||||
if file not in files_with_expected_warnings: | ||||||||
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( | ||||||||
warnings: list[dict], | ||||||||
files_with_expected_warnings: set[str], | ||||||||
files_with_warnings: set[str], | ||||||||
) -> int: | ||||||||
""" | ||||||||
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 | ||||||||
""" | ||||||||
unexpected_improvements = [] | ||||||||
for file in files_with_expected_warnings: | ||||||||
if file not in files_with_warnings.keys(): | ||||||||
unexpected_improvements.append(file) | ||||||||
|
||||||||
if unexpected_improvements: | ||||||||
print("Unexpected improvements:") | ||||||||
for file in unexpected_improvements: | ||||||||
print(file) | ||||||||
return 1 | F438 tr>||||||||
|
||||||||
return 0 | ||||||||
|
||||||||
|
||||||||
def main(argv: list[str] | None = None) -> int: | ||||||||
parser = argparse.ArgumentParser() | ||||||||
parser.add_argument( | ||||||||
"--compiler-output-file-path", | ||||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
type=str, | ||||||||
required=True, | ||||||||
help="Path to the compiler output file", | ||||||||
) | ||||||||
parser.add_argument( | ||||||||
"--warning-ignore-file-path", | ||||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
type=str, | ||||||||
required=True, | ||||||||
help="Path to the warning ignore file", | ||||||||
) | ||||||||
parser.add_argument( | ||||||||
"--fail-on-regression", | ||||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
action="store_true", | ||||||||
default=False, | ||||||||
help="Flag to fail if new warnings are found", | ||||||||
) | ||||||||
parser.add_argument( | ||||||||
"--fail-on-improvement", | ||||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
action="store_true", | ||||||||
default=False, | ||||||||
help="Flag to fail if files that were expected " | ||||||||
"to have warnings have no warnings", | ||||||||
) | ||||||||
|
||||||||
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( | ||||||||
"Compiler output file does not exist: " | ||||||||
f"{args.compiler_output_file_path}" | ||||||||
) | ||||||||
return 1 | ||||||||
|
||||||||
# Check that the warning ignore file is a valid path | ||||||||
if not Path(args.warning_ignore_file_path).is_file(): | ||||||||
print( | ||||||||
"Warning ignore file does not exist: " | ||||||||
f"{args.warning_ignore_file_path}" | ||||||||
) | ||||||||
return 1 | ||||||||
|
||||||||
with Path(args.compiler_output_file_path).open(encoding="UTF-8") as f: | ||||||||
compiler_output_file_contents = f.read() | ||||||||
|
||||||||
with Path(args.warning_ignore_file_path).open( | ||||||||
encoding="UTF-8" | ||||||||
) as clean_files: | ||||||||
files_with_expected_warnings = { | ||||||||
file.strip() | ||||||||
for file in clean_files | ||||||||
if file.strip() and not file.startswith("#") | ||||||||
} | ||||||||
|
||||||||
warnings = extract_warnings_from_compiler_output( | ||||||||
compiler_output_file_contents | ||||||||
) | ||||||||
files_with_warnings = get_warnings_by_file(warnings) | ||||||||
|
||||||||
status = get_unexpected_warnings( | ||||||||
warnings, files_with_expected_warnings, files_with_warnings | ||||||||
) | ||||||||
if args.fail_on_regression: | ||||||||
exit_code |= status | ||||||||
|
||||||||
status = get_unexpected_improvements( | ||||||||
warnings, files_with_expected_warnings, files_with_warnings | ||||||||
) | ||||||||
if args.fail_on_improvement: | ||||||||
exit_code |= status | ||||||||
|
||||||||
return exit_code | ||||||||
|
||||||||
|
||||||||
if __name__ == "__main__": | ||||||||
sys.exit(main()) |
Uh oh!
There was an error while loading. Please reload this page.