from collections import namedtuple import re from .util import classonly, _NTBase # XXX need tests: # * ID.match() UNKNOWN = '???' NAME_RE = re.compile(r'^([a-zA-Z]|_\w*[a-zA-Z]\w*|[a-zA-Z]\w*)$') class ID(_NTBase, namedtuple('ID', 'filename funcname name')): """A unique ID for a single symbol or declaration.""" __slots__ = () # XXX Add optional conditions (tuple of strings) field. #conditions = Slot() @classonly def from_raw(cls, raw): if not raw: return None if isinstance(raw, str): return cls(None, None, raw) try: name, = raw filename = None except ValueError: try: filename, name = raw except ValueError: return super().from_raw(raw) return cls(filename, None, name) 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, ) #cls.conditions.set(self, tuple(str(s) if s else None # for s in conditions or ())) return self def validate(self): """Fail if the object is invalid (i.e. init with bad data).""" if not self.name: raise TypeError('missing name') else: if not NAME_RE.match(self.name): raise ValueError( f'name must be an identifier, got {self.name!r}') # Symbols from a binary might not have filename/funcname info. if self.funcname: if not self.filename: raise TypeError('missing filename') if not NAME_RE.match(self.funcname) and self.funcname != UNKNOWN: raise ValueError( f'name must be an identifier, got {self.funcname!r}') # XXX Require the filename (at least UNKONWN)? # XXX Check the filename? @property def islocal(self): return self.funcname is not None def match(self, other, *, match_files=(lambda f1, f2: f1 == f2), ): """Return True if the two match. At least one of the two must be completely valid (no UNKNOWN anywhere). Otherwise False is returned. The remaining one *may* have UNKNOWN for both funcname and filename. It must have a valid name though. The caller is responsible for knowing which of the two is valid (and which to use if both are valid). """ # First check the name. if self.name is None: return False if other.name != self.name: return False # Then check the filename. if self.filename is None: return False if other.filename is None: return False if self.filename == UNKNOWN: # "other" must be the valid one. if other.funcname == UNKNOWN: return False elif self.funcname != UNKNOWN: # XXX Try matching funcname even though we don't # know the filename? raise NotImplementedError else: return True elif other.filename == UNKNOWN: # "self" must be the valid one. if self.funcname == UNKNOWN: return False elif other.funcname != UNKNOWN: # XXX Try matching funcname even though we don't # know the filename? raise NotImplementedError else: return True elif not match_files(self.filename, other.filename): return False # Finally, check the funcname. if self.funcname == UNKNOWN: # "other" must be the valid one. if other.funcname == UNKNOWN: return False else: return other.funcname is not None elif other.funcname == UNKNOWN: # "self" must be the valid one. if self.funcname == UNKNOWN: return False else: return self.funcname is not None elif self.funcname == other.funcname: # Both are valid. return True return False