| #!/usr/bin/env python3 |
| # Copyright (C) 2024 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import os |
| import argparse |
| |
| EXPECTATIONS_PATH = '../../Source/{project}/SaferCPPExpectations/{checker_type}Expectations' |
| CHECKERS = ['MemoryUnsafeCastChecker', 'NoUncountedMemberChecker', 'NoUncheckedPtrMemberChecker', 'RefCntblBaseVirtualDtor', |
| 'UncheckedCallArgsChecker', 'UncheckedLocalVarsChecker', 'UncountedCallArgsChecker', 'UncountedLambdaCapturesChecker', 'UncountedLocalVarsChecker'] |
| PROJECTS = ['JavaScriptCore', 'WebCore', 'WebDriver', 'WebGPU', 'WebInspectorUI', 'WebKit', 'WebKitLegacy', 'WTF'] |
| |
| |
| def parser(): |
| parser = argparse.ArgumentParser(description='Automated tooling for updating safer CPP expectations', epilog='Example: update-safer-cpp-expectations -p WebKit --RefCntblBaseVirtualDtor platform/Scrollbar.h --UncountedCallArgsChecker platform/ScrollAnimator.h') |
| |
| checkers_group = parser.add_argument_group('checker arguments', 'List files to update for each checker') |
| for checker in CHECKERS: |
| checkers_group.add_argument(f'--{checker}', type=str, nargs='+') |
| |
| parser.add_argument( |
| '--project', '-p', |
| choices=PROJECTS, |
| help='Specify which project expectations you want to update' |
| ) |
| parser.add_argument( |
| '--find-expectations', '-f', |
| dest='expected_file', |
| default=None, |
| help='Check if the given file has expected failures' |
| ) |
| parser.add_argument( |
| '--add-expected-failures', |
| dest='add', |
| action='store_true', |
| default=False, |
| help='OVERRIDE: Add expected failures to the expectations files in SaferCPPExpectations' |
| ) |
| parser.add_argument( |
| '--unexpected-results-file', '-r', |
| dest='unexpected_results_file', |
| default=None, |
| help='Path to unexpected results file' |
| ) |
| return parser.parse_args() |
| |
| |
| def modify_expectations_for_checker_from_file(checker_type, unexpected_contents, project, add=False): |
| path_to_expectations = os.path.join(os.path.dirname(__file__), EXPECTATIONS_PATH.format(project=project, checker_type=checker_type)) |
| with open(path_to_expectations) as expectations_file: |
| expectations = expectations_file.readlines() |
| prev_len = len(expectations) |
| for line in unexpected_contents: |
| if '=>' in line: |
| new_checker_type = line.split()[-1] |
| modify_expectations_for_checker_from_file(new_checker_type, unexpected_contents, project, add) |
| elif line.strip(): |
| if not add: |
| try: |
| expectations.remove(line) |
| except ValueError: |
| print(f'Error: {line.strip()} is not in {checker_type}Expectations!') |
| elif line not in expectations: |
| expectations.append(line) |
| else: |
| print(f'Error: {line.strip()} is already in {checker_type}Expectations!\n') |
| with open(path_to_expectations, 'w') as expectations_file: |
| expectations_file.writelines(sorted(expectations)) |
| print(f'Updated expectations for {checker_type}!') |
| if not add: |
| print(f'Removed {prev_len - len(expectations)} fixed files.\n') |
| else: |
| print(f'Added {len(expectations) - prev_len} files with issues.\n') |
| |
| |
| def update_expectations_from_file(unexpected_results, project, add=False): |
| filename = unexpected_results.split('/')[-1] |
| if not project: |
| projects = [p for p in PROJECTS if p in filename] |
| if projects: |
| project = projects[0] |
| else: |
| print(f'Could not find a project to update. Please include the project in the filename or pass in the --project argument.') |
| return |
| print(f"{'Adding' if add else 'Removing'} unexpected failures in {project}...\n") |
| with open(unexpected_results, 'r') as unexpected_contents: |
| for line in unexpected_contents: |
| if '=>' in line: |
| checker_type = line.split()[-1] |
| modify_expectations_for_checker_from_file(checker_type, unexpected_contents, project, add) |
| print(f'Please add any changes to your commit using `git add` and `git commit --amend`.') |
| |
| |
| def modify_expectations_for_checker(checker_type, unexpected_contents, project, add=False): |
| path_to_expectations = os.path.join(os.path.dirname(__file__), EXPECTATIONS_PATH.format(project=project, checker_type=checker_type)) |
| with open(path_to_expectations) as expectations_file: |
| expectations = expectations_file.readlines() |
| prev_len = len(expectations) |
| for line in unexpected_contents: |
| if not add: |
| try: |
| expectations.remove(f'{line}\n') |
| except ValueError: |
| print(f'Error: {line} is not in {checker_type}Expectations!') |
| elif line not in expectations: |
| expectations.append(f'{line}\n') |
| else: |
| print(f'Error: {line} is already in {checker_type}Expectations!\n') |
| with open(path_to_expectations, 'w') as expectations_file: |
| expectations_file.writelines(sorted(expectations)) |
| print(f'Updated expectations for {checker_type}!') |
| if not add: |
| print(f'Removed {prev_len - len(expectations)} fixed files.\n') |
| else: |
| print(f'Added {len(expectations) - prev_len} files with issues.\n') |
| |
| |
| def update_expectations(args, project, add=False): |
| if not project: |
| print(f'Could not find a project to update. Please pass in the --project argument.') |
| return |
| print(f"{'Adding' if add else 'Removing'} unexpected failures in {project}...\n") |
| for checker_type in CHECKERS: |
| files_per_checker = args[checker_type] |
| if files_per_checker: |
| modify_expectations_for_checker(checker_type, files_per_checker, project, add) |
| print(f'Please add any changes to your commit using `git add` and `git commit --amend`.') |
| |
| |
| ''' |
| This currently checks against the files in your local checkout at SaferCPPExpectations. |
| Ensure that it is up-to-date before using this script. |
| ''' |
| def is_expected_file(expected_file): |
| print('This checks against local expectations. Ensure your checkout is up-to-date.\n') |
| line = f'{expected_file}\n' |
| issues = False |
| for project in PROJECTS: |
| for checker in CHECKERS: |
| path_to_expectations = os.path.join(os.path.dirname(__file__), EXPECTATIONS_PATH.format(project=project, checker_type=checker)) |
| with open(path_to_expectations, 'r') as f: |
| expectations = f.read() |
| if line in expectations: |
| if not issues: |
| print(f'{expected_file} has the following issues:') |
| issues = True |
| print(f'- {project} {checker}') |
| if not issues: |
| print(f'{expected_file} has no known issues!') |
| else: |
| print('Follow this link for the latest results: https://build.webkit.org/#/builders/Apple-Sonoma-Safer-CPP-Checks\n') |
| |
| |
| def main(): |
| args = parser() |
| if args.expected_file: |
| is_expected_file(args.expected_file) |
| return |
| if args.add: |
| print('WARNING: Adding expected failures. Please only add expected failures when you fix false negatives in our tools.') |
| user_input = input('Are you sure you want to proceed? [y/N]: ') or 'N' |
| if user_input[0].lower() != 'y': |
| return |
| if args.unexpected_results_file: |
| update_expectations_from_file(args.unexpected_results_file, args.project, args.add) |
| else: |
| update_expectations(vars(args), args.project, args.add) |
| |
| |
| if __name__ == '__main__': |
| main() |