scanner: allow for cross-referencing enums

Add another run to the XML parser so we parse enums before Arguments
that cross-reference enums from other interfaces.

This also fixes a type bug - the enum name string was passed to
Argument.create() as Enum and no-one noticed.
This commit is contained in:
Peter Hutterer 2023-02-27 14:08:14 +10:00
parent eecf69af48
commit 1db37c33d5

View file

@ -299,6 +299,16 @@ class Interface:
mode: str = attr.ib() # "ei" or "eis"
description: Optional[Description] = attr.ib(default=None)
@staticmethod
def mangle_name(name: str, component: str) -> str:
"""
Returns the mangled interface name with the component as prefix (e.g. eis_device).
The XML only uses `ei_` as prefix, so let's replace that accordingly.
"""
if name.startswith("ei"):
return f"{component}{name[2:]}"
return name
def add_request(self, request: Request) -> None:
if request.name in [r.name for r in self.requests]:
raise ValueError(f"Duplicate request name '{request.name}'")
@ -314,6 +324,12 @@ class Interface:
raise ValueError(f"Duplicate enum name '{enum.name}'")
self.enums.append(enum)
def find_enum(self, name: str) -> Optional[Enum]:
for e in self.enums:
if e.name == name:
return e
return None
@property
def outgoing(self) -> list[Message]:
"""
@ -454,6 +470,63 @@ class ProtocolParser(xml.sax.handler.ContentHandler):
if self._run_counter <= 1:
return
if element == "enum":
if self.current_interface is None:
raise XmlError.create(
f"Invalid element '{element}' outside an <interface>",
self.location,
)
if self.current_message is not None:
raise XmlError.create(
f"Invalid element '{element}' inside '{self.current_message.name}'",
self.location,
)
try:
name = attrs["name"]
since = attrs["since"]
except KeyError as e:
raise XmlError.create(
f"Missing attribute {e} in element '{element}'",
self.location,
)
try:
is_bitfield = {
"true": True,
"false": False,
}[attrs.get("bitfield", "false")]
except KeyError as e:
raise XmlError.create(
f"Invalid value {e} for boolean bitfield attribute in '{element}'",
self.location,
)
# We only create the enum on the second run, in subsequent runs
# we re-use them so we can cross-reference correctly
if self._run_counter > 2:
enum = self.current_interface.find_enum(name)
if enum is None:
raise XmlError.create(
f"Invalid enum {name}. This is a parser bug",
self.location,
)
else:
enum = Enum.create(
name=name,
since=since,
interface=self.current_interface,
is_bitfield=is_bitfield,
)
try:
self.current_interface.add_enum(enum)
except ValueError as e:
raise XmlError.create(str(e), self.location)
self.current_message = enum
# second run only parses enums
if self._run_counter <= 2:
return
if element == "request":
if self.current_interface is None:
raise XmlError.create(
@ -517,48 +590,6 @@ class ProtocolParser(xml.sax.handler.ContentHandler):
except ValueError as e:
raise XmlError.create(str(e), self.location)
self.current_message = event
elif element == "enum":
if self.current_interface is None:
raise XmlError.create(
f"Invalid element '{element}' outside an <interface>",
self.location,
)
if self.current_message is not None:
raise XmlError.create(
f"Invalid element '{element}' inside '{self.current_message.name}'",
self.location,
)
try:
name = attrs["name"]
since = attrs["since"]
except KeyError as e:
raise XmlError.create(
f"Missing attribute {e} in element '{element}'",
self.location,
)
try:
is_bitfield = {
"true": True,
"false": False,
}[attrs.get("bitfield", "false")]
except KeyError as e:
raise XmlError.create(
f"Invalid value {e} for boolean bitfield attribute in '{element}'",
self.location,
)
enum = Enum.create(
name=name,
since=since,
interface=self.current_interface,
is_bitfield=is_bitfield,
)
try:
self.current_interface.add_enum(enum)
except ValueError as e:
raise XmlError.create(str(e), self.location)
self.current_message = enum
elif element == "arg":
if self.current_interface is None:
raise XmlError.create(
@ -581,19 +612,28 @@ class ProtocolParser(xml.sax.handler.ContentHandler):
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)
interface = self.interface_by_name(
Interface.mangle_name(interface_name, self.component)
)
else:
interface = None
enum = attrs.get("enum", None)
if enum is not None and enum not in [
e.name for e in self.current_interface.enums
]:
raise XmlError.create(
f"Failed to find enum '{self.current_interface.name}.{enum}'",
self.location,
)
enum_name = attrs.get("enum", None)
enum = None
if enum_name is not None:
if "." in enum_name:
iname, enum_name = enum_name.split(".")
intf = self.interface_by_name(
Interface.mangle_name(iname, self.component)
)
else:
intf = self.current_interface
enum = intf.find_enum(enum_name)
if enum is None:
raise XmlError.create(
f"Failed to find enum '{intf.name}.{enum_name}'",
self.location,
)
arg = Argument.create(
name=name,
protocol_type=proto_type,
@ -647,15 +687,20 @@ class ProtocolParser(xml.sax.handler.ContentHandler):
if self._run_counter <= 1:
return
if name == "enum":
assert isinstance(self.current_message, Enum)
self.current_message = None
# second run only parses interfaces and enums
if self._run_counter <= 2:
return
if name == "request":
assert isinstance(self.current_message, Request)
self.current_message = None
elif name == "event":
assert isinstance(self.current_message, Event)
self.current_message = None
elif name == "enum":
assert isinstance(self.current_message, Enum)
self.current_message = None
elif name == "description":
assert self.current_description is not None
self.current_description.text = dedent(self.current_description.text)
@ -685,7 +730,8 @@ class ProtocolParser(xml.sax.handler.ContentHandler):
def parse(protofile: Path, component: str) -> Protocol:
proto = ProtocolParser.create(component=component)
xml.sax.parse(os.fspath(protofile), proto)
# We parse two times, once to fetch all the interfaces, then to parse the details
# We parse three times, once to fetch all the interfaces, one for enums, then to parse the details
xml.sax.parse(os.fspath(protofile), proto)
xml.sax.parse(os.fspath(protofile), proto)
copyright = proto.copyright.text if proto.copyright else None
return Protocol(copyright=copyright, interfaces=proto.interfaces)