scanner: switch to a two-run parsing of the XML file

On the first run we extract the interfaces only, on the second run all
the rest. This allows us to pass the interface to the Argument where
appropriate.
This commit is contained in:
Peter Hutterer 2023-02-09 11:14:53 +10:00
parent 21df02e499
commit a0b2cfffa9

View file

@ -71,6 +71,12 @@ class Argument:
signature: str = attr.ib(converter=proto_to_type) signature: str = attr.ib(converter=proto_to_type)
summary: str = attr.ib() summary: str = attr.ib()
enum: Optional["Enum"] = attr.ib() enum: Optional["Enum"] = attr.ib()
interface: Optional["Interface"] = attr.ib()
@interface.validator # type: ignore
def _validate_interface(self, attribute, value):
if value is not None and self.signature not in ["n", "o"]:
raise ValueError("Interface may only be set for object types")
@property @property
def as_arg(self) -> str: def as_arg(self) -> str:
@ -107,9 +113,14 @@ class Argument:
@classmethod @classmethod
def create( def create(
cls, name: str, signature: str, summary: str = "", enum: Optional["Enum"] = None cls,
name: str,
signature: str,
summary: str = "",
enum: Optional["Enum"] = None,
interface: Optional["Interface"] = None,
) -> "Argument": ) -> "Argument":
return cls(name, signature, summary, enum) return cls(name, signature, summary, enum, interface=interface)
@attr.s @attr.s
@ -291,6 +302,8 @@ class Protocol(xml.sax.handler.ContentHandler):
current_interface: Optional[Interface] = attr.ib(init=False, default=None) current_interface: Optional[Interface] = attr.ib(init=False, default=None)
current_message: Optional[Union[Message, Enum]] = attr.ib(init=False, default=None) current_message: Optional[Union[Message, Enum]] = attr.ib(init=False, default=None)
run: int = attr.ib(init=False, default=0)
def xmlerror(self, msg) -> None: def xmlerror(self, msg) -> None:
line = self._locator.getLineNumber() # type: ignore line = self._locator.getLineNumber() # type: ignore
col = self._locator.getColumnNumber() # type: ignore col = self._locator.getColumnNumber() # type: ignore
@ -303,6 +316,15 @@ class Protocol(xml.sax.handler.ContentHandler):
col = self._locator.getColumnNumber() # type: ignore col = self._locator.getColumnNumber() # type: ignore
return line, col return line, col
def interface_by_name(self, name) -> Interface:
try:
return [iface for iface in self.interfaces if iface.name == name].pop()
except IndexError:
raise XmlError(*self.location, f"Unable to find interface {name}")
def startDocument(self):
self.run += 1
def startElement(self, element: str, attrs: dict): def startElement(self, element: str, attrs: dict):
if element == "interface": if element == "interface":
if self.current_interface is not None: if self.current_interface is not None:
@ -321,10 +343,21 @@ class Protocol(xml.sax.handler.ContentHandler):
if name.startswith("ei"): if name.startswith("ei"):
name = f"{self.component}{name[2:]}" name = f"{self.component}{name[2:]}"
intf = Interface.create(name=name, version=version, mode=self.component) # We only create the interface on the first run, in subsequent runs we
# re-use them so we can cross reference correctly
if self.run > 1:
intf = self.interface_by_name(name)
else:
intf = Interface.create(name=name, version=version, mode=self.component)
self.interfaces.append(intf)
self.current_interface = intf self.current_interface = intf
self.interfaces.append(intf)
elif element == "request": # first run only parses interfaces
if self.run <= 1:
return
if element == "request":
if self.current_interface is None: if self.current_interface is None:
raise XmlError( raise XmlError(
*self.location, *self.location,
@ -419,6 +452,13 @@ class Protocol(xml.sax.handler.ContentHandler):
name = attrs["name"] name = attrs["name"]
sig = attrs["type"] sig = attrs["type"]
summary = attrs.get("summary", "") summary = attrs.get("summary", "")
interface_name = attrs.get("interface", None)
if interface_name is not None:
if interface_name.startswith("ei"):
interface_name = f"{self.component}{interface_name[2:]}"
interface = self.interface_by_name(interface_name)
else:
interface = None
enum = attrs.get("enum", None) enum = attrs.get("enum", None)
if enum is not None and enum not in [ if enum is not None and enum not in [
e.name for e in self.current_interface.enums e.name for e in self.current_interface.enums
@ -427,7 +467,13 @@ class Protocol(xml.sax.handler.ContentHandler):
*self.location, *self.location,
f"Failed to find enum '{self.current_interface.name}.{enum}'", f"Failed to find enum '{self.current_interface.name}.{enum}'",
) )
arg = Argument.create(name=name, signature=sig, summary=summary, enum=enum) arg = Argument.create(
name=name,
signature=sig,
summary=summary,
enum=enum,
interface=interface,
)
self.current_message.add_argument(arg) self.current_message.add_argument(arg)
elif element == "entry": elif element == "entry":
if self.current_interface is None: if self.current_interface is None:
@ -452,7 +498,12 @@ class Protocol(xml.sax.handler.ContentHandler):
if name == "interface": if name == "interface":
assert self.current_interface is not None assert self.current_interface is not None
self.current_interface = None self.current_interface = None
elif name == "request":
# first run only parses interfaces
if self.run <= 1:
return
if name == "request":
assert isinstance(self.current_message, Request) assert isinstance(self.current_message, Request)
self.current_message = None self.current_message = None
elif name == "event": elif name == "event":
@ -474,6 +525,8 @@ class Protocol(xml.sax.handler.ContentHandler):
def parse(protofile: Path, component: str) -> Protocol: def parse(protofile: Path, component: str) -> Protocol:
proto = Protocol.create(component=component) proto = Protocol.create(component=component)
xml.sax.parse(os.fspath(protofile), proto) xml.sax.parse(os.fspath(protofile), proto)
# We parse two times, once to fetch all the interfaces, then to parse the details
xml.sax.parse(os.fspath(protofile), proto)
return proto return proto