ladybird/Meta/Utils/webidl_parser.py
Shannon Booth 2e09af3116 Meta: Parse WebIDL declarations into structured objects
Teach the python WebIDL parser to retain typed declarations instead of
keeping or skipping raw text.
2026-06-06 19:16:52 +02:00

1084 lines
36 KiB
Python

# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
from dataclasses import dataclass
from dataclasses import field
from enum import Enum
from pathlib import Path
from typing import Dict
from typing import List
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Tuple
from Utils.lexer import Lexer
@dataclass(frozen=True)
class IDLType:
name: str
nullable: bool = False
def __str__(self) -> str:
nullable_suffix = "?" if self.nullable else ""
return f"{self.name}{nullable_suffix}"
def clone_with_nullable(self, nullable: bool) -> "IDLType":
return IDLType(self.name, nullable)
def without_nullable(self) -> "IDLType":
return self.clone_with_nullable(False)
def flattened_member_types(self) -> List["IDLType"]:
return [self]
def includes_undefined(self) -> bool:
return self.name == "undefined"
def includes_nullable_type(self) -> bool:
return self.nullable
def child_types(self) -> Tuple["IDLType", ...]:
return ()
def nested_types(self) -> List["IDLType"]:
nested_types: List["IDLType"] = [self]
for child_type in self.child_types():
nested_types.extend(child_type.nested_types())
return nested_types
@dataclass(init=False, frozen=True)
class IDLUnionType(IDLType):
member_types: Tuple[IDLType, ...]
def __init__(self, member_types: Sequence[IDLType], nullable: bool = False) -> None:
object.__setattr__(self, "member_types", tuple(member_types))
super().__init__(self.name_for_member_types(member_types), nullable)
@staticmethod
def name_for_member_types(member_types: Sequence[IDLType]) -> str:
return f"({' or '.join(str(member_type) for member_type in member_types)})"
def __str__(self) -> str:
nullable_suffix = "?" if self.nullable else ""
return f"{self.name_for_member_types(self.member_types)}{nullable_suffix}"
def clone_with_nullable(self, nullable: bool) -> "IDLUnionType":
return IDLUnionType(self.member_types, nullable)
def flattened_member_types(self) -> List[IDLType]:
flattened_member_types: List[IDLType] = []
for member_type in self.member_types:
flattened_member_types.extend(member_type.flattened_member_types())
return flattened_member_types
def includes_undefined(self) -> bool:
return any(member_type.includes_undefined() for member_type in self.member_types)
def includes_nullable_type(self) -> bool:
return self.nullable or any(member_type.includes_nullable_type() for member_type in self.member_types)
def child_types(self) -> Tuple[IDLType, ...]:
return self.member_types
@dataclass(init=False, frozen=True)
class IDLParameterizedType(IDLType):
parameters: Tuple[IDLType, ...]
def __init__(self, name: str, parameters: Sequence[IDLType], nullable: bool = False) -> None:
object.__setattr__(self, "parameters", tuple(parameters))
super().__init__(name, nullable)
def __str__(self) -> str:
nullable_suffix = "?" if self.nullable else ""
return f"{self.name}<{', '.join(str(parameter) for parameter in self.parameters)}>{nullable_suffix}"
def clone_with_nullable(self, nullable: bool) -> "IDLParameterizedType":
return IDLParameterizedType(self.name, self.parameters, nullable)
def child_types(self) -> Tuple[IDLType, ...]:
return self.parameters
@dataclass
class Constant:
type: IDLType
name: str
value: str
@dataclass
class OperationParameter:
name: str
type: IDLType
optional: bool = False
default_value: Optional[str] = None
variadic: bool = False
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class SpecialOperation:
identifier_type: IDLType
return_type: IDLType
name: str
parameters: List[OperationParameter]
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class Operation:
name: str
return_type: IDLType
parameters: List[OperationParameter]
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class Constructor:
parameters: List[OperationParameter]
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class IterableDeclaration:
value_type: IDLType
key_type: Optional[IDLType] = None
@dataclass
class AsyncIterableDeclaration:
value_type: IDLType
parameters: List[OperationParameter] = field(default_factory=list)
key_type: Optional[IDLType] = None
@dataclass
class MaplikeDeclaration:
key_type: IDLType
value_type: IDLType
readonly: bool = False
@dataclass
class SetlikeDeclaration:
value_type: IDLType
readonly: bool = False
@dataclass
class Attribute:
name: str
type: IDLType
readonly: bool = False
inherit: bool = False
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class Stringifier:
extended_attributes: Dict[str, str] = field(default_factory=dict)
attribute: Optional[Attribute] = None
class InterfaceKind(Enum):
Interface = "interface"
Namespace = "namespace"
CallbackInterface = "callback interface"
@dataclass
class Interface:
name: str
path: Path
extended_attributes: Dict[str, str] = field(default_factory=dict)
kind: InterfaceKind = InterfaceKind.Interface
parent_name: str = ""
constants: List[Constant] = field(default_factory=list)
regular_attributes: List[Attribute] = field(default_factory=list)
static_attributes: List[Attribute] = field(default_factory=list)
regular_operations: List[Operation] = field(default_factory=list)
static_operations: List[Operation] = field(default_factory=list)
constructors: List[Constructor] = field(default_factory=list)
stringifier: Optional[Stringifier] = None
iterable: Optional[IterableDeclaration] = None
async_iterable: Optional[AsyncIterableDeclaration] = None
named_property_getter: Optional[SpecialOperation] = None
indexed_property_getter: Optional[SpecialOperation] = None
named_property_setter: Optional[SpecialOperation] = None
named_property_deleter: Optional[SpecialOperation] = None
indexed_property_setter: Optional[SpecialOperation] = None
maplike: Optional[MaplikeDeclaration] = None
setlike: Optional[SetlikeDeclaration] = None
@property
def is_namespace(self) -> bool:
return self.kind == InterfaceKind.Namespace
@property
def is_callback_interface(self) -> bool:
return self.kind == InterfaceKind.CallbackInterface
@property
def has_special_member(self) -> bool:
return (
self.iterable is not None
or self.async_iterable is not None
or self.maplike is not None
or self.setlike is not None
)
@property
def namespaced_name(self) -> str:
legacy_namespace = self.extended_attributes.get("LegacyNamespace")
if legacy_namespace:
return f"{legacy_namespace}.{self.name}"
return self.name
@property
def implemented_name(self) -> str:
return self.extended_attributes.get("ImplementedAs", self.name)
@property
def constructor_class(self) -> str:
return f"{self.implemented_name}Constructor"
@property
def prototype_class(self) -> str:
return f"{self.implemented_name}Prototype"
@property
def namespace_class(self) -> str:
return f"{self.name}Namespace"
@property
def supports_named_properties(self) -> bool:
return self.named_property_getter is not None
@dataclass
class Dictionary:
name: str
path: Path
parent_name: str = ""
members: List["DictionaryMember"] = field(default_factory=list)
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class DictionaryMember:
name: str
type: IDLType
required: bool = False
default_value: Optional[str] = None
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class CallbackFunction:
name: str
path: Path
return_type: IDLType
parameters: List[OperationParameter] = field(default_factory=list)
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class Enumeration:
name: str
path: Path
values: List[str]
extended_attributes: Dict[str, str] = field(default_factory=dict)
@dataclass
class Typedef:
name: str
path: Path
type: IDLType
@dataclass
class IncludedMixin:
interface_name: str
mixin_name: str
@dataclass
class Module:
path: Path
interface: Optional[Interface] = None
partial_interfaces: List[Interface] = field(default_factory=list)
mixins: List[Interface] = field(default_factory=list)
partial_mixins: List[Interface] = field(default_factory=list)
included_mixins: List[IncludedMixin] = field(default_factory=list)
callback_functions: List[CallbackFunction] = field(default_factory=list)
dictionaries: List[Dictionary] = field(default_factory=list)
partial_dictionaries: List[Dictionary] = field(default_factory=list)
enumerations: List[Enumeration] = field(default_factory=list)
typedefs: List[Typedef] = field(default_factory=list)
class ParseError(RuntimeError):
pass
def parse_module(path: Path, contents: str) -> Module:
return Parser(path, contents).parse()
class Parser:
def __init__(self, path: Path, contents: str) -> None:
self.path = path
self.contents = contents
self.lexer = Lexer(contents)
def parse(self) -> Module:
module = Module(path=self.path)
self.consume_whitespace()
while not self.lexer.is_eof():
self.parse_module_declaration(module, self.parse_leading_extended_attributes())
self.consume_whitespace()
return module
def parse_module_declaration(self, module: Module, extended_attributes: Dict[str, str]) -> None:
if self.consume_optional_keyword("dictionary"):
module.dictionaries.append(self.parse_dictionary(extended_attributes))
return
if self.consume_optional_keyword("partial dictionary"):
module.partial_dictionaries.append(self.parse_dictionary(extended_attributes))
return
if self.consume_optional_keyword("enum"):
module.enumerations.append(self.parse_enumeration(extended_attributes))
return
if self.consume_optional_keyword("typedef"):
module.typedefs.append(self.parse_typedef())
return
if self.consume_optional_keyword("partial interface mixin"):
module.partial_mixins.append(self.parse_interface_declaration(extended_attributes))
return
if self.consume_optional_keyword("partial interface"):
module.partial_interfaces.append(self.parse_interface_declaration(extended_attributes))
return
if self.consume_optional_keyword("interface mixin"):
module.mixins.append(self.parse_interface_declaration(extended_attributes))
return
if self.consume_optional_keyword("partial namespace"):
module.partial_interfaces.append(
self.parse_interface_declaration(extended_attributes, InterfaceKind.Namespace)
)
return
if self.consume_optional_keyword("callback interface"):
module.interface = self.parse_interface_declaration(extended_attributes, InterfaceKind.CallbackInterface)
return
if self.consume_optional_keyword("callback"):
module.callback_functions.append(self.parse_callback_function(extended_attributes))
return
if self.consume_optional_keyword("namespace"):
module.interface = self.parse_interface_declaration(extended_attributes, InterfaceKind.Namespace)
return
if self.consume_optional_keyword("interface"):
module.interface = self.parse_interface_declaration(extended_attributes)
return
module.included_mixins.append(self.parse_includes_statement())
def parse_includes_statement(self) -> IncludedMixin:
interface_name = self.parse_identifier()
self.consume_whitespace()
if not self.next_is_keyword("includes"):
self.raise_parse_error("expected a declaration or an includes statement")
self.consume_keyword("includes")
self.consume_whitespace()
mixin_name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific(";")
return IncludedMixin(interface_name=interface_name, mixin_name=mixin_name)
def parse_interface_declaration(
self, extended_attributes: Dict[str, str], kind: InterfaceKind = InterfaceKind.Interface
) -> Interface:
name, parent_name = self.parse_named_body_header()
interface = Interface(
name=name,
path=self.path,
extended_attributes=extended_attributes,
kind=kind,
parent_name=parent_name,
)
self.parse_interface_body(interface)
self.consume_whitespace()
self.assert_specific(";")
return interface
def parse_dictionary(self, extended_attributes: Dict[str, str]) -> Dictionary:
dictionary_extended_attributes = extended_attributes
dictionary_name, parent_name = self.parse_named_body_header()
members: List[DictionaryMember] = []
while True:
self.consume_whitespace()
if self.lexer.consume_specific("}"):
self.consume_whitespace()
self.assert_specific(";")
break
member_extended_attributes = self.parse_leading_extended_attributes()
required = self.consume_optional_keyword("required")
member_extended_attributes.update(self.parse_leading_extended_attributes())
member_type = self.parse_type()
self.consume_whitespace()
member_name = self.parse_identifier()
self.consume_whitespace()
default_value: Optional[str] = None
if self.lexer.consume_specific("="):
if required:
self.raise_parse_error("required dictionary member must not have a default value")
self.consume_whitespace()
default_value = self.consume_until_top_level(";").strip()
self.consume_whitespace()
self.assert_specific(";")
members.append(
DictionaryMember(
name=member_name,
type=member_type,
required=required,
default_value=default_value,
extended_attributes=member_extended_attributes,
)
)
members.sort(key=lambda member: member.name)
return Dictionary(
name=dictionary_name,
path=self.path,
parent_name=parent_name,
members=members,
extended_attributes=dictionary_extended_attributes,
)
def parse_callback_function(self, extended_attributes: Dict[str, str]) -> CallbackFunction:
name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific("=")
self.consume_whitespace()
return_type = self.parse_type()
self.consume_whitespace()
parameters = self.parse_parenthesized_parameters()
self.assert_specific(";")
return CallbackFunction(
name=name,
path=self.path,
return_type=return_type,
parameters=parameters,
extended_attributes=extended_attributes,
)
def parse_typedef(self) -> Typedef:
typedef_type = self.parse_type()
self.consume_whitespace()
name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific(";")
return Typedef(name=name, path=self.path, type=typedef_type)
def parse_type(self) -> IDLType:
if self.lexer.consume_specific("("):
return self.parse_union_type()
type_name = self.parse_type_name()
parameters: List[IDLType] = []
if self.lexer.next_is("<"):
parameters = self.parse_type_parameters()
nullable = self.lexer.consume_specific("?")
if parameters:
return IDLParameterizedType(type_name, parameters, nullable)
return IDLType(type_name, nullable)
def parse_union_type(self) -> IDLUnionType:
member_types = [self.parse_type()]
self.consume_whitespace()
self.consume_keyword("or")
self.consume_whitespace()
member_types.append(self.parse_type())
self.consume_whitespace()
while self.lexer.consume_specific("or"):
self.consume_whitespace()
member_types.append(self.parse_type())
self.consume_whitespace()
self.assert_specific(")")
return IDLUnionType(member_types, self.lexer.consume_specific("?"))
def parse_type_name(self) -> str:
type_words: List[str] = []
if self.consume_optional_keyword("unsigned"):
type_words.append("unsigned")
if self.consume_optional_keyword("unrestricted"):
type_words.append("unrestricted")
name = self.lexer.consume_while(lambda character: character.isalnum() or character == "_")
if not name:
self.raise_parse_error("type can't be an empty string")
type_words.append(name)
if name.lower() == "long":
position_before_whitespace = self.lexer.tell()
self.consume_whitespace()
if self.lexer.consume_specific("long"):
type_words.append("long")
else:
self.lexer.position = position_before_whitespace
return " ".join(type_words)
def parse_named_body_header(self) -> Tuple[str, str]:
name = self.parse_identifier()
self.consume_whitespace()
parent_name = ""
if self.lexer.consume_specific(":"):
self.consume_whitespace()
parent_name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific("{")
return name, parent_name
def parse_enumeration(self, extended_attributes: Dict[str, str]) -> Enumeration:
name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific("{")
values: List[str] = []
while not self.lexer.is_eof():
self.consume_whitespace()
if self.lexer.consume_specific("}"):
break
self.assert_specific('"')
value = self.lexer.consume_until(lambda character: character == '"')
self.assert_specific('"')
self.consume_whitespace()
values.append(value)
self.consume_whitespace()
if self.lexer.next_is("}"):
continue
self.assert_specific(",")
self.consume_whitespace()
self.assert_specific(";")
return Enumeration(
name=name,
path=self.path,
values=values,
extended_attributes=extended_attributes,
)
def parse_interface_body(self, interface: Interface) -> None:
while not self.lexer.is_eof():
self.consume_whitespace()
if self.lexer.consume_specific("}"):
return
extended_attributes = self.parse_leading_extended_attributes()
self.consume_whitespace()
if self.next_is_keyword("const"):
interface.constants.append(self.parse_constant())
continue
if self.consume_optional_keyword("static"):
readonly = self.consume_optional_keyword("readonly")
if readonly or self.next_is_keyword("attribute"):
interface.static_attributes.append(self.parse_attribute(extended_attributes, readonly=readonly))
else:
interface.static_operations.append(self.parse_operation(extended_attributes))
continue
if self.consume_optional_keyword("stringifier"):
readonly = self.consume_optional_keyword("readonly")
if readonly or self.next_is_keyword("attribute"):
attribute = self.parse_attribute(extended_attributes, readonly=readonly)
interface.regular_attributes.append(attribute)
interface.stringifier = Stringifier(extended_attributes, attribute)
continue
self.consume_member_semicolon()
interface.stringifier = Stringifier(extended_attributes)
continue
if self.consume_optional_keyword("inherit"):
interface.regular_attributes.append(self.parse_attribute(extended_attributes, inherit=True))
continue
readonly = self.consume_optional_keyword("readonly")
if self.next_is_keyword("attribute"):
interface.regular_attributes.append(
self.parse_attribute(
extended_attributes,
readonly=readonly,
)
)
continue
if self.next_is_keyword("maplike"):
interface.maplike = self.parse_maplike_declaration(readonly=readonly)
continue
if self.next_is_keyword("setlike"):
interface.setlike = self.parse_setlike_declaration(readonly=readonly)
continue
if readonly:
interface.regular_attributes.append(self.parse_attribute(extended_attributes, readonly=True))
continue
if self.next_is_keyword("constructor"):
interface.constructors.append(self.parse_constructor(extended_attributes))
continue
if self.next_is_keyword("iterable"):
interface.iterable = self.parse_iterable_declaration()
continue
if self.next_is_keyword("async"):
interface.async_iterable = self.parse_async_iterable_declaration()
continue
if self.next_is_keyword("getter"):
special_operation = self.parse_special_operation("getter", extended_attributes)
identifier_type = special_operation.identifier_type
if identifier_type.name == "DOMString" and not identifier_type.nullable:
interface.named_property_getter = special_operation
elif identifier_type.name == "unsigned long" and not identifier_type.nullable:
interface.indexed_property_getter = special_operation
else:
self.raise_parse_error(
f"named/indexed property getter must use DOMString or unsigned long, got '{identifier_type}'"
)
continue
if self.next_is_keyword("setter"):
special_operation = self.parse_special_operation("setter", extended_attributes)
identifier_type = special_operation.identifier_type
if identifier_type.name == "DOMString" and not identifier_type.nullable:
interface.named_property_setter = special_operation
elif identifier_type.name == "unsigned long" and not identifier_type.nullable:
interface.indexed_property_setter = special_operation
else:
self.raise_parse_error(
f"named/indexed property setter must use DOMString or unsigned long, got '{identifier_type}'"
)
continue
if self.next_is_keyword("deleter"):
special_operation = self.parse_special_operation("deleter", extended_attributes)
identifier_type = special_operation.identifier_type
if identifier_type.name == "DOMString" and not identifier_type.nullable:
interface.named_property_deleter = special_operation
else:
self.raise_parse_error(f"named property deleter must use DOMString, got '{identifier_type}'")
continue
interface.regular_operations.append(self.parse_operation(extended_attributes))
self.raise_parse_error("unterminated interface body")
def parse_iterable_declaration(self) -> IterableDeclaration:
self.consume_keyword("iterable")
key_type, value_type = self.parse_iterable_type_parameters()
self.consume_member_semicolon()
return IterableDeclaration(
value_type=value_type,
key_type=key_type,
)
def parse_async_iterable_declaration(self) -> AsyncIterableDeclaration:
self.consume_keyword("async")
self.consume_whitespace()
self.consume_keyword("iterable")
key_type, value_type = self.parse_iterable_type_parameters()
self.consume_whitespace()
parameters: List[OperationParameter] = []
if self.lexer.next_is("("):
parameters = self.parse_parenthesized_parameters()
self.consume_member_semicolon()
return AsyncIterableDeclaration(
value_type=value_type,
key_type=key_type,
parameters=parameters,
)
def parse_iterable_type_parameters(self) -> Tuple[Optional[IDLType], IDLType]:
parameters = self.parse_type_parameters()
if len(parameters) == 1:
return None, parameters[0]
if len(parameters) == 2:
return parameters[0], parameters[1]
self.raise_parse_error("expected one or two iterable type parameters")
def parse_type_parameters(self) -> List[IDLType]:
self.assert_specific("<")
parameters = [self.parse_type()]
self.consume_whitespace()
while self.lexer.consume_specific(","):
self.consume_whitespace()
parameters.append(self.parse_type())
self.consume_whitespace()
self.assert_specific(">")
return parameters
def parse_maplike_declaration(self, readonly: bool = False) -> MaplikeDeclaration:
self.consume_keyword("maplike")
parameters = self.parse_type_parameters()
if len(parameters) != 2:
self.raise_parse_error("expected two maplike type parameters")
self.consume_member_semicolon()
return MaplikeDeclaration(
key_type=parameters[0],
value_type=parameters[1],
readonly=readonly,
)
def parse_setlike_declaration(self, readonly: bool = False) -> SetlikeDeclaration:
self.consume_keyword("setlike")
parameters = self.parse_type_parameters()
if len(parameters) != 1:
self.raise_parse_error("expected one setlike type parameter")
self.consume_member_semicolon()
return SetlikeDeclaration(
value_type=parameters[0],
readonly=readonly,
)
def parse_special_operation(
self,
keyword: str,
extended_attributes: Dict[str, str],
) -> SpecialOperation:
self.consume_keyword(keyword)
self.consume_whitespace()
return_type = self.parse_type()
self.consume_whitespace()
name = ""
if not self.lexer.next_is("("):
name = self.parse_identifier()
self.consume_whitespace()
parameters = self.parse_parenthesized_parameters()
self.consume_member_semicolon()
if not parameters:
self.raise_parse_error("special operation must have an identifier parameter")
return SpecialOperation(
identifier_type=parameters[0].type,
return_type=return_type,
name=name,
parameters=parameters,
extended_attributes=extended_attributes,
)
def parse_parenthesized_parameters(self) -> List[OperationParameter]:
self.assert_specific("(")
self.consume_whitespace()
parameters: List[OperationParameter] = []
if not self.lexer.next_is(")"):
parameters = self.parse_operation_parameters()
self.consume_whitespace()
self.assert_specific(")")
self.consume_whitespace()
return parameters
def parse_operation_parameters(self) -> List[OperationParameter]:
parameters: List[OperationParameter] = []
self.consume_whitespace()
while not self.lexer.next_is(")"):
extended_attributes = self.parse_leading_extended_attributes()
optional = self.consume_optional_keyword("optional")
extended_attributes.update(self.parse_leading_extended_attributes())
parameter_type = self.parse_type()
self.consume_whitespace()
variadic = self.lexer.consume_specific("...")
self.consume_whitespace()
parameter_name = self.parse_identifier()
self.consume_whitespace()
default_value: Optional[str] = None
if self.lexer.consume_specific("="):
default_value = self.consume_until_top_level(",", ")").strip()
self.consume_whitespace()
parameters.append(
OperationParameter(
name=parameter_name,
type=parameter_type,
optional=optional,
default_value=default_value,
variadic=variadic,
extended_attributes=extended_attributes,
)
)
if not self.lexer.consume_specific(","):
break
self.consume_whitespace()
return parameters
def parse_operation(self, extended_attributes: Dict[str, str]) -> Operation:
return_type = self.parse_type()
self.consume_whitespace()
name = self.parse_identifier()
self.consume_whitespace()
parameters = self.parse_parenthesized_parameters()
self.consume_member_semicolon()
return Operation(
name=name,
return_type=return_type,
parameters=parameters,
extended_attributes=extended_attributes,
)
def parse_constructor(self, extended_attributes: Dict[str, str]) -> Constructor:
self.consume_keyword("constructor")
self.consume_whitespace()
parameters = self.parse_parenthesized_parameters()
self.consume_member_semicolon()
return Constructor(
parameters=parameters,
extended_attributes=extended_attributes,
)
def parse_constant(self) -> Constant:
self.consume_keyword("const")
self.consume_whitespace()
constant_type = self.parse_type()
self.consume_whitespace()
name = self.parse_identifier()
self.consume_whitespace()
self.assert_specific("=")
self.consume_whitespace()
value = self.consume_until_top_level(";").strip()
self.consume_member_semicolon()
return Constant(
type=constant_type,
name=name,
value=value,
)
def parse_attribute(
self,
extended_attributes: Dict[str, str],
readonly: bool = False,
inherit: bool = False,
) -> Attribute:
self.consume_keyword("attribute")
self.consume_whitespace()
attribute_type = self.parse_type()
self.consume_whitespace()
name = self.parse_identifier()
self.consume_member_semicolon()
return Attribute(
name=name,
type=attribute_type,
readonly=readonly,
inherit=inherit,
extended_attributes=extended_attributes,
)
def parse_extended_attributes(self) -> Dict[str, str]:
extended_attributes: Dict[str, str] = {}
while True:
self.consume_whitespace()
if self.lexer.consume_specific("]"):
break
name = self.parse_identifier()
value = ""
if self.lexer.consume_specific("="):
value = self.consume_until_top_level(",", "]").strip()
extended_attributes[name] = value
self.lexer.consume_specific(",")
self.consume_whitespace()
return extended_attributes
def parse_leading_extended_attributes(self) -> Dict[str, str]:
self.consume_whitespace()
if self.lexer.consume_specific("["):
return self.parse_extended_attributes()
return {}
def consume_member_semicolon(self) -> None:
self.consume_whitespace()
self.assert_specific(";")
def consume_until_top_level(self, *terminators: str) -> str:
start = self.lexer.tell()
delimiter_stack: List[str] = []
active_quote = ""
matching_delimiter = {
"(": ")",
"[": "]",
"{": "}",
"<": ">",
}
while not self.lexer.is_eof():
character = self.lexer.peek()
if active_quote:
self.lexer.consume()
if character == active_quote:
active_quote = ""
continue
if character in ('"', "'"):
active_quote = character
self.lexer.consume()
continue
if character in terminators and not delimiter_stack:
return self.contents[start : self.lexer.tell()]
character = self.lexer.consume()
if character in matching_delimiter:
delimiter_stack.append(matching_delimiter[character])
elif delimiter_stack and character == delimiter_stack[-1]:
delimiter_stack.pop()
return self.contents[start : self.lexer.tell()]
def parse_identifier(self) -> str:
identifier = self.lexer.consume_while(lambda character: character.isalnum() or character in "_-")
if not identifier:
self.raise_parse_error("expected identifier")
return identifier.lstrip("_")
def consume_whitespace(self) -> None:
consumed = True
while consumed:
consumed = False
whitespace = self.lexer.consume_while(str.isspace)
if whitespace:
consumed = True
if self.lexer.consume_specific("//"):
self.lexer.ignore_until("\n")
if self.lexer.peek() == "\n":
self.lexer.ignore()
consumed = True
def next_is_keyword(self, keyword: str) -> bool:
if not self.lexer.next_is(keyword):
return False
end = self.lexer.tell() + len(keyword)
if end >= len(self.contents):
return True
trailing_character = self.contents[end]
return trailing_character.isspace() or trailing_character in "{([;:<"
def consume_optional_keyword(self, keyword: str) -> bool:
if not self.next_is_keyword(keyword):
return False
self.consume_keyword(keyword)
self.consume_whitespace()
return True
def consume_keyword(self, keyword: str) -> None:
if not self.next_is_keyword(keyword):
self.raise_parse_error(f"expected '{keyword}'")
self.lexer.ignore(len(keyword))
def assert_specific(self, expected_character: str) -> None:
if not self.lexer.consume_specific(expected_character):
self.raise_parse_error(f"expected '{expected_character}'")
def raise_parse_error(self, message: str) -> NoReturn:
line_number = 1
column_number = 1
for index, character in enumerate(self.contents):
if index == self.lexer.tell():
break
if character == "\n":
line_number += 1
column_number = 1
else:
column_number += 1
raise ParseError(f"{self.path}:{line_number}:{column_number}: error: {message}")