| #!/usr/bin/env python3 | |
| # Copyright (C) 2025 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 argparse | |
| import os | |
| import re | |
| from webkitpy.safer_cpp.checkers import Checker | |
| # FIXME: Make this configurable | |
| EXCLUDED_DIRNAMES = ['CoordinatedGraphics', 'atoms', 'cairo', 'curl', 'gbm', 'glib', 'gstreamer', 'gtk', 'libwpe', 'linux', 'manette', 'playstation', 'skia', 'socket', 'soup', 'unified-sources', 'unix', 'wc', 'win', 'wpe'] | |
| RELEVANT_FILE_EXTENSIONS = ['.c', '.cpp', '.h', '.m', '.mm'] | |
| EXPECTATION_LINE_RE = r'(\s*\[\s*(?P<platform>\w+)\s*\]\s*)?(?P<path>.+)' | |
| def parser(): | |
| parser = argparse.ArgumentParser(description='Automated tooling for gathering statistics about safer CPP', epilog='Example: compute-safer-cpp-statisticsfor -p WebKit --build-configuration Release') | |
| parser.add_argument( | |
| '--project', '-p', | |
| choices=Checker.projects(), | |
| required=True, | |
| help='Specify which project expectations you want to update' | |
| ) | |
| parser.add_argument( | |
| '--build-configuration', '-b', | |
| required=True, | |
| help='Specify build configuration (e.g. Debug, Release-iphonesimulator) to look for derived sources' | |
| ) | |
| parser.add_argument( | |
| '--checker', '-c', | |
| choices=[checker.name() for checker in Checker.enumerate()], | |
| help='Specify the checker to gather statistics (e.g. UncountedCallArgsChecker). When omitted, all checkers are included' | |
| ) | |
| parser.add_argument( | |
| '--ignore', '-i', | |
| help='Specify the directory to ignore relative to the project root (e.g. WebProcess for Source/WebKit/WebProcess)' | |
| ) | |
| parser.add_argument( | |
| '--platform', | |
| help='Specify the platform to compute statistics for. e.g. mac' | |
| ) | |
| parser.add_argument( | |
| '--csv', | |
| help='Specify a path to a CSV file to be generated. CSV file will not be generated if omitted' | |
| ) | |
| return parser.parse_args() | |
| def matching_checkers(checker): | |
| if not checker: | |
| return Checker.enumerate() | |
| matching_checker = Checker.find_checker_by_name(checker) | |
| if not checker: | |
| print('Did not find a checker matching {}'.format(checker)) | |
| return None | |
| return [matching_checker] | |
| def load_expectations(checker_list, project, platform): | |
| file_to_expectations_map = {} | |
| expectation_line_re = re.compile(EXPECTATION_LINE_RE) | |
| for checker in checker_list: | |
| expectations_path = checker.expectations_path(project) | |
| if not os.path.isfile(expectations_path): | |
| continue | |
| with open(expectations_path) as expectation_file: | |
| for line in expectation_file: | |
| if line.startswith('//') or line.startswith('#'): | |
| continue | |
| match = expectation_line_re.match(line) | |
| if not match: | |
| print(f'Unexpected line: {line}') | |
| continue | |
| expectation_platform = match.group('platform') | |
| file_key = match.group('path') | |
| if platform and expectation_platform and expectation_platform.lower() != platform.lower(): | |
| print(f'Excluding: {file_key} ({expectation_platform})') | |
| continue | |
| file_to_expectations_map.setdefault(file_key, set()) | |
| file_to_expectations_map[file_key].add(checker.name()) | |
| return file_to_expectations_map | |
| def count_number_of_lines(file_path): | |
| with open(file_path) as file_handle: | |
| return sum(1 for _ in file_handle) | |
| def enumerate_relevant_files(root_dir_path, dir_to_ignore=None): | |
| file_path_list = [] | |
| for (dirpath, dirnames, filenames) in os.walk(root_dir_path): | |
| dirname, base = os.path.split(dirpath) | |
| is_excluded_dir = base in EXCLUDED_DIRNAMES or os.path.basename(dirname) in EXCLUDED_DIRNAMES | |
| relative_path = os.path.relpath(dirpath, root_dir_path) | |
| if is_excluded_dir or (dir_to_ignore and relative_path.startswith(dir_to_ignore)): | |
| print('Excluding: ' + relative_path) | |
| continue | |
| for filename in filenames: | |
| file_path = os.path.abspath(os.path.join(dirpath, filename)) | |
| (_, ext) = os.path.splitext(file_path) | |
| if ext not in RELEVANT_FILE_EXTENSIONS: | |
| continue | |
| file_path_list.append(file_path) | |
| return file_path_list | |
| def gather_statistics(line_counts, checkers, file_to_expectations_map, dir_path, dir_to_ignore=None): | |
| for file_path in enumerate_relevant_files(dir_path, dir_to_ignore): | |
| file_key = os.path.relpath(file_path, dir_path) | |
| number_of_lines = count_number_of_lines(file_path) | |
| expected_checkers = file_to_expectations_map.get(file_key) | |
| if expected_checkers: | |
| line_counts['unsafe'] += number_of_lines | |
| line_counts['unsafeFiles'] += 1 | |
| else: | |
| line_counts['safe'] += number_of_lines | |
| line_counts['safeFiles'] += 1 | |
| for checker in checkers: | |
| per_checker_counts = line_counts['checkers'][checker.name()] | |
| if expected_checkers and checker.name() in expected_checkers: | |
| per_checker_counts['unsafe'] += number_of_lines | |
| per_checker_counts['unsafeFiles'] += 1 | |
| else: | |
| per_checker_counts['safe'] += number_of_lines | |
| per_checker_counts['safeFiles'] += 1 | |
| def safe_ratio(line_counts): | |
| return line_counts['safe'] / (line_counts['safe'] + line_counts['unsafe']) | |
| def print_percentage(label, line_counts): | |
| return print('{}: {:.2f}% safe ({} safe files / {} unsafe files)'.format(label, safe_ratio(line_counts) * 100, line_counts['safeFiles'], line_counts['unsafeFiles'])) | |
| def main(): | |
| args = parser() | |
| checkers = matching_checkers(args.checker) | |
| if not checkers: | |
| return | |
| file_to_expectations_map = load_expectations(checkers, args.project, args.platform) | |
| line_counts = {'safe': 0, 'unsafe': 0, 'safeFiles': 0, 'unsafeFiles': 0, 'checkers': {}} | |
| for checker in checkers: | |
| line_counts['checkers'][checker.name()] = {'safe': 0, 'unsafe': 0, 'safeFiles': 0, 'unsafeFiles': 0} | |
| project = args.project | |
| project_path = Checker.enumerate()[0].project_path(project) | |
| gather_statistics(line_counts, checkers, file_to_expectations_map, project_path, args.ignore) | |
| derived_sources_path = Checker.enumerate()[0].derived_sources_path(project, args.build_configuration) | |
| gather_statistics(line_counts, checkers, file_to_expectations_map, derived_sources_path) | |
| print_percentage('Overall', line_counts) | |
| for checker in checkers: | |
| print_percentage(checker.name(), line_counts['checkers'][checker.name()]) | |
| if not args.csv: | |
| return | |
| with open(args.csv, 'w') as csv_file: | |
| print(',' + ','.join([checker.name() for checker in checkers]), file=csv_file) | |
| print('Coverage,' + ','.join([str(safe_ratio(line_counts['checkers'][checker.name()])) for checker in checkers]), file=csv_file) | |
| for file_key in sorted(file_to_expectations_map.keys()): | |
| if args.ignore and file_key.startswith(args.ignore): | |
| continue | |
| csv_file.write(file_key + ',') | |
| for checker in checkers: | |
| csv_file.write('FAIL,' if checker.name() in file_to_expectations_map[file_key] else ',') | |
| csv_file.write('\n') | |
| if __name__ == '__main__': | |
| main() |