[PDNCF] Python 3.12 compatibility
[platform/framework/web/chromium-efl.git] / tools / sample_clang_tidy_results.py
1 #!/usr/bin/env python3
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.
6
7 Provides information about number of checks triggered and a summary of some of
8 the checks with links back to code search.
9
10 Usage:
11 tools/sample_clang_tidy_results.py out/all_findings.json
12 """
13
14 import argparse
15 import collections
16 import functools
17 import json
18 import logging
19 import os
20 import random
21 import subprocess
22 import sys
23 from pathlib import Path
24 from typing import Any, Dict, List
25
26
27 @functools.lru_cache(maxsize=None)
28 def get_src_path() -> str:
29   src_path = Path(__file__).parent.parent.resolve()
30   if not src_path:
31     raise NotFoundError(
32         'Could not find checkout in any parent of the current path.')
33   return src_path
34
35
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'],
40                                    encoding='utf-8',
41                                    cwd=path).strip()
42   return git_rev_parse_head(path.parent)
43
44
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'])
50
51   sha = git_rev_parse_head(get_src_path() / path)
52
53   # https://source.chromium.org/chromium/chromium/src/+/main:apps/app_restore_service.cc
54   sha_and_path = f'{sha}:{path}'
55   return {
56       'name':
57       name,
58       'path': ('https://source.chromium.org/chromium/chromium/src/+/'
59                f'{sha}:{path};l={line}'),
60       'replacement':
61       replacement
62   }
63
64
65 @functools.lru_cache(maxsize=None)
66 def is_first_party_path(path: Path) -> bool:
67   if path == get_src_path():
68     return True
69
70   if path == '/':
71     return False
72
73   if (path / '.git').exists() or (path / '.gclient').exists():
74     return False
75
76   return is_first_party_path(path.parent)
77
78
79 def is_first_party_diag(diag: Dict[str, Any]) -> bool:
80   path = diag['file_path']
81   if path.startswith('out/') or path.startswith('/'):
82     return False
83   return is_first_party_path(get_src_path() / path)
84
85
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:
89     return first_party
90   return random.sample(first_party, number)
91
92
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)
97
98
99 def is_diag_in_third_party(diag: Dict[str, Any]) -> bool:
100   return 'third_party' in diag['file_path']
101
102
103 def main(argv: List[str]):
104   logging.basicConfig(
105       format='>> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: '
106       '%(message)s',
107       level=logging.INFO,
108   )
109
110   parser = argparse.ArgumentParser(
111       description=__doc__,
112       formatter_class=argparse.RawDescriptionHelpFormatter,
113   )
114   parser.add_argument('-n',
115                       '--number',
116                       type=int,
117                       default=30,
118                       help='How many checks to sample')
119   parser.add_argument('--ignore-tests',
120                       action='store_true',
121                       help='Filters lints in test/unittest files if specified.')
122   parser.add_argument('--include-third-party',
123                       action='store_true',
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)
127
128   with open(opts.file) as f:
129     data = json.load(f)
130
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']
134
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')
138     diags = new_diags
139
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')
143     diags = new_diags
144
145   counts = collections.defaultdict(int)
146   for x in diags:
147     name = x['diag_name']
148     counts[name] += 1
149
150   print(f'Total number of diagnostics: {len(diags)}')
151   for x in sorted(counts.keys()):
152     print(f'\t{x}: {counts[x]}')
153   print()
154
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: **')
158   for x in data:
159     print(x['path'])
160     print(f'\tDiagnostic: {x["name"]}')
161     print(f'\tReplacement: {x["replacement"]}')
162     print()
163
164   print('** Link summary **')
165   for x in data:
166     print(x['path'])
167
168
169 if __name__ == '__main__':
170   main(sys.argv[1:])