mirror of
https://github.com/python/cpython.git
synced 2025-10-25 02:43:41 +00:00
The original tool wasn't working right and it was simpler to create a new one, partially re-using some of the old code. At this point the tool runs properly on the master. (Try: ./python Tools/c-analyzer/c-analyzer.py analyze.) It take ~40 seconds on my machine to analyze the full CPython code base. Note that we'll need to iron out some OS-specific stuff (e.g. preprocessor). We're okay though since this tool isn't used yet in our workflow. We will also need to verify the analysis results in detail before activating the check in CI, though I'm pretty sure it's close. https://bugs.python.org/issue36876
1658 lines
48 KiB
Python
1658 lines
48 KiB
Python
from collections import namedtuple
|
|
import enum
|
|
import os.path
|
|
import re
|
|
|
|
from c_common.clsutil import classonly
|
|
import c_common.misc as _misc
|
|
import c_common.strutil as _strutil
|
|
import c_common.tables as _tables
|
|
from .parser._regexes import SIMPLE_TYPE
|
|
|
|
|
|
FIXED_TYPE = _misc.Labeled('FIXED_TYPE')
|
|
|
|
POTS_REGEX = re.compile(rf'^{SIMPLE_TYPE}$', re.VERBOSE)
|
|
|
|
|
|
def is_pots(typespec):
|
|
if not typespec:
|
|
return None
|
|
if type(typespec) is not str:
|
|
_, _, _, typespec, _ = get_parsed_vartype(typespec)
|
|
return POTS_REGEX.match(typespec) is not None
|
|
|
|
|
|
def is_funcptr(vartype):
|
|
if not vartype:
|
|
return None
|
|
_, _, _, _, abstract = get_parsed_vartype(vartype)
|
|
return _is_funcptr(abstract)
|
|
|
|
|
|
def _is_funcptr(declstr):
|
|
if not declstr:
|
|
return None
|
|
# XXX Support "(<name>*)(".
|
|
return '(*)(' in declstr.replace(' ', '')
|
|
|
|
|
|
def is_exported_symbol(decl):
|
|
_, storage, _, _, _ = get_parsed_vartype(decl)
|
|
raise NotImplementedError
|
|
|
|
|
|
def is_process_global(vardecl):
|
|
kind, storage, _, _, _ = get_parsed_vartype(vardecl)
|
|
if kind is not KIND.VARIABLE:
|
|
raise NotImplementedError(vardecl)
|
|
if 'static' in (storage or ''):
|
|
return True
|
|
|
|
if hasattr(vardecl, 'parent'):
|
|
parent = vardecl.parent
|
|
else:
|
|
parent = vardecl.get('parent')
|
|
return not parent
|
|
|
|
|
|
def is_fixed_type(vardecl):
|
|
if not vardecl:
|
|
return None
|
|
_, _, _, typespec, abstract = get_parsed_vartype(vardecl)
|
|
if 'typeof' in typespec:
|
|
raise NotImplementedError(vardecl)
|
|
elif not abstract:
|
|
return True
|
|
|
|
if '*' not in abstract:
|
|
# XXX What about []?
|
|
return True
|
|
elif _is_funcptr(abstract):
|
|
return True
|
|
else:
|
|
for after in abstract.split('*')[1:]:
|
|
if not after.lstrip().startswith('const'):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def is_immutable(vardecl):
|
|
if not vardecl:
|
|
return None
|
|
if not is_fixed_type(vardecl):
|
|
return False
|
|
_, _, typequal, _, _ = get_parsed_vartype(vardecl)
|
|
# If there, it can only be "const" or "volatile".
|
|
return typequal == 'const'
|
|
|
|
|
|
#############################
|
|
# kinds
|
|
|
|
@enum.unique
|
|
class KIND(enum.Enum):
|
|
|
|
# XXX Use these in the raw parser code.
|
|
TYPEDEF = 'typedef'
|
|
STRUCT = 'struct'
|
|
UNION = 'union'
|
|
ENUM = 'enum'
|
|
FUNCTION = 'function'
|
|
VARIABLE = 'variable'
|
|
STATEMENT = 'statement'
|
|
|
|
@classonly
|
|
def _from_raw(cls, raw):
|
|
if raw is None:
|
|
return None
|
|
elif isinstance(raw, cls):
|
|
return raw
|
|
elif type(raw) is str:
|
|
# We could use cls[raw] for the upper-case form,
|
|
# but there's no need to go to the trouble.
|
|
return cls(raw.lower())
|
|
else:
|
|
raise NotImplementedError(raw)
|
|
|
|
@classonly
|
|
def by_priority(cls, group=None):
|
|
if group is None:
|
|
return cls._ALL_BY_PRIORITY.copy()
|
|
elif group == 'type':
|
|
return cls._TYPE_DECLS_BY_PRIORITY.copy()
|
|
elif group == 'decl':
|
|
return cls._ALL_DECLS_BY_PRIORITY.copy()
|
|
elif isinstance(group, str):
|
|
raise NotImplementedError(group)
|
|
else:
|
|
# XXX Treat group as a set of kinds & return in priority order?
|
|
raise NotImplementedError(group)
|
|
|
|
@classonly
|
|
def is_type_decl(cls, kind):
|
|
if kind in cls.TYPES:
|
|
return True
|
|
if not isinstance(kind, cls):
|
|
raise TypeError(f'expected KIND, got {kind!r}')
|
|
return False
|
|
|
|
@classonly
|
|
def is_decl(cls, kind):
|
|
if kind in cls.DECLS:
|
|
return True
|
|
if not isinstance(kind, cls):
|
|
raise TypeError(f'expected KIND, got {kind!r}')
|
|
return False
|
|
|
|
@classonly
|
|
def get_group(cls, kind, *, groups=None):
|
|
if not isinstance(kind, cls):
|
|
raise TypeError(f'expected KIND, got {kind!r}')
|
|
if groups is None:
|
|
groups = ['type']
|
|
elif not groups:
|
|
groups = ()
|
|
elif isinstance(groups, str):
|
|
group = groups
|
|
if group not in cls._GROUPS:
|
|
raise ValueError(f'unsupported group {group!r}')
|
|
groups = [group]
|
|
else:
|
|
unsupported = [g for g in groups if g not in cls._GROUPS]
|
|
if unsupported:
|
|
raise ValueError(f'unsupported groups {", ".join(repr(unsupported))}')
|
|
for group in groups:
|
|
if kind in cls._GROUPS[group]:
|
|
return group
|
|
else:
|
|
return kind.value
|
|
|
|
@classonly
|
|
def resolve_group(cls, group):
|
|
if isinstance(group, cls):
|
|
return {group}
|
|
elif isinstance(group, str):
|
|
try:
|
|
return cls._GROUPS[group].copy()
|
|
except KeyError:
|
|
raise ValueError(f'unsupported group {group!r}')
|
|
else:
|
|
resolved = set()
|
|
for gr in group:
|
|
resolve.update(cls.resolve_group(gr))
|
|
return resolved
|
|
#return {*cls.resolve_group(g) for g in group}
|
|
|
|
|
|
KIND._TYPE_DECLS_BY_PRIORITY = [
|
|
# These are in preferred order.
|
|
KIND.TYPEDEF,
|
|
KIND.STRUCT,
|
|
KIND.UNION,
|
|
KIND.ENUM,
|
|
]
|
|
KIND._ALL_DECLS_BY_PRIORITY = [
|
|
# These are in preferred order.
|
|
*KIND._TYPE_DECLS_BY_PRIORITY,
|
|
KIND.FUNCTION,
|
|
KIND.VARIABLE,
|
|
]
|
|
KIND._ALL_BY_PRIORITY = [
|
|
# These are in preferred order.
|
|
*KIND._ALL_DECLS_BY_PRIORITY,
|
|
KIND.STATEMENT,
|
|
]
|
|
|
|
KIND.TYPES = frozenset(KIND._TYPE_DECLS_BY_PRIORITY)
|
|
KIND.DECLS = frozenset(KIND._ALL_DECLS_BY_PRIORITY)
|
|
KIND._GROUPS = {
|
|
'type': KIND.TYPES,
|
|
'decl': KIND.DECLS,
|
|
}
|
|
KIND._GROUPS.update((k.value, {k}) for k in KIND)
|
|
|
|
|
|
# The module-level kind-related helpers (below) deal with <item>.kind:
|
|
|
|
def is_type_decl(kind):
|
|
# Handle ParsedItem, Declaration, etc..
|
|
kind = getattr(kind, 'kind', kind)
|
|
return KIND.is_type_decl(kind)
|
|
|
|
|
|
def is_decl(kind):
|
|
# Handle ParsedItem, Declaration, etc..
|
|
kind = getattr(kind, 'kind', kind)
|
|
return KIND.is_decl(kind)
|
|
|
|
|
|
def filter_by_kind(items, kind):
|
|
if kind == 'type':
|
|
kinds = KIND._TYPE_DECLS
|
|
elif kind == 'decl':
|
|
kinds = KIND._TYPE_DECLS
|
|
try:
|
|
okay = kind in KIND
|
|
except TypeError:
|
|
kinds = set(kind)
|
|
else:
|
|
kinds = {kind} if okay else set(kind)
|
|
for item in items:
|
|
if item.kind in kinds:
|
|
yield item
|
|
|
|
|
|
def collate_by_kind(items):
|
|
collated = {kind: [] for kind in KIND}
|
|
for item in items:
|
|
try:
|
|
collated[item.kind].append(item)
|
|
except KeyError:
|
|
raise ValueError(f'unsupported kind in {item!r}')
|
|
return collated
|
|
|
|
|
|
def get_kind_group(kind):
|
|
# Handle ParsedItem, Declaration, etc..
|
|
kind = getattr(kind, 'kind', kind)
|
|
return KIND.get_group(kind)
|
|
|
|
|
|
def collate_by_kind_group(items):
|
|
collated = {KIND.get_group(k): [] for k in KIND}
|
|
for item in items:
|
|
group = KIND.get_group(item.kind)
|
|
collated[group].append(item)
|
|
return collated
|
|
|
|
|
|
#############################
|
|
# low-level
|
|
|
|
class FileInfo(namedtuple('FileInfo', 'filename lno')):
|
|
@classmethod
|
|
def from_raw(cls, raw):
|
|
if isinstance(raw, cls):
|
|
return raw
|
|
elif isinstance(raw, tuple):
|
|
return cls(*raw)
|
|
elif not raw:
|
|
return None
|
|
elif isinstance(raw, str):
|
|
return cls(raw, -1)
|
|
else:
|
|
raise TypeError(f'unsupported "raw": {raw:!r}')
|
|
|
|
def __str__(self):
|
|
return self.filename
|
|
|
|
def fix_filename(self, relroot):
|
|
filename = os.path.relpath(self.filename, relroot)
|
|
return self._replace(filename=filename)
|
|
|
|
|
|
class SourceLine(namedtuple('Line', 'file kind data conditions')):
|
|
KINDS = (
|
|
#'directive', # data is ...
|
|
'source', # "data" is the line
|
|
#'comment', # "data" is the text, including comment markers
|
|
)
|
|
|
|
@property
|
|
def filename(self):
|
|
return self.file.filename
|
|
|
|
@property
|
|
def lno(self):
|
|
return self.file.lno
|
|
|
|
|
|
class DeclID(namedtuple('DeclID', 'filename funcname name')):
|
|
"""The globally-unique identifier for a declaration."""
|
|
|
|
@classmethod
|
|
def from_row(cls, row, **markers):
|
|
row = _tables.fix_row(row, **markers)
|
|
return cls(*row)
|
|
|
|
def __new__(cls, filename, funcname, name):
|
|
self = super().__new__(
|
|
cls,
|
|
filename=str(filename) if filename else None,
|
|
funcname=str(funcname) if funcname else None,
|
|
name=str(name) if name else None,
|
|
)
|
|
self._compare = tuple(v or '' for v in self)
|
|
return self
|
|
|
|
def __hash__(self):
|
|
return super().__hash__()
|
|
|
|
def __eq__(self, other):
|
|
try:
|
|
other = tuple(v or '' for v in other)
|
|
except TypeError:
|
|
return NotImplemented
|
|
return self._compare == other
|
|
|
|
def __gt__(self, other):
|
|
try:
|
|
other = tuple(v or '' for v in other)
|
|
except TypeError:
|
|
return NotImplemented
|
|
return self._compare > other
|
|
|
|
|
|
class ParsedItem(namedtuple('ParsedItem', 'file kind parent name data')):
|
|
|
|
@classmethod
|
|
def from_raw(cls, raw):
|
|
if isinstance(raw, cls):
|
|
return raw
|
|
elif isinstance(raw, tuple):
|
|
return cls(*raw)
|
|
else:
|
|
raise TypeError(f'unsupported "raw": {raw:!r}')
|
|
|
|
@classmethod
|
|
def from_row(cls, row, columns=None):
|
|
if not columns:
|
|
colnames = 'filename funcname name kind data'.split()
|
|
else:
|
|
colnames = list(columns)
|
|
for i, column in enumerate(colnames):
|
|
if column == 'file':
|
|
colnames[i] = 'filename'
|
|
elif column == 'funcname':
|
|
colnames[i] = 'parent'
|
|
if len(row) != len(set(colnames)):
|
|
raise NotImplementedError(columns, row)
|
|
kwargs = {}
|
|
for column, value in zip(colnames, row):
|
|
if column == 'filename':
|
|
kwargs['file'] = FileInfo.from_raw(value)
|
|
elif column == 'kind':
|
|
kwargs['kind'] = KIND(value)
|
|
elif column in cls._fields:
|
|
kwargs[column] = value
|
|
else:
|
|
raise NotImplementedError(column)
|
|
return cls(**kwargs)
|
|
|
|
@property
|
|
def id(self):
|
|
try:
|
|
return self._id
|
|
except AttributeError:
|
|
if self.kind is KIND.STATEMENT:
|
|
self._id = None
|
|
else:
|
|
self._id = DeclID(str(self.file), self.funcname, self.name)
|
|
return self._id
|
|
|
|
@property
|
|
def filename(self):
|
|
if not self.file:
|
|
return None
|
|
return self.file.filename
|
|
|
|
@property
|
|
def lno(self):
|
|
if not self.file:
|
|
return -1
|
|
return self.file.lno
|
|
|
|
@property
|
|
def funcname(self):
|
|
if not self.parent:
|
|
return None
|
|
if type(self.parent) is str:
|
|
return self.parent
|
|
else:
|
|
return self.parent.name
|
|
|
|
def as_row(self, columns=None):
|
|
if not columns:
|
|
columns = self._fields
|
|
row = []
|
|
for column in columns:
|
|
if column == 'file':
|
|
value = self.filename
|
|
elif column == 'kind':
|
|
value = self.kind.value
|
|
elif column == 'data':
|
|
value = self._render_data()
|
|
else:
|
|
value = getattr(self, column)
|
|
row.append(value)
|
|
return row
|
|
|
|
def _render_data(self):
|
|
if not self.data:
|
|
return None
|
|
elif isinstance(self.data, str):
|
|
return self.data
|
|
else:
|
|
# XXX
|
|
raise NotImplementedError
|
|
|
|
|
|
def _get_vartype(data):
|
|
try:
|
|
vartype = dict(data['vartype'])
|
|
except KeyError:
|
|
vartype = dict(data)
|
|
storage = data.get('storage')
|
|
else:
|
|
storage = data.get('storage') or vartype.get('storage')
|
|
del vartype['storage']
|
|
return storage, vartype
|
|
|
|
|
|
def get_parsed_vartype(decl):
|
|
kind = getattr(decl, 'kind', None)
|
|
if isinstance(decl, ParsedItem):
|
|
storage, vartype = _get_vartype(decl.data)
|
|
typequal = vartype['typequal']
|
|
typespec = vartype['typespec']
|
|
abstract = vartype['abstract']
|
|
elif isinstance(decl, dict):
|
|
kind = decl.get('kind')
|
|
storage, vartype = _get_vartype(decl)
|
|
typequal = vartype['typequal']
|
|
typespec = vartype['typespec']
|
|
abstract = vartype['abstract']
|
|
elif isinstance(decl, VarType):
|
|
storage = None
|
|
typequal, typespec, abstract = decl
|
|
elif isinstance(decl, TypeDef):
|
|
storage = None
|
|
typequal, typespec, abstract = decl.vartype
|
|
elif isinstance(decl, Variable):
|
|
storage = decl.storage
|
|
typequal, typespec, abstract = decl.vartype
|
|
elif isinstance(decl, Function):
|
|
storage = decl.storage
|
|
typequal, typespec, abstract = decl.signature.returntype
|
|
elif isinstance(decl, str):
|
|
vartype, storage = VarType.from_str(decl)
|
|
typequal, typespec, abstract = vartype
|
|
else:
|
|
raise NotImplementedError(decl)
|
|
return kind, storage, typequal, typespec, abstract
|
|
|
|
|
|
#############################
|
|
# high-level
|
|
|
|
class HighlevelParsedItem:
|
|
|
|
kind = None
|
|
|
|
FIELDS = ('file', 'parent', 'name', 'data')
|
|
|
|
@classmethod
|
|
def from_parsed(cls, parsed):
|
|
if parsed.kind is not cls.kind:
|
|
raise TypeError(f'kind mismatch ({parsed.kind.value} != {cls.kind.value})')
|
|
data, extra = cls._resolve_data(parsed.data)
|
|
self = cls(
|
|
cls._resolve_file(parsed),
|
|
parsed.name,
|
|
data,
|
|
cls._resolve_parent(parsed) if parsed.parent else None,
|
|
**extra or {}
|
|
)
|
|
self._parsed = parsed
|
|
return self
|
|
|
|
@classmethod
|
|
def _resolve_file(cls, parsed):
|
|
fileinfo = FileInfo.from_raw(parsed.file)
|
|
if not fileinfo:
|
|
raise NotImplementedError(parsed)
|
|
return fileinfo
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
return data, None
|
|
|
|
@classmethod
|
|
def _raw_data(cls, data, extra):
|
|
if isinstance(data, str):
|
|
return data
|
|
else:
|
|
raise NotImplementedError(data)
|
|
|
|
@classmethod
|
|
def _data_as_row(cls, data, extra, colnames):
|
|
row = {}
|
|
for colname in colnames:
|
|
if colname in row:
|
|
continue
|
|
rendered = cls._render_data_row_item(colname, data, extra)
|
|
if rendered is iter(rendered):
|
|
rendered, = rendered
|
|
row[colname] = rendered
|
|
return row
|
|
|
|
@classmethod
|
|
def _render_data_row_item(cls, colname, data, extra):
|
|
if colname == 'data':
|
|
return str(data)
|
|
else:
|
|
return None
|
|
|
|
@classmethod
|
|
def _render_data_row(cls, fmt, data, extra, colnames):
|
|
if fmt != 'row':
|
|
raise NotImplementedError
|
|
datarow = cls._data_as_row(data, extra, colnames)
|
|
unresolved = [c for c, v in datarow.items() if v is None]
|
|
if unresolved:
|
|
raise NotImplementedError(unresolved)
|
|
for colname, value in datarow.items():
|
|
if type(value) != str:
|
|
if colname == 'kind':
|
|
datarow[colname] = value.value
|
|
else:
|
|
datarow[colname] = str(value)
|
|
return datarow
|
|
|
|
@classmethod
|
|
def _render_data(cls, fmt, data, extra):
|
|
row = cls._render_data_row(fmt, data, extra, ['data'])
|
|
yield ' '.join(row.values())
|
|
|
|
@classmethod
|
|
def _resolve_parent(cls, parsed, *, _kind=None):
|
|
fileinfo = FileInfo(parsed.file.filename, -1)
|
|
if isinstance(parsed.parent, str):
|
|
if parsed.parent.isidentifier():
|
|
name = parsed.parent
|
|
else:
|
|
# XXX It could be something like "<kind> <name>".
|
|
raise NotImplementedError(repr(parsed.parent))
|
|
parent = ParsedItem(fileinfo, _kind, None, name, None)
|
|
elif type(parsed.parent) is tuple:
|
|
# XXX It could be something like (kind, name).
|
|
raise NotImplementedError(repr(parsed.parent))
|
|
else:
|
|
return parsed.parent
|
|
Parent = KIND_CLASSES.get(_kind, Declaration)
|
|
return Parent.from_parsed(parent)
|
|
|
|
@classmethod
|
|
def _parse_columns(cls, columns):
|
|
colnames = {} # {requested -> actual}
|
|
columns = list(columns or cls.FIELDS)
|
|
datacolumns = []
|
|
for i, colname in enumerate(columns):
|
|
if colname == 'file':
|
|
columns[i] = 'filename'
|
|
colnames['file'] = 'filename'
|
|
elif colname == 'lno':
|
|
columns[i] = 'line'
|
|
colnames['lno'] = 'line'
|
|
elif colname in ('filename', 'line'):
|
|
colnames[colname] = colname
|
|
elif colname == 'data':
|
|
datacolumns.append(colname)
|
|
colnames[colname] = None
|
|
elif colname in cls.FIELDS or colname == 'kind':
|
|
colnames[colname] = colname
|
|
else:
|
|
datacolumns.append(colname)
|
|
colnames[colname] = None
|
|
return columns, datacolumns, colnames
|
|
|
|
def __init__(self, file, name, data, parent=None, *,
|
|
_extra=None,
|
|
_shortkey=None,
|
|
_key=None,
|
|
):
|
|
self.file = file
|
|
self.parent = parent or None
|
|
self.name = name
|
|
self.data = data
|
|
self._extra = _extra or {}
|
|
self._shortkey = _shortkey
|
|
self._key = _key
|
|
|
|
def __repr__(self):
|
|
args = [f'{n}={getattr(self, n)!r}'
|
|
for n in ['file', 'name', 'data', 'parent', *(self._extra or ())]]
|
|
return f'{type(self).__name__}({", ".join(args)})'
|
|
|
|
def __str__(self):
|
|
try:
|
|
return self._str
|
|
except AttributeError:
|
|
self._str = next(self.render())
|
|
return self._str
|
|
|
|
def __getattr__(self, name):
|
|
try:
|
|
return self._extra[name]
|
|
except KeyError:
|
|
raise AttributeError(name)
|
|
|
|
def __hash__(self):
|
|
return hash(self._key)
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, HighlevelParsedItem):
|
|
return self._key == other._key
|
|
elif type(other) is tuple:
|
|
return self._key == other
|
|
else:
|
|
return NotImplemented
|
|
|
|
def __gt__(self, other):
|
|
if isinstance(other, HighlevelParsedItem):
|
|
return self._key > other._key
|
|
elif type(other) is tuple:
|
|
return self._key > other
|
|
else:
|
|
return NotImplemented
|
|
|
|
@property
|
|
def id(self):
|
|
return self.parsed.id
|
|
|
|
@property
|
|
def shortkey(self):
|
|
return self._shortkey
|
|
|
|
@property
|
|
def key(self):
|
|
return self._key
|
|
|
|
@property
|
|
def filename(self):
|
|
if not self.file:
|
|
return None
|
|
return self.file.filename
|
|
|
|
@property
|
|
def parsed(self):
|
|
try:
|
|
return self._parsed
|
|
except AttributeError:
|
|
parent = self.parent
|
|
if parent is not None and not isinstance(parent, str):
|
|
parent = parent.name
|
|
self._parsed = ParsedItem(
|
|
self.file,
|
|
self.kind,
|
|
parent,
|
|
self.name,
|
|
self._raw_data(),
|
|
)
|
|
return self._parsed
|
|
|
|
def fix_filename(self, relroot):
|
|
if self.file:
|
|
self.file = self.file.fix_filename(relroot)
|
|
|
|
def as_rowdata(self, columns=None):
|
|
columns, datacolumns, colnames = self._parse_columns(columns)
|
|
return self._as_row(colnames, datacolumns, self._data_as_row)
|
|
|
|
def render_rowdata(self, columns=None):
|
|
columns, datacolumns, colnames = self._parse_columns(columns)
|
|
def data_as_row(data, ext, cols):
|
|
return self._render_data_row('row', data, ext, cols)
|
|
rowdata = self._as_row(colnames, datacolumns, data_as_row)
|
|
for column, value in rowdata.items():
|
|
colname = colnames.get(column)
|
|
if not colname:
|
|
continue
|
|
if column == 'kind':
|
|
value = value.value
|
|
else:
|
|
if column == 'parent':
|
|
if self.parent:
|
|
value = f'({self.parent.kind.value} {self.parent.name})'
|
|
if not value:
|
|
value = '-'
|
|
elif type(value) is VarType:
|
|
value = repr(str(value))
|
|
else:
|
|
value = str(value)
|
|
rowdata[column] = value
|
|
return rowdata
|
|
|
|
def _as_row(self, colnames, datacolumns, data_as_row):
|
|
try:
|
|
data = data_as_row(self.data, self._extra, datacolumns)
|
|
except NotImplementedError:
|
|
data = None
|
|
row = data or {}
|
|
for column, colname in colnames.items():
|
|
if colname == 'filename':
|
|
value = self.file.filename if self.file else None
|
|
elif colname == 'line':
|
|
value = self.file.lno if self.file else None
|
|
elif colname is None:
|
|
value = getattr(self, column, None)
|
|
else:
|
|
value = getattr(self, colname, None)
|
|
row.setdefault(column, value)
|
|
return row
|
|
|
|
def render(self, fmt='line'):
|
|
fmt = fmt or 'line'
|
|
try:
|
|
render = _FORMATS[fmt]
|
|
except KeyError:
|
|
raise TypeError(f'unsupported fmt {fmt!r}')
|
|
try:
|
|
data = self._render_data(fmt, self.data, self._extra)
|
|
except NotImplementedError:
|
|
data = '-'
|
|
yield from render(self, data)
|
|
|
|
|
|
### formats ###
|
|
|
|
def _fmt_line(parsed, data=None):
|
|
parts = [
|
|
f'<{parsed.kind.value}>',
|
|
]
|
|
parent = ''
|
|
if parsed.parent:
|
|
parent = parsed.parent
|
|
if not isinstance(parent, str):
|
|
if parent.kind is KIND.FUNCTION:
|
|
parent = f'{parent.name}()'
|
|
else:
|
|
parent = parent.name
|
|
name = f'<{parent}>.{parsed.name}'
|
|
else:
|
|
name = parsed.name
|
|
if data is None:
|
|
data = parsed.data
|
|
elif data is iter(data):
|
|
data, = data
|
|
parts.extend([
|
|
name,
|
|
f'<{data}>' if data else '-',
|
|
f'({str(parsed.file or "<unknown file>")})',
|
|
])
|
|
yield '\t'.join(parts)
|
|
|
|
|
|
def _fmt_full(parsed, data=None):
|
|
if parsed.kind is KIND.VARIABLE and parsed.parent:
|
|
prefix = 'local '
|
|
suffix = f' ({parsed.parent.name})'
|
|
else:
|
|
# XXX Show other prefixes (e.g. global, public)
|
|
prefix = suffix = ''
|
|
yield f'{prefix}{parsed.kind.value} {parsed.name!r}{suffix}'
|
|
for column, info in parsed.render_rowdata().items():
|
|
if column == 'kind':
|
|
continue
|
|
if column == 'name':
|
|
continue
|
|
if column == 'parent' and parsed.kind is not KIND.VARIABLE:
|
|
continue
|
|
if column == 'data':
|
|
if parsed.kind in (KIND.STRUCT, KIND.UNION):
|
|
column = 'members'
|
|
elif parsed.kind is KIND.ENUM:
|
|
column = 'enumerators'
|
|
elif parsed.kind is KIND.STATEMENT:
|
|
column = 'text'
|
|
data, = data
|
|
else:
|
|
column = 'signature'
|
|
data, = data
|
|
if not data:
|
|
# yield f'\t{column}:\t-'
|
|
continue
|
|
elif isinstance(data, str):
|
|
yield f'\t{column}:\t{data!r}'
|
|
else:
|
|
yield f'\t{column}:'
|
|
for line in data:
|
|
yield f'\t\t- {line}'
|
|
else:
|
|
yield f'\t{column}:\t{info}'
|
|
|
|
|
|
_FORMATS = {
|
|
'raw': (lambda v, _d: [repr(v)]),
|
|
'brief': _fmt_line,
|
|
'line': _fmt_line,
|
|
'full': _fmt_full,
|
|
}
|
|
|
|
|
|
### declarations ##
|
|
|
|
class Declaration(HighlevelParsedItem):
|
|
|
|
@classmethod
|
|
def from_row(cls, row, **markers):
|
|
fixed = tuple(_tables.fix_row(row, **markers))
|
|
if cls is Declaration:
|
|
_, _, _, kind, _ = fixed
|
|
sub = KIND_CLASSES.get(KIND(kind))
|
|
if not sub or not issubclass(sub, Declaration):
|
|
raise TypeError(f'unsupported kind, got {row!r}')
|
|
else:
|
|
sub = cls
|
|
return sub._from_row(fixed)
|
|
|
|
@classmethod
|
|
def _from_row(cls, row):
|
|
filename, funcname, name, kind, data = row
|
|
kind = KIND._from_raw(kind)
|
|
if kind is not cls.kind:
|
|
raise TypeError(f'expected kind {cls.kind.value!r}, got {row!r}')
|
|
fileinfo = FileInfo.from_raw(filename)
|
|
if isinstance(data, str):
|
|
data, extra = cls._parse_data(data, fmt='row')
|
|
if extra:
|
|
return cls(fileinfo, name, data, funcname, _extra=extra)
|
|
else:
|
|
return cls(fileinfo, name, data, funcname)
|
|
|
|
@classmethod
|
|
def _resolve_parent(cls, parsed, *, _kind=None):
|
|
if _kind is None:
|
|
raise TypeError(f'{cls.kind.value} declarations do not have parents ({parsed})')
|
|
return super()._resolve_parent(parsed, _kind=_kind)
|
|
|
|
@classmethod
|
|
def _render_data(cls, fmt, data, extra):
|
|
if not data:
|
|
# XXX There should be some! Forward?
|
|
yield '???'
|
|
else:
|
|
yield from cls._format_data(fmt, data, extra)
|
|
|
|
@classmethod
|
|
def _render_data_row_item(cls, colname, data, extra):
|
|
if colname == 'data':
|
|
return cls._format_data('row', data, extra)
|
|
else:
|
|
return None
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _parse_data(cls, datastr, fmt=None):
|
|
"""This is the reverse of _render_data."""
|
|
if not datastr or datastr is _tables.UNKNOWN or datastr == '???':
|
|
return None, None
|
|
elif datastr is _tables.EMPTY or datastr == '-':
|
|
# All the kinds have *something* even it is unknown.
|
|
raise TypeError('all declarations have data of some sort, got none')
|
|
else:
|
|
return cls._unformat_data(datastr, fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
raise NotImplementedError(fmt)
|
|
|
|
|
|
class VarType(namedtuple('VarType', 'typequal typespec abstract')):
|
|
|
|
@classmethod
|
|
def from_str(cls, text):
|
|
orig = text
|
|
storage, sep, text = text.strip().partition(' ')
|
|
if not sep:
|
|
text = storage
|
|
storage = None
|
|
elif storage not in ('auto', 'register', 'static', 'extern'):
|
|
text = orig
|
|
storage = None
|
|
return cls._from_str(text), storage
|
|
|
|
@classmethod
|
|
def _from_str(cls, text):
|
|
orig = text
|
|
if text.startswith(('const ', 'volatile ')):
|
|
typequal, _, text = text.partition(' ')
|
|
else:
|
|
typequal = None
|
|
|
|
# Extract a series of identifiers/keywords.
|
|
m = re.match(r"^ *'?([a-zA-Z_]\w*(?:\s+[a-zA-Z_]\w*)*)\s*(.*?)'?\s*$", text)
|
|
if not m:
|
|
raise ValueError(f'invalid vartype text {orig!r}')
|
|
typespec, abstract = m.groups()
|
|
|
|
return cls(typequal, typespec, abstract or None)
|
|
|
|
def __str__(self):
|
|
parts = []
|
|
if self.qualifier:
|
|
parts.append(self.qualifier)
|
|
parts.append(self.spec + (self.abstract or ''))
|
|
return ' '.join(parts)
|
|
|
|
@property
|
|
def qualifier(self):
|
|
return self.typequal
|
|
|
|
@property
|
|
def spec(self):
|
|
return self.typespec
|
|
|
|
|
|
class Variable(Declaration):
|
|
kind = KIND.VARIABLE
|
|
|
|
@classmethod
|
|
def _resolve_parent(cls, parsed):
|
|
return super()._resolve_parent(parsed, _kind=KIND.FUNCTION)
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
if not data:
|
|
return None, None
|
|
storage, vartype = _get_vartype(data)
|
|
return VarType(**vartype), {'storage': storage}
|
|
|
|
@classmethod
|
|
def _raw_data(self, data, extra):
|
|
vartype = data._asdict()
|
|
return {
|
|
'storage': extra['storage'],
|
|
'vartype': vartype,
|
|
}
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
storage = extra.get('storage')
|
|
text = f'{storage} {data}' if storage else str(data)
|
|
if fmt in ('line', 'brief'):
|
|
yield text
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
yield text
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
if fmt in ('line', 'brief'):
|
|
vartype, storage = VarType.from_str(datastr)
|
|
return vartype, {'storage': storage}
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
vartype, storage = VarType.from_str(datastr)
|
|
return vartype, {'storage': storage}
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
def __init__(self, file, name, data, parent=None, storage=None):
|
|
super().__init__(file, name, data, parent,
|
|
_extra={'storage': storage},
|
|
_shortkey=f'({parent.name}).{name}' if parent else name,
|
|
_key=(str(file),
|
|
# Tilde comes after all other ascii characters.
|
|
f'~{parent or ""}~',
|
|
name,
|
|
),
|
|
)
|
|
|
|
@property
|
|
def vartype(self):
|
|
return self.data
|
|
|
|
|
|
class Signature(namedtuple('Signature', 'params returntype inline isforward')):
|
|
|
|
@classmethod
|
|
def from_str(cls, text):
|
|
orig = text
|
|
storage, sep, text = text.strip().partition(' ')
|
|
if not sep:
|
|
text = storage
|
|
storage = None
|
|
elif storage not in ('auto', 'register', 'static', 'extern'):
|
|
text = orig
|
|
storage = None
|
|
return cls._from_str(text), storage
|
|
|
|
@classmethod
|
|
def _from_str(cls, text):
|
|
orig = text
|
|
inline, sep, text = text.partition('|')
|
|
if not sep:
|
|
text = inline
|
|
inline = None
|
|
|
|
isforward = False
|
|
if text.endswith(';'):
|
|
text = text[:-1]
|
|
isforward = True
|
|
elif text.endswith('{}'):
|
|
text = text[:-2]
|
|
|
|
index = text.rindex('(')
|
|
if index < 0:
|
|
raise ValueError(f'bad signature text {orig!r}')
|
|
params = text[index:]
|
|
while params.count('(') <= params.count(')'):
|
|
index = text.rindex('(', 0, index)
|
|
if index < 0:
|
|
raise ValueError(f'bad signature text {orig!r}')
|
|
params = text[index:]
|
|
text = text[:index]
|
|
|
|
returntype = VarType._from_str(text.rstrip())
|
|
|
|
return cls(params, returntype, inline, isforward)
|
|
|
|
def __str__(self):
|
|
parts = []
|
|
if self.inline:
|
|
parts.extend([
|
|
self.inline,
|
|
'|',
|
|
])
|
|
parts.extend([
|
|
str(self.returntype),
|
|
self.params,
|
|
';' if self.isforward else '{}',
|
|
])
|
|
return ' '.join(parts)
|
|
|
|
@property
|
|
def returns(self):
|
|
return self.returntype
|
|
|
|
|
|
class Function(Declaration):
|
|
kind = KIND.FUNCTION
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
if not data:
|
|
return None, None
|
|
kwargs = dict(data)
|
|
returntype = dict(data['returntype'])
|
|
del returntype['storage']
|
|
kwargs['returntype'] = VarType(**returntype)
|
|
storage = kwargs.pop('storage')
|
|
return Signature(**kwargs), {'storage': storage}
|
|
|
|
@classmethod
|
|
def _raw_data(self, data):
|
|
# XXX finsh!
|
|
return data
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
storage = extra.get('storage')
|
|
text = f'{storage} {data}' if storage else str(data)
|
|
if fmt in ('line', 'brief'):
|
|
yield text
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
yield text
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
if fmt in ('line', 'brief'):
|
|
sig, storage = Signature.from_str(sig)
|
|
return sig, {'storage': storage}
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
sig, storage = Signature.from_str(sig)
|
|
return sig, {'storage': storage}
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
def __init__(self, file, name, data, parent=None, storage=None):
|
|
super().__init__(file, name, data, parent, _extra={'storage': storage})
|
|
self._shortkey = f'~{name}~ {self.data}'
|
|
self._key = (
|
|
str(file),
|
|
self._shortkey,
|
|
)
|
|
|
|
@property
|
|
def signature(self):
|
|
return self.data
|
|
|
|
|
|
class TypeDeclaration(Declaration):
|
|
|
|
def __init__(self, file, name, data, parent=None, *, _shortkey=None):
|
|
if not _shortkey:
|
|
_shortkey = f'{self.kind.value} {name}'
|
|
super().__init__(file, name, data, parent,
|
|
_shortkey=_shortkey,
|
|
_key=(
|
|
str(file),
|
|
_shortkey,
|
|
),
|
|
)
|
|
|
|
|
|
class POTSType(TypeDeclaration):
|
|
|
|
def __init__(self, name):
|
|
_file = _data = _parent = None
|
|
super().__init__(_file, name, _data, _parent, _shortkey=name)
|
|
|
|
|
|
class FuncPtr(TypeDeclaration):
|
|
|
|
def __init__(self, vartype):
|
|
_file = _name = _parent = None
|
|
data = vartype
|
|
self.vartype = vartype
|
|
super().__init__(_file, _name, data, _parent, _shortkey=f'<{vartype}>')
|
|
|
|
|
|
class TypeDef(TypeDeclaration):
|
|
kind = KIND.TYPEDEF
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
if not data:
|
|
raise NotImplementedError(data)
|
|
vartype = dict(data)
|
|
del vartype['storage']
|
|
return VarType(**vartype), None
|
|
|
|
@classmethod
|
|
def _raw_data(self, data):
|
|
# XXX finish!
|
|
return data
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
text = str(data)
|
|
if fmt in ('line', 'brief'):
|
|
yield text
|
|
elif fmt == 'full':
|
|
yield text
|
|
elif fmt == 'row':
|
|
yield text
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
if fmt in ('line', 'brief'):
|
|
vartype, _ = VarType.from_str(datastr)
|
|
return vartype, None
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
vartype, _ = VarType.from_str(datastr)
|
|
return vartype, None
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
def __init__(self, file, name, data, parent=None):
|
|
super().__init__(file, name, data, parent, _shortkey=name)
|
|
|
|
@property
|
|
def vartype(self):
|
|
return self.data
|
|
|
|
|
|
class Member(namedtuple('Member', 'name vartype size')):
|
|
|
|
@classmethod
|
|
def from_data(cls, raw, index):
|
|
name = raw.name if raw.name else index
|
|
vartype = size = None
|
|
if type(raw.data) is int:
|
|
size = raw.data
|
|
elif isinstance(raw.data, str):
|
|
size = int(raw.data)
|
|
elif raw.data:
|
|
vartype = dict(raw.data)
|
|
del vartype['storage']
|
|
if 'size' in vartype:
|
|
size = int(vartype.pop('size'))
|
|
vartype = VarType(**vartype)
|
|
return cls(name, vartype, size)
|
|
|
|
@classmethod
|
|
def from_str(cls, text):
|
|
name, _, vartype = text.partition(': ')
|
|
if name.startswith('#'):
|
|
name = int(name[1:])
|
|
if vartype.isdigit():
|
|
size = int(vartype)
|
|
vartype = None
|
|
else:
|
|
vartype, _ = VarType.from_str(vartype)
|
|
size = None
|
|
return cls(name, vartype, size)
|
|
|
|
def __str__(self):
|
|
name = self.name if isinstance(self.name, str) else f'#{self.name}'
|
|
return f'{name}: {self.vartype or self.size}'
|
|
|
|
|
|
class _StructUnion(TypeDeclaration):
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
if not data:
|
|
# XXX There should be some! Forward?
|
|
return None, None
|
|
return [Member.from_data(v, i) for i, v in enumerate(data)], None
|
|
|
|
@classmethod
|
|
def _raw_data(self, data):
|
|
# XXX finish!
|
|
return data
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
if fmt in ('line', 'brief'):
|
|
members = ', '.join(f'<{m}>' for m in data)
|
|
yield f'[{members}]'
|
|
elif fmt == 'full':
|
|
for member in data:
|
|
yield f'{member}'
|
|
elif fmt == 'row':
|
|
members = ', '.join(f'<{m}>' for m in data)
|
|
yield f'[{members}]'
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
if fmt in ('line', 'brief'):
|
|
members = [Member.from_str(m[1:-1])
|
|
for m in datastr[1:-1].split(', ')]
|
|
return members, None
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
members = [Member.from_str(m.rstrip('>').lstrip('<'))
|
|
for m in datastr[1:-1].split('>, <')]
|
|
return members, None
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
def __init__(self, file, name, data, parent=None):
|
|
super().__init__(file, name, data, parent)
|
|
|
|
@property
|
|
def members(self):
|
|
return self.data
|
|
|
|
|
|
class Struct(_StructUnion):
|
|
kind = KIND.STRUCT
|
|
|
|
|
|
class Union(_StructUnion):
|
|
kind = KIND.UNION
|
|
|
|
|
|
class Enum(TypeDeclaration):
|
|
kind = KIND.ENUM
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
if not data:
|
|
# XXX There should be some! Forward?
|
|
return None, None
|
|
enumerators = [e if isinstance(e, str) else e.name
|
|
for e in data]
|
|
return enumerators, None
|
|
|
|
@classmethod
|
|
def _raw_data(self, data):
|
|
# XXX finsih!
|
|
return data
|
|
|
|
@classmethod
|
|
def _format_data(cls, fmt, data, extra):
|
|
if fmt in ('line', 'brief'):
|
|
yield repr(data)
|
|
elif fmt == 'full':
|
|
for enumerator in data:
|
|
yield f'{enumerator}'
|
|
elif fmt == 'row':
|
|
# XXX This won't work with CSV...
|
|
yield ','.join(data)
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
@classmethod
|
|
def _unformat_data(cls, datastr, fmt=None):
|
|
if fmt in ('line', 'brief'):
|
|
return _strutil.unrepr(datastr), None
|
|
#elif fmt == 'full':
|
|
elif fmt == 'row':
|
|
return datastr.split(','), None
|
|
else:
|
|
raise NotImplementedError(fmt)
|
|
|
|
def __init__(self, file, name, data, parent=None):
|
|
super().__init__(file, name, data, parent)
|
|
|
|
@property
|
|
def enumerators(self):
|
|
return self.data
|
|
|
|
|
|
### statements ###
|
|
|
|
class Statement(HighlevelParsedItem):
|
|
kind = KIND.STATEMENT
|
|
|
|
@classmethod
|
|
def _resolve_data(cls, data):
|
|
# XXX finsih!
|
|
return data, None
|
|
|
|
@classmethod
|
|
def _raw_data(self, data):
|
|
# XXX finsih!
|
|
return data
|
|
|
|
@classmethod
|
|
def _render_data(cls, fmt, data, extra):
|
|
# XXX Handle other formats?
|
|
return repr(data)
|
|
|
|
@classmethod
|
|
def _parse_data(self, datastr, fmt=None):
|
|
# XXX Handle other formats?
|
|
return _strutil.unrepr(datastr), None
|
|
|
|
def __init__(self, file, name, data, parent=None):
|
|
super().__init__(file, name, data, parent,
|
|
_shortkey=data or '',
|
|
_key=(
|
|
str(file),
|
|
file.lno,
|
|
# XXX Only one stmt per line?
|
|
),
|
|
)
|
|
|
|
@property
|
|
def text(self):
|
|
return self.data
|
|
|
|
|
|
###
|
|
|
|
KIND_CLASSES = {cls.kind: cls for cls in [
|
|
Variable,
|
|
Function,
|
|
TypeDef,
|
|
Struct,
|
|
Union,
|
|
Enum,
|
|
Statement,
|
|
]}
|
|
|
|
|
|
def resolve_parsed(parsed):
|
|
if isinstance(parsed, HighlevelParsedItem):
|
|
return parsed
|
|
try:
|
|
cls = KIND_CLASSES[parsed.kind]
|
|
except KeyError:
|
|
raise ValueError(f'unsupported kind in {parsed!r}')
|
|
return cls.from_parsed(parsed)
|
|
|
|
|
|
#############################
|
|
# composite
|
|
|
|
class Declarations:
|
|
|
|
@classmethod
|
|
def from_decls(cls, decls):
|
|
return cls(decls)
|
|
|
|
@classmethod
|
|
def from_parsed(cls, items):
|
|
decls = (resolve_parsed(item)
|
|
for item in items
|
|
if item.kind is not KIND.STATEMENT)
|
|
return cls.from_decls(decls)
|
|
|
|
@classmethod
|
|
def _resolve_key(cls, raw):
|
|
if isinstance(raw, str):
|
|
raw = [raw]
|
|
elif isinstance(raw, Declaration):
|
|
raw = (
|
|
raw.filename if cls._is_public(raw) else None,
|
|
# `raw.parent` is always None for types and functions.
|
|
raw.parent if raw.kind is KIND.VARIABLE else None,
|
|
raw.name,
|
|
)
|
|
|
|
extra = None
|
|
if len(raw) == 1:
|
|
name, = raw
|
|
if name:
|
|
name = str(name)
|
|
if name.endswith(('.c', '.h')):
|
|
# This is only legit as a query.
|
|
key = (name, None, None)
|
|
else:
|
|
key = (None, None, name)
|
|
else:
|
|
key = (None, None, None)
|
|
elif len(raw) == 2:
|
|
parent, name = raw
|
|
name = str(name)
|
|
if isinstance(parent, Declaration):
|
|
key = (None, parent.name, name)
|
|
elif not parent:
|
|
key = (None, None, name)
|
|
else:
|
|
parent = str(parent)
|
|
if parent.endswith(('.c', '.h')):
|
|
key = (parent, None, name)
|
|
else:
|
|
key = (None, parent, name)
|
|
else:
|
|
key, extra = raw[:3], raw[3:]
|
|
filename, funcname, name = key
|
|
filename = str(filename) if filename else None
|
|
if isinstance(funcname, Declaration):
|
|
funcname = funcname.name
|
|
else:
|
|
funcname = str(funcname) if funcname else None
|
|
name = str(name) if name else None
|
|
key = (filename, funcname, name)
|
|
return key, extra
|
|
|
|
@classmethod
|
|
def _is_public(cls, decl):
|
|
# For .c files don't we need info from .h files to make this decision?
|
|
# XXX Check for "extern".
|
|
# For now we treat all decls a "private" (have filename set).
|
|
return False
|
|
|
|
def __init__(self, decls):
|
|
# (file, func, name) -> decl
|
|
# "public":
|
|
# * (None, None, name)
|
|
# "private", "global":
|
|
# * (file, None, name)
|
|
# "private", "local":
|
|
# * (file, func, name)
|
|
if hasattr(decls, 'items'):
|
|
self._decls = decls
|
|
else:
|
|
self._decls = {}
|
|
self._extend(decls)
|
|
|
|
# XXX always validate?
|
|
|
|
def validate(self):
|
|
for key, decl in self._decls.items():
|
|
if type(key) is not tuple or len(key) != 3:
|
|
raise ValueError(f'expected 3-tuple key, got {key!r} (for decl {decl!r})')
|
|
filename, funcname, name = key
|
|
if not name:
|
|
raise ValueError(f'expected name in key, got {key!r} (for decl {decl!r})')
|
|
elif type(name) is not str:
|
|
raise ValueError(f'expected name in key to be str, got {key!r} (for decl {decl!r})')
|
|
# XXX Check filename type?
|
|
# XXX Check funcname type?
|
|
|
|
if decl.kind is KIND.STATEMENT:
|
|
raise ValueError(f'expected a declaration, got {decl!r}')
|
|
|
|
def __repr__(self):
|
|
return f'{type(self).__name__}({list(self)})'
|
|
|
|
def __len__(self):
|
|
return len(self._decls)
|
|
|
|
def __iter__(self):
|
|
yield from self._decls
|
|
|
|
def __getitem__(self, key):
|
|
# XXX Be more exact for the 3-tuple case?
|
|
if type(key) not in (str, tuple):
|
|
raise KeyError(f'unsupported key {key!r}')
|
|
resolved, extra = self._resolve_key(key)
|
|
if extra:
|
|
raise KeyError(f'key must have at most 3 parts, got {key!r}')
|
|
if not resolved[2]:
|
|
raise ValueError(f'expected name in key, got {key!r}')
|
|
try:
|
|
return self._decls[resolved]
|
|
except KeyError:
|
|
if type(key) is tuple and len(key) == 3:
|
|
filename, funcname, name = key
|
|
else:
|
|
filename, funcname, name = resolved
|
|
if filename and not filename.endswith(('.c', '.h')):
|
|
raise KeyError(f'invalid filename in key {key!r}')
|
|
elif funcname and funcname.endswith(('.c', '.h')):
|
|
raise KeyError(f'invalid funcname in key {key!r}')
|
|
elif name and name.endswith(('.c', '.h')):
|
|
raise KeyError(f'invalid name in key {key!r}')
|
|
else:
|
|
raise # re-raise
|
|
|
|
@property
|
|
def types(self):
|
|
return self._find(kind=KIND.TYPES)
|
|
|
|
@property
|
|
def functions(self):
|
|
return self._find(None, None, None, KIND.FUNCTION)
|
|
|
|
@property
|
|
def variables(self):
|
|
return self._find(None, None, None, KIND.VARIABLE)
|
|
|
|
def iter_all(self):
|
|
yield from self._decls.values()
|
|
|
|
def get(self, key, default=None):
|
|
try:
|
|
return self[key]
|
|
except KeyError:
|
|
return default
|
|
|
|
#def add_decl(self, decl, key=None):
|
|
# decl = _resolve_parsed(decl)
|
|
# self._add_decl(decl, key)
|
|
|
|
def find(self, *key, **explicit):
|
|
if not key:
|
|
if not explicit:
|
|
return iter(self)
|
|
return self._find(**explicit)
|
|
|
|
resolved, extra = self._resolve_key(key)
|
|
filename, funcname, name = resolved
|
|
if not extra:
|
|
kind = None
|
|
elif len(extra) == 1:
|
|
kind, = extra
|
|
else:
|
|
raise KeyError(f'key must have at most 4 parts, got {key!r}')
|
|
|
|
implicit= {}
|
|
if filename:
|
|
implicit['filename'] = filename
|
|
if funcname:
|
|
implicit['funcname'] = funcname
|
|
if name:
|
|
implicit['name'] = name
|
|
if kind:
|
|
implicit['kind'] = kind
|
|
return self._find(**implicit, **explicit)
|
|
|
|
def _find(self, filename=None, funcname=None, name=None, kind=None):
|
|
for decl in self._decls.values():
|
|
if filename and decl.filename != filename:
|
|
continue
|
|
if funcname:
|
|
if decl.kind is not KIND.VARIABLE:
|
|
continue
|
|
if decl.parent.name != funcname:
|
|
continue
|
|
if name and decl.name != name:
|
|
continue
|
|
if kind:
|
|
kinds = KIND.resolve_group(kind)
|
|
if decl.kind not in kinds:
|
|
continue
|
|
yield decl
|
|
|
|
def _add_decl(self, decl, key=None):
|
|
if key:
|
|
if type(key) not in (str, tuple):
|
|
raise NotImplementedError((key, decl))
|
|
# Any partial key will be turned into a full key, but that
|
|
# same partial key will still match a key lookup.
|
|
resolved, _ = self._resolve_key(key)
|
|
if not resolved[2]:
|
|
raise ValueError(f'expected name in key, got {key!r}')
|
|
key = resolved
|
|
# XXX Also add with the decl-derived key if not the same?
|
|
else:
|
|
key, _ = self._resolve_key(decl)
|
|
self._decls[key] = decl
|
|
|
|
def _extend(self, decls):
|
|
decls = iter(decls)
|
|
# Check only the first item.
|
|
for decl in decls:
|
|
if isinstance(decl, Declaration):
|
|
self._add_decl(decl)
|
|
# Add the rest without checking.
|
|
for decl in decls:
|
|
self._add_decl(decl)
|
|
elif isinstance(decl, HighlevelParsedItem):
|
|
raise NotImplementedError(decl)
|
|
else:
|
|
try:
|
|
key, decl = decl
|
|
except ValueError:
|
|
raise NotImplementedError(decl)
|
|
if not isinstance(decl, Declaration):
|
|
raise NotImplementedError(decl)
|
|
self._add_decl(decl, key)
|
|
# Add the rest without checking.
|
|
for key, decl in decls:
|
|
self._add_decl(decl, key)
|
|
# The iterator will be exhausted at this point.
|