Files
harbour-henflow/tools/check_qml.py
T
kaneda 248e0db899 schermate di configurazione complete: razze disponibili, mio pollaio, registro galline, clienti, backup/ripristino nel formato dell'app Android, lingua a runtime, introduzione e informazioni
- datastore: export/import JSON con le chiavi camelCase dell'app Android (breedSettings/reservations/customers), scorte e ultimo aggiornamento preservati al ripristino
- settings: flag 'introduzione vista'
- desktop: permesso Documents (dove finisce il backup)
- nuovi controlli: tools/check_qml.py (chiavi di traduzione, simboli JS, pagine, componenti) e qmllint nella suite
- versione 0.2.0
2026-10-09 09:21:47 +02:00

176 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Controlli statici sulla QML di harbour-henflow.
La UI Silica non e' eseguibile qui (serve l'SDK Sailfish), ma quasi tutti gli
errori che si vedono sul device sono verificabili PRIMA di consegnare:
* chiavi di traduzione inesistenti (schermata con la chiave grezza a video)
* funzioni del motore JS chiamate ma non esistenti (TypeError a runtime)
* pagine aperte con pageStack ma senza file (pagina che non si apre)
* componenti Silica/QtQuick inesistenti (\"Impossibile caricare la pagina\")
* parentesi non bilanciate (l'intero file QML non compila)
Uso: python3 tools/check_qml.py # esce 1 se trova problemi
"""
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
QML_DIR = os.path.join(ROOT, "qml")
JS_DIR = os.path.join(QML_DIR, "js")
# Componenti disponibili: Silica 1.0 (elenco ufficiale) + QtQuick/JS di base.
SILICA = """
AddAnimation ApplicationWindow BackgroundItem BusyIndicator BusyLabel Button ButtonLayout
Clipboard ColorPicker ColorPickerDialog ColorPickerPage ColumnView ComboBox ContextMenu Cover
CoverAction CoverActionList CoverBackground CoverPlaceholder DatePicker DatePickerDialog DetailItem
Dialog DialogHeader DockedPanel Drawer EnterKey FadeAnimation FadeAnimator FirstTimeUseCounter
GridItem HighlightImage HorizontalScrollDecorator Icon IconButton IconTextSwitch InteractionHintLabel
Keypad Label LinkedLabel ListItem MenuItem MenuLabel Notices OpacityRampEffect Page PageBusyIndicator
PagedView PageHeader PageStack Palette PasswordField ProgressBar PullDownMenu PushUpMenu Remorse
RemorseItem RemorsePopup RemoveAnimation Screen ScrollDecorator SearchField SectionHeader Separator
SilicaControl SilicaFlickable SilicaGridView SilicaItem SilicaListView Slider SlideshowView
StandardPaths Switch TapInteractionHint TextArea TextField TextSwitch Theme TimePicker TimePickerDialog
TouchBlocker TouchInteractionHint ValueButton VerticalScrollDecorator ViewPlaceholder
""".split()
QTQUICK = """
AnchorChanges AnchorAnimation Behavior Binding BorderImage Canvas Column Connections Component
DoubleValidator Easing Enumerators Flipable Flow FontLoader FontMetrics Grid Image IntValidator
Item ItemGradient KeyNavigation Keys LayoutMirroring ListElement ListModel ListView Loader
MouseArea NumberAnimation NumberAnimationOnRotation ParallelAnimation ParentAnimation ParentChange
Path PathAnimation PauseAnimation PointDirection PropertyAction PropertyAnimation PropertyChanges
Rectangle Repeater RotationAnimation Row ScaleAnimator ScriptAction SequentialAnimation Shape
SmoothedAnimation SpringAnimation State StateGroup StateChangeScript StateMachine Text TextEdit
TextInput TextMetrics Timer Transition Translate VectorImage ViewTransition XAnimator YAnimator
NumberAnimator OpacityAnimator RegExpValidator ScrollView SplitView StackView TabBar TabView
ToolBar ToolButton GroupBox CheckBox RadioButton Slider SpinBox DialogButtonBox
""".split()
KNOWN = set(SILICA) | set(QTQUICK)
def strip_strings(src):
"""Rimuove stringhe e commenti (per bilanciamento e per i controlli sui tipi)."""
out = []
i, n = 0, len(src)
while i < n:
c = src[i]
if c == "/" and i + 1 < n and src[i + 1] == "/":
while i < n and src[i] != "\n":
i += 1
elif c == "/" and i + 1 < n and src[i + 1] == "*":
i += 2
while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
i += 1
i += 2
elif c in "\"'":
quote = c
i += 1
while i < n and src[i] != quote:
i += 2 if src[i] == "\\" else 1
i += 1
out.append('""')
else:
out.append(c)
i += 1
return "".join(out)
def js_symbols(path):
"""Simboli di primo livello esportati da un modulo JS (var e function)."""
src = open(path, encoding="utf-8").read()
names = set(re.findall(r"^function\s+(\w+)", src, re.M))
names |= set(re.findall(r"^var\s+(\w+)", src, re.M))
names |= set(re.findall(r"^ (\w+):\s", src, re.M)) # voci di module.exports
return names
def i18n_languages(path):
"""{lingua: insieme di chiavi} dal modulo i18n.js."""
src = open(path, encoding="utf-8").read()
m = re.search(r"var HF_STRINGS = (\{.*?\n\});", src, re.S)
if not m:
raise SystemExit("check_qml: HF_STRINGS non trovato in i18n.js")
data = json.loads(m.group(1))
return {lang: set(keys) for lang, keys in data.items()}
def qml_files():
found = []
for base, _dirs, files in os.walk(QML_DIR):
for f in files:
if f.endswith(".qml"):
found.append(os.path.join(base, f))
return sorted(found)
def main():
problems = []
files = qml_files()
if not files:
raise SystemExit("check_qml: nessun file QML trovato")
hf = js_symbols(os.path.join(JS_DIR, "henflow.js"))
cat = js_symbols(os.path.join(JS_DIR, "catalog.js"))
i18n = js_symbols(os.path.join(JS_DIR, "i18n.js"))
langs = i18n_languages(os.path.join(JS_DIR, "i18n.js"))
all_keys = set.intersection(*langs.values()) if langs else set()
for path in files:
rel = os.path.relpath(path, ROOT)
raw = open(path, encoding="utf-8").read()
src = strip_strings(raw)
# 1. bilanciamento parentesi
for opener, closer in (("{", "}"), ("(", ")"), ("[", "]")):
if src.count(opener) != src.count(closer):
problems.append("%s: %s%s non bilanciate (%d vs %d)"
% (rel, opener, closer, src.count(opener), src.count(closer)))
# 2. riferimenti ai moduli JS
for mod, symbols, spelled in (("HF", hf, "henflow.js"),
("Catalog", cat, "catalog.js"),
("I18N", i18n, "i18n.js")):
for name in sorted(set(re.findall(r"\b%s\.(\w+)" % mod, src))):
if name not in symbols:
problems.append("%s: %s.%s non esiste in %s" % (rel, mod, name, spelled))
# 3. chiavi di traduzione usate
for key in sorted(set(re.findall(r'\b(?:I18N\.t|tr)\(\s*"([A-Za-z0-9_]+)"', raw))):
if key not in all_keys:
problems.append("%s: chiave di traduzione mancante: %s" % (rel, key))
# 4. pagine aperte con pageStack
for target in sorted(set(re.findall(r'Qt\.resolvedUrl\(\s*"([^"]+\.qml)"', raw))):
if not os.path.exists(os.path.join(os.path.dirname(path), target)):
problems.append("%s: pagina inesistente: %s" % (rel, target))
# 5. componenti sconosciuti (Toast & co.)
for name in sorted(set(re.findall(r"^\s*([A-Z][A-Za-z0-9_]*)\s*\{", src, re.M))):
if name not in KNOWN:
problems.append("%s: componente non riconosciuto: %s" % (rel, name))
# 6. le chiavi devono esistere in TUTTE le lingue
for lang, keys in sorted(langs.items()):
missing = all_keys - keys
if missing:
problems.append("i18n.js: chiavi assenti in '%s': %s"
% (lang, ", ".join(sorted(missing))))
print("check_qml: %d file QML, %d chiavi di traduzione, %d lingue"
% (len(files), len(all_keys), len(langs)))
if problems:
print("\nPROBLEMI (%d):" % len(problems))
for p in problems:
print(" - " + p)
return 1
print("nessun problema trovato")
return 0
if __name__ == "__main__":
sys.exit(main())