1import io 2import logging 3import os 4import os.path 5import re 6import sys 7 8from c_common import fsutil 9from c_common.logging import VERBOSITY, Printer 10from c_common.scriptutil import ( 11 add_verbosity_cli, 12 add_traceback_cli, 13 add_sepval_cli, 14 add_progress_cli, 15 add_files_cli, 16 add_commands_cli, 17 process_args_by_key, 18 configure_logger, 19 get_prog, 20 filter_filenames, 21 iter_marks, 22) 23from c_parser.info import KIND 24from c_parser.match import is_type_decl 25from .match import filter_forward 26from . import ( 27 analyze as _analyze, 28 datafiles as _datafiles, 29 check_all as _check_all, 30) 31 32 33KINDS = [ 34 KIND.TYPEDEF, 35 KIND.STRUCT, 36 KIND.UNION, 37 KIND.ENUM, 38 KIND.FUNCTION, 39 KIND.VARIABLE, 40 KIND.STATEMENT, 41] 42 43logger = logging.getLogger(__name__) 44 45 46####################################### 47# table helpers 48 49TABLE_SECTIONS = { 50 'types': ( 51 ['kind', 'name', 'data', 'file'], 52 KIND.is_type_decl, 53 (lambda v: (v.kind.value, v.filename or '', v.name)), 54 ), 55 'typedefs': 'types', 56 'structs': 'types', 57 'unions': 'types', 58 'enums': 'types', 59 'functions': ( 60 ['name', 'data', 'file'], 61 (lambda kind: kind is KIND.FUNCTION), 62 (lambda v: (v.filename or '', v.name)), 63 ), 64 'variables': ( 65 ['name', 'parent', 'data', 'file'], 66 (lambda kind: kind is KIND.VARIABLE), 67 (lambda v: (v.filename or '', str(v.parent) if v.parent else '', v.name)), 68 ), 69 'statements': ( 70 ['file', 'parent', 'data'], 71 (lambda kind: kind is KIND.STATEMENT), 72 (lambda v: (v.filename or '', str(v.parent) if v.parent else '', v.name)), 73 ), 74 KIND.TYPEDEF: 'typedefs', 75 KIND.STRUCT: 'structs', 76 KIND.UNION: 'unions', 77 KIND.ENUM: 'enums', 78 KIND.FUNCTION: 'functions', 79 KIND.VARIABLE: 'variables', 80 KIND.STATEMENT: 'statements', 81} 82 83 84def _render_table(items, columns, relroot=None): 85 # XXX improve this 86 header = '\t'.join(columns) 87 div = '--------------------' 88 yield header 89 yield div 90 total = 0 91 for item in items: 92 rowdata = item.render_rowdata(columns) 93 row = [rowdata[c] for c in columns] 94 if relroot and 'file' in columns: 95 index = columns.index('file') 96 row[index] = os.path.relpath(row[index], relroot) 97 yield '\t'.join(row) 98 total += 1 99 yield div 100 yield f'total: {total}' 101 102 103def build_section(name, groupitems, *, relroot=None): 104 info = TABLE_SECTIONS[name] 105 while type(info) is not tuple: 106 if name in KINDS: 107 name = info 108 info = TABLE_SECTIONS[info] 109 110 columns, match_kind, sortkey = info 111 items = (v for v in groupitems if match_kind(v.kind)) 112 items = sorted(items, key=sortkey) 113 def render(): 114 yield '' 115 yield f'{name}:' 116 yield '' 117 for line in _render_table(items, columns, relroot): 118 yield line 119 return items, render 120 121 122####################################### 123# the checks 124 125CHECKS = { 126 #'globals': _check_globals, 127} 128 129 130def add_checks_cli(parser, checks=None, *, add_flags=None): 131 default = False 132 if not checks: 133 checks = list(CHECKS) 134 default = True 135 elif isinstance(checks, str): 136 checks = [checks] 137 if (add_flags is None and len(checks) > 1) or default: 138 add_flags = True 139 140 process_checks = add_sepval_cli(parser, '--check', 'checks', checks) 141 if add_flags: 142 for check in checks: 143 parser.add_argument(f'--{check}', dest='checks', 144 action='append_const', const=check) 145 return [ 146 process_checks, 147 ] 148 149 150def _get_check_handlers(fmt, printer, verbosity=VERBOSITY): 151 div = None 152 def handle_after(): 153 pass 154 if not fmt: 155 div = '' 156 def handle_failure(failure, data): 157 data = repr(data) 158 if verbosity >= 3: 159 logger.info(f'failure: {failure}') 160 logger.info(f'data: {data}') 161 else: 162 logger.warn(f'failure: {failure} (data: {data})') 163 elif fmt == 'raw': 164 def handle_failure(failure, data): 165 print(f'{failure!r} {data!r}') 166 elif fmt == 'brief': 167 def handle_failure(failure, data): 168 parent = data.parent or '' 169 funcname = parent if isinstance(parent, str) else parent.name 170 name = f'({funcname}).{data.name}' if funcname else data.name 171 failure = failure.split('\t')[0] 172 print(f'{data.filename}:{name} - {failure}') 173 elif fmt == 'summary': 174 def handle_failure(failure, data): 175 print(_fmt_one_summary(data, failure)) 176 elif fmt == 'full': 177 div = '' 178 def handle_failure(failure, data): 179 name = data.shortkey if data.kind is KIND.VARIABLE else data.name 180 parent = data.parent or '' 181 funcname = parent if isinstance(parent, str) else parent.name 182 known = 'yes' if data.is_known else '*** NO ***' 183 print(f'{data.kind.value} {name!r} failed ({failure})') 184 print(f' file: {data.filename}') 185 print(f' func: {funcname or "-"}') 186 print(f' name: {data.name}') 187 print(f' data: ...') 188 print(f' type unknown: {known}') 189 else: 190 if fmt in FORMATS: 191 raise NotImplementedError(fmt) 192 raise ValueError(f'unsupported fmt {fmt!r}') 193 return handle_failure, handle_after, div 194 195 196####################################### 197# the formats 198 199def fmt_raw(analysis): 200 for item in analysis: 201 yield from item.render('raw') 202 203 204def fmt_brief(analysis): 205 # XXX Support sorting. 206 items = sorted(analysis) 207 for kind in KINDS: 208 if kind is KIND.STATEMENT: 209 continue 210 for item in items: 211 if item.kind is not kind: 212 continue 213 yield from item.render('brief') 214 yield f' total: {len(items)}' 215 216 217def fmt_summary(analysis): 218 # XXX Support sorting and grouping. 219 items = list(analysis) 220 total = len(items) 221 222 def section(name): 223 _, render = build_section(name, items) 224 yield from render() 225 226 yield from section('types') 227 yield from section('functions') 228 yield from section('variables') 229 yield from section('statements') 230 231 yield '' 232# yield f'grand total: {len(supported) + len(unsupported)}' 233 yield f'grand total: {total}' 234 235 236def _fmt_one_summary(item, extra=None): 237 parent = item.parent or '' 238 funcname = parent if isinstance(parent, str) else parent.name 239 if extra: 240 return f'{item.filename:35}\t{funcname or "-":35}\t{item.name:40}\t{extra}' 241 else: 242 return f'{item.filename:35}\t{funcname or "-":35}\t{item.name}' 243 244 245def fmt_full(analysis): 246 # XXX Support sorting. 247 items = sorted(analysis, key=lambda v: v.key) 248 yield '' 249 for item in items: 250 yield from item.render('full') 251 yield '' 252 yield f'total: {len(items)}' 253 254 255FORMATS = { 256 'raw': fmt_raw, 257 'brief': fmt_brief, 258 'summary': fmt_summary, 259 'full': fmt_full, 260} 261 262 263def add_output_cli(parser, *, default='summary'): 264 parser.add_argument('--format', dest='fmt', default=default, choices=tuple(FORMATS)) 265 266 def process_args(args, *, argv=None): 267 pass 268 return process_args 269 270 271####################################### 272# the commands 273 274def _cli_check(parser, checks=None, **kwargs): 275 if isinstance(checks, str): 276 checks = [checks] 277 if checks is False: 278 process_checks = None 279 elif checks is None: 280 process_checks = add_checks_cli(parser) 281 elif len(checks) == 1 and type(checks) is not dict and re.match(r'^<.*>$', checks[0]): 282 check = checks[0][1:-1] 283 def process_checks(args, *, argv=None): 284 args.checks = [check] 285 else: 286 process_checks = add_checks_cli(parser, checks=checks) 287 process_progress = add_progress_cli(parser) 288 process_output = add_output_cli(parser, default=None) 289 process_files = add_files_cli(parser, **kwargs) 290 return [ 291 process_checks, 292 process_progress, 293 process_output, 294 process_files, 295 ] 296 297 298def cmd_check(filenames, *, 299 checks=None, 300 ignored=None, 301 fmt=None, 302 failfast=False, 303 iter_filenames=None, 304 relroot=fsutil.USE_CWD, 305 track_progress=None, 306 verbosity=VERBOSITY, 307 _analyze=_analyze, 308 _CHECKS=CHECKS, 309 **kwargs 310 ): 311 if not checks: 312 checks = _CHECKS 313 elif isinstance(checks, str): 314 checks = [checks] 315 checks = [_CHECKS[c] if isinstance(c, str) else c 316 for c in checks] 317 printer = Printer(verbosity) 318 (handle_failure, handle_after, div 319 ) = _get_check_handlers(fmt, printer, verbosity) 320 321 filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot) 322 filenames = filter_filenames(filenames, iter_filenames, relroot) 323 if track_progress: 324 filenames = track_progress(filenames) 325 326 logger.info('analyzing files...') 327 analyzed = _analyze(filenames, **kwargs) 328 analyzed.fix_filenames(relroot, normalize=False) 329 decls = filter_forward(analyzed, markpublic=True) 330 331 logger.info('checking analysis results...') 332 failed = [] 333 for data, failure in _check_all(decls, checks, failfast=failfast): 334 if data is None: 335 printer.info('stopping after one failure') 336 break 337 if div is not None and len(failed) > 0: 338 printer.info(div) 339 failed.append(data) 340 handle_failure(failure, data) 341 handle_after() 342 343 printer.info('-------------------------') 344 logger.info(f'total failures: {len(failed)}') 345 logger.info('done checking') 346 347 if fmt == 'summary': 348 print('Categorized by storage:') 349 print() 350 from .match import group_by_storage 351 grouped = group_by_storage(failed, ignore_non_match=False) 352 for group, decls in grouped.items(): 353 print() 354 print(group) 355 for decl in decls: 356 print(' ', _fmt_one_summary(decl)) 357 print(f'subtotal: {len(decls)}') 358 359 if len(failed) > 0: 360 sys.exit(len(failed)) 361 362 363def _cli_analyze(parser, **kwargs): 364 process_progress = add_progress_cli(parser) 365 process_output = add_output_cli(parser) 366 process_files = add_files_cli(parser, **kwargs) 367 return [ 368 process_progress, 369 process_output, 370 process_files, 371 ] 372 373 374# XXX Support filtering by kind. 375def cmd_analyze(filenames, *, 376 fmt=None, 377 iter_filenames=None, 378 relroot=fsutil.USE_CWD, 379 track_progress=None, 380 verbosity=None, 381 _analyze=_analyze, 382 formats=FORMATS, 383 **kwargs 384 ): 385 verbosity = verbosity if verbosity is not None else 3 386 387 try: 388 do_fmt = formats[fmt] 389 except KeyError: 390 raise ValueError(f'unsupported fmt {fmt!r}') 391 392 filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot) 393 filenames = filter_filenames(filenames, iter_filenames, relroot) 394 if track_progress: 395 filenames = track_progress(filenames) 396 397 logger.info('analyzing files...') 398 analyzed = _analyze(filenames, **kwargs) 399 analyzed.fix_filenames(relroot, normalize=False) 400 decls = filter_forward(analyzed, markpublic=True) 401 402 for line in do_fmt(decls): 403 print(line) 404 405 406def _cli_data(parser, filenames=None, known=None): 407 ArgumentParser = type(parser) 408 common = ArgumentParser(add_help=False) 409 # These flags will get processed by the top-level parse_args(). 410 add_verbosity_cli(common) 411 add_traceback_cli(common) 412 413 subs = parser.add_subparsers(dest='datacmd') 414 415 sub = subs.add_parser('show', parents=[common]) 416 if known is None: 417 sub.add_argument('--known', required=True) 418 if filenames is None: 419 sub.add_argument('filenames', metavar='FILE', nargs='+') 420 421 sub = subs.add_parser('dump', parents=[common]) 422 if known is None: 423 sub.add_argument('--known') 424 sub.add_argument('--show', action='store_true') 425 process_progress = add_progress_cli(sub) 426 427 sub = subs.add_parser('check', parents=[common]) 428 if known is None: 429 sub.add_argument('--known', required=True) 430 431 def process_args(args, *, argv): 432 if args.datacmd == 'dump': 433 process_progress(args, argv) 434 return process_args 435 436 437def cmd_data(datacmd, filenames, known=None, *, 438 _analyze=_analyze, 439 formats=FORMATS, 440 extracolumns=None, 441 relroot=fsutil.USE_CWD, 442 track_progress=None, 443 **kwargs 444 ): 445 kwargs.pop('verbosity', None) 446 usestdout = kwargs.pop('show', None) 447 if datacmd == 'show': 448 do_fmt = formats['summary'] 449 if isinstance(known, str): 450 known, _ = _datafiles.get_known(known, extracolumns, relroot) 451 for line in do_fmt(known): 452 print(line) 453 elif datacmd == 'dump': 454 filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot) 455 if track_progress: 456 filenames = track_progress(filenames) 457 analyzed = _analyze(filenames, **kwargs) 458 analyzed.fix_filenames(relroot, normalize=False) 459 if known is None or usestdout: 460 outfile = io.StringIO() 461 _datafiles.write_known(analyzed, outfile, extracolumns, 462 relroot=relroot) 463 print(outfile.getvalue()) 464 else: 465 _datafiles.write_known(analyzed, known, extracolumns, 466 relroot=relroot) 467 elif datacmd == 'check': 468 raise NotImplementedError(datacmd) 469 else: 470 raise ValueError(f'unsupported data command {datacmd!r}') 471 472 473COMMANDS = { 474 'check': ( 475 'analyze and fail if the given C source/header files have any problems', 476 [_cli_check], 477 cmd_check, 478 ), 479 'analyze': ( 480 'report on the state of the given C source/header files', 481 [_cli_analyze], 482 cmd_analyze, 483 ), 484 'data': ( 485 'check/manage local data (e.g. known types, ignored vars, caches)', 486 [_cli_data], 487 cmd_data, 488 ), 489} 490 491 492####################################### 493# the script 494 495def parse_args(argv=sys.argv[1:], prog=sys.argv[0], *, subset=None): 496 import argparse 497 parser = argparse.ArgumentParser( 498 prog=prog or get_prog(), 499 ) 500 501 processors = add_commands_cli( 502 parser, 503 commands={k: v[1] for k, v in COMMANDS.items()}, 504 commonspecs=[ 505 add_verbosity_cli, 506 add_traceback_cli, 507 ], 508 subset=subset, 509 ) 510 511 args = parser.parse_args(argv) 512 ns = vars(args) 513 514 cmd = ns.pop('cmd') 515 516 verbosity, traceback_cm = process_args_by_key( 517 args, 518 argv, 519 processors[cmd], 520 ['verbosity', 'traceback_cm'], 521 ) 522 # "verbosity" is sent to the commands, so we put it back. 523 args.verbosity = verbosity 524 525 return cmd, ns, verbosity, traceback_cm 526 527 528def main(cmd, cmd_kwargs): 529 try: 530 run_cmd = COMMANDS[cmd][0] 531 except KeyError: 532 raise ValueError(f'unsupported cmd {cmd!r}') 533 run_cmd(**cmd_kwargs) 534 535 536if __name__ == '__main__': 537 cmd, cmd_kwargs, verbosity, traceback_cm = parse_args() 538 configure_logger(verbosity) 539 with traceback_cm: 540 main(cmd, cmd_kwargs) 541