mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Parse values as different AST nodes rather than just strings. This allows for better validation and will eventually make expressions possible.
177 lines
5.4 KiB
Python
177 lines
5.4 KiB
Python
# ast_utils.py
|
|
#
|
|
# Copyright 2021 James Westman <james@jwestman.net>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
import typing as T
|
|
from collections import ChainMap, defaultdict
|
|
|
|
from . import ast
|
|
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 = []
|
|
cls.validators = [getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator")]
|
|
|
|
|
|
@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 validator in self.validators:
|
|
try:
|
|
validator(self)
|
|
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
|
|
|
|
|
|
def validate(token_name=None, end_token_name=None, skip_incomplete=False):
|
|
""" 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):
|
|
def inner(self):
|
|
if skip_incomplete and self.incomplete:
|
|
return
|
|
|
|
try:
|
|
func(self)
|
|
except CompileError as e:
|
|
# If the node is only partially complete, then an error must
|
|
# have already been reported at the parsing stage
|
|
if self.incomplete:
|
|
return
|
|
|
|
# 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 e.start is None:
|
|
if token := self.group.tokens.get(token_name):
|
|
e.start = token.start
|
|
else:
|
|
e.start = self.group.start
|
|
|
|
if e.end is None:
|
|
if token := self.group.tokens.get(token_name):
|
|
e.end = token.end
|
|
elif token := self.group.tokens.get(end_token_name):
|
|
e.end = token.end
|
|
else:
|
|
e.end = self.group.end
|
|
|
|
# Re-raise the exception
|
|
raise e
|
|
|
|
inner._validator = True
|
|
return inner
|
|
|
|
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
|