diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index a63729d..c874358 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -125,7 +125,7 @@ class AstNode: return self.parent.root @property - def range(self): + def range(self) -> Range: return Range(self.group.start, self.group.end, self.group.text) def parent_by_type(self, type: T.Type[TType]) -> TType: diff --git a/blueprintcompiler/errors.py b/blueprintcompiler/errors.py index a28c05d..01058fd 100644 --- a/blueprintcompiler/errors.py +++ b/blueprintcompiler/errors.py @@ -140,6 +140,10 @@ class DeprecatedWarning(CompileWarning): pass +class UnusedWarning(CompileWarning): + pass + + class UpgradeWarning(CompileWarning): category = "upgrade" color = Colors.PURPLE diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 8a6ce9b..29df47d 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -35,6 +35,7 @@ from ..errors import ( CompileWarning, DeprecatedWarning, MultipleErrors, + UnusedWarning, UpgradeWarning, ) from ..gir import ( diff --git a/blueprintcompiler/language/imports.py b/blueprintcompiler/language/imports.py index e34901c..bf5dddd 100644 --- a/blueprintcompiler/language/imports.py +++ b/blueprintcompiler/language/imports.py @@ -88,6 +88,17 @@ class Import(AstNode): def namespace_exists(self): gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) + @validate() + def unused(self): + if self.namespace not in self.root.used_imports: + raise UnusedWarning( + f"Unused import: {self.namespace}", + self.range, + actions=[ + CodeAction("Remove import", "", self.range.with_trailing_newline) + ], + ) + @property def gir_namespace(self): try: diff --git a/blueprintcompiler/language/types.py b/blueprintcompiler/language/types.py index b3fb586..fe45c4d 100644 --- a/blueprintcompiler/language/types.py +++ b/blueprintcompiler/language/types.py @@ -78,9 +78,10 @@ class TypeName(AstNode): ) @property - def gir_ns(self): + def gir_ns(self) -> T.Optional[gir.Namespace]: if not self.tokens["extern"]: return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") + return None @property def gir_type(self) -> gir.GirType: diff --git a/blueprintcompiler/language/ui.py b/blueprintcompiler/language/ui.py index 8aaf359..d55a22a 100644 --- a/blueprintcompiler/language/ui.py +++ b/blueprintcompiler/language/ui.py @@ -27,6 +27,7 @@ from .gtk_menu import Menu, menu from .gtkbuilder_template import Template from .imports import GtkDirective, Import from .translation_domain import TranslationDomain +from .types import TypeName class UI(AstNode): @@ -121,6 +122,22 @@ class UI(AstNode): Range(pos, pos, self.group.text), ) + @cached_property + def used_imports(self) -> T.Optional[T.Set[str]]: + def _iter_recursive(node: AstNode): + yield node + for child in node.children: + if isinstance(child, AstNode): + yield from _iter_recursive(child) + + result = set() + for node in _iter_recursive(self): + if isinstance(node, TypeName): + ns = node.gir_ns + if ns is not None: + result.add(ns.name) + return result + @context(ScopeCtx) def scope_ctx(self) -> ScopeCtx: return ScopeCtx(node=self) diff --git a/blueprintcompiler/lsp.py b/blueprintcompiler/lsp.py index cdf0c83..903bfb7 100644 --- a/blueprintcompiler/lsp.py +++ b/blueprintcompiler/lsp.py @@ -475,6 +475,9 @@ class LanguageServer: if isinstance(err, DeprecationWarning): result["tags"] = [DiagnosticTag.Deprecated] + if isinstance(err, UnusedWarning): + result["tags"] = [DiagnosticTag.Unnecessary] + if len(err.references) > 0: result["relatedInformation"] = [ { diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 6cc0761..85bce95 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -127,6 +127,13 @@ class Range: def text(self) -> str: return self.original_text[self.start : self.end] + @property + def with_trailing_newline(self) -> "Range": + if len(self.original_text) > self.end and self.original_text[self.end] == "\n": + return Range(self.start, self.end + 1, self.original_text) + else: + return self + @staticmethod def join(a: T.Optional["Range"], b: T.Optional["Range"]) -> T.Optional["Range"]: if a is None: diff --git a/tests/sample_errors/deprecations.blp b/tests/sample_errors/deprecations.blp index 1554a0b..f67f002 100644 --- a/tests/sample_errors/deprecations.blp +++ b/tests/sample_errors/deprecations.blp @@ -1,5 +1,4 @@ using Gtk 4.0; -using Gio 2.0; Dialog { use-header-bar: 1; diff --git a/tests/sample_errors/deprecations.err b/tests/sample_errors/deprecations.err index 6059412..e3abd61 100644 --- a/tests/sample_errors/deprecations.err +++ b/tests/sample_errors/deprecations.err @@ -1 +1 @@ -4,1,6,Gtk.Dialog is deprecated \ No newline at end of file +3,1,6,Gtk.Dialog is deprecated \ No newline at end of file diff --git a/tests/sample_errors/warn_unused_import.blp b/tests/sample_errors/warn_unused_import.blp new file mode 100644 index 0000000..bcfd1f6 --- /dev/null +++ b/tests/sample_errors/warn_unused_import.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; +using GLib 2.0; +using Gio 2.0; + +Gio.Cancellable {} diff --git a/tests/sample_errors/warn_unused_import.err b/tests/sample_errors/warn_unused_import.err new file mode 100644 index 0000000..d779e56 --- /dev/null +++ b/tests/sample_errors/warn_unused_import.err @@ -0,0 +1 @@ +2,1,15,Unused import: GLib \ No newline at end of file diff --git a/tests/samples/parseable.blp b/tests/samples/parseable.blp index f4e8c2f..5320baf 100644 --- a/tests/samples/parseable.blp +++ b/tests/samples/parseable.blp @@ -1,5 +1,4 @@ using Gtk 4.0; -using Gio 2.0; Gtk.Shortcut { trigger: "Escape";