# ast_utils.py # # Copyright 2021 James Westman # # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or (at your option) any later version. # # This file is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see . # # SPDX-License-Identifier: LGPL-3.0-or-later import typing as T from collections import ChainMap, defaultdict from .errors import * from .utils import lazy_prop from .xml_emitter import XmlEmitter class Children: """ Allows accessing children by type using array syntax. """ def __init__(self, children): self._children = children def __iter__(self): return iter(self._children) def __getitem__(self, key): return [child for child in self._children if isinstance(child, key)] class AstNode: """ Base class for nodes in the abstract syntax tree. """ completers: T.List = [] def __init__(self, group, children, tokens, incomplete=False): self.group = group self.children = Children(children) self.tokens = ChainMap(tokens, defaultdict(lambda: None)) self.incomplete = incomplete self.parent = None for child in self.children: child.parent = self def __init_subclass__(cls): cls.completers = [] @property def root(self): if self.parent is None: return self else: return self.parent.root @lazy_prop def errors(self): return list(self._get_errors()) def _get_errors(self): for name, attr in self._attrs_by_type(Validator): try: getattr(self, name) except AlreadyCaughtError: pass except CompileError as e: yield e for child in self.children: yield from child._get_errors() def _attrs_by_type(self, attr_type): for name in dir(type(self)): item = getattr(type(self), name) if isinstance(item, attr_type): yield name, item def generate(self) -> str: """ Generates an XML string from the node. """ xml = XmlEmitter() self.emit_xml(xml) return xml.result def emit_xml(self, xml: XmlEmitter): """ Emits the XML representation of this AST node to the XmlEmitter. """ raise NotImplementedError() def get_docs(self, idx: int) -> T.Optional[str]: for name, attr in self._attrs_by_type(Docs): if attr.token_name: token = self.group.tokens.get(attr.token_name) if token and token.start <= idx < token.end: return getattr(self, name) else: return getattr(self, name) for child in self.children: if child.group.start <= idx < child.group.end: docs = child.get_docs(idx) if docs is not None: return docs return None class Validator: def __init__(self, func, token_name=None, end_token_name=None): self.func = func self.token_name = token_name self.end_token_name = end_token_name def __get__(self, instance, owner): if instance is None: return self key = "_validation_result_" + self.func.__name__ if key + "_err" in instance.__dict__: # If the validator has failed before, raise a generic Exception. # We want anything that depends on this validation result to # fail, but not report the exception twice. raise AlreadyCaughtError() if key not in instance.__dict__: try: instance.__dict__[key] = self.func(instance) except CompileError as e: # Mark the validator as already failed so we don't print the # same message again instance.__dict__[key + "_err"] = True # If the node is only partially complete, then an error must # have already been reported at the parsing stage if instance.incomplete: return None # This mess of code sets the error's start and end positions # from the tokens passed to the decorator, if they have not # already been set if self.token_name is not None and e.start is None: group = instance.group.tokens.get(self.token_name) if self.end_token_name is not None and group is None: group = instance.group.tokens[self.end_token_name] e.start = group.start if (self.token_name is not None or self.end_token_name is not None) and e.end is None: e.end = instance.group.tokens[self.end_token_name or self.token_name].end # Re-raise the exception raise e except Exception as e: # If the node is only partially complete, then an error must # have already been reported at the parsing stage if instance.incomplete: return None else: raise e # Return the validation result (which other validators, or the code # generation phase, might depend on) return instance.__dict__[key] def validate(*args, **kwargs): """ Decorator for functions that validate an AST node. Exceptions raised during validation are marked with range information from the tokens. Also creates a cached property out of the function. """ def decorator(func): return Validator(func, *args, **kwargs) return decorator class Docs: def __init__(self, func, token_name=None): self.func = func self.token_name = token_name def __get__(self, instance, owner): if instance is None: return self return self.func(instance) def docs(*args, **kwargs): """ Decorator for functions that return documentation for tokens. """ def decorator(func): return Docs(func, *args, **kwargs) return decorator