diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/python/fluent.syntax/fluent/syntax/ast.py | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/fluent.syntax/fluent/syntax/ast.py')
-rw-r--r-- | third_party/python/fluent.syntax/fluent/syntax/ast.py | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/third_party/python/fluent.syntax/fluent/syntax/ast.py b/third_party/python/fluent.syntax/fluent/syntax/ast.py new file mode 100644 index 0000000000..d2e4849079 --- /dev/null +++ b/third_party/python/fluent.syntax/fluent/syntax/ast.py @@ -0,0 +1,376 @@ +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] |