diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index ab692bc..5019098 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -33,7 +33,7 @@ class Children: def __init__(self, children): self._children = children - def __iter__(self): + def __iter__(self) -> T.Iterator["AstNode"]: return iter(self._children) @T.overload @@ -175,11 +175,6 @@ class AstNode: for child in self.children: yield from child.get_semantic_tokens() - def iterate_children_recursive(self) -> T.Iterator["AstNode"]: - yield self - for child in self.children: - yield from child.iterate_children_recursive() - def validate_unique_in_parent(self, error, check=None): for child in self.parent.children: if child is self: @@ -270,7 +265,12 @@ class Context: def __get__(self, instance, owner): if instance is None: return self - return self.func(instance) + if ctx := getattr(instance, "_context_" + self.type.__name__, None): + return ctx + else: + ctx = self.func(instance) + setattr(instance, "_context_" + self.type.__name__, ctx) + return ctx def context(type: T.Type[TCtx]): diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index fec38f7..49b7b40 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -19,9 +19,44 @@ import typing as T from dataclasses import dataclass +from functools import cached_property + from .common import * +from .gobject_object import Object @dataclass class ValueTypeCtx: value_type: T.Optional[GirType] + + +@dataclass +class ScopeCtx: + node: AstNode + + @cached_property + def objects(self) -> T.Dict[str, Object]: + return { + obj.tokens["id"]: obj + for obj in self._iter_recursive(self.node) + if obj.tokens["id"] is not None + } + + def validate_unique_ids(self) -> None: + passed = {} + for obj in self._iter_recursive(self.node): + if obj.tokens["id"] is None: + continue + + if obj.tokens["id"] in passed: + token = obj.group.tokens["id"] + raise CompileError( + f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end + ) + passed[obj.tokens["id"]] = obj + + def _iter_recursive(self, node: AstNode): + yield node + for child in node.children: + if child.context[ScopeCtx] is self: + yield from self._iter_recursive(child) diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 16ee33c..0cf41b0 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -19,7 +19,7 @@ from .common import * -from .contexts import ValueTypeCtx +from .contexts import ScopeCtx, ValueTypeCtx from .types import TypeName from .gtkbuilder_template import Template @@ -87,7 +87,7 @@ class LiteralExpr(Expr): return ( isinstance(self.literal.value, IdentLiteral) - and self.literal.value.ident in self.root.objects_by_id + and self.literal.value.ident in self.context[ScopeCtx].objects ) @property @@ -105,7 +105,7 @@ class LiteralExpr(Expr): from .values import IdentLiteral if isinstance(self.literal.value, IdentLiteral): - if object := self.root.objects_by_id.get(self.literal.value.ident): + if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): return not isinstance(object, Template) return True diff --git a/blueprintcompiler/language/gobject_signal.py b/blueprintcompiler/language/gobject_signal.py index 25d789b..e5c71c6 100644 --- a/blueprintcompiler/language/gobject_signal.py +++ b/blueprintcompiler/language/gobject_signal.py @@ -20,6 +20,7 @@ import typing as T from .gtkbuilder_template import Template +from .contexts import ScopeCtx from .common import * @@ -112,7 +113,7 @@ class Signal(AstNode): if object_id is None: return - if self.root.objects_by_id.get(object_id) is None: + if self.context[ScopeCtx].objects.get(object_id) is None: raise CompileError(f"Could not find object with ID '{object_id}'") @docs("name") diff --git a/blueprintcompiler/language/gtk_size_group.py b/blueprintcompiler/language/gtk_size_group.py index 80c47af..6a3ecb1 100644 --- a/blueprintcompiler/language/gtk_size_group.py +++ b/blueprintcompiler/language/gtk_size_group.py @@ -20,6 +20,7 @@ from .gobject_object import ObjectContent, validate_parent_type from .common import * +from .contexts import ScopeCtx class Widget(AstNode): @@ -27,12 +28,15 @@ class Widget(AstNode): @validate("name") def obj_widget(self): - object = self.root.objects_by_id.get(self.tokens["name"]) + object = self.context[ScopeCtx].objects.get(self.tokens["name"]) type = self.root.gir.get_type("Widget", "Gtk") if object is None: raise CompileError( f"Could not find object with ID {self.tokens['name']}", - did_you_mean=(self.tokens["name"], self.root.objects_by_id.keys()), + did_you_mean=( + self.tokens["name"], + self.context[ScopeCtx].objects.keys(), + ), ) elif object.gir_class and not object.gir_class.assignable_to(type): raise CompileError( diff --git a/blueprintcompiler/language/property_binding.py b/blueprintcompiler/language/property_binding.py index ba8a29d..101bef5 100644 --- a/blueprintcompiler/language/property_binding.py +++ b/blueprintcompiler/language/property_binding.py @@ -18,7 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from .common import * -from .contexts import ValueTypeCtx +from .contexts import ScopeCtx from .gobject_object import Object from .gtkbuilder_template import Template @@ -71,7 +71,7 @@ class PropertyBinding(AstNode): @property def source_obj(self) -> T.Optional[Object]: - return self.root.objects_by_id.get(self.source) + return self.context[ScopeCtx].objects.get(self.source) @property def property_name(self) -> str: @@ -98,7 +98,7 @@ class PropertyBinding(AstNode): if self.source_obj is None: raise CompileError( f"Could not find object with ID {self.source}", - did_you_mean=(self.source, self.root.objects_by_id.keys()), + did_you_mean=(self.source, self.context[ScopeCtx].objects.keys()), ) @validate("property") diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 8d27c0e..ea2dac8 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -17,14 +17,13 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later -from functools import cached_property - from .. import gir from .imports import GtkDirective, Import from .gtkbuilder_template import Template from .gobject_object import Object from .gtk_menu import menu, Menu from .common import * +from .contexts import ScopeCtx class UI(AstNode): @@ -85,13 +84,9 @@ class UI(AstNode): or isinstance(child, Menu) ] - @cached_property - def objects_by_id(self): - return { - obj.tokens["id"]: obj - for obj in self.iterate_children_recursive() - if obj.tokens["id"] is not None - } + @context(ScopeCtx) + def scope_ctx(self) -> ScopeCtx: + return ScopeCtx(node=self) @validate() def gir_errors(self): @@ -102,14 +97,4 @@ class UI(AstNode): @validate() def unique_ids(self): - passed = {} - for obj in self.iterate_children_recursive(): - if obj.tokens["id"] is None: - continue - - if obj.tokens["id"] in passed: - token = obj.group.tokens["id"] - raise CompileError( - f"Duplicate object ID '{obj.tokens['id']}'", token.start, token.end - ) - passed[obj.tokens["id"]] = obj + self.context[ScopeCtx].validate_unique_ids() diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index a1666cc..596823d 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -24,7 +24,7 @@ from .types import TypeName from .property_binding import PropertyBinding from .binding import Binding from .gobject_object import Object -from .contexts import ValueTypeCtx +from .contexts import ScopeCtx, ValueTypeCtx class TranslatedWithoutContext(AstNode): @@ -274,7 +274,7 @@ class IdentLiteral(AstNode): return expected_type elif self.ident in ["true", "false"]: return gir.BoolType() - elif object := self.root.objects_by_id.get(self.ident): + elif object := self.context[ScopeCtx].objects.get(self.ident): return object.gir_class else: return None @@ -294,11 +294,11 @@ class IdentLiteral(AstNode): ) elif expected_type is not None: - object = self.root.objects_by_id.get(self.ident) + object = self.context[ScopeCtx].objects.get(self.ident) if object is None: raise CompileError( f"Could not find object with ID {self.ident}", - did_you_mean=(self.ident, self.root.objects_by_id.keys()), + did_you_mean=(self.ident, self.context[ScopeCtx].objects.keys()), ) elif object.gir_class and not object.gir_class.assignable_to(expected_type): raise CompileError(