| #!/usr/bin/env python3 |
| # Copyright 2023 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Samples clang-tidy results from a JSON file. |
| |
| Provides information about number of checks triggered and a summary of some of |
| the checks with links back to code search. |
| |
| Usage: |
| tools/sample_clang_tidy_results.py out/all_findings.json |
| """ |
| |
| import argparse |
| import collections |
| import functools |
| import json |
| import logging |
| import os |
| import random |
| import subprocess |
| import sys |
| from pathlibimportPath |
| from typingimportAny,Dict,List |
| |
| |
| @functools.lru_cache(maxsize=None) |
| def get_src_path()-> str: |
| src_path=Path(__file__).parent.parent.resolve() |
| ifnot src_path: |
| raiseNotFoundError( |
| 'Could not find checkout in any parent of the current path.') |
| return src_path |
| |
| |
| @functools.lru_cache(maxsize=None) |
| def git_rev_parse_head(path:Path): |
| if(path/'.git').exists(): |
| return subprocess.check_output(['git','rev-parse','HEAD'], |
| encoding='utf-8', |
| cwd=path).strip() |
| return git_rev_parse_head(path.parent) |
| |
| |
| def convert_diag_to_cs(diag:Dict[str,Any])-> str: |
| path= diag['file_path'] |
| line= diag['line_number'] |
| name= diag['diag_name'] |
| replacement='\n'.join(x['new_text']for xin diag['replacements']) |
| |
| sha= git_rev_parse_head(get_src_path()/ path) |
| |
| # https://source.chromium.org/chromium/chromium/src/+/main:apps/app_restore_service.cc |
| sha_and_path= f'{sha}:{path}' |
| return{ |
| 'name': |
| name, |
| 'path':('https://source.chromium.org/chromium/chromium/src/+/' |
| f'{sha}:{path};l={line}'), |
| 'replacement': |
| replacement |
| } |
| |
| |
| @functools.lru_cache(maxsize=None) |
| def is_first_party_path(path:Path)-> bool: |
| if path== get_src_path(): |
| returnTrue |
| |
| if path=='/': |
| returnFalse |
| |
| if(path/'.git').exists()or(path/'.gclient').exists(): |
| returnFalse |
| |
| return is_first_party_path(path.parent) |
| |
| |
| def is_first_party_diag(diag:Dict[str,Any])-> bool: |
| path= diag['file_path'] |
| if path.startswith('out/')or path.startswith('/'): |
| returnFalse |
| return is_first_party_path(get_src_path()/ path) |
| |
| |
| def select_random_diags(diags:List[Dict[str,Any]], number: int)->List[Any]: |
| first_party=[xfor xin diagsif is_first_party_diag(x)] |
| if len(first_party)<= number: |
| return first_party |
| return random.sample(first_party, number) |
| |
| |
| def is_diag_in_test_file(diag:Dict[str,Any])-> bool: |
| file_stem= os.path.splitext(diag['file_path'])[0] |
| return(file_stem.endswith('test')or file_stem.endswith('tests') |
| or'_test_'in file_stemor'_unittest_'in file_stem) |
| |
| |
| def is_diag_in_third_party(diag:Dict[str,Any])-> bool: |
| return'third_party'in diag['file_path'] |
| |
| |
| def main(argv:List[str]): |
| logging.basicConfig( |
| format='>> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: ' |
| '%(message)s', |
| level=logging.INFO, |
| ) |
| |
| parser= argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| parser.add_argument('-n', |
| '--number', |
| type=int, |
| default=30, |
| help='How many checks to sample') |
| parser.add_argument('--ignore-tests', |
| action='store_true', |
| help='Filters lints in test/unittest files if specified.') |
| parser.add_argument('--include-third-party', |
| action='store_true', |
| help='Includes lints in third_party if specified.') |
| parser.add_argument('file', help='JSON file to parse') |
| opts= parser.parse_args(argv) |
| |
| with open(opts.file)as f: |
| data= json.load(f) |
| |
| print(f'Files with tidy errors: {len(data["failed_tidy_files"])}') |
| print(f'Timed out files: {len(data["timed_out_src_files"])}') |
| diags= data['diagnostics'] |
| |
| ifnot opts.include_third_party: |
| new_diags=[xfor xin diagsifnot is_diag_in_third_party(x)] |
| print(f'Dropped {len(diags) - len(new_diags)} diags from third_party') |
| diags= new_diags |
| |
| if opts.ignore_tests: |
| new_diags=[xfor xin diagsifnot is_diag_in_test_file(x)] |
| print(f'Dropped {len(diags) - len(new_diags)} diags from test files') |
| diags= new_diags |
| |
| counts= collections.defaultdict(int) |
| for xin diags: |
| name= x['diag_name'] |
| counts[name]+=1 |
| |
| print(f'Total number of diagnostics: {len(diags)}') |
| for xin sorted(counts.keys()): |
| print(f'\t{x}: {counts[x]}') |
| print() |
| |
| diags= select_random_diags(diags, opts.number) |
| data=[convert_diag_to_cs(x)for xin diags] |
| print(f'** Sample of first-party lints: **') |
| for xin data: |
| print(x['path']) |
| print(f'\tDiagnostic: {x["name"]}') |
| print(f'\tReplacement: {x["replacement"]}') |
| print() |
| |
| print('** Link summary **') |
| for xin data: |
| print(x['path']) |
| |
| |
| if __name__=='__main__': |
| main(sys.argv[1:]) |