mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Merge branch 'blueprint-linter' into 'main'
Draft: GTK linter Closes #124 See merge request jwestman/blueprint-compiler!176
This commit is contained in:
commit
808d664301
3 changed files with 236 additions and 2 deletions
137
blueprintcompiler/linter.py
Normal file
137
blueprintcompiler/linter.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
# 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
|
||||||
|
from .errors import CompileError, CompileWarning
|
||||||
|
from blueprintcompiler.language.gtk_a11y import ExtAccessibility
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# problems are for logical errors
|
||||||
|
# suggestions are for alternative/recommended way of doing things
|
||||||
|
|
||||||
|
# rule problem/number-of-children
|
||||||
|
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)
|
||||||
|
|
||||||
|
# rule suggestion/prefer-adwbin
|
||||||
|
# FIXME: Only if use Adw is in scope and no Gtk.Box properties are used
|
||||||
|
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)
|
||||||
|
|
||||||
|
# rule suggestion/translatable-display-string
|
||||||
|
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)
|
||||||
|
|
||||||
|
# rule suggestion/no-visible-true
|
||||||
|
# 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'In GTK4 widgets are visible by default', range)
|
||||||
|
problems.append(problem)
|
||||||
|
|
||||||
|
# rule problem/no-gtkswitch-state
|
||||||
|
if (type == 'Gtk.Switch'):
|
||||||
|
for property in properties:
|
||||||
|
if (property.name == 'state'):
|
||||||
|
range = property.range
|
||||||
|
problem = CompileError(f'Use the active property instead of the state property', range)
|
||||||
|
problems.append(problem)
|
||||||
|
|
||||||
|
# rule suggestion/require-a11y-label
|
||||||
|
if (type == 'Gtk.Button'):
|
||||||
|
label = None
|
||||||
|
tooltip_text = None
|
||||||
|
accessibility_label = False
|
||||||
|
|
||||||
|
# FIXME: Check what ATs actually do
|
||||||
|
|
||||||
|
for property in properties:
|
||||||
|
if (property.name == 'label'):
|
||||||
|
label = property.value
|
||||||
|
elif (property.name == 'tooltip-text'):
|
||||||
|
tooltip_text = property.value
|
||||||
|
|
||||||
|
accessibility__child = child.content.children[ExtAccessibility]
|
||||||
|
if len(accessibility__child) > 0:
|
||||||
|
accessibility_properties = child.content.children[ExtAccessibility][0].properties
|
||||||
|
for accessibility_property in accessibility_properties:
|
||||||
|
if (accessibility_property.name == 'label'):
|
||||||
|
accessibility_label = True
|
||||||
|
|
||||||
|
if (label is None and tooltip_text is None and accessibility_label is False):
|
||||||
|
problem = CompileWarning(f'{type} is missing an accessibility label', child.range)
|
||||||
|
problems.append(problem)
|
||||||
|
|
||||||
|
# rule suggestion/require-a11y-label
|
||||||
|
elif (type == 'Gtk.Image' or type == 'Gtk.Picture'):
|
||||||
|
accessibility_label = False
|
||||||
|
|
||||||
|
accessibility__child = child.content.children[ExtAccessibility]
|
||||||
|
if len(accessibility__child) > 0:
|
||||||
|
accessibility_properties = child.content.children[ExtAccessibility][0].properties
|
||||||
|
for accessibility_property in accessibility_properties:
|
||||||
|
if (accessibility_property.name == 'label'):
|
||||||
|
accessibility_label = True
|
||||||
|
|
||||||
|
if (accessibility_label is False):
|
||||||
|
problem = CompileWarning(f'{type} is missing an accessibility label', child.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'),
|
||||||
|
('Gtk.Button', 'label')
|
||||||
|
]
|
|
@ -24,8 +24,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
from . import formatter, interactive_port, parser, tokenizer
|
from . import formatter, interactive_port, parser, tokenizer, linter
|
||||||
from .errors import CompileError, CompilerBugError, PrintableError, report_bug
|
from .errors import CompileError, CompileWarning, CompilerBugError, PrintableError, report_bug
|
||||||
from .gir import add_typelib_search_path
|
from .gir import add_typelib_search_path
|
||||||
from .lsp import LanguageServer
|
from .lsp import LanguageServer
|
||||||
from .outputs import XmlOutput
|
from .outputs import XmlOutput
|
||||||
|
@ -103,6 +103,15 @@ class BlueprintApp:
|
||||||
metavar="filenames",
|
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)
|
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
|
||||||
|
|
||||||
lsp = self.add_subcommand(
|
lsp = self.add_subcommand(
|
||||||
|
@ -300,6 +309,46 @@ class BlueprintApp:
|
||||||
if panic:
|
if panic:
|
||||||
sys.exit(1)
|
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):
|
def cmd_lsp(self, opts):
|
||||||
langserv = LanguageServer()
|
langserv = LanguageServer()
|
||||||
langserv.run()
|
langserv.run()
|
||||||
|
|
48
linter.blp
Normal file
48
linter.blp
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
// I don't have an accessibility label
|
||||||
|
Gtk.Image {
|
||||||
|
icon-name: "cat-symbolic";
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
icon-name: "foobar";
|
||||||
|
}
|
||||||
|
|
||||||
|
// I don't display children
|
||||||
|
Label {
|
||||||
|
Box {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I only diplay the last child
|
||||||
|
Adw.StatusPage {
|
||||||
|
Box {}
|
||||||
|
Box {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer Adw.Bin if you have a single child
|
||||||
|
Gtk.Box {
|
||||||
|
Label {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I'm a user visible string that is not marked as translatable
|
||||||
|
Label {
|
||||||
|
label: "foo";
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
tooltip-text: "foo";
|
||||||
|
}
|
||||||
|
Window {
|
||||||
|
title: "foobar";
|
||||||
|
}
|
||||||
|
|
||||||
|
// State isn't the right property to use here
|
||||||
|
// use active instead
|
||||||
|
Switch {
|
||||||
|
state: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In GTK4 widgets are visible by default
|
||||||
|
Box {
|
||||||
|
visible: true;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue