forked from mirrors/gecko-dev
376 lines
11 KiB
Python
376 lines
11 KiB
Python
import re
|
||
import sys
|
||
import json
|
||
from typing import Any, Callable, Dict, List, TypeVar, Union, cast
|
||
|
||
Node = TypeVar('Node', bound='BaseNode')
|
||
ToJsonFn = Callable[[Dict[str, Any]], Any]
|
||
|
||
|
||
def to_json(value: Any, fn: Union[ToJsonFn, None] = None) -> Any:
|
||
if isinstance(value, BaseNode):
|
||
return value.to_json(fn)
|
||
if isinstance(value, list):
|
||
return list(to_json(item, fn) for item in value)
|
||
if isinstance(value, tuple):
|
||
return list(to_json(item, fn) for item in value)
|
||
else:
|
||
return value
|
||
|
||
|
||
def from_json(value: Any) -> Any:
|
||
if isinstance(value, dict):
|
||
cls = getattr(sys.modules[__name__], value['type'])
|
||
args = {
|
||
k: from_json(v)
|
||
for k, v in value.items()
|
||
if k != 'type'
|
||
}
|
||
return cls(**args)
|
||
if isinstance(value, list):
|
||
return list(map(from_json, value))
|
||
else:
|
||
return value
|
||
|
||
|
||
def scalars_equal(node1: Any, node2: Any, ignored_fields: List[str]) -> bool:
|
||
"""Compare two nodes which are not lists."""
|
||
|
||
if type(node1) != type(node2):
|
||
return False
|
||
|
||
if isinstance(node1, BaseNode):
|
||
return node1.equals(node2, ignored_fields)
|
||
|
||
return cast(bool, node1 == node2)
|
||
|
||
|
||
class BaseNode:
|
||
"""Base class for all Fluent AST nodes.
|
||
|
||
All productions described in the ASDL subclass BaseNode, including Span and
|
||
Annotation. Implements __str__, to_json and traverse.
|
||
"""
|
||
|
||
def clone(self: Node) -> Node:
|
||
"""Create a deep clone of the current node."""
|
||
def visit(value: Any) -> Any:
|
||
"""Clone node and its descendants."""
|
||
if isinstance(value, BaseNode):
|
||
return value.clone()
|
||
if isinstance(value, list):
|
||
return [visit(child) for child in value]
|
||
if isinstance(value, tuple):
|
||
return tuple(visit(child) for child in value)
|
||
return value
|
||
|
||
# Use all attributes found on the node as kwargs to the constructor.
|
||
return self.__class__(
|
||
**{name: visit(value) for name, value in vars(self).items()}
|
||
)
|
||
|
||
def equals(self, other: 'BaseNode', ignored_fields: List[str] = ['span']) -> bool:
|
||
"""Compare two nodes.
|
||
|
||
Nodes are deeply compared on a field by field basis. If possible, False
|
||
is returned early. When comparing attributes and variants in
|
||
SelectExpressions, the order doesn't matter. By default, spans are not
|
||
taken into account.
|
||
"""
|
||
|
||
self_keys = set(vars(self).keys())
|
||
other_keys = set(vars(other).keys())
|
||
|
||
if ignored_fields:
|
||
for key in ignored_fields:
|
||
self_keys.discard(key)
|
||
other_keys.discard(key)
|
||
|
||
if self_keys != other_keys:
|
||
return False
|
||
|
||
for key in self_keys:
|
||
field1 = getattr(self, key)
|
||
field2 = getattr(other, key)
|
||
|
||
# List-typed nodes are compared item-by-item. When comparing
|
||
# attributes and variants, the order of items doesn't matter.
|
||
if isinstance(field1, list) and isinstance(field2, list):
|
||
if len(field1) != len(field2):
|
||
return False
|
||
|
||
for elem1, elem2 in zip(field1, field2):
|
||
if not scalars_equal(elem1, elem2, ignored_fields):
|
||
return False
|
||
|
||
elif not scalars_equal(field1, field2, ignored_fields):
|
||
return False
|
||
|
||
return True
|
||
|
||
def to_json(self, fn: Union[ToJsonFn, None] = None) -> Any:
|
||
obj = {
|
||
name: to_json(value, fn)
|
||
for name, value in vars(self).items()
|
||
}
|
||
obj.update(
|
||
{'type': self.__class__.__name__}
|
||
)
|
||
return fn(obj) if fn else obj
|
||
|
||
def __str__(self) -> str:
|
||
return json.dumps(self.to_json())
|
||
|
||
|
||
class SyntaxNode(BaseNode):
|
||
"""Base class for AST nodes which can have Spans."""
|
||
|
||
def __init__(self, span: Union['Span', None] = None, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.span = span
|
||
|
||
def add_span(self, start: int, end: int) -> None:
|
||
self.span = Span(start, end)
|
||
|
||
|
||
class Resource(SyntaxNode):
|
||
def __init__(self, body: Union[List['EntryType'], None] = None, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.body = body or []
|
||
|
||
|
||
class Entry(SyntaxNode):
|
||
"""An abstract base class for useful elements of Resource.body."""
|
||
|
||
|
||
class Message(Entry):
|
||
def __init__(self,
|
||
id: 'Identifier',
|
||
value: Union['Pattern', None] = None,
|
||
attributes: Union[List['Attribute'], None] = None,
|
||
comment: Union['Comment', None] = None,
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.value = value
|
||
self.attributes = attributes or []
|
||
self.comment = comment
|
||
|
||
|
||
class Term(Entry):
|
||
def __init__(self, id: 'Identifier', value: 'Pattern', attributes: Union[List['Attribute'], None] = None,
|
||
comment: Union['Comment', None] = None, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.value = value
|
||
self.attributes = attributes or []
|
||
self.comment = comment
|
||
|
||
|
||
class Pattern(SyntaxNode):
|
||
def __init__(self, elements: List[Union['TextElement', 'Placeable']], **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.elements = elements
|
||
|
||
|
||
class PatternElement(SyntaxNode):
|
||
"""An abstract base class for elements of Patterns."""
|
||
|
||
|
||
class TextElement(PatternElement):
|
||
def __init__(self, value: str, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.value = value
|
||
|
||
|
||
class Placeable(PatternElement):
|
||
def __init__(self,
|
||
expression: Union['InlineExpression', 'Placeable', 'SelectExpression'],
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.expression = expression
|
||
|
||
|
||
class Expression(SyntaxNode):
|
||
"""An abstract base class for expressions."""
|
||
|
||
|
||
class Literal(Expression):
|
||
"""An abstract base class for literals."""
|
||
|
||
def __init__(self, value: str, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.value = value
|
||
|
||
def parse(self) -> Dict[str, Any]:
|
||
return {'value': self.value}
|
||
|
||
|
||
class StringLiteral(Literal):
|
||
def parse(self) -> Dict[str, str]:
|
||
def from_escape_sequence(matchobj: Any) -> str:
|
||
c, codepoint4, codepoint6 = matchobj.groups()
|
||
if c:
|
||
return cast(str, c)
|
||
codepoint = int(codepoint4 or codepoint6, 16)
|
||
if codepoint <= 0xD7FF or 0xE000 <= codepoint:
|
||
return chr(codepoint)
|
||
# Escape sequences reresenting surrogate code points are
|
||
# well-formed but invalid in Fluent. Replace them with U+FFFD
|
||
# REPLACEMENT CHARACTER.
|
||
return '<EFBFBD>'
|
||
|
||
value = re.sub(
|
||
r'\\(?:(\\|")|u([0-9a-fA-F]{4})|U([0-9a-fA-F]{6}))',
|
||
from_escape_sequence,
|
||
self.value
|
||
)
|
||
return {'value': value}
|
||
|
||
|
||
class NumberLiteral(Literal):
|
||
def parse(self) -> Dict[str, Union[float, int]]:
|
||
value = float(self.value)
|
||
decimal_position = self.value.find('.')
|
||
precision = 0
|
||
if decimal_position >= 0:
|
||
precision = len(self.value) - decimal_position - 1
|
||
return {
|
||
'value': value,
|
||
'precision': precision
|
||
}
|
||
|
||
|
||
class MessageReference(Expression):
|
||
def __init__(self, id: 'Identifier', attribute: Union['Identifier', None] = None, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.attribute = attribute
|
||
|
||
|
||
class TermReference(Expression):
|
||
def __init__(self,
|
||
id: 'Identifier',
|
||
attribute: Union['Identifier', None] = None,
|
||
arguments: Union['CallArguments', None] = None,
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.attribute = attribute
|
||
self.arguments = arguments
|
||
|
||
|
||
class VariableReference(Expression):
|
||
def __init__(self, id: 'Identifier', **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
|
||
|
||
class FunctionReference(Expression):
|
||
def __init__(self, id: 'Identifier', arguments: 'CallArguments', **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.arguments = arguments
|
||
|
||
|
||
class SelectExpression(Expression):
|
||
def __init__(self, selector: 'InlineExpression', variants: List['Variant'], **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.selector = selector
|
||
self.variants = variants
|
||
|
||
|
||
class CallArguments(SyntaxNode):
|
||
def __init__(self,
|
||
positional: Union[List[Union['InlineExpression', Placeable]], None] = None,
|
||
named: Union[List['NamedArgument'], None] = None,
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.positional = [] if positional is None else positional
|
||
self.named = [] if named is None else named
|
||
|
||
|
||
class Attribute(SyntaxNode):
|
||
def __init__(self, id: 'Identifier', value: Pattern, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.id = id
|
||
self.value = value
|
||
|
||
|
||
class Variant(SyntaxNode):
|
||
def __init__(self, key: Union['Identifier', NumberLiteral], value: Pattern, default: bool = False, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.key = key
|
||
self.value = value
|
||
self.default = default
|
||
|
||
|
||
class NamedArgument(SyntaxNode):
|
||
def __init__(self, name: 'Identifier', value: Union[NumberLiteral, StringLiteral], **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.name = name
|
||
self.value = value
|
||
|
||
|
||
class Identifier(SyntaxNode):
|
||
def __init__(self, name: str, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.name = name
|
||
|
||
|
||
class BaseComment(Entry):
|
||
def __init__(self, content: Union[str, None] = None, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.content = content
|
||
|
||
|
||
class Comment(BaseComment):
|
||
def __init__(self, content: Union[str, None] = None, **kwargs: Any):
|
||
super().__init__(content, **kwargs)
|
||
|
||
|
||
class GroupComment(BaseComment):
|
||
def __init__(self, content: Union[str, None] = None, **kwargs: Any):
|
||
super().__init__(content, **kwargs)
|
||
|
||
|
||
class ResourceComment(BaseComment):
|
||
def __init__(self, content: Union[str, None] = None, **kwargs: Any):
|
||
super().__init__(content, **kwargs)
|
||
|
||
|
||
class Junk(SyntaxNode):
|
||
def __init__(self,
|
||
content: Union[str, None] = None,
|
||
annotations: Union[List['Annotation'], None] = None,
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.content = content
|
||
self.annotations = annotations or []
|
||
|
||
def add_annotation(self, annot: 'Annotation') -> None:
|
||
self.annotations.append(annot)
|
||
|
||
|
||
class Span(BaseNode):
|
||
def __init__(self, start: int, end: int, **kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.start = start
|
||
self.end = end
|
||
|
||
|
||
class Annotation(SyntaxNode):
|
||
def __init__(self,
|
||
code: str,
|
||
arguments: Union[List[Any], None] = None,
|
||
message: Union[str, None] = None,
|
||
**kwargs: Any):
|
||
super().__init__(**kwargs)
|
||
self.code = code
|
||
self.arguments = arguments or []
|
||
self.message = message
|
||
|
||
|
||
EntryType = Union[Message, Term, Comment, GroupComment, ResourceComment, Junk]
|
||
InlineExpression = Union[NumberLiteral, StringLiteral, MessageReference,
|
||
TermReference, VariableReference, FunctionReference]
|