from collections import namedtuple import enum import os.path 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 SIMPLE_TYPE, _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 provde _make() becaose 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, 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 " ". 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 "")})', ]) 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 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 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) 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 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.