blob: e87927b42590fc17be744421685568e541a36967 [file] [log] [blame] [edit]
#!/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()