| from collections import namedtuple | |
| import enum | |
| import re | |
| from c_common import fsutil | |
| 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 _STORAGE | |
| FIXED_TYPE = _misc.Labeled('FIXED_TYPE') | |
| STORAGE = frozenset(_STORAGE) | |
| ############################# | |
| # 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) | |
| def get_kind_group(item): | |
| return KIND.get_group(item.kind) | |
| ############################# | |
| # low-level | |
| def _fix_filename(filename, relroot, *, | |
| formatted=True, | |
| **kwargs): | |
| if formatted: | |
| fix = fsutil.format_filename | |
| else: | |
| fix = fsutil.fix_filename | |
| return fix(filename, relroot=relroot, **kwargs) | |
| 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=fsutil.USE_CWD, **kwargs): | |
| filename = _fix_filename(self.filename, relroot, **kwargs) | |
| if filename == self.filename: | |
| return self | |
| 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) | |
| # We have to provide _make() because we implemented __new__(). | |
| @classmethod | |
| def _make(cls, iterable): | |
| try: | |
| return cls(*iterable) | |
| except Exception: | |
| super()._make(iterable) | |
| raise # re-raise | |
| 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 | |
| def fix_filename(self, relroot=fsutil.USE_CWD, **kwargs): | |
| filename = _fix_filename(self.filename, relroot, **kwargs) | |
| if filename == self.filename: | |
| return self | |
| return self._replace(filename=filename) | |
| 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 fix_filename(self, relroot=fsutil.USE_CWD, **kwargs): | |
| fixed = self.file.fix_filename(relroot, **kwargs) | |
| if fixed == self.file: | |
| return self | |
| return self._replace(file=fixed) | |
| 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, Signature): | |
| storage = None | |
| typequal, typespec, abstract = decl.returntype | |
| 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 | |
| def get_default_storage(decl): | |
| if decl.kind not in (KIND.VARIABLE, KIND.FUNCTION): | |
| return None | |
| return 'extern' if decl.parent is None else 'auto' | |
| def get_effective_storage(decl, *, default=None): | |
| # Note that "static" limits access to just that C module | |
| # and "extern" (the default for module-level) allows access | |
| # outside the C module. | |
| if default is None: | |
| default = get_default_storage(decl) | |
| if default is None: | |
| return None | |
| try: | |
| storage = decl.storage | |
| except AttributeError: | |
| storage, _ = _get_vartype(decl.data) | |
| return storage or default | |
| ############################# | |
| # 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=fsutil.USE_CWD, **kwargs): | |
| if self.file: | |
| self.file = self.file.fix_filename(relroot, **kwargs) | |
| return self | |
| 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) | |
| extra = None | |
| 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 or None}, | |
| _shortkey=f'({parent.name}).{name}' if parent else name, | |
| _key=(str(file), | |
| # Tilde comes after all other ascii characters. | |
| f'~{parent or ""}~', | |
| name, | |
| ), | |
| ) | |
| if storage: | |
| if storage not in STORAGE: | |
| # The parser must need an update. | |
| raise NotImplementedError(storage) | |
| # Otherwise we trust the compiler to have validated it. | |
| @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 | |
| @property | |
| def typequal(self): | |
| return self.returntype.typequal | |
| @property | |
| def typespec(self): | |
| return self.returntype.typespec | |
| @property | |
| def abstract(self): | |
| return self.returntype.abstract | |
| 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 finish! | |
| 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) | |
| kwargs = dict(data) | |
| del kwargs['storage'] | |
| if 'returntype' in kwargs: | |
| vartype = kwargs['returntype'] | |
| del vartype['storage'] | |
| kwargs['returntype'] = VarType(**vartype) | |
| datacls = Signature | |
| else: | |
| datacls = VarType | |
| return datacls(**kwargs), 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 = vartype.pop('size') | |
| if isinstance(size, str) and size.isdigit(): | |
| size = int(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 finish! | |
| 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 finish! | |
| return data, None | |
| @classmethod | |
| def _raw_data(self, data): | |
| # XXX finish! | |
| 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) | |
| def set_flag(item, name, value): | |
| try: | |
| setattr(item, name, value) | |
| except AttributeError: | |
| object.__setattr__(item, name, value) | |
| ############################# | |
| # 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. |