cpython/Tools/c-analyzer/c_parser/__main__.py

import logging
import sys

from c_common.scriptutil import (
    add_verbosity_cli,
    add_traceback_cli,
    add_kind_filtering_cli,
    add_files_cli,
    add_commands_cli,
    process_args_by_key,
    configure_logger,
    get_prog,
    main_for_filenames,
)
from .preprocessor.__main__ import (
    add_common_cli as add_preprocessor_cli,
)
from .info import KIND
from . import parse_file as _iter_parsed


logger = logging.getLogger(__name__)


def _format_vartype(vartype):
    if isinstance(vartype, str):
        return vartype

    data = vartype
    try:
        vartype = data['vartype']
    except KeyError:
        storage, typequal, typespec, abstract = vartype.values()
    else:
        storage = data.get('storage')
        if storage:
            _, typequal, typespec, abstract = vartype.values()
        else:
            storage, typequal, typespec, abstract = vartype.values()

    vartype = f'{typespec} {abstract}'
    if typequal:
        vartype = f'{typequal} {vartype}'
    if storage:
        vartype = f'{storage} {vartype}'
    return vartype


def _get_preprocessor(filename, **kwargs):
    return get_processor(filename,
                         log_err=print,
                         **kwargs
                         )


#######################################
# the formats

def fmt_raw(filename, item, *, showfwd=None):
    yield str(tuple(item))


def fmt_summary(filename, item, *, showfwd=None):
    if item.filename != filename:
        yield f'> {item.filename}'

    if showfwd is None:
        LINE = ' {lno:>5} {kind:10} {funcname:40} {fwd:1} {name:40} {data}'
    else:
        LINE = ' {lno:>5} {kind:10} {funcname:40} {name:40} {data}'
    lno = kind = funcname = fwd = name = data = ''
    MIN_LINE = len(LINE.format(**locals()))

    fileinfo, kind, funcname, name, data = item
    lno = fileinfo.lno if fileinfo and fileinfo.lno >= 0 else ''
    funcname = funcname or ' --'
    name = name or ' --'
    isforward = False
    if kind is KIND.FUNCTION:
        storage, inline, params, returntype, isforward = data.values()
        returntype = _format_vartype(returntype)
        data = returntype + params
        if inline:
            data = f'inline {data}'
        if storage:
            data = f'{storage} {data}'
    elif kind is KIND.VARIABLE:
        data = _format_vartype(data)
    elif kind is KIND.STRUCT or kind is KIND.UNION:
        if data is None:
            isforward = True
        else:
            fields = data
            data = f'({len(data)}) {{ '
            indent = ',\n' + ' ' * (MIN_LINE + len(data))
            data += ', '.join(f.name for f in fields[:5])
            fields = fields[5:]
            while fields:
                data = f'{data}{indent}{", ".join(f.name for f in fields[:5])}'
                fields = fields[5:]
            data += ' }'
    elif kind is KIND.ENUM:
        if data is None:
            isforward = True
        else:
            names = [d if isinstance(d, str) else d.name
                     for d in data]
            data = f'({len(data)}) {{ '
            indent = ',\n' + ' ' * (MIN_LINE + len(data))
            data += ', '.join(names[:5])
            names = names[5:]
            while names:
                data = f'{data}{indent}{", ".join(names[:5])}'
                names = names[5:]
            data += ' }'
    elif kind is KIND.TYPEDEF:
        data = f'typedef {data}'
    elif kind == KIND.STATEMENT:
        pass
    else:
        raise NotImplementedError(item)
    if isforward:
        fwd = '*'
        if not showfwd and showfwd is not None:
            return
    elif showfwd:
        return
    kind = kind.value
    yield LINE.format(**locals())


def fmt_full(filename, item, *, showfwd=None):
    raise NotImplementedError


FORMATS = {
    'raw': fmt_raw,
    'summary': fmt_summary,
    'full': fmt_full,
}


def add_output_cli(parser):
    parser.add_argument('--format', dest='fmt', default='summary', choices=tuple(FORMATS))
    parser.add_argument('--showfwd', action='store_true', default=None)
    parser.add_argument('--no-showfwd', dest='showfwd', action='store_false', default=None)

    def process_args(args, *, argv=None):
        pass
    return process_args


#######################################
# the commands

def _cli_parse(parser, excluded=None, **prepr_kwargs):
    process_output = add_output_cli(parser)
    process_kinds = add_kind_filtering_cli(parser)
    process_preprocessor = add_preprocessor_cli(parser, **prepr_kwargs)
    process_files = add_files_cli(parser, excluded=excluded)
    return [
        process_output,
        process_kinds,
        process_preprocessor,
        process_files,
    ]


def cmd_parse(filenames, *,
              fmt='summary',
              showfwd=None,
              iter_filenames=None,
              relroot=None,
              **kwargs
              ):
    if 'get_file_preprocessor' not in kwargs:
        kwargs['get_file_preprocessor'] = _get_preprocessor()
    try:
        do_fmt = FORMATS[fmt]
    except KeyError:
        raise ValueError(f'unsupported fmt {fmt!r}')
    for filename, relfile in main_for_filenames(filenames, iter_filenames, relroot):
        for item in _iter_parsed(filename, **kwargs):
            item = item.fix_filename(relroot, fixroot=False, normalize=False)
            for line in do_fmt(relfile, item, showfwd=showfwd):
                print(line)


def _cli_data(parser):
    ...

    return []


def cmd_data(filenames,
             **kwargs
             ):
    # XXX
    raise NotImplementedError


COMMANDS = {
    'parse': (
        'parse the given C source & header files',
        [_cli_parse],
        cmd_parse,
    ),
    'data': (
        'check/manage local data (e.g. excludes, macros)',
        [_cli_data],
        cmd_data,
    ),
}


#######################################
# the script

def parse_args(argv=sys.argv[1:], prog=sys.argv[0], *, subset='parse'):
    import argparse
    parser = argparse.ArgumentParser(
        prog=prog or get_prog,
    )

    processors = add_commands_cli(
        parser,
        commands={k: v[1] for k, v in COMMANDS.items()},
        commonspecs=[
            add_verbosity_cli,
            add_traceback_cli,
        ],
        subset=subset,
    )

    args = parser.parse_args(argv)
    ns = vars(args)

    cmd = ns.pop('cmd')

    verbosity, traceback_cm = process_args_by_key(
        args,
        argv,
        processors[cmd],
        ['verbosity', 'traceback_cm'],
    )

    return cmd, ns, verbosity, traceback_cm


def main(cmd, cmd_kwargs):
    try:
        run_cmd = COMMANDS[cmd][0]
    except KeyError:
        raise ValueError(f'unsupported cmd {cmd!r}')
    run_cmd(**cmd_kwargs)


if __name__ == '__main__':
    cmd, cmd_kwargs, verbosity, traceback_cm = parse_args()
    configure_logger(verbosity)
    with traceback_cm:
        main(cmd, cmd_kwargs)