Source code

Revision control

Copy as Markdown

Other Tools

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 '�'
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]