- 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
176 lines
7.4 KiB
Python
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())
|