From 0a0389b1f8bdd918f6716cd7e9c9531f04c0da2c Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 25 Jun 2022 01:04:41 -0500 Subject: [PATCH] grammar: Create an AST node for type names --- blueprintcompiler/completions.py | 3 +- blueprintcompiler/gir.py | 17 +-- blueprintcompiler/language/common.py | 1 - blueprintcompiler/language/gobject_object.py | 43 +------- .../language/gtkbuilder_template.py | 18 +++- blueprintcompiler/language/types.py | 101 ++++++++++++++++++ blueprintcompiler/parser.py | 1 - blueprintcompiler/parser_utils.py | 36 ------- tests/sample_errors/class_dne.err | 2 +- tests/sample_errors/not_a_class.err | 2 +- 10 files changed, 127 insertions(+), 97 deletions(-) create mode 100644 blueprintcompiler/language/types.py delete mode 100644 blueprintcompiler/parser_utils.py diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index e030961..7566f18 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -20,6 +20,7 @@ import typing as T from . import gir, language +from .language.types import ClassName from .ast_utils import AstNode from .completions_utils import * from .lsp_utils import Completion, CompletionItemKind @@ -140,7 +141,7 @@ def signal_completer(ast_node, match_variables): if not isinstance(ast_node.parent, language.Object): name = "on" else: - name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower()) + name = "on_" + (ast_node.parent.children[ClassName][0].tokens["id"] or ast_node.parent.children[ClassName][0].tokens["class_name"].lower()) yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index 2b09027..3786078 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -651,24 +651,17 @@ class GirContext: did_you_mean=(ns, self.namespaces.keys()), ) + def validate_type(self, name: str, ns: str): + """ Raises an exception if there is a problem looking up the given type. """ - def validate_class(self, name: str, ns: str): - """ Raises an exception if there is a problem looking up the given - class (it doesn't exist, it isn't a class, etc.) """ - - ns = ns or "Gtk" self.validate_ns(ns) type = self.get_type(name, ns) + ns = ns or "Gtk" + if type is None: raise CompileError( - f"Namespace {ns} does not contain a class called {name}", + f"Namespace {ns} does not contain a type called {name}", did_you_mean=(name, self.namespaces[ns].classes.keys()), ) - elif not isinstance(type, Class): - raise CompileError( - f"{ns}.{name} is not a class", - did_you_mean=(name, self.namespaces[ns].classes.keys()), - ) - diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 03cb428..deeb5fc 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -27,7 +27,6 @@ from ..decompiler import DecompileCtx, decompiler from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..parse_tree import * -from ..parser_utils import * from ..xml_emitter import XmlEmitter diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index e3960ab..9a78667 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -23,6 +23,7 @@ from functools import cached_property from .common import * from .response_id import ResponseId +from .types import ClassName, ConcreteClassName class ObjectContent(AstNode): @@ -38,50 +39,14 @@ class ObjectContent(AstNode): class Object(AstNode): grammar: T.Any = [ - class_name, + ConcreteClassName, Optional(UseIdent("id")), ObjectContent, ] - @validate("namespace") - def gir_ns_exists(self): - if not self.tokens["ignore_gir"]: - self.root.gir.validate_ns(self.tokens["namespace"]) - - @validate("class_name") - def gir_class_exists(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None: - self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"]) - - @validate("namespace", "class_name") - def not_abstract(self): - if self.gir_class is not None and self.gir_class.abstract: - raise CompileError( - f"{self.gir_class.full_name} can't be instantiated because it's abstract", - hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"] - ) - - @property - def gir_ns(self): - if not self.tokens["ignore_gir"]: - return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") - @property def gir_class(self): - if self.tokens["class_name"] and not self.tokens["ignore_gir"]: - return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) - - - @docs("namespace") - def namespace_docs(self): - if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): - return ns.doc - - - @docs("class_name") - def class_docs(self): - if self.gir_class: - return self.gir_class.doc + return self.children[ClassName][0].gir_type @cached_property def action_widgets(self) -> T.List[ResponseId]: @@ -99,7 +64,7 @@ class Object(AstNode): def emit_start_tag(self, xml: XmlEmitter): xml.start_tag("object", **{ - "class": self.gir_class or self.tokens["class_name"], + "class": self.children[ClassName][0].glib_type_name, "id": self.tokens["id"], }) diff --git a/blueprintcompiler/language/gtkbuilder_template.py b/blueprintcompiler/language/gtkbuilder_template.py index 7806285..ffee11c 100644 --- a/blueprintcompiler/language/gtkbuilder_template.py +++ b/blueprintcompiler/language/gtkbuilder_template.py @@ -20,6 +20,7 @@ from .gobject_object import Object, ObjectContent from .common import * +from .types import ClassName class Template(Object): @@ -28,24 +29,31 @@ class Template(Object): UseIdent("name").expected("template class name"), Optional([ Match(":"), - class_name.expected("parent class"), + to_parse_node(ClassName).expected("parent class"), ]), ObjectContent, ] - @validate() - def not_abstract(self): - pass # does not apply to templates + @property + def gir_class(self): + # Templates might not have a parent class defined + if len(self.children[ClassName]): + return self.children[ClassName][0].gir_type @validate("name") def unique_in_parent(self): self.validate_unique_in_parent(f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}",) def emit_start_tag(self, xml: XmlEmitter): + if len(self.children[ClassName]): + parent = self.children[ClassName][0].glib_type_name + else: + parent = None + xml.start_tag( "template", **{"class": self.tokens["name"]}, - parent=self.gir_class or self.tokens["class_name"] + parent=parent ) diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py new file mode 100644 index 0000000..0d6b38c --- /dev/null +++ b/blueprintcompiler/language/types.py @@ -0,0 +1,101 @@ +# types.py +# +# Copyright 2022 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 .common import * +from ..gir import Class, Interface + + +class TypeName(AstNode): + grammar = AnyOf( + [ + UseIdent("namespace"), + ".", + UseIdent("class_name"), + ], + [ + ".", + UseIdent("class_name"), + UseLiteral("ignore_gir", True), + ], + UseIdent("class_name"), + ) + + @validate("class_name") + def type_exists(self): + if not self.tokens["ignore_gir"] and self.gir_ns is not None: + self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"]) + + @validate("namespace") + def gir_ns_exists(self): + if not self.tokens["ignore_gir"]: + self.root.gir.validate_ns(self.tokens["namespace"]) + + @property + def gir_ns(self): + if not self.tokens["ignore_gir"]: + return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") + + @property + def gir_type(self) -> T.Optional[gir.Class]: + if self.tokens["class_name"] and not self.tokens["ignore_gir"]: + return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"]) + return None + + @property + def glib_type_name(self) -> str: + if gir_type := self.gir_type: + return gir_type.glib_type_name + else: + return self.tokens["class_name"] + + @docs("namespace") + def namespace_docs(self): + if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): + return ns.doc + + @docs("class_name") + def class_docs(self): + if self.gir_type: + return self.gir_type.doc + + def emit_xml(self, xml: XmlEmitter): + pass + + +class ClassName(TypeName): + @validate("namespace", "class_name") + def gir_class_exists(self): + if self.gir_type is not None and not isinstance(self.gir_type, Class): + if isinstance(self.gir_type, Interface): + raise CompileError(f"{self.gir_type.full_name} is an interface, not a class") + else: + raise CompileError(f"{self.gir_type.full_name} is not a class") + + +class ConcreteClassName(ClassName): + @validate("namespace", "class_name") + def not_abstract(self): + if isinstance(self.gir_type, Class) and self.gir_type.abstract: + raise CompileError( + f"{self.gir_type.full_name} can't be instantiated because it's abstract", + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"] + ) + diff --git a/blueprintcompiler/parser.py b/blueprintcompiler/parser.py index 6064481..7c62ae6 100644 --- a/blueprintcompiler/parser.py +++ b/blueprintcompiler/parser.py @@ -20,7 +20,6 @@ from .errors import MultipleErrors, PrintableError from .parse_tree import * -from .parser_utils import * from .tokenizer import TokenType from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI diff --git a/blueprintcompiler/parser_utils.py b/blueprintcompiler/parser_utils.py deleted file mode 100644 index af951fe..0000000 --- a/blueprintcompiler/parser_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -# parser_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 - - -from .parse_tree import * - - -class_name = AnyOf( - [ - UseIdent("namespace"), - ".", - UseIdent("class_name"), - ], - [ - ".", - UseIdent("class_name"), - UseLiteral("ignore_gir", True), - ], - UseIdent("class_name"), -) diff --git a/tests/sample_errors/class_dne.err b/tests/sample_errors/class_dne.err index 573baf3..57d74f8 100644 --- a/tests/sample_errors/class_dne.err +++ b/tests/sample_errors/class_dne.err @@ -1 +1 @@ -3,29,13,Namespace Gtk does not contain a class called NotARealClass +3,29,13,Namespace Gtk does not contain a type called NotARealClass diff --git a/tests/sample_errors/not_a_class.err b/tests/sample_errors/not_a_class.err index d0a7260..c69f602 100644 --- a/tests/sample_errors/not_a_class.err +++ b/tests/sample_errors/not_a_class.err @@ -1 +1 @@ -3,29,10,Gtk.Orientable is not a class +3,25,14,Gtk.Orientable is an interface, not a class