8000 gh-112301: Compiler warning management tooling by nohlson · Pull Request #121730 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

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

Merged
merged 19 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ jobs:
with:
save: false
- name: Configure CPython
run: ./configure --config-cache --enable-slower-safety --with-pydebug --with-openssl=$OPENSSL_DIR
run: ./configure CFLAGS="-fdiagnostics-format=json" --config-cache --enable-slower-safety --with-pydebug --with-openssl=$OPENSSL_DIR
- name: Build CPython
run: make -j4
- name: Display build info
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/reusable-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,20 @@ jobs:
working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: >-
../cpython-ro-srcdir/configure
CFLAGS="-fdiagnostics-format=json"
--config-cache
--with-pydebug
--enable-slower-safety
--with-openssl=$OPENSSL_DIR
${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }}
- name: Build CPython out-of-tree
working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: make -j4
run: make -j4 &> compiler_output.txt
- name: Display build info
working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: make pythoninfo
- name: Check compiler warnings
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
- name: Remount sources writable for tests
# some tests write to srcdir, lack of pyc files slows down testing
run: sudo mount $CPYTHON_RO_SRCDIR -oremount,rw
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add tooling to check for changes in compiler warnings.
Patch by Nate Ohlson.
3 changes: 3 additions & 0 deletions Tools/build/.warningignore_ubuntu
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.
195 changes: 195 additions & 0 deletions Tools/build/check_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this one?

Copy link
Member

Choose a reason for hiding this comment

The 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 python Tools/build/check_warnings.py

"""
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]:
Copy link
Member
@hugovk hugovk Jul 26, 2024

Choose a reason for hiding this comment

The 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 :)

black --line-length 79 Tools/build/check_warnings.py

Then some comments and long strings will need manual wrapping.

Suggested change
def extract_warnings_from_compiler_output(compiler_output: str) -> list[dict]:
def extract_warnings_from_compiler_output(compiler_output: str) -> list[dict]:

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.

In order to keep the commit history intact, please avoid squashing or amending history and then force-pushing to the PR. Reviewers often want to look at individual commits.

https://devguide.python.org/getting-started/pull-request-lifecycle/#quick-guide

Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

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(
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

return 0


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--compiler-output-file-path",
type=str,
required=True,
help="Path to the compiler output file",
)
parser.add_argument(
"--warning-ignore-file-path",
type=str,
required=True,
help="Path to the warning ignore file",
)
parser.add_argument(
"--fail-on-regression",
action="store_true",
default=False,
help="Flag to fail if new warnings are found",
)
parser.add_argument(
"--fail-on-improvement",
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())
Loading
0