Path: blob/main/Tools/c-analyzer/c_analyzer/__main__.py
12 views
import io1import logging2import os3import os.path4import re5import sys67from c_common import fsutil8from c_common.logging import VERBOSITY, Printer9from c_common.scriptutil import (10add_verbosity_cli,11add_traceback_cli,12add_sepval_cli,13add_progress_cli,14add_files_cli,15add_commands_cli,16process_args_by_key,17configure_logger,18get_prog,19filter_filenames,20)21from c_parser.info import KIND22from .match import filter_forward23from . import (24analyze as _analyze,25datafiles as _datafiles,26check_all as _check_all,27)282930KINDS = [31KIND.TYPEDEF,32KIND.STRUCT,33KIND.UNION,34KIND.ENUM,35KIND.FUNCTION,36KIND.VARIABLE,37KIND.STATEMENT,38]3940logger = logging.getLogger(__name__)414243#######################################44# table helpers4546TABLE_SECTIONS = {47'types': (48['kind', 'name', 'data', 'file'],49KIND.is_type_decl,50(lambda v: (v.kind.value, v.filename or '', v.name)),51),52'typedefs': 'types',53'structs': 'types',54'unions': 'types',55'enums': 'types',56'functions': (57['name', 'data', 'file'],58(lambda kind: kind is KIND.FUNCTION),59(lambda v: (v.filename or '', v.name)),60),61'variables': (62['name', 'parent', 'data', 'file'],63(lambda kind: kind is KIND.VARIABLE),64(lambda v: (v.filename or '', str(v.parent) if v.parent else '', v.name)),65),66'statements': (67['file', 'parent', 'data'],68(lambda kind: kind is KIND.STATEMENT),69(lambda v: (v.filename or '', str(v.parent) if v.parent else '', v.name)),70),71KIND.TYPEDEF: 'typedefs',72KIND.STRUCT: 'structs',73KIND.UNION: 'unions',74KIND.ENUM: 'enums',75KIND.FUNCTION: 'functions',76KIND.VARIABLE: 'variables',77KIND.STATEMENT: 'statements',78}798081def _render_table(items, columns, relroot=None):82# XXX improve this83header = '\t'.join(columns)84div = '--------------------'85yield header86yield div87total = 088for item in items:89rowdata = item.render_rowdata(columns)90row = [rowdata[c] for c in columns]91if relroot and 'file' in columns:92index = columns.index('file')93row[index] = os.path.relpath(row[index], relroot)94yield '\t'.join(row)95total += 196yield div97yield f'total: {total}'9899100def build_section(name, groupitems, *, relroot=None):101info = TABLE_SECTIONS[name]102while type(info) is not tuple:103if name in KINDS:104name = info105info = TABLE_SECTIONS[info]106107columns, match_kind, sortkey = info108items = (v for v in groupitems if match_kind(v.kind))109items = sorted(items, key=sortkey)110def render():111yield ''112yield f'{name}:'113yield ''114for line in _render_table(items, columns, relroot):115yield line116return items, render117118119#######################################120# the checks121122CHECKS = {123#'globals': _check_globals,124}125126127def add_checks_cli(parser, checks=None, *, add_flags=None):128default = False129if not checks:130checks = list(CHECKS)131default = True132elif isinstance(checks, str):133checks = [checks]134if (add_flags is None and len(checks) > 1) or default:135add_flags = True136137process_checks = add_sepval_cli(parser, '--check', 'checks', checks)138if add_flags:139for check in checks:140parser.add_argument(f'--{check}', dest='checks',141action='append_const', const=check)142return [143process_checks,144]145146147def _get_check_handlers(fmt, printer, verbosity=VERBOSITY):148div = None149def handle_after():150pass151if not fmt:152div = ''153def handle_failure(failure, data):154data = repr(data)155if verbosity >= 3:156logger.info(f'failure: {failure}')157logger.info(f'data: {data}')158else:159logger.warn(f'failure: {failure} (data: {data})')160elif fmt == 'raw':161def handle_failure(failure, data):162print(f'{failure!r} {data!r}')163elif fmt == 'brief':164def handle_failure(failure, data):165parent = data.parent or ''166funcname = parent if isinstance(parent, str) else parent.name167name = f'({funcname}).{data.name}' if funcname else data.name168failure = failure.split('\t')[0]169print(f'{data.filename}:{name} - {failure}')170elif fmt == 'summary':171def handle_failure(failure, data):172print(_fmt_one_summary(data, failure))173elif fmt == 'full':174div = ''175def handle_failure(failure, data):176name = data.shortkey if data.kind is KIND.VARIABLE else data.name177parent = data.parent or ''178funcname = parent if isinstance(parent, str) else parent.name179known = 'yes' if data.is_known else '*** NO ***'180print(f'{data.kind.value} {name!r} failed ({failure})')181print(f' file: {data.filename}')182print(f' func: {funcname or "-"}')183print(f' name: {data.name}')184print(f' data: ...')185print(f' type unknown: {known}')186else:187if fmt in FORMATS:188raise NotImplementedError(fmt)189raise ValueError(f'unsupported fmt {fmt!r}')190return handle_failure, handle_after, div191192193#######################################194# the formats195196def fmt_raw(analysis):197for item in analysis:198yield from item.render('raw')199200201def fmt_brief(analysis):202# XXX Support sorting.203items = sorted(analysis)204for kind in KINDS:205if kind is KIND.STATEMENT:206continue207for item in items:208if item.kind is not kind:209continue210yield from item.render('brief')211yield f' total: {len(items)}'212213214def fmt_summary(analysis):215# XXX Support sorting and grouping.216items = list(analysis)217total = len(items)218219def section(name):220_, render = build_section(name, items)221yield from render()222223yield from section('types')224yield from section('functions')225yield from section('variables')226yield from section('statements')227228yield ''229# yield f'grand total: {len(supported) + len(unsupported)}'230yield f'grand total: {total}'231232233def _fmt_one_summary(item, extra=None):234parent = item.parent or ''235funcname = parent if isinstance(parent, str) else parent.name236if extra:237return f'{item.filename:35}\t{funcname or "-":35}\t{item.name:40}\t{extra}'238else:239return f'{item.filename:35}\t{funcname or "-":35}\t{item.name}'240241242def fmt_full(analysis):243# XXX Support sorting.244items = sorted(analysis, key=lambda v: v.key)245yield ''246for item in items:247yield from item.render('full')248yield ''249yield f'total: {len(items)}'250251252FORMATS = {253'raw': fmt_raw,254'brief': fmt_brief,255'summary': fmt_summary,256'full': fmt_full,257}258259260def add_output_cli(parser, *, default='summary'):261parser.add_argument('--format', dest='fmt', default=default, choices=tuple(FORMATS))262263def process_args(args, *, argv=None):264pass265return process_args266267268#######################################269# the commands270271def _cli_check(parser, checks=None, **kwargs):272if isinstance(checks, str):273checks = [checks]274if checks is False:275process_checks = None276elif checks is None:277process_checks = add_checks_cli(parser)278elif len(checks) == 1 and type(checks) is not dict and re.match(r'^<.*>$', checks[0]):279check = checks[0][1:-1]280def process_checks(args, *, argv=None):281args.checks = [check]282else:283process_checks = add_checks_cli(parser, checks=checks)284process_progress = add_progress_cli(parser)285process_output = add_output_cli(parser, default=None)286process_files = add_files_cli(parser, **kwargs)287return [288process_checks,289process_progress,290process_output,291process_files,292]293294295def cmd_check(filenames, *,296checks=None,297ignored=None,298fmt=None,299failfast=False,300iter_filenames=None,301relroot=fsutil.USE_CWD,302track_progress=None,303verbosity=VERBOSITY,304_analyze=_analyze,305_CHECKS=CHECKS,306**kwargs307):308if not checks:309checks = _CHECKS310elif isinstance(checks, str):311checks = [checks]312checks = [_CHECKS[c] if isinstance(c, str) else c313for c in checks]314printer = Printer(verbosity)315(handle_failure, handle_after, div316) = _get_check_handlers(fmt, printer, verbosity)317318filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot)319filenames = filter_filenames(filenames, iter_filenames, relroot)320if track_progress:321filenames = track_progress(filenames)322323logger.info('analyzing files...')324analyzed = _analyze(filenames, **kwargs)325analyzed.fix_filenames(relroot, normalize=False)326decls = filter_forward(analyzed, markpublic=True)327328logger.info('checking analysis results...')329failed = []330for data, failure in _check_all(decls, checks, failfast=failfast):331if data is None:332printer.info('stopping after one failure')333break334if div is not None and len(failed) > 0:335printer.info(div)336failed.append(data)337handle_failure(failure, data)338handle_after()339340printer.info('-------------------------')341logger.info(f'total failures: {len(failed)}')342logger.info('done checking')343344if fmt == 'summary':345print('Categorized by storage:')346print()347from .match import group_by_storage348grouped = group_by_storage(failed, ignore_non_match=False)349for group, decls in grouped.items():350print()351print(group)352for decl in decls:353print(' ', _fmt_one_summary(decl))354print(f'subtotal: {len(decls)}')355356if len(failed) > 0:357sys.exit(len(failed))358359360def _cli_analyze(parser, **kwargs):361process_progress = add_progress_cli(parser)362process_output = add_output_cli(parser)363process_files = add_files_cli(parser, **kwargs)364return [365process_progress,366process_output,367process_files,368]369370371# XXX Support filtering by kind.372def cmd_analyze(filenames, *,373fmt=None,374iter_filenames=None,375relroot=fsutil.USE_CWD,376track_progress=None,377verbosity=None,378_analyze=_analyze,379formats=FORMATS,380**kwargs381):382verbosity = verbosity if verbosity is not None else 3383384try:385do_fmt = formats[fmt]386except KeyError:387raise ValueError(f'unsupported fmt {fmt!r}')388389filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot)390filenames = filter_filenames(filenames, iter_filenames, relroot)391if track_progress:392filenames = track_progress(filenames)393394logger.info('analyzing files...')395analyzed = _analyze(filenames, **kwargs)396analyzed.fix_filenames(relroot, normalize=False)397decls = filter_forward(analyzed, markpublic=True)398399for line in do_fmt(decls):400print(line)401402403def _cli_data(parser, filenames=None, known=None):404ArgumentParser = type(parser)405common = ArgumentParser(add_help=False)406# These flags will get processed by the top-level parse_args().407add_verbosity_cli(common)408add_traceback_cli(common)409410subs = parser.add_subparsers(dest='datacmd')411412sub = subs.add_parser('show', parents=[common])413if known is None:414sub.add_argument('--known', required=True)415if filenames is None:416sub.add_argument('filenames', metavar='FILE', nargs='+')417418sub = subs.add_parser('dump', parents=[common])419if known is None:420sub.add_argument('--known')421sub.add_argument('--show', action='store_true')422process_progress = add_progress_cli(sub)423424sub = subs.add_parser('check', parents=[common])425if known is None:426sub.add_argument('--known', required=True)427428def process_args(args, *, argv):429if args.datacmd == 'dump':430process_progress(args, argv)431return process_args432433434def cmd_data(datacmd, filenames, known=None, *,435_analyze=_analyze,436formats=FORMATS,437extracolumns=None,438relroot=fsutil.USE_CWD,439track_progress=None,440**kwargs441):442kwargs.pop('verbosity', None)443usestdout = kwargs.pop('show', None)444if datacmd == 'show':445do_fmt = formats['summary']446if isinstance(known, str):447known, _ = _datafiles.get_known(known, extracolumns, relroot)448for line in do_fmt(known):449print(line)450elif datacmd == 'dump':451filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot)452if track_progress:453filenames = track_progress(filenames)454analyzed = _analyze(filenames, **kwargs)455analyzed.fix_filenames(relroot, normalize=False)456if known is None or usestdout:457outfile = io.StringIO()458_datafiles.write_known(analyzed, outfile, extracolumns,459relroot=relroot)460print(outfile.getvalue())461else:462_datafiles.write_known(analyzed, known, extracolumns,463relroot=relroot)464elif datacmd == 'check':465raise NotImplementedError(datacmd)466else:467raise ValueError(f'unsupported data command {datacmd!r}')468469470COMMANDS = {471'check': (472'analyze and fail if the given C source/header files have any problems',473[_cli_check],474cmd_check,475),476'analyze': (477'report on the state of the given C source/header files',478[_cli_analyze],479cmd_analyze,480),481'data': (482'check/manage local data (e.g. known types, ignored vars, caches)',483[_cli_data],484cmd_data,485),486}487488489#######################################490# the script491492def parse_args(argv=sys.argv[1:], prog=sys.argv[0], *, subset=None):493import argparse494parser = argparse.ArgumentParser(495prog=prog or get_prog(),496)497498processors = add_commands_cli(499parser,500commands={k: v[1] for k, v in COMMANDS.items()},501commonspecs=[502add_verbosity_cli,503add_traceback_cli,504],505subset=subset,506)507508args = parser.parse_args(argv)509ns = vars(args)510511cmd = ns.pop('cmd')512513verbosity, traceback_cm = process_args_by_key(514args,515argv,516processors[cmd],517['verbosity', 'traceback_cm'],518)519# "verbosity" is sent to the commands, so we put it back.520args.verbosity = verbosity521522return cmd, ns, verbosity, traceback_cm523524525def main(cmd, cmd_kwargs):526try:527run_cmd = COMMANDS[cmd][0]528except KeyError:529raise ValueError(f'unsupported cmd {cmd!r}')530run_cmd(**cmd_kwargs)531532533if __name__ == '__main__':534cmd, cmd_kwargs, verbosity, traceback_cm = parse_args()535configure_logger(verbosity)536with traceback_cm:537main(cmd, cmd_kwargs)538539540