summaryrefslogtreecommitdiffstats
path: root/Tools/c-analyzer/cpython/_capi.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/c-analyzer/cpython/_capi.py')
-rw-r--r--Tools/c-analyzer/cpython/_capi.py362
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,
}