Add a WIP linter

This commit is contained in:
Sonny Piers 2024-01-23 01:00:22 +01:00
parent 71a43a4a86
commit 07ca07e08d
3 changed files with 173 additions and 2 deletions

View file

@ -0,0 +1,87 @@
# parser.py
#
# Copyright © 2024 GNOME Foundation Inc.
# Original Author: Sonny Piers <sonnyp@gnome.org>
#
# 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
from blueprintcompiler.language.gobject_object import Object
from blueprintcompiler.language.gtkbuilder_child import Child
from blueprintcompiler.language.gobject_property import Property
from blueprintcompiler.language.values import Translated, Literal
from .errors import CompileError, CompileWarning
def lint(ast, problems = []):
for child in ast.children:
if isinstance(child, Object):
children = child.content.children[Child]
properties = child.content.children[Property]
type = child.class_name.gir_type.full_name
if (type in gir_types_no_children and len(children) > 0):
range = children[0].range
problem = CompileError(f'{type} cannot have children', range)
problems.append(problem)
elif (type in gir_types_single_child and len(children) > 1):
range = children[1].range
problem = CompileError(f'{type} cannot have more than one child', range)
problems.append(problem)
if (type == 'Gtk.Box' and len(children) == 1):
range = children[0].range
problem = CompileWarning(f'Use Adw.Bin instead of a Gtk.Box for a single child', range)
problems.append(problem)
for translatable_property in translatable_properties:
if type == translatable_property[0] or translatable_property[0] == None:
for property in properties:
if (property.name == translatable_property[1]):
value = property.children[0].child
if (not isinstance(value, Translated)):
range = value.range
problem = CompileWarning(f'Mark {type} {property.name} as translatable using _("...")', range)
problems.append(problem)
# FIXME GTK4 only
for property in properties:
if (property.name == 'visible'):
value = property.children[0].child
ident = value.value.ident
if ident == 'true':
range = value.range
problem = CompileWarning(f'Property {property.name} default value is already true', range)
problems.append(problem)
if (type == 'Gtk.Switch'):
for property in properties:
if (property.name == 'state'):
range = property.range
problem = CompileError(f'Use the Gtk.Switch active property instead of the state property', range)
problems.append(problem)
lint(child, problems)
return problems
gir_types_no_children = ['Gtk.Label']
gir_types_single_child = ['Adw.Bin', 'Adw.StatusPage']
translatable_properties = [
(None, 'tooltip-text'),
('Gtk.Label', 'label'),
('Gtk.Window', 'title'),
]

View file

@ -24,8 +24,8 @@ import os
import sys
import typing as T
from . import formatter, interactive_port, parser, tokenizer
from .errors import CompileError, CompilerBugError, PrintableError, report_bug
from . import formatter, interactive_port, parser, tokenizer, linter
from .errors import CompileError, CompileWarning, CompilerBugError, PrintableError, report_bug
from .gir import add_typelib_search_path
from .lsp import LanguageServer
from .outputs import XmlOutput
@ -96,6 +96,15 @@ class BlueprintApp:
metavar="filenames",
)
lint = self.add_subcommand(
"lint", "Lint given blueprint files", self.cmd_lint
)
lint.add_argument(
"inputs",
nargs="+",
metavar="filenames",
)
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
lsp = self.add_subcommand(
@ -291,6 +300,46 @@ class BlueprintApp:
if panic:
sys.exit(1)
def cmd_lint(self, opts):
input_files = []
missing_files = []
panic = False
for path in opts.inputs:
if os.path.isfile(path):
input_files.append(path)
elif os.path.isdir(path):
for root, subfolders, files in os.walk(path):
for file in files:
if file.endswith(".blp"):
input_files.append(os.path.join(root, file))
else:
missing_files.append(path)
for file in input_files:
with open(file, "r+") as file:
data = file.read()
errored = False
tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens)
if errors:
raise errors
if ast is None:
raise CompilerBugError()
problems = linter.lint(ast)
for problem in problems:
if isinstance(problem, CompileError):
problem.pretty_print(file.name, problem.range.original_text, stream=sys.stderr)
panic = True
elif isinstance(problem, CompileWarning):
problem.pretty_print(file.name, problem.range.original_text, stream=sys.stderr)
if panic:
sys.exit(1)
def cmd_lsp(self, opts):
langserv = LanguageServer()
langserv.run()