diff options
Diffstat (limited to 'Tools/c-analyzer/cpython/_capi.py')
-rw-r--r-- | Tools/c-analyzer/cpython/_capi.py | 362 |
1 files changed, 274 insertions, 88 deletions
diff --git a/Tools/c-analyzer/cpython/_capi.py b/Tools/c-analyzer/cpython/_capi.py index 38d7cd3..df8159a 100644 --- a/Tools/c-analyzer/cpython/_capi.py +++ b/Tools/c-analyzer/cpython/_capi.py @@ -1,4 +1,5 @@ from collections import namedtuple +import logging import os import os.path import re @@ -10,6 +11,9 @@ from ._files import iter_header_files, resolve_filename from . import REPO_ROOT +logger = logging.getLogger(__name__) + + INCLUDE_ROOT = os.path.join(REPO_ROOT, 'Include') INCLUDE_CPYTHON = os.path.join(INCLUDE_ROOT, 'cpython') INCLUDE_INTERNAL = os.path.join(INCLUDE_ROOT, 'internal') @@ -122,31 +126,34 @@ def _parse_line(line, prev=None): results = zip(KINDS, m.groups()) for kind, name in results: if name: - clean = last.split('//')[0].strip() + clean = last.split('//')[0].rstrip() if clean.endswith('*/'): clean = clean.split('/*')[0].rstrip() + if kind == 'macro' or kind == 'constant': - if clean.endswith('\\'): - return line # the new "prev" + if not clean.endswith('\\'): + return name, kind elif kind == 'inline': - if not prev: - if not clean.endswith('}'): - return line # the new "prev" - elif clean != '}': - return line # the new "prev" - elif not clean.endswith(';'): - return line # the new "prev" - return name, kind + if clean.endswith('}'): + if not prev or clean == '}': + return name, kind + elif kind == 'func' or kind == 'data': + if clean.endswith(';'): + return name, kind + else: + # This should not be reached. + raise NotImplementedError + return line # the new "prev" # It was a plain #define. return None -LEVELS = { +LEVELS = [ 'stable', 'cpython', 'private', 'internal', -} +] def _get_level(filename, name, *, _cpython=INCLUDE_CPYTHON + os.path.sep, @@ -165,6 +172,12 @@ def _get_level(filename, name, *, #return '???' +GROUPINGS = { + 'kind': KINDS, + 'level': LEVELS, +} + + class CAPIItem(namedtuple('CAPIItem', 'file lno name kind level')): @classmethod @@ -231,34 +244,70 @@ def _parse_groupby(raw): else: raise NotImplementedError - if not all(v in ('kind', 'level') for v in groupby): + if not all(v in GROUPINGS for v in groupby): raise ValueError(f'invalid groupby value {raw!r}') return groupby -def summarize(items, *, groupby='kind'): - summary = {} +def _resolve_full_groupby(groupby): + if isinstance(groupby, str): + groupby = [groupby] + groupings = [] + for grouping in groupby + list(GROUPINGS): + if grouping not in groupings: + groupings.append(grouping) + return groupings + + +def summarize(items, *, groupby='kind', includeempty=True, minimize=None): + if minimize is None: + if includeempty is None: + minimize = True + includeempty = False + else: + minimize = includeempty + elif includeempty is None: + includeempty = minimize + elif minimize and includeempty: + raise ValueError(f'cannot minimize and includeempty at the same time') groupby = _parse_groupby(groupby)[0] - if groupby == 'kind': - outers = KINDS - inners = LEVELS - def increment(item): - summary[item.kind][item.level] += 1 - elif groupby == 'level': - outers = LEVELS - inners = KINDS - def increment(item): - summary[item.level][item.kind] += 1 - else: - raise NotImplementedError + _outer, _inner = _resolve_full_groupby(groupby) + outers = GROUPINGS[_outer] + inners = GROUPINGS[_inner] + + summary = { + 'totals': { + 'all': 0, + 'subs': {o: 0 for o in outers}, + 'bygroup': {o: {i: 0 for i in inners} + for o in outers}, + }, + } - for outer in outers: - summary[outer] = _outer = {} - for inner in inners: - _outer[inner] = 0 for item in items: - increment(item) + outer = getattr(item, _outer) + inner = getattr(item, _inner) + # Update totals. + summary['totals']['all'] += 1 + summary['totals']['subs'][outer] += 1 + summary['totals']['bygroup'][outer][inner] += 1 + + if not includeempty: + subtotals = summary['totals']['subs'] + bygroup = summary['totals']['bygroup'] + for outer in outers: + if subtotals[outer] == 0: + del subtotals[outer] + del bygroup[outer] + continue + + for inner in inners: + if bygroup[outer][inner] == 0: + del bygroup[outer][inner] + if minimize: + if len(bygroup[outer]) == 1: + del bygroup[outer] return summary @@ -289,20 +338,72 @@ def iter_capi(filenames=None): yield item -def _collate(items, groupby): +def resolve_filter(ignored): + if not ignored: + return None + ignored = set(_resolve_ignored(ignored)) + def filter(item, *, log=None): + if item.name not in ignored: + return True + if log is not None: + log(f'ignored {item.name!r}') + return False + return filter + + +def _resolve_ignored(ignored): + if isinstance(ignored, str): + ignored = [ignored] + for raw in ignored: + if isinstance(raw, str): + if raw.startswith('|'): + yield raw[1:] + elif raw.startswith('<') and raw.endswith('>'): + filename = raw[1:-1] + try: + infile = open(filename) + except Exception as exc: + logger.error(f'ignore file failed: {exc}') + continue + logger.log(1, f'reading ignored names from {filename!r}') + with infile: + for line in infile: + if not line: + continue + if line[0].isspace(): + continue + line = line.partition('#')[0].rstrip() + if line: + # XXX Recurse? + yield line + else: + raw = raw.strip() + if raw: + yield raw + else: + raise NotImplementedError + + +def _collate(items, groupby, includeempty): groupby = _parse_groupby(groupby)[0] maxfilename = maxname = maxkind = maxlevel = 0 + collated = {} + groups = GROUPINGS[groupby] + for group in groups: + collated[group] = [] + for item in items: key = getattr(item, groupby) - if key in collated: - collated[key].append(item) - else: - collated[key] = [item] + collated[key].append(item) maxfilename = max(len(item.relfile), maxfilename) maxname = max(len(item.name), maxname) maxkind = max(len(item.kind), maxkind) maxlevel = max(len(item.level), maxlevel) + if not includeempty: + for group in groups: + if not collated[group]: + del collated[group] maxextra = { 'kind': maxkind, 'level': maxlevel, @@ -310,27 +411,55 @@ def _collate(items, groupby): return collated, groupby, maxfilename, maxname, maxextra +def _get_sortkey(sort, _groupby, _columns): + if sort is True or sort is None: + # For now: + def sortkey(item): + return ( + item.level == 'private', + LEVELS.index(item.level), + KINDS.index(item.kind), + os.path.dirname(item.file), + os.path.basename(item.file), + item.name, + ) + return sortkey + + sortfields = 'not-private level kind dirname basename name'.split() + elif isinstance(sort, str): + sortfields = sort.replace(',', ' ').strip().split() + elif callable(sort): + return sort + else: + raise NotImplementedError + + # XXX Build a sortkey func from sortfields. + raise NotImplementedError + + ################################## # CLI rendering -_LEVEL_MARKERS = { - 'S': 'stable', - 'C': 'cpython', - 'P': 'private', - 'I': 'internal', -} -_KIND_MARKERS = { - 'F': 'func', - 'D': 'data', - 'I': 'inline', - 'M': 'macro', - 'C': 'constant', +_MARKERS = { + 'level': { + 'S': 'stable', + 'C': 'cpython', + 'P': 'private', + 'I': 'internal', + }, + 'kind': { + 'F': 'func', + 'D': 'data', + 'I': 'inline', + 'M': 'macro', + 'C': 'constant', + }, } def resolve_format(format): if not format: - return 'brief' + return 'table' elif isinstance(format, str) and format in _FORMATS: return format else: @@ -350,19 +479,29 @@ def get_renderer(format): return render -def render_table(items, *, columns=None, groupby='kind', verbose=False): +def render_table(items, *, + columns=None, + groupby='kind', + sort=True, + showempty=False, + verbose=False, + ): + if groupby is None: + groupby = 'kind' + if showempty is None: + showempty = False + if groupby: - collated, groupby, maxfilename, maxname, maxextra = _collate(items, groupby) - if groupby == 'kind': - groups = KINDS - extras = ['level'] - markers = {'level': _LEVEL_MARKERS} - elif groupby == 'level': - groups = LEVELS - extras = ['kind'] - markers = {'kind': _KIND_MARKERS} - else: - raise NotImplementedError + (collated, groupby, maxfilename, maxname, maxextra, + ) = _collate(items, groupby, showempty) + for grouping in GROUPINGS: + maxextra[grouping] = max(len(g) for g in GROUPINGS[grouping]) + + _, extra = _resolve_full_groupby(groupby) + extras = [extra] + markers = {extra: _MARKERS[extra]} + + groups = GROUPINGS[groupby] else: # XXX Support no grouping? raise NotImplementedError @@ -373,8 +512,6 @@ def render_table(items, *, columns=None, groupby='kind', verbose=False): for extra in ('kind', 'level')} else: if verbose: - maxextra['kind'] = max(len(kind) for kind in KINDS) - maxextra['level'] = max(len(level) for level in LEVELS) extracols = [f'{extra}:{maxextra[extra]}' for extra in extras] def get_extra(item): @@ -404,32 +541,51 @@ def render_table(items, *, columns=None, groupby='kind', verbose=False): ] header, div, fmt = build_table(columns) + if sort: + sortkey = _get_sortkey(sort, groupby, columns) + total = 0 - for group in groups: - if group not in collated: + for group, grouped in collated.items(): + if not showempty and group not in collated: continue yield '' yield f' === {group} ===' yield '' yield header yield div - for item in collated[group]: - yield fmt.format( - filename=item.relfile, - name=item.name, - **get_extra(item), - ) + if grouped: + if sort: + grouped = sorted(grouped, key=sortkey) + for item in grouped: + yield fmt.format( + filename=item.relfile, + name=item.name, + **get_extra(item), + ) yield div - subtotal = len(collated[group]) + subtotal = len(grouped) yield f' sub-total: {subtotal}' total += subtotal yield '' yield f'total: {total}' -def render_full(items, *, groupby=None, verbose=False): +def render_full(items, *, + groupby='kind', + sort=None, + showempty=None, + verbose=False, + ): + if groupby is None: + groupby = 'kind' + if showempty is None: + showempty = False + + if sort: + sortkey = _get_sortkey(sort, groupby, None) + if groupby: - collated, groupby, _, _, _ = _collate(items, groupby) + collated, groupby, _, _, _ = _collate(items, groupby, showempty) for group, grouped in collated.items(): yield '#' * 25 yield f'# {group} ({len(grouped)})' @@ -437,10 +593,14 @@ def render_full(items, *, groupby=None, verbose=False): yield '' if not grouped: continue + if sort: + grouped = sorted(grouped, key=sortkey) for item in grouped: yield from _render_item_full(item, groupby, verbose) yield '' else: + if sort: + items = sorted(items, key=sortkey) for item in items: yield from _render_item_full(item, None, verbose) yield '' @@ -459,21 +619,47 @@ def _render_item_full(item, groupby, verbose): print(' ---------------------------------------') -def render_summary(items, *, groupby='kind', verbose=False): - total = 0 - summary = summarize(items, groupby=groupby) - # XXX Stablize the sorting to match KINDS/LEVELS. - for outer, counts in summary.items(): - subtotal = sum(c for _, c in counts.items()) - yield f'{outer + ":":20} ({subtotal})' - for inner, count in counts.items(): - yield f' {inner + ":":9} {count}' - total += subtotal - yield f'{"total:":20} ({total})' +def render_summary(items, *, + groupby='kind', + sort=None, + showempty=None, + verbose=False, + ): + if groupby is None: + groupby = 'kind' + summary = summarize( + items, + groupby=groupby, + includeempty=showempty, + minimize=None if showempty else not verbose, + ) + + subtotals = summary['totals']['subs'] + bygroup = summary['totals']['bygroup'] + lastempty = False + for outer, subtotal in subtotals.items(): + if bygroup: + subtotal = f'({subtotal})' + yield f'{outer + ":":20} {subtotal:>8}' + else: + yield f'{outer + ":":10} {subtotal:>8}' + if outer in bygroup: + for inner, count in bygroup[outer].items(): + yield f' {inner + ":":9} {count}' + lastempty = False + else: + lastempty = True + + total = f'*{summary["totals"]["all"]}*' + label = '*total*:' + if bygroup: + yield f'{label:20} {total:>8}' + else: + yield f'{label:10} {total:>9}' _FORMATS = { - 'brief': render_table, + 'table': render_table, 'full': render_full, 'summary': render_summary, } |