aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/c-analyzer/cpython/__main__.py')
-rw-r--r--Tools/c-analyzer/cpython/__main__.py446
1 files changed, 257 insertions, 189 deletions
diff --git a/Tools/c-analyzer/cpython/__main__.py b/Tools/c-analyzer/cpython/__main__.py
index 6b0f9bcb968..23a3de06f63 100644
--- a/Tools/c-analyzer/cpython/__main__.py
+++ b/Tools/c-analyzer/cpython/__main__.py
@@ -1,212 +1,280 @@
-import argparse
-import re
+import logging
import sys
-from c_analyzer.common import show
-from c_analyzer.common.info import UNKNOWN
+from c_common.fsutil import expand_filenames, iter_files_by_suffix
+from c_common.scriptutil import (
+ add_verbosity_cli,
+ add_traceback_cli,
+ add_commands_cli,
+ add_kind_filtering_cli,
+ add_files_cli,
+ process_args_by_key,
+ configure_logger,
+ get_prog,
+)
+from c_parser.info import KIND
+import c_parser.__main__ as c_parser
+import c_analyzer.__main__ as c_analyzer
+import c_analyzer as _c_analyzer
+from c_analyzer.info import UNKNOWN
+from . import _analyzer, _parser, REPO_ROOT
+
+
+logger = logging.getLogger(__name__)
+
+
+def _resolve_filenames(filenames):
+ if filenames:
+ resolved = (_parser.resolve_filename(f) for f in filenames)
+ else:
+ resolved = _parser.iter_filenames()
+ return resolved
+
+
+def fmt_summary(analysis):
+ # XXX Support sorting and grouping.
+ supported = []
+ unsupported = []
+ for item in analysis:
+ if item.supported:
+ supported.append(item)
+ else:
+ unsupported.append(item)
+ total = 0
+
+ def section(name, groupitems):
+ nonlocal total
+ items, render = c_analyzer.build_section(name, groupitems,
+ relroot=REPO_ROOT)
+ yield from render()
+ total += len(items)
+
+ yield ''
+ yield '===================='
+ yield 'supported'
+ yield '===================='
+
+ yield from section('types', supported)
+ yield from section('variables', supported)
+
+ yield ''
+ yield '===================='
+ yield 'unsupported'
+ yield '===================='
+
+ yield from section('types', unsupported)
+ yield from section('variables', unsupported)
+
+ yield ''
+ yield f'grand total: {total}'
+
-from . import SOURCE_DIRS
-from .find import supported_vars
-from .known import (
- from_file as known_from_file,
- DATA_FILE as KNOWN_FILE,
+#######################################
+# the checks
+
+CHECKS = dict(c_analyzer.CHECKS, **{
+ 'globals': _analyzer.check_globals,
+})
+
+#######################################
+# the commands
+
+FILES_KWARGS = dict(excluded=_parser.EXCLUDED, nargs='*')
+
+
+def _cli_parse(parser):
+ process_output = c_parser.add_output_cli(parser)
+ process_kind = add_kind_filtering_cli(parser)
+ process_preprocessor = c_parser.add_preprocessor_cli(
+ parser,
+ get_preprocessor=_parser.get_preprocessor,
+ )
+ process_files = add_files_cli(parser, **FILES_KWARGS)
+ return [
+ process_output,
+ process_kind,
+ process_preprocessor,
+ process_files,
+ ]
+
+
+def cmd_parse(filenames=None, **kwargs):
+ filenames = _resolve_filenames(filenames)
+ if 'get_file_preprocessor' not in kwargs:
+ kwargs['get_file_preprocessor'] = _parser.get_preprocessor()
+ c_parser.cmd_parse(filenames, **kwargs)
+
+
+def _cli_check(parser, **kwargs):
+ return c_analyzer._cli_check(parser, CHECKS, **kwargs, **FILES_KWARGS)
+
+
+def cmd_check(filenames=None, **kwargs):
+ filenames = _resolve_filenames(filenames)
+ kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print)
+ c_analyzer.cmd_check(
+ filenames,
+ relroot=REPO_ROOT,
+ _analyze=_analyzer.analyze,
+ _CHECKS=CHECKS,
+ **kwargs
)
-from .supported import IGNORED_FILE
-
-
-def _check_results(unknown, knownvars, used):
- def _match_unused_global(variable):
- found = []
- for varid in knownvars:
- if varid in used:
- continue
- if varid.funcname is not None:
- continue
- if varid.name != variable.name:
- continue
- if variable.filename and variable.filename != UNKNOWN:
- if variable.filename == varid.filename:
- found.append(varid)
- else:
- found.append(varid)
- return found
-
- badknown = set()
- for variable in sorted(unknown):
- msg = None
- if variable.funcname != UNKNOWN:
- msg = f'could not find global symbol {variable.id}'
- elif m := _match_unused_global(variable):
- assert isinstance(m, list)
- badknown.update(m)
- elif variable.name in ('completed', 'id'): # XXX Figure out where these variables are.
- unknown.remove(variable)
- else:
- msg = f'could not find local symbol {variable.id}'
- if msg:
- #raise Exception(msg)
- print(msg)
- if badknown:
- print('---')
- print(f'{len(badknown)} globals in known.tsv, but may actually be local:')
- for varid in sorted(badknown):
- print(f'{varid.filename:30} {varid.name}')
- unused = sorted(varid
- for varid in set(knownvars) - used
- if varid.name != 'id') # XXX Figure out where these variables are.
- if unused:
- print('---')
- print(f'did not use {len(unused)} known vars:')
- for varid in unused:
- print(f'{varid.filename:30} {varid.funcname or "-":20} {varid.name}')
- raise Exception('not all known symbols used')
- if unknown:
- print('---')
- raise Exception('could not find all symbols')
-
-
-# XXX Move this check to its own command.
-def cmd_check_cache(cmd, *,
- known=KNOWN_FILE,
- ignored=IGNORED_FILE,
- _known_from_file=known_from_file,
- _find=supported_vars,
- ):
- known = _known_from_file(known)
-
- used = set()
- unknown = set()
- for var, supported in _find(known=known, ignored=ignored):
- if supported is None:
- unknown.add(var)
- continue
- used.add(var.id)
- _check_results(unknown, known['variables'], used)
-
-
-def cmd_check(cmd, *,
- known=KNOWN_FILE,
- ignored=IGNORED_FILE,
- _find=supported_vars,
- _show=show.basic,
- _print=print,
- ):
- """
- Fail if there are unsupported globals variables.
-
- In the failure case, the list of unsupported variables
- will be printed out.
- """
- unsupported = []
- for var, supported in _find(known=known, ignored=ignored):
- if not supported:
- unsupported.append(var)
-
- if not unsupported:
- #_print('okay')
- return
-
- _print('ERROR: found unsupported global variables')
- _print()
- _show(sorted(unsupported))
- _print(f' ({len(unsupported)} total)')
- sys.exit(1)
-
-
-def cmd_show(cmd, *,
- known=KNOWN_FILE,
- ignored=IGNORED_FILE,
- skip_objects=False,
- _find=supported_vars,
- _show=show.basic,
- _print=print,
- ):
- """
- Print out the list of found global variables.
-
- The variables will be distinguished as "supported" or "unsupported".
- """
- allsupported = []
- allunsupported = []
- for found, supported in _find(known=known,
- ignored=ignored,
- skip_objects=skip_objects,
- ):
- if supported is None:
- continue
- (allsupported if supported else allunsupported
- ).append(found)
-
- _print('supported:')
- _print('----------')
- _show(sorted(allsupported))
- _print(f' ({len(allsupported)} total)')
- _print()
- _print('unsupported:')
- _print('------------')
- _show(sorted(allunsupported))
- _print(f' ({len(allunsupported)} total)')
-
-
-#############################
-# the script
-COMMANDS = {
- 'check': cmd_check,
- 'show': cmd_show,
- }
-
-PROG = sys.argv[0]
-PROG = 'c-globals.py'
-
-
-def parse_args(prog=PROG, argv=sys.argv[1:], *, _fail=None):
- common = argparse.ArgumentParser(add_help=False)
- common.add_argument('--ignored', metavar='FILE',
- default=IGNORED_FILE,
- help='path to file that lists ignored vars')
- common.add_argument('--known', metavar='FILE',
- default=KNOWN_FILE,
- help='path to file that lists known types')
- #common.add_argument('dirs', metavar='DIR', nargs='*',
- # default=SOURCE_DIRS,
- # help='a directory to check')
- parser = argparse.ArgumentParser(
- prog=prog,
+def cmd_analyze(filenames=None, **kwargs):
+ formats = dict(c_analyzer.FORMATS)
+ formats['summary'] = fmt_summary
+ filenames = _resolve_filenames(filenames)
+ kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print)
+ c_analyzer.cmd_analyze(
+ filenames,
+ _analyze=_analyzer.analyze,
+ formats=formats,
+ **kwargs
+ )
+
+
+def _cli_data(parser):
+ filenames = False
+ known = True
+ return c_analyzer._cli_data(parser, filenames, known)
+
+
+def cmd_data(datacmd, **kwargs):
+ formats = dict(c_analyzer.FORMATS)
+ formats['summary'] = fmt_summary
+ filenames = (file
+ for file in _resolve_filenames(None)
+ if file not in _parser.EXCLUDED)
+ kwargs['get_file_preprocessor'] = _parser.get_preprocessor(log_err=print)
+ if datacmd == 'show':
+ types = _analyzer.read_known()
+ results = []
+ for decl, info in types.items():
+ if info is UNKNOWN:
+ if decl.kind in (KIND.STRUCT, KIND.UNION):
+ extra = {'unsupported': ['type unknown'] * len(decl.members)}
+ else:
+ extra = {'unsupported': ['type unknown']}
+ info = (info, extra)
+ results.append((decl, info))
+ if decl.shortkey == 'struct _object':
+ tempinfo = info
+ known = _analyzer.Analysis.from_results(results)
+ analyze = None
+ elif datacmd == 'dump':
+ known = _analyzer.KNOWN_FILE
+ def analyze(files, **kwargs):
+ decls = []
+ for decl in _analyzer.iter_decls(files, **kwargs):
+ if not KIND.is_type_decl(decl.kind):
+ continue
+ if not decl.filename.endswith('.h'):
+ if decl.shortkey not in _analyzer.KNOWN_IN_DOT_C:
+ continue
+ decls.append(decl)
+ results = _c_analyzer.analyze_decls(
+ decls,
+ known={},
+ analyze_resolved=_analyzer.analyze_resolved,
)
- subs = parser.add_subparsers(dest='cmd')
+ return _analyzer.Analysis.from_results(results)
+ else:
+ known = _analyzer.read_known()
+ def analyze(files, **kwargs):
+ return _analyzer.iter_decls(files, **kwargs)
+ extracolumns = None
+ c_analyzer.cmd_data(
+ datacmd,
+ filenames,
+ known,
+ _analyze=analyze,
+ formats=formats,
+ extracolumns=extracolumns,
+ relroot=REPO_ROOT,
+ **kwargs
+ )
- check = subs.add_parser('check', parents=[common])
- show = subs.add_parser('show', parents=[common])
- show.add_argument('--skip-objects', action='store_true')
+# We do not define any other cmd_*() handlers here,
+# favoring those defined elsewhere.
- if _fail is None:
- def _fail(msg):
- parser.error(msg)
+COMMANDS = {
+ 'check': (
+ 'analyze and fail if the CPython source code has any problems',
+ [_cli_check],
+ cmd_check,
+ ),
+ 'analyze': (
+ 'report on the state of the CPython source code',
+ [(lambda p: c_analyzer._cli_analyze(p, **FILES_KWARGS))],
+ cmd_analyze,
+ ),
+ 'parse': (
+ 'parse the CPython source files',
+ [_cli_parse],
+ cmd_parse,
+ ),
+ 'data': (
+ 'check/manage local data (e.g. knwon types, ignored vars, caches)',
+ [_cli_data],
+ cmd_data,
+ ),
+}
+
+
+#######################################
+# the script
+
+def parse_args(argv=sys.argv[1:], prog=None, *, subset=None):
+ import argparse
+ parser = argparse.ArgumentParser(
+ prog=prog or get_prog(),
+ )
+
+# if subset == 'check' or subset == ['check']:
+# if checks is not None:
+# commands = dict(COMMANDS)
+# commands['check'] = list(commands['check'])
+# cli = commands['check'][1][0]
+# commands['check'][1][0] = (lambda p: cli(p, checks=checks))
+ processors = add_commands_cli(
+ parser,
+ commands=COMMANDS,
+ commonspecs=[
+ add_verbosity_cli,
+ add_traceback_cli,
+ ],
+ subset=subset,
+ )
- # Now parse the args.
args = parser.parse_args(argv)
ns = vars(args)
cmd = ns.pop('cmd')
- if not cmd:
- _fail('missing command')
- return cmd, ns
+ verbosity, traceback_cm = process_args_by_key(
+ args,
+ processors[cmd],
+ ['verbosity', 'traceback_cm'],
+ )
+ if cmd != 'parse':
+ # "verbosity" is sent to the commands, so we put it back.
+ args.verbosity = verbosity
+
+ return cmd, ns, verbosity, traceback_cm
-def main(cmd, cmdkwargs=None, *, _COMMANDS=COMMANDS):
+def main(cmd, cmd_kwargs):
try:
- cmdfunc = _COMMANDS[cmd]
+ run_cmd = COMMANDS[cmd][-1]
except KeyError:
- raise ValueError(
- f'unsupported cmd {cmd!r}' if cmd else 'missing cmd')
-
- cmdfunc(cmd, **cmdkwargs or {})
+ raise ValueError(f'unsupported cmd {cmd!r}')
+ run_cmd(**cmd_kwargs)
if __name__ == '__main__':
- cmd, cmdkwargs = parse_args()
- main(cmd, cmdkwargs)
+ cmd, cmd_kwargs, verbosity, traceback_cm = parse_args()
+ configure_logger(verbosity)
+ with traceback_cm:
+ main(cmd, cmd_kwargs)