2 # Copyright 2023 The Chromium Authors
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """Samples clang-tidy results from a JSON file.
7 Provides information about number of checks triggered and a summary of some of
8 the checks with links back to code search.
11 tools/sample_clang_tidy_results.py out/all_findings.json
23 from pathlib import Path
24 from typing import Any, Dict, List
27 @functools.lru_cache(maxsize=None)
28 def get_src_path() -> str:
29 src_path = Path(__file__).parent.parent.resolve()
32 'Could not find checkout in any parent of the current path.')
36 @functools.lru_cache(maxsize=None)
37 def git_rev_parse_head(path: Path):
38 if (path / '.git').exists():
39 return subprocess.check_output(['git', 'rev-parse', 'HEAD'],
42 return git_rev_parse_head(path.parent)
45 def convert_diag_to_cs(diag: Dict[str, Any]) -> str:
46 path = diag['file_path']
47 line = diag['line_number']
48 name = diag['diag_name']
49 replacement = '\n'.join(x['new_text'] for x in diag['replacements'])
51 sha = git_rev_parse_head(get_src_path() / path)
53 # https://source.chromium.org/chromium/chromium/src/+/main:apps/app_restore_service.cc
54 sha_and_path = f'{sha}:{path}'
58 'path': ('https://source.chromium.org/chromium/chromium/src/+/'
59 f'{sha}:{path};l={line}'),
65 @functools.lru_cache(maxsize=None)
66 def is_first_party_path(path: Path) -> bool:
67 if path == get_src_path():
73 if (path / '.git').exists() or (path / '.gclient').exists():
76 return is_first_party_path(path.parent)
79 def is_first_party_diag(diag: Dict[str, Any]) -> bool:
80 path = diag['file_path']
81 if path.startswith('out/') or path.startswith('/'):
83 return is_first_party_path(get_src_path() / path)
86 def select_random_diags(diags: List[Dict[str, Any]], number: int) -> List[Any]:
87 first_party = [x for x in diags if is_first_party_diag(x)]
88 if len(first_party) <= number:
90 return random.sample(first_party, number)
93 def is_diag_in_test_file(diag: Dict[str, Any]) -> bool:
94 file_stem = os.path.splitext(diag['file_path'])[0]
95 return (file_stem.endswith('test') or file_stem.endswith('tests')
96 or '_test_' in file_stem or '_unittest_' in file_stem)
99 def is_diag_in_third_party(diag: Dict[str, Any]) -> bool:
100 return 'third_party' in diag['file_path']
103 def main(argv: List[str]):
105 format='>> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: '
110 parser = argparse.ArgumentParser(
112 formatter_class=argparse.RawDescriptionHelpFormatter,
114 parser.add_argument('-n',
118 help='How many checks to sample')
119 parser.add_argument('--ignore-tests',
121 help='Filters lints in test/unittest files if specified.')
122 parser.add_argument('--include-third-party',
124 help='Includes lints in third_party if specified.')
125 parser.add_argument('file', help='JSON file to parse')
126 opts = parser.parse_args(argv)
128 with open(opts.file) as f:
131 print(f'Files with tidy errors: {len(data["failed_tidy_files"])}')
132 print(f'Timed out files: {len(data["timed_out_src_files"])}')
133 diags = data['diagnostics']
135 if not opts.include_third_party:
136 new_diags = [x for x in diags if not is_diag_in_third_party(x)]
137 print(f'Dropped {len(diags) - len(new_diags)} diags from third_party')
140 if opts.ignore_tests:
141 new_diags = [x for x in diags if not is_diag_in_test_file(x)]
142 print(f'Dropped {len(diags) - len(new_diags)} diags from test files')
145 counts = collections.defaultdict(int)
147 name = x['diag_name']
150 print(f'Total number of diagnostics: {len(diags)}')
151 for x in sorted(counts.keys()):
152 print(f'\t{x}: {counts[x]}')
155 diags = select_random_diags(diags, opts.number)
156 data = [convert_diag_to_cs(x) for x in diags]
157 print(f'** Sample of first-party lints: **')
160 print(f'\tDiagnostic: {x["name"]}')
161 print(f'\tReplacement: {x["replacement"]}')
164 print('** Link summary **')
169 if __name__ == '__main__':