summaryrefslogtreecommitdiff
path: root/Tools/c-analyzer/c_parser/info.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/c-analyzer/c_parser/info.py')
-rw-r--r--Tools/c-analyzer/c_parser/info.py1658
1 files changed, 1658 insertions, 0 deletions
diff --git a/Tools/c-analyzer/c_parser/info.py b/Tools/c-analyzer/c_parser/info.py
new file mode 100644
index 0000000000..a07ce2e0cc
--- /dev/null
+++ b/Tools/c-analyzer/c_parser/info.py
@@ -0,0 +1,1658 @@
+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.