| #!/usr/bin/env python3 |
| # Copyright 2025 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Tool to collect stats about progress of adding @NullMarked.""" |
| |
| import argparse |
| import collections |
| import csv |
| from datetime import date |
| import logging |
| import pathlib |
| import sys |
| import time |
| |
| _SRC_ROOT = pathlib.Path(__file__).resolve().parents[3] |
| |
| _EXCLUDED_SUBDIRS = ('out', 'third_party') |
| _TEST_PATH_SUBSTRINGS = ('test', 'Test') |
| |
| _SUBDIRS_FOR_STATS = [ |
| 'base', |
| 'chrome', |
| 'android_webview', |
| 'components', |
| 'content', |
| 'clank', |
| ] |
| |
| |
| def _is_test(file): |
| return any(s in str(file.absolute()) for s in _TEST_PATH_SUBSTRINGS) |
| |
| |
| def _collect_java_files(start_dir): |
| path = pathlib.Path(start_dir) |
| for file in path.glob('**/*.java'): |
| file.resolve() |
| if not file.is_file(): |
| continue |
| # Ignore files in excluded subdirs. |
| for excluded_subdir in _EXCLUDED_SUBDIRS: |
| if (_SRC_ROOT / excluded_subdir) in file.parents: |
| break |
| else: |
| yield file |
| |
| |
| def _check_if_marked(java_files): |
| marked_all = set() |
| unmarked_all = set() |
| nomark_all = set() |
| for path in java_files: |
| data = path.read_text() |
| marked = '@NullMarked' in data |
| unmarked = '@NullUnmarked' in data |
| if marked: |
| marked_all.add(path) |
| elif not unmarked: |
| nomark_all.add(path) |
| if unmarked: |
| unmarked_all.add(path) |
| |
| return marked_all, nomark_all, unmarked_all |
| |
| |
| def _breadown_stats_by_subdir(files): |
| ret = collections.defaultdict(int) |
| for file in files: |
| for subdir in _SUBDIRS_FOR_STATS: |
| if (_SRC_ROOT / subdir) in file.parents: |
| ret[subdir] += 1 |
| return ret |
| |
| |
| def _print_stats(marked_all, nomark_all, unmarked_all): |
| marked_by_subdirs = _breadown_stats_by_subdir(marked_all) |
| nomark_by_subdirs = _breadown_stats_by_subdir(nomark_all) |
| unmarked_by_subdirs = _breadown_stats_by_subdir(unmarked_all) |
| count_marked = len(marked_all) |
| count_nomark = len(nomark_all) |
| count_unmarked = len(unmarked_all) |
| total = count_marked + count_nomark |
| |
| def stat(c, t): |
| pct_string = str(round(c / t * 100)) if t != 0 else '-' |
| return f'{c}/{t} ({pct_string}%)' |
| |
| print() |
| print(f'Overall:') |
| print(f' @NullMarked:', stat(count_marked, total)) |
| print(f' Neither:', stat(count_nomark, total)) |
| print(f' @NullUnmarked:', stat(count_unmarked, total)) |
| print() |
| print(f'By Directory (@NullMarked / Neither / @NullUnmarked):') |
| for subdir in _SUBDIRS_FOR_STATS: |
| subdir_marked_count = marked_by_subdirs[subdir] |
| subdir_nomark_count = nomark_by_subdirs[subdir] |
| subdir_unmarked_count = unmarked_by_subdirs[subdir] |
| subdir_total = subdir_marked_count + subdir_nomark_count |
| # Skip non-existent subdirs. |
| if subdir_total == 0: |
| continue |
| print(f' //{subdir}:', stat(subdir_marked_count, subdir_total), '/', |
| stat(subdir_nomark_count, subdir_total), '/', |
| stat(subdir_unmarked_count, subdir_total)) |
| |
| |
| def _read_file_list(filepath): |
| with open(filepath, 'rt') as f: |
| return (pathlib.Path(java_file.strip()) for java_file in f.readlines()) |
| |
| |
| def _write_file_list(filepath, filelist): |
| sorted_filelist = sorted(filelist) |
| with open(filepath, 'wt') as f: |
| f.writelines(f'{str(p)}\n' for p in sorted_filelist) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument('-C', |
| dest='src_dir', |
| default=_SRC_ROOT, |
| help='Path to CHROMIUM_SRC.') |
| parser.add_argument( |
| '--unmarked-list-path', |
| help='Path to output the list of files with @NullUnmarked.') |
| parser.add_argument( |
| '--marked-list-path', |
| help='Path to output the list of files with @NullMarked.') |
| parser.add_argument( |
| '--nomark-list-path', |
| help='Path to output the list of files without any annotation.') |
| parser.add_argument( |
| '--cached-file-list', |
| help='Path to list of java files instead of walking the tree.') |
| parser.add_argument( |
| '--output-file-list', |
| help='Path to output list of java files for use by --cached-file-list.' |
| ) |
| parser.add_argument('--csv', action='store_true', help='Output a .csv') |
| parser.add_argument('-v', '--verbose', action='store_true') |
| options = parser.parse_args(sys.argv[1:]) |
| |
| logging_level = logging.INFO |
| if options.verbose: |
| logging_level = logging.DEBUG |
| logging.basicConfig(level=logging_level) |
| |
| if options.cached_file_list and options.output_file_list: |
| parser.error( |
| 'Cant pass in both --cached-file-list and --output-file-list') |
| |
| logging.info('Collecting java files') |
| start = time.time() |
| if options.cached_file_list: |
| java_files = _read_file_list(options.cached_file_list) |
| else: |
| java_files = list(_collect_java_files(options.src_dir)) |
| logging.info(f'Collecting java files done in {time.time()-start:.1f}s') |
| |
| logging.info('Processing files') |
| start = time.time() |
| marked, nomark, unmarked = _check_if_marked(java_files) |
| logging.info(f'Processing files files done in {time.time()-start:.1f}s') |
| |
| if options.unmarked_list_path: |
| _write_file_list(options.unmarked_list_path, unmarked) |
| if options.marked_list_path: |
| _write_file_list(options.marked_list_path, marked) |
| if options.nomark_list_path: |
| _write_file_list(options.nomark_list_path, nomark) |
| if options.output_file_list: |
| _write_file_list(options.output_file_list, java_files) |
| |
| logging.info('Calculating stats') |
| start = time.time() |
| marked_tests = {x for x in marked if _is_test(x)} |
| marked.difference_update(marked_tests) |
| nomark_tests = {x for x in nomark if _is_test(x)} |
| nomark.difference_update(nomark_tests) |
| unmarked_tests = {x for x in unmarked if _is_test(x)} |
| unmarked.difference_update(unmarked_tests) |
| |
| if options.csv: |
| csv.writer(sys.stdout).writerow( |
| (date.today(), len(marked), len(nomark), len(unmarked), |
| len(marked_tests), len(nomark_tests), len(unmarked_tests))) |
| else: |
| print(date.today()) |
| print('==== Non-test Files ====') |
| _print_stats(marked, nomark, unmarked) |
| print() |
| print('====== Test Files ======') |
| _print_stats(marked_tests, nomark_tests, unmarked_tests) |
| logging.info(f'Calculating stats done in {time.time()-start:.1f}s') |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |