Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…

, re.M)\nRE_IMP_QTQUICK_WINDOW = re.compile(r'^\\s*import\\s+QtQuick\\.Window\\b')\n\n# IMP-2: Version numbers on imports (Qt 6 dropped the requirement).\n# Research: qmlsc requires unversioned imports for optimal compilation;\n# versioned imports cap API surface and cause \"missing type\" confusion\n# when copying Qt 5 examples.\nRE_IMP_VERSIONED = re.compile(r'^\\s*import\\s+\\w[\\w.]*\\s+\\d+\\.\\d+')\n\n# IMP-3: Plain QtQuick.Controls without style qualifier when customizing.\n# Research: Customizing contentItem/background/indicator/handle without a\n# style-specific import uses the \"default style\" abstraction, which can\n# produce unexpected rendering. Import Controls.Basic (or another style).\nRE_IMP_CONTROLS_PLAIN = re.compile(\n r'^\\s*import\\s+QtQuick\\.Controls(\\s+\\d[\\d.]*)?\\s*

Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…

, re.M\n)\nRE_CONTROL_CUSTOMIZATION = re.compile(\n r'\\b(contentItem|background|indicator|handle)\\s*:'\n)\n\n# IMP-5: Qt.include() is deprecated since Qt 5.14.\n# Research: Removed in Qt 6 documentation; use explicit imports or\n# JS module imports instead.\nRE_IMP_QT_INCLUDE = re.compile(r'\\bQt\\.include\\s*\\(')\n\n# IMP-6: Duplicate import detection (post-scan)\nRE_IMPORT_LINE = re.compile(r'^\\s*import\\s+(.+)')\n\n# IMP-7: Qt 5 graphical effects module. QtGraphicalEffects does not exist\n# in Qt 6; Qt5Compat.GraphicalEffects is a migration bridge only.\n# Research: MultiEffect (QtQuick.Effects, Qt 6.5+) combines blur, shadow\n# and colorization in a single pass.\nRE_IMP_GRAPHICAL_EFFECTS = re.compile(\n r'^\\s*import\\s+(QtGraphicalEffects|Qt5Compat\\.GraphicalEffects)\\b'\n)\n\n# --- BND (Bindings & Properties) ---\n\n# BND-1: property var usage. Research: qmllint warns via\n# prefer-non-var-properties; typed properties enable qmlsc compilation\n# to C++ and eliminate meta-object overhead in property access.\nRE_BND_PROP_VAR = re.compile(r'^\\s*(required\\s+)?property\\s+var\\s+\\w+')\n\n# BND-2: Imperative = on property that likely had a binding.\n# Research: any `prop = value` in a JS block destroys the binding\n# permanently. The qt.qml.binding.removal logging category (Qt 5.10+)\n# is the only runtime diagnostic. qmllint does NOT detect this.\n# Two patterns: bare `prop = value` and qualified `id.prop = value`.\nRE_BND_IMPERATIVE_ASSIGN = re.compile(\n r'^\\s+(\\w+)\\s*=\\s*(?!==)' # assignment in JS (indented, not ==)\n)\nRE_BND_QUALIFIED_ASSIGN = re.compile(\n r'^\\s+(\\w+)\\.(\\w+)\\s*=\\s*(?!==)' # id.prop = value\n)\nRE_BND_IMPERATIVE_EXCLUDE = re.compile(\n r'(var |let |const |function |return |for |if |else |while |'\n r'switch |case |property |signal |import |//|readonly )'\n)\n\n# BND-3: Qt.binding with old-style function (not arrow).\n# Research: arrow functions are cleaner and the recommended syntax\n# per Qt docs. Old-style function(){} has `this` context issues\n# inside Qt.binding().\nRE_BND_BINDING_OLDFUNC = re.compile(r'Qt\\.binding\\s*\\(\\s*function\\s*\\(')\n\n# BND-5: list\u003c> property type warning.\n# Research: QML list properties have no granular change signals for\n# add/move/remove operations -- only whole-list replacement triggers\n# notification. Binding expensive operations to list properties\n# causes subtle update bugs.\nRE_BND_LIST_PROP = re.compile(r'property\\s+list\\s*\u003c')\n\n# --- LAY (Layout & Anchoring) ---\n\n# LAY-1: anchors + Layout on same item (detected in block tracker).\n# LAY-3: Four anchor edges instead of fill (detected in block tracker).\n\n# LAY-5: Cross-branch anchoring via parent.parent.\n# Research: anchoring to parent.parent references a grandparent,\n# which is fragile if the visual tree is refactored. Use an explicit\n# id on the target instead.\nRE_LAY_PARENT_PARENT = re.compile(r'anchors\\.\\w+\\s*:\\s*parent\\.parent\\b')\n\n# LAY-2 / LAY-6: bare width/height or x/y inside Layout child\n# (detected in block tracker by checking parent_type).\n\n# --- LDR (Loader & Dynamic Creation) ---\n\n# LDR-1: Loader.item access without status guard.\n# Research: with asynchronous:true, Loader.item is null until\n# status===Loader.Ready. Binding to Loader.item.prop without a\n# null guard causes TypeError. qmllint does not catch this.\nRE_LDR_ITEM_ACCESS = re.compile(r'\\bloader\\w*\\.item\\b', re.IGNORECASE)\nRE_LDR_STATUS_GUARD = re.compile(\n r'(status\\s*===?\\s*Loader\\.Ready|Loader\\.status|\\.item\\?\\.|\\bonLoaded\\b)'\n)\n\n# LDR-2: Qt.createComponent with string URL.\n# Research: String-based createComponent loses tooling support and\n# type checking. Prefer inline Component{} definitions.\nRE_LDR_CREATE_COMPONENT = re.compile(r'Qt\\.createComponent\\s*\\(')\n\n# LDR-3: Qt.createQmlObject is slow and uncacheable.\n# Research: parses QML string at runtime on every call; no component\n# caching. Use Loader or createComponent for all non-trivial cases.\nRE_LDR_CREATE_OBJECT = re.compile(r'Qt\\.createQmlObject\\s*\\(')\n\n# LDR-5: Loader with both source and sourceComponent.\n# Research: Qt docs state these are mutually exclusive; setting both\n# is unsupported and behavior is undefined.\nRE_LDR_SOURCE = re.compile(r'^\\s*source\\s*:')\nRE_LDR_SOURCE_COMPONENT = re.compile(r'^\\s*sourceComponent\\s*:')\n\n# --- DEL (ListView & Delegates) ---\n\n# DEL-1: Delegate using model.roleName without required property.\n# Research: Once any required property is declared, the implicit\n# model context object is no longer injected. But using model.*\n# without required properties misses qmlsc compilation and type\n# safety. Modern Qt 6 best practice is required properties.\nRE_DEL_MODEL_ACCESS = re.compile(r'\\bmodel\\.\\w+')\nRE_DEL_REQUIRED_PROP = re.compile(r'required\\s+property\\b')\n\n# DEL-2: var declaration in delegate (mutable JS state that won't\n# reset on delegate reuse).\n# Research: with reuseItems:true, Component.onCompleted doesn't\n# re-fire. JS vars keep their old values, causing state bleed.\n# Use QML properties instead (they get model-bound on reuse).\nRE_DEL_VAR_IN_JS = re.compile(r'\\bvar\\s+\\w+\\s*=')\n\n# DEL-3: connect() in Component.onCompleted (survives delegate destruction).\n# Research: Direct connect() creates signal connections that outlive\n# the delegate, causing TypeError when the signal fires after the\n# delegate is destroyed. Use Connections{} objects instead.\nRE_DEL_CONNECT = re.compile(r'\\.connect\\s*\\(')\nRE_DEL_ON_COMPLETED = re.compile(r'Component\\.onCompleted\\s*:')\n\n# DEL-4: Component.onCompleted in delegate with reuseItems.\n# Research: onCompleted fires once at creation, NOT on reuse.\n# State initialization that belongs in onReused will be missed.\nRE_DEL_REUSE_ITEMS = re.compile(r'reuseItems\\s*:\\s*true')\n\n# --- STA (States & Transitions) ---\n\n# STA-1: PropertyChanges with target: property (Qt 6 deprecated form).\n# Research: Qt 6 uses id.property: value syntax inside PropertyChanges.\n# The old target: form still works but is not recommended and is\n# incompatible with Qt Design Studio.\nRE_STA_TARGET_LINE = re.compile(r'^\\s*target\\s*:')\n\n# STA-2: Transition without from/to (catch-all).\n# Research: Transition{from:\"*\";to:\"*\"} fires on every state change\n# including unintended ones. Explicit from/to prevents unexpected\n# animations when new states are added.\nRE_STA_TRANSITION = re.compile(r'Transition\\s*\\{')\nRE_STA_FROM_TO = re.compile(r'\\b(from|to)\\s*:')\n\n# STA-3: Top-level states in reusable component (detected in block tracker).\n# Research: QML states is a QQmlListProperty -- assigning from outside\n# adds rather than replaces, causing conflicts. Use StateGroup for\n# internal states of reusable components.\nRE_STA_STATES = re.compile(r'^\\s*states\\s*:\\s*\\[')\n\n# --- IMG (Images) ---\n\n# IMG-1: Image without sourceSize (detected in block tracker).\n# Research: Without sourceSize, Qt decodes the full-resolution image\n# into GPU memory. A 4000x3000 photo displayed at 100x75 still\n# allocates ~48MB of texture memory.\n\n# IMG-2: Image with network source without asynchronous:true\n# (detected in block tracker).\n# Research: Image decoding blocks the UI thread by default. For\n# network sources this means the entire download+decode is synchronous.\n\n# --- PRF (Performance & Rendering) ---\n\n# PRF-1: Rectangle with color:\"transparent\" (block tracker).\n# Research: Qt docs explicitly recommend Item for grouping.\n# Rectangle creates a scene graph geometry node even when transparent,\n# adding to batch count. Item generates no geometry node.\n# Matches both literal `color: \"transparent\"` and conditional\n# expressions that include \"transparent\" (e.g., ternary).\n# Also matches continuation lines where \"transparent\" appears\n# without the `color:` prefix (multiline ternary).\nRE_PRF_TRANSPARENT = re.compile(\n r'color\\s*:.*[\"\\']transparent[\"\\']'\n)\nRE_PRF_TRANSPARENT_CONT = re.compile(\n r'[\"\\']transparent[\"\\']'\n)\n\n# PRF-2: opacity: 0 without animation context.\n# Research: opacity:0 still incurs rendering overhead and retains\n# keyboard focus. visible:false skips rendering entirely and removes\n# from input handling. Use opacity:0 only during fade animations.\nRE_PRF_OPACITY_ZERO = re.compile(r'^\\s*opacity\\s*:\\s*0\\s*

Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…

)\nRE_PRF_OPACITY_ANIM = re.compile(\n r'(Behavior\\s+on\\s+opacity|NumberAnimation.*opacity|'\n r'OpacityAnimator|PropertyAnimation.*\"opacity\")'\n)\n\n# PRF-3: clip:true warning.\n# Research: Qt docs: \"Clipping is a visual effect, NOT an optimization.\"\n# Forces a separate scene graph batch (scissor/stencil). Acceptable\n# on ListView (many children) but costly on small items.\nRE_PRF_CLIP = re.compile(r'^\\s*clip\\s*:\\s*true')\n\n# PRF-4: font.pixelSize bound to animation.\n# Research: Every font.pixelSize change triggers full text relayout\n# (glyph shaping, line breaking). Use scale transform on the Text\n# element instead for size animations.\nRE_PRF_FONT_ANIM = re.compile(\n r'Behavior\\s+on\\s+font\\.pixelSize|'\n r'NumberAnimation\\s*\\{[^}]*property\\s*:\\s*\"font\\.pixelSize\"'\n)\n\n# PRF-5: Text with RichText format.\n# Research: RichText is significantly more expensive than PlainText\n# or StyledText. It invokes a full HTML/CSS parser. Use PlainText\n# unless rich formatting is required.\nRE_PRF_RICHTEXT = re.compile(r'textFormat\\s*:\\s*Text\\.RichText')\n\n# PRF-6: layer.enabled:true without clear animation purpose.\n# Research: Renders the subtree to an offscreen FBO, then composites\n# as texture. The layered item cannot be batched with siblings.\n# Multisampling on layers is especially expensive.\nRE_PRF_LAYER = re.compile(r'^\\s*layer\\.enabled\\s*:\\s*true')\n\n# --- STY (Style & Conventions) ---\n\n# STY-1: Top-level component missing id:root (detected in block tracker).\n# Research: The QML coding convention is that the root element of every\n# file uses id:root. This enables qualified lookup (root.prop) and\n# future-proofs against QML 3 unqualified lookup removal.\nRE_STY_ID_ROOT = re.compile(r'^\\s*id\\s*:\\s*root\\b')\n\n# STY-3: Multiple grouped properties using dot notation\n# (detected in block tracker).\n# Research: Qt style guide recommends group notation when setting\n# multiple sub-properties of the same group (e.g. sourceSize {},\n# anchors {}, font {}).\n\n# STY-6: id not using camelCase.\n# Research: QML convention is lowerCamelCase for ids. Under_score\n# or UPPER ids break convention and confuse tooling.\nRE_STY_ID = re.compile(r'^\\s*id\\s*:\\s*(\\w+)')\nRE_STY_CAMELCASE = re.compile(r'^[a-z][a-zA-Z0-9]*

Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…

)\n\n# --- SIG (Signals & Connections) ---\n\n# SIG-1: Connections without explicit target.\n# Research: Default target is parent, which causes unintended signal\n# handling if the parent type changes. The coding instructions mandate\n# always setting target explicitly.\nRE_SIG_CONNECTIONS = re.compile(r'Connections\\s*\\{')\n\n# SIG-2: Old onFoo: handler syntax in Connections block (deprecated 5.15).\n# Research: The old syntax produces deprecation warnings in Qt 6.\n# Worse: mixing old onFoo: with new function onFoo() in the same\n# Connections block silently ignores the function-based handlers.\nRE_SIG_OLD_HANDLER = re.compile(r'^\\s*on[A-Z]\\w*\\s*:')\nRE_SIG_NEW_HANDLER = re.compile(r'^\\s*function\\s+on[A-Z]\\w*\\s*\\(')\n\n# --- JS (JavaScript Quality) ---\n\n# JS-1: var instead of let/const.\n# Research: var has function scope and hoisting, causing subtle bugs.\n# let/const have block scope. Qt coding instructions mandate\n# let/const. qmlsc can optimize const better than var.\nRE_JS_VAR = re.compile(r'(?\u003c!\\w)var\\s+\\w+\\s*[=;]')\nRE_JS_VAR_EXCLUDE = re.compile(\n r'(property\\s+var|^\\s*//|^\\s*/?\\*)'\n)\n\n# JS-2: Loose equality (== / !=) instead of strict (=== / !==).\n# Research: QML's JS engine follows ECMAScript; loose equality\n# performs type coercion which is almost never desired in QML\n# property comparisons. qmllint has equality-type-coercion warning.\nRE_JS_LOOSE_EQ = re.compile(r'(?\u003c![=!])[!=]=(?!=)')\nRE_JS_LOOSE_EXCLUDE = re.compile(\n r'(^\\s*//|^\\s*/?\\*|^\\s*\\*|import |property |signal )'\n)\n\n# --- ERR (Error Handling & Security) ---\n\n# ERR-1: Hardcoded http:// instead of https://.\n# Research: Unencrypted HTTP exposes data in plaintext. Same rule\n# as the C++ linter's ERR-4. Excludes localhost/127.0.0.1/example.\nRE_ERR_HTTP = re.compile(r'\"http://[a-zA-Z]')\nRE_ERR_HTTP_EXCLUDE = re.compile(\n r'(localhost|127\\.0\\.0\\.1|::1|example\\.test)'\n)\n\n# ERR-2: Hardcoded Unix paths (non-portable).\n# Research: /tmp/ does not exist on Windows. Qt provides\n# QStandardPaths for cross-platform temporary file access.\nRE_ERR_UNIX_PATH = re.compile(r'\"/tmp/')\n\n# JS-3: forbidden dynamic code execution.\n# Research: blocks JIT compilation in QV4, is a security risk,\n# and qmllint flags it. There is never a valid use case in QML.\nRE_JS_EVAL = re.compile(r'\\beval\\s*\\(')\n\n\n# ---------------------------------------------------------------------------\n# Rule tables\n# ---------------------------------------------------------------------------\n\n# Tier A: Simple match + optional exclude.\nRULES_SIMPLE: \"List[Rule]\" = [\n # --- IMP ---\n Rule(\"IMP-2\", RE_IMP_VERSIONED,\n \"Versioned import -- Qt 6 does not require version numbers on imports\"),\n Rule(\"IMP-5\", RE_IMP_QT_INCLUDE,\n \"Qt.include() is deprecated -- use ES module imports or explicit QML imports\"),\n Rule(\"IMP-7\", RE_IMP_GRAPHICAL_EFFECTS,\n \"Qt 5 graphical effects import -- migrate to MultiEffect \"\n \"(import QtQuick.Effects, Qt 6.5+)\"),\n\n # --- BND ---\n Rule(\"BND-1\", RE_BND_PROP_VAR,\n \"property var -- use a typed property (int, string, etc.) for \"\n \"qmlsc compilation and type safety\"),\n Rule(\"BND-3\", RE_BND_BINDING_OLDFUNC,\n \"Qt.binding(function(){}) -- use arrow function: Qt.binding(() => expr)\"),\n Rule(\"BND-5\", RE_BND_LIST_PROP,\n \"list\u003c> property has no granular change signals -- \"\n \"add/move/remove won't notify; consider a ListModel or notify manually\"),\n\n # --- LAY ---\n Rule(\"LAY-5\", RE_LAY_PARENT_PARENT,\n \"Anchoring to parent.parent -- fragile cross-branch reference; \"\n \"use an explicit id on the target\"),\n\n # --- LDR ---\n Rule(\"LDR-2\", RE_LDR_CREATE_COMPONENT,\n \"Qt.createComponent(url) -- prefer inline Component{} for \"\n \"type safety and tooling support\"),\n Rule(\"LDR-3\", RE_LDR_CREATE_OBJECT,\n \"Qt.createQmlObject() -- slow and uncacheable; \"\n \"use Loader or Component.createObject()\"),\n\n # --- PRF ---\n Rule(\"PRF-3\", RE_PRF_CLIP,\n \"clip: true disables scene graph batching -- \"\n \"verify this is needed (acceptable on ListView)\"),\n Rule(\"PRF-5\", RE_PRF_RICHTEXT,\n \"Text.RichText invokes full HTML/CSS parser -- \"\n \"use PlainText or StyledText if possible\"),\n Rule(\"PRF-4\", RE_PRF_FONT_ANIM,\n \"Animating font.pixelSize triggers full text relayout each frame -- \"\n \"use scale transform on Text instead\"),\n Rule(\"PRF-6\", RE_PRF_LAYER,\n \"layer.enabled forces offscreen FBO rendering -- \"\n \"enable only during effects/animations, then disable\"),\n\n # --- STY ---\n # STY-6 is pattern-matched here but handled specially inline\n Rule(\"STY-6\", RE_STY_ID,\n \"\"), # placeholder; actual check is inline\n\n # --- ERR ---\n # ERR-1 uses raw lines (http:// contains // which comment\n # stripping removes) -- handled inline, not here.\n Rule(\"ERR-2\", RE_ERR_UNIX_PATH,\n \"Hardcoded /tmp/ path is not cross-platform -- \"\n \"use QStandardPaths.writableLocation from C++ or a \"\n \"platform-aware helper\"),\n\n # --- JS ---\n Rule(\"JS-3\", RE_JS_EVAL,\n \"Dynamic code execution blocks JIT and is a security risk -- \"\n \"never use in QML\"),\n]\n\n# Tier B: Match + context window check.\nRULES_CONTEXT: \"List[Rule]\" = [\n # LDR-1: Loader.item without status guard\n Rule(\"LDR-1\", RE_LDR_ITEM_ACCESS,\n \"Loader.item accessed without status guard -- \"\n \"check Loader.status === Loader.Ready or use optional chaining (?.)\",\n context_before=5, context_after=5,\n context_pattern=RE_LDR_STATUS_GUARD,\n context_must_match=False),\n\n # STA-2: Transition without from/to\n Rule(\"STA-2\", RE_STA_TRANSITION,\n \"Transition without from/to -- catch-all fires on every state change; \"\n \"specify explicit from/to pairs\",\n context_after=3, context_pattern=RE_STA_FROM_TO,\n context_must_match=False),\n]\n\n# Tier C: Match + file-level flag guard.\nRULES_FLAG: \"List[Rule]\" = [\n # DEL-1: model. access without required property\n Rule(\"DEL-1\", RE_DEL_MODEL_ACCESS,\n \"model.roleName without required property -- \"\n \"declare required properties for type safety and qmlsc compilation\",\n requires_no_flag=\"has_required_prop\"),\n\n # PRF-2: opacity: 0 without animation context\n Rule(\"PRF-2\", RE_PRF_OPACITY_ZERO,\n \"opacity: 0 without opacity animation -- prefer visible: false \"\n \"(skips rendering entirely and removes from input handling)\",\n requires_no_flag=\"has_opacity_anim\"),\n]\n\n\n# ---------------------------------------------------------------------------\n# Per-line rule dispatch\n# ---------------------------------------------------------------------------\n\ndef _check_line_rules(\n lineno: int,\n i: int,\n line: str,\n stripped: str,\n code_line: str,\n code_lines: \"List[str]\",\n code_text: str,\n num_lines: int,\n brace_depth: int,\n is_delegate_file: bool,\n file_flags: \"Dict[str, bool]\",\n emit,\n) -> None:\n \"\"\"Run all per-line rule checks (Tiers A, B, C, and special rules).\"\"\"\n\n # --- Tier A: simple rules ---\n for rule in RULES_SIMPLE:\n if not rule.pattern.search(code_line):\n continue\n if rule.exclude and rule.exclude.search(code_line):\n continue\n # STY-6 special handling: check the actual id value\n if rule.id == \"STY-6\":\n m = RE_STY_ID.match(stripped)\n if m:\n id_val = m.group(1)\n if not RE_STY_CAMELCASE.match(id_val):\n emit(lineno, \"STY-6\",\n f\"id '{id_val}' -- use lowerCamelCase for QML ids\")\n continue\n emit(lineno, rule.id, rule.message)\n\n # --- Tier A special: multi-pattern rules ---\n\n # BND-2: Imperative assignment destroying binding (heuristic)\n # Pattern 1: bare `prop = value` inside handler/function\n if (RE_BND_IMPERATIVE_ASSIGN.search(code_line)\n and not RE_BND_IMPERATIVE_EXCLUDE.search(code_line)\n and brace_depth >= 2): # inside a handler or function\n m = RE_BND_IMPERATIVE_ASSIGN.search(code_line)\n if m:\n prop_name = m.group(1)\n # Check if this property was bound with : in the file\n # (use code_text to skip commented-out bindings)\n if re.search(\n rf'^\\s*{re.escape(prop_name)}\\s*:',\n code_text, re.M\n ):\n emit(lineno, \"BND-2\",\n f\"Imperative '=' on '{prop_name}' destroys \"\n \"its binding -- use Qt.binding() to restore,\"\n \" or verify this is intentional\")\n\n # Pattern 2: qualified `id.prop = value` (e.g., in\n # Connections handler: `syncLabel.text = \"done\"`)\n # Only flag when the target id exists and its property has\n # a binding to a non-literal expression (not just\n # `text: \"static\"`), since overwriting a static value is\n # intentional and harmless.\n if (RE_BND_QUALIFIED_ASSIGN.search(code_line)\n and not RE_BND_IMPERATIVE_EXCLUDE.search(code_line)\n and brace_depth >= 1):\n m = RE_BND_QUALIFIED_ASSIGN.search(code_line)\n if m:\n target_id = m.group(1)\n prop_name = m.group(2)\n # Verify target id exists in file\n if re.search(\n rf'\\bid\\s*:\\s*{re.escape(target_id)}\\b',\n code_text\n ):\n # Check if the property has a dynamic binding\n # (references another id/property, not just a\n # string/number literal). Look for `prop: \u003cexpr>`\n # where expr starts with a word char (property\n # reference) rather than a literal (\" ' digit).\n binding_match = re.search(\n rf'^\\s*{re.escape(prop_name)}\\s*:\\s*'\n rf'(?![\"\\'\\d])'\n rf'(?!true\\b|false\\b|null\\b|undefined\\b)'\n rf'[a-zA-Z_]',\n code_text, re.M\n )\n if binding_match:\n emit(lineno, \"BND-2\",\n f\"Imperative '=' on \"\n f\"'{target_id}.{prop_name}' destroys \"\n \"its binding -- use Qt.binding() to \"\n \"restore, or verify this is intentional\"\n )\n\n # ERR-1: Hardcoded http:// URL (uses raw line because //\n # in URLs is stripped by comment removal)\n if (RE_ERR_HTTP.search(line)\n and not RE_ERR_HTTP_EXCLUDE.search(line)):\n emit(lineno, \"ERR-1\",\n \"Hardcoded http:// URL -- \"\n \"use https:// for secure transport\")\n\n # JS-1: var in JS (not property var)\n if (RE_JS_VAR.search(code_line)\n and not RE_JS_VAR_EXCLUDE.search(code_line)):\n emit(lineno, \"JS-1\",\n \"Use let/const instead of var -- \"\n \"var has function scope and hoisting bugs\")\n\n # JS-2: Loose equality\n if (RE_JS_LOOSE_EQ.search(code_line)\n and not RE_JS_LOOSE_EXCLUDE.search(code_line)):\n emit(lineno, \"JS-2\",\n \"Loose equality (==/!=) -- use strict equality (===/!==)\")\n\n # DEL-2: mutable var in delegate with reuseItems\n if (RE_DEL_VAR_IN_JS.search(code_line)\n and not RE_JS_VAR_EXCLUDE.search(code_line)\n and is_delegate_file\n and file_flags[\"has_reuse_items\"]):\n emit(lineno, \"DEL-2\",\n \"var declaration in delegate with reuseItems -- \"\n \"JS vars don't reset on reuse; use QML properties \"\n \"or reset in ListView.onReused\")\n\n # --- Tier B: context-aware rules ---\n for rule in RULES_CONTEXT:\n if not rule.pattern.search(code_line):\n continue\n if rule.exclude and rule.exclude.search(code_line):\n continue\n if rule.context_pattern is None:\n emit(lineno, rule.id, rule.message)\n continue\n # Build context window\n ctx_parts = []\n if rule.context_before:\n start = max(i - rule.context_before, 0)\n ctx_parts.append(\"\\n\".join(code_lines[start:i]))\n if rule.context_after:\n end = min(i + 1 + rule.context_after, num_lines)\n ctx_parts.append(\"\\n\".join(code_lines[i + 1:end]))\n ctx = \"\\n\".join(ctx_parts)\n has_ctx = bool(rule.context_pattern.search(ctx))\n if has_ctx == rule.context_must_match:\n emit(lineno, rule.id, rule.message)\n\n # --- Tier C: file-flag-gated rules ---\n for rule in RULES_FLAG:\n if rule.requires_flag and not file_flags.get(rule.requires_flag):\n continue\n if (rule.requires_no_flag\n and file_flags.get(rule.requires_no_flag)):\n continue\n if not rule.pattern.search(code_line):\n continue\n if rule.exclude and rule.exclude.search(code_line):\n continue\n emit(lineno, rule.id, rule.message)\n\n\n# ---------------------------------------------------------------------------\n# Post-scan file-level checks\n# ---------------------------------------------------------------------------\n\ndef _post_scan_checks(\n filepath: str,\n lines: \"List[str]\",\n code_lines: \"List[str]\",\n file_flags: \"Dict[str, bool]\",\n tracker: BlockTracker,\n emit,\n) -> None:\n \"\"\"File-level checks + second-pass scans. Uses emit for all findings.\"\"\"\n # STY-1: Root component missing id: root\n if tracker.root_block_seen and not tracker.root_block_has_id_root:\n for j, ln in enumerate(lines):\n if '{' in ln and not RE_COMMENT_LINE.match(ln.strip()):\n emit(j + 1, \"STY-1\",\n \"Top-level component should have id: root \"\n \"(enables qualified lookup, future-proofs \"\n \"for QML 3)\")\n break\n\n # STA-3: Top-level states (not in StateGroup)\n # Only flag when the file appears to define a reusable component\n # (has required properties, indicating it's meant to be instantiated\n # by others). Plain application root items with states are fine.\n if file_flags[\"has_required_prop\"]:\n state_depth = 0\n for j, ln in enumerate(lines):\n code_ln = code_lines[j]\n state_depth += code_ln.count('{') - code_ln.count('}')\n if state_depth == 1 and RE_STA_STATES.match(\n code_ln.strip()\n ):\n context_back = \"\\n\".join(\n code_lines[max(0, j - 3):j]\n )\n if \"StateGroup\" not in context_back:\n emit(j + 1, \"STA-3\",\n \"Top-level states: in reusable component -- \"\n \"use StateGroup (states is a QQmlListProperty,\"\n \" appending from outside adds rather than \"\n \"replaces)\")\n break\n\n # DEL-4: Component.onCompleted with reuseItems (file-level)\n if file_flags[\"has_reuse_items\"]:\n for j, ln in enumerate(lines):\n if RE_DEL_ON_COMPLETED.search(code_lines[j]):\n emit(j + 1, \"DEL-4\",\n \"Component.onCompleted with reuseItems: true -- \"\n \"onCompleted does NOT re-fire on reuse; \"\n \"use ListView.onReused\")\n break\n\n # PRF-1: transparent Rectangle (second pass with line content)\n for f in _scan_transparent_rects(filepath, lines, code_lines):\n emit(f.line, f.rule, f.message)\n\n # IMG-2: Image with network source without async\n for f in _scan_image_async(filepath, lines, code_lines):\n emit(f.line, f.rule, f.message)\n\n\n# ---------------------------------------------------------------------------\n# Per-file scanner\n# ---------------------------------------------------------------------------\n\ndef scan_file(filepath: str) -> \"List[Finding]\":\n \"\"\"Scan a single QML file and return all findings.\"\"\"\n findings: \"List[Finding]\" = []\n path = Path(filepath)\n\n try:\n raw = path.read_text(encoding=\"utf-8\", errors=\"replace\")\n lines = raw.splitlines()\n except OSError:\n return findings\n\n num_lines = len(lines)\n\n def emit(line: int, rule: str, msg: str) -> None:\n findings.append(Finding(filepath, line, rule, msg))\n\n # --- Comment stripping for flag/context scans ---\n code_lines = [re.sub(r'//.*

Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…

, '', ln) for ln in lines]\n code_text = \"\\n\".join(code_lines)\n\n # --- File-level pre-scan flags ---\n file_flags: \"Dict[str, bool]\" = {\n \"has_required_prop\": bool(RE_DEL_REQUIRED_PROP.search(code_text)),\n \"has_reuse_items\": bool(RE_DEL_REUSE_ITEMS.search(code_text)),\n \"has_import_qtquick\": bool(RE_IMP_QTQUICK.search(code_text)),\n \"has_controls_plain\": bool(RE_IMP_CONTROLS_PLAIN.search(code_text)),\n \"has_control_custom\": bool(RE_CONTROL_CUSTOMIZATION.search(code_text)),\n \"has_opacity_anim\": bool(RE_PRF_OPACITY_ANIM.search(code_text)),\n }\n\n imports = ImportTracker()\n\n is_delegate_file = bool(re.search(\n r'(delegate\\s*:|ListView|GridView|Repeater|DelegateModel|'\n r'DelegateChooser|required\\s+property\\s+\\w+\\s+\\w+.*model)',\n code_text\n ))\n tracker = BlockTracker(is_delegate_file=is_delegate_file)\n\n # --- Line-by-line scan ---\n for i, line in enumerate(lines):\n lineno = i + 1\n stripped = line.strip()\n code_line = code_lines[i]\n\n # Skip pure comment lines\n if RE_COMMENT_LINE.match(stripped):\n continue\n\n # --- Import analysis (falls through to rules + structure) ---\n imports.process_line(lineno, stripped, emit)\n\n # Rule dispatch -- reads tracker.brace_depth before it updates\n _check_line_rules(lineno, i, line, stripped, code_line,\n code_lines, code_text, num_lines,\n tracker.brace_depth, is_delegate_file,\n file_flags, emit)\n\n # Structural tracking -- updates brace_depth, block_stack, and\n # emits structural findings. Called AFTER rule dispatch so that\n # rules see the pre-update brace_depth (BND-2 needs this).\n tracker.process_line(lineno, stripped, code_line, emit,\n file_flags)\n\n # --- Post-scan file-level checks ---\n imports.post_scan_checks(lines, file_flags, emit)\n _post_scan_checks(filepath, lines, code_lines, file_flags,\n tracker, emit)\n\n # Sort by line number\n findings.sort(key=lambda f: (f.file, f.line))\n return findings\n\n\ndef _check_closed_block(\n block: Block,\n emit,\n file_flags: \"Dict[str, bool]\",\n) -> None:\n \"\"\"Run block-level checks when a QML object block closes.\"\"\"\n props = block.properties\n\n # --- LAY-1: anchors + Layout on same item ---\n has_anchors = any(k.startswith(\"anchors\") for k in props)\n has_layout = any(k.startswith(\"Layout\") for k in props)\n if has_anchors and has_layout:\n emit(block.start_line, \"LAY-1\",\n f\"{block.type_name or 'Item'}: anchors and Layout.* \"\n \"on same item -- they conflict; pick one\")\n\n # --- LAY-2: bare width/height inside Layout parent ---\n if block.parent_type in LAYOUT_TYPES:\n for prop_name in (\"width\", \"height\"):\n if prop_name in props:\n cap = prop_name.capitalize()\n emit(props[prop_name], \"LAY-2\",\n f\"{prop_name}: inside {block.parent_type} child \"\n f\"-- use Layout.preferred{cap} or Layout.fill{cap}\")\n # LAY-6: bare x/y inside Layout parent\n for prop_name in (\"x\", \"y\"):\n if prop_name in props:\n emit(props[prop_name], \"LAY-6\",\n f\"{prop_name}: inside {block.parent_type} child \"\n \"-- Layout manages positioning; remove explicit \"\n f\"{prop_name}\")\n\n # --- LAY-3: Four anchor edges instead of fill ---\n anchor_edges = {\n \"anchors.left\", \"anchors.right\",\n \"anchors.top\", \"anchors.bottom\",\n }\n found_edges = anchor_edges.intersection(props.keys())\n if len(found_edges) == 4 and \"anchors.fill\" not in props:\n first_line = min(props[e] for e in found_edges)\n emit(first_line, \"LAY-3\",\n \"Four separate anchor edges -- \"\n \"use anchors.fill: parent instead\")\n\n # --- IMG-1: Image without sourceSize ---\n if block.type_name == \"Image\":\n has_ss = any(k.startswith(\"sourceSize\") for k in props)\n if not has_ss:\n emit(block.start_line, \"IMG-1\",\n \"Image without sourceSize -- decodes full resolution \"\n \"into GPU memory; set sourceSize to display dimensions\")\n\n # IMG-2 is checked in _scan_image_async (needs line content).\n\n # --- LDR-5: Loader with both source and sourceComponent ---\n if block.type_name == \"Loader\":\n if \"source\" in props and \"sourceComponent\" in props:\n emit(block.start_line, \"LDR-5\",\n \"Loader has both source and sourceComponent -- \"\n \"these are mutually exclusive; use one or the other\")\n\n # --- ORD checks: attribute ordering within block ---\n # Skip blocks where the standard attribute ordering convention\n # does not apply (Connections has target: + function handlers;\n # Behavior has property-specific internal structure).\n if (len(block.categories) >= 2\n and block.type_name not in SKIP_ORD_TYPES):\n _check_ordering(block, emit)\n\n # --- STY-3: Multiple dot-notation properties from same group ---\n group_counts: \"Dict[str, List[int]]\" = {}\n for prop_name, prop_line in props.items():\n if \".\" in prop_name:\n group = prop_name.split(\".\")[0]\n # Skip attached property namespaces (these use dot by convention)\n if group not in (\n \"Layout\", \"Component\", \"Drag\", \"Keys\",\n \"Accessible\", \"LayoutMirroring\", \"ListView\",\n \"GridView\", \"TableView\", \"SwipeView\",\n ):\n group_counts.setdefault(group, []).append(prop_line)\n for group, group_lines in group_counts.items():\n if len(group_lines) >= 3:\n emit(min(group_lines), \"STY-3\",\n f\"{len(group_lines)} {group}.* properties using dot \"\n f\"notation -- use group notation: {group} {{ ... }}\")\n\n\ndef _check_ordering(block: Block, emit) -> None:\n \"\"\"Check that QML attributes appear in the expected order.\"\"\"\n prev_cat = -1\n for cat, lineno in block.categories:\n if cat \u003c prev_cat:\n cat_names = {\n CAT_ID: \"id\",\n CAT_PROP_DECL: \"property declaration\",\n CAT_SIGNAL_DECL: \"signal declaration\",\n CAT_PROP_ASSIGN: \"property assignment\",\n CAT_ATTACHED: \"attached property\",\n CAT_STATES: \"states\",\n CAT_TRANSITIONS: \"transitions\",\n CAT_HANDLER: \"signal handler\",\n CAT_CHILD: \"child object\",\n CAT_FUNCTION: \"function\",\n }\n expected = cat_names.get(prev_cat, \"previous attribute\")\n actual = cat_names.get(cat, \"this attribute\")\n emit(lineno, \"ORD-1\",\n f\"{actual} appears after {expected}\")\n return # Only report first ordering violation per block\n prev_cat = cat\n\n\ndef _scan_transparent_rects(\n filepath: str,\n lines: \"List[str]\",\n code_lines: \"List[str]\",\n) -> \"List[Finding]\":\n \"\"\"Find Rectangle blocks with color: 'transparent'.\n\n Handles both single-line `color: \"transparent\"` and multiline\n ternary expressions where \"transparent\" appears on a continuation\n line after `color:`.\n \"\"\"\n findings: \"List[Finding]\" = []\n depth = 0\n rect_depth = -1\n rect_start = 0\n in_color_expr = False\n\n for i, code_line in enumerate(code_lines):\n lineno = i + 1\n stripped = code_line.strip()\n\n open_b = code_line.count('{')\n close_b = code_line.count('}')\n\n if re.search(r'\\bRectangle\\s*\\{', stripped):\n rect_depth = depth + open_b\n rect_start = lineno\n in_color_expr = False\n\n # Check only direct properties of the Rectangle (same depth)\n elif rect_depth > 0 and depth == rect_depth:\n # Single-line match: color: \"transparent\" or\n # color: cond ? \"x\" : \"transparent\"\n if RE_PRF_TRANSPARENT.search(code_line):\n findings.append(Finding(\n filepath, lineno, \"PRF-1\",\n \"Rectangle with 'transparent' color -- \"\n \"use Item instead, or toggle visible on the \"\n \"Rectangle (creates geometry node even when \"\n \"transparent)\"\n ))\n rect_depth = -1\n # Track multiline color: expressions\n elif re.match(r'^\\s*color\\s*:', stripped):\n in_color_expr = True\n elif in_color_expr:\n # Continuation of color expression\n if RE_PRF_TRANSPARENT_CONT.search(code_line):\n findings.append(Finding(\n filepath, rect_start, \"PRF-1\",\n \"Rectangle with 'transparent' in color \"\n \"expression -- use Item instead, or toggle \"\n \"visible (creates geometry node even when \"\n \"transparent)\"\n ))\n rect_depth = -1\n in_color_expr = False\n # End of continuation if line doesn't look like\n # a continued expression\n if not stripped.endswith(('?', ':', '||', '&&')):\n in_color_expr = False\n else:\n in_color_expr = False\n\n depth += open_b - close_b\n if depth \u003c rect_depth:\n rect_depth = -1\n\n return findings\n\n\nRE_IMG_NETWORK_SRC = re.compile(r'source\\s*:.*https?://')\n\n\ndef _scan_image_async(\n filepath: str,\n lines: \"List[str]\",\n code_lines: \"List[str]\",\n) -> \"List[Finding]\":\n \"\"\"Flag Image blocks with network/dynamic source but no async.\n\n Uses raw lines (not comment-stripped) because URLs contain //\n which the comment stripper would remove.\n \"\"\"\n findings: \"List[Finding]\" = []\n depth = 0\n img_depth = -1\n img_start = 0\n has_async = False\n has_network_src = False\n\n for i, raw_line in enumerate(lines):\n lineno = i + 1\n code_line = code_lines[i]\n stripped = raw_line.strip()\n\n open_b = code_line.count('{')\n close_b = code_line.count('}')\n\n if re.match(r'Image\\s*\\{', stripped):\n img_depth = depth + open_b\n img_start = lineno\n has_async = False\n has_network_src = False\n elif img_depth > 0 and depth == img_depth:\n if \"asynchronous\" in raw_line:\n has_async = True\n if RE_IMG_NETWORK_SRC.search(raw_line):\n has_network_src = True\n\n depth += open_b - close_b\n if depth \u003c img_depth:\n # Image block closed\n if not has_async and has_network_src:\n findings.append(Finding(\n filepath, img_start, \"IMG-2\",\n \"Image with network source without \"\n \"asynchronous: true -- download+decode \"\n \"blocks the UI thread\"\n ))\n img_depth = -1\n\n return findings\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\nQML_SUFFIXES = (\".qml\",)\nRE_HUNK = re.compile(r\"^@@ -\\S+ \\+(\\d+)(?:,(\\d+))? @@\")\n\n\ndef changed_lines(diff_args: \"List[str]\") -> \"Tuple[str, Dict[str, Set[int]]]\":\n \"\"\"Return the repository root and, per changed QML file (relative to\n the root), the line numbers added or modified by `git diff \u003cdiff_args>`.\n Files with deletions only map to an empty set.\"\"\"\n def git(*args: str) -> str:\n return subprocess.run([\"git\", *args], stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n universal_newlines=True, check=True).stdout\n\n root = git(\"rev-parse\", \"--show-toplevel\").strip()\n diff = git(\"-C\", root, \"-c\", \"core.quotePath=false\", \"diff\", \"-U0\",\n \"--no-color\", \"--no-ext-diff\", \"--src-prefix=a/\",\n \"--dst-prefix=b/\", *diff_args)\n changed: \"Dict[str, Set[int]]\" = {}\n current: \"Optional[Set[int]]\" = None\n for line in diff.splitlines():\n if line.startswith(\"+++ \"):\n path = line[4:].rstrip(\"\\t\")\n current = None\n if path.startswith(\"b/\") and path.endswith(QML_SUFFIXES):\n current = changed.setdefault(path[2:], set())\n elif current is not None and line.startswith(\"@@\"):\n match = RE_HUNK.match(line)\n if match:\n start = int(match.group(1))\n count = int(match.group(2)) if match.group(2) is not None else 1\n current.update(range(start, start + count))\n return root, changed\n\n\nQMLLINT_HINT = (\"pass --qmllint=\u003cQt dir>/\u003cversion>/\u003ckit>/bin, \"\n \"e.g. ~/Qt/6.8.0/gcc_64/bin or C:\\\\Qt\\\\6.8.0\\\\msvc2022_64\\\\bin\")\n\n\ndef qmllint_in(directory: str) -> \"Optional[str]\":\n \"\"\"qmllint in a bin directory. which() adds .exe on Windows.\"\"\"\n if not directory or not os.path.isdir(directory):\n return None\n return shutil.which(\"qmllint\", path=directory)\n\n\ndef qt_host_bins() -> \"List[str]\":\n \"\"\"Host bin directories reported by qtpaths or qmake on PATH.\"\"\"\n dirs: \"List[str]\" = []\n for tool, flag in ((\"qtpaths6\", \"--query\"), (\"qtpaths\", \"--query\"),\n (\"qmake6\", \"-query\"), (\"qmake\", \"-query\")):\n exe = shutil.which(tool)\n if not exe:\n continue\n try:\n proc = subprocess.run([exe, flag, \"QT_HOST_BINS\"],\n stdout=subprocess.PIPE,\n stderr=subprocess.DEVNULL,\n universal_newlines=True, timeout=10)\n except (OSError, subprocess.SubprocessError):\n continue\n if proc.returncode == 0 and proc.stdout.strip():\n dirs.append(proc.stdout.strip())\n return dirs\n\n\ndef qt_installer_bins() -> \"List[str]\":\n \"\"\"Bin directories of Qt 6 kits installed by the Qt online\n installer (\u003croot>/\u003cversion>/\u003ckit>/bin), newest version first.\n Default roots: ~/Qt on Linux and macOS, C:\\\\Qt on Windows, /opt/Qt.\"\"\"\n roots = [os.path.join(os.path.expanduser(\"~\"), \"Qt\"), \"/opt/Qt\"]\n if os.name == \"nt\":\n roots.append(os.environ.get(\"SystemDrive\", \"C:\") + \"\\\\Qt\")\n found: \"List[Tuple[Tuple[int, int], str]]\" = []\n for root in roots:\n if not os.path.isdir(root):\n continue\n for version in os.listdir(root):\n match = re.fullmatch(r\"6\\.(\\d+)\\.(\\d+)\", version)\n if not match:\n continue\n version_dir = os.path.join(root, version)\n for kit in sorted(os.listdir(version_dir)):\n bin_dir = os.path.join(version_dir, kit, \"bin\")\n if os.path.isdir(bin_dir):\n found.append(((int(match.group(1)), int(match.group(2))),\n bin_dir))\n found.sort(key=lambda item: item[0], reverse=True)\n return [bin_dir for _, bin_dir in found]\n\n\ndef find_qmllint(explicit: str) -> \"Optional[str]\":\n \"\"\"Resolve the qmllint binary, in this order: an explicit binary or\n bin directory, $QT_HOST_PATH/bin, qmllint on PATH, the host bin\n directory from qtpaths or qmake, then the newest Qt 6 kit from the\n Qt online installer.\"\"\"\n if explicit:\n return qmllint_in(explicit) or shutil.which(explicit)\n candidate = qmllint_in(os.path.join(os.environ.get(\"QT_HOST_PATH\", \"\"), \"bin\"))\n if candidate:\n return candidate\n candidate = shutil.which(\"qmllint\")\n if candidate:\n return candidate\n for directory in qt_host_bins() + qt_installer_bins():\n candidate = qmllint_in(directory)\n if candidate:\n return candidate\n return None\n\n\ndef run_qmllint(qmllint: str, extra_args: \"List[str]\",\n files: \"List[str]\") -> \"List[Finding]\":\n \"\"\"Run qmllint on files and return its warnings as findings with\n rule `QL:\u003cwarning id>`. File-level warnings get line 0.\"\"\"\n proc = subprocess.run([qmllint, \"--json\", \"-\", *extra_args, *files],\n stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n universal_newlines=True)\n report = json.loads(proc.stdout)\n by_abs = {os.path.abspath(f): f for f in files}\n findings: \"List[Finding]\" = []\n for entry in report.get(\"files\", []):\n name = entry.get(\"filename\", \"\")\n name = by_abs.get(os.path.abspath(name), name)\n for w in entry.get(\"warnings\", []):\n findings.append(Finding(name, int(w.get(\"line\", 0)),\n \"QL:\" + w.get(\"id\", w.get(\"type\", \"?\")),\n w.get(\"message\", \"\").rstrip(\":\")))\n return findings\n\n\ndef main() -> int:\n if len(sys.argv) \u003c 2:\n print(\n \"Usage: qt_qml_lint.py [--json|--summary] [--qmllint[=\u003cpath>]] \"\n \"\u003cfile> [file ...]\",\n file=sys.stderr,\n )\n print(\n \" qt_qml_lint.py --files-from=- \u003c list.txt\",\n file=sys.stderr,\n )\n print(\n \" qt_qml_lint.py [--json|--summary] [--qmllint[=\u003cpath>]] \"\n \"--diff=\u003cgit diff args>\",\n file=sys.stderr,\n )\n return 2\n\n json_output = False\n summary_output = False\n diff_args: \"Optional[List[str]]\" = None\n qmllint_path: \"Optional[str]\" = None\n qmllint_args: \"List[str]\" = []\n files: \"List[str]\" = []\n for arg in sys.argv[1:]:\n if arg == \"--json\":\n json_output = True\n elif arg == \"--summary\":\n summary_output = True\n elif arg.startswith(\"--diff=\"):\n diff_args = shlex.split(arg[len(\"--diff=\"):])\n elif arg == \"--qmllint\" or arg.startswith(\"--qmllint=\"):\n qmllint_path = arg[len(\"--qmllint=\"):]\n elif arg.startswith(\"--qmllint-args=\"):\n qmllint_args = shlex.split(arg[len(\"--qmllint-args=\"):])\n elif arg == \"--files-from=-\":\n files.extend(\n line.strip() for line in sys.stdin if line.strip()\n )\n else:\n files.append(arg)\n\n root: \"Optional[str]\" = None\n changed: \"Dict[str, Set[int]]\" = {}\n if diff_args is not None:\n if files:\n print(\"Error: --diff takes no file arguments\", file=sys.stderr)\n return 2\n try:\n root, changed = changed_lines(diff_args)\n except (OSError, subprocess.CalledProcessError) as e:\n detail = getattr(e, \"stderr\", None) or str(e)\n print(f\"Error: git diff failed: {detail.strip()}\", file=sys.stderr)\n return 2\n files = [os.path.join(root, rel) for rel in sorted(changed)\n if os.path.isfile(os.path.join(root, rel))]\n if not files:\n print(f\"No changed QML files in git diff {' '.join(diff_args)}\")\n return 0\n elif not files:\n print(\"Error: no input files specified\", file=sys.stderr)\n return 2\n\n all_findings: \"List[Finding]\" = []\n for filepath in files:\n all_findings.extend(scan_file(filepath))\n\n qmllint_status = \"not requested\"\n if qmllint_path is not None:\n qmllint = find_qmllint(qmllint_path)\n if qmllint is None:\n qmllint_status = f\"not available ({QMLLINT_HINT})\"\n else:\n try:\n all_findings.extend(run_qmllint(qmllint, qmllint_args, files))\n qmllint_status = f\"ran ({qmllint})\"\n except (OSError, ValueError) as e:\n qmllint_status = f\"failed ({e})\"\n if not summary_output:\n print(f\"qmllint: {qmllint_status}\", file=sys.stderr)\n\n if root is not None:\n all_findings = [\n f for f in all_findings\n if f.line in changed.get(os.path.relpath(f.file, root), ())\n ]\n\n # Sort by file, then line\n all_findings.sort(key=lambda f: (f.file, f.line))\n\n # Deduplicate (same file + line + rule; qmllint can report several\n # distinct messages per location, so its rules also key on message)\n seen: \"Set[Tuple[str, int, str, str]]\" = set()\n deduped: \"List[Finding]\" = []\n for f in all_findings:\n key = (f.file, f.line, f.rule,\n f.message if f.rule.startswith(\"QL:\") else \"\")\n if key not in seen:\n seen.add(key)\n deduped.append(f)\n\n if json_output:\n print(json.dumps(\n [asdict(f) for f in deduped],\n indent=2,\n ))\n elif summary_output:\n if root is None:\n dirs = [os.path.dirname(os.path.abspath(f)) for f in files]\n root = os.path.commonpath(dirs)\n print(f\"Paths relative to {root}/\")\n if diff_args is not None:\n total = sum(len(lines) for lines in changed.values())\n print(f\"Diff `git diff {' '.join(diff_args)}`: \"\n f\"{len(changed)} QML file(s), \"\n f\"{total} added or modified line(s)\")\n print(\" \" + \" \".join(\n f\"{rel} +{len(lines)}\"\n for rel, lines in sorted(changed.items())\n ))\n if qmllint_path is not None:\n print(f\"qmllint: {qmllint_status}\")\n print_summary(deduped, root)\n else:\n for finding in deduped:\n print(finding)\n\n return 1 if deduped else 0\n\n\ndef print_summary(findings: \"List[Finding]\", root: str) -> None:\n \"\"\"Print findings grouped by (rule, message), locations per file,\n with paths relative to root. Line 0 (file-level qmllint warnings)\n prints as the bare path.\n\n Example:\n ORD-1 property assignment appears after child object (5)\n Main.qml:19,24,29 controls/Panel.qml:12,40\n \"\"\"\n groups: \"Dict[Tuple[str, str], Dict[str, List[int]]]\" = {}\n for f in findings:\n path = os.path.relpath(os.path.abspath(f.file), root)\n by_file = groups.setdefault((f.rule, f.message), {})\n by_file.setdefault(path, []).append(f.line)\n\n total = len(findings)\n print(f\"{total} finding(s), {len(groups)} distinct rule/message pair(s)\")\n for (rule, message), by_file in sorted(groups.items()):\n count = sum(len(v) for v in by_file.values())\n print(f\"{rule} {message} ({count})\")\n locs = []\n for path, lines in sorted(by_file.items()):\n nums = [str(n) for n in sorted(lines) if n > 0]\n locs.append(f\"{path}:{','.join(nums)}\" if nums else path)\n print(\" \" + \" \".join(locs))\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":69770,"content_sha256":"beaac8c7047211963d59b8121d5144708bee9cf7e6f5c53b111a38d99ac5e336"},{"filename":"references/qt-qml-review-checklist.md","content":"# Qt6 QML Review Checklist (linter rules)\n\nRationale for every rule enforced by `qt_qml_lint.py`. The linter\nmessage carries the actionable part; this file is the long-form\nreference for humans and for writing mitigations. Semantic rules that\nneed an agent live in `agent-rules.md`.\n\n**Token note**: the orchestrator does not need to load this file\nduring a review -- lint messages are self-describing. Consult it only\nwhen a mitigation needs more background than the message gives.\n\n---\n\n## 1. Imports\n\n### IMP-1: Redundant QtQuick.Window\n`import QtQuick.Window` is unnecessary when `import QtQuick` is\npresent. In Qt 6, Window types were folded into the QtQuick module.\n\n### IMP-2: Versioned imports\nQt 6 dropped the requirement for version numbers on all imports.\nVersioned imports (`import QtQuick 2.15`) cap the API surface and\ncause \"missing type\" confusion.\n\n### IMP-3: Plain Controls import with customization\nWhen customizing `contentItem`, `background`, `indicator`, or\n`handle`, import a specific style (`QtQuick.Controls.Basic`) rather\nthan plain `QtQuick.Controls`. The default style abstraction layer\ncan produce unexpected rendering.\n\n### IMP-4: Import ordering\nOrder imports: Qt modules first, then third-party, then local C++,\nthen QML folder imports. Consistent ordering aids readability and\nmatches `qmlformat --sort-imports`.\nIf this causes shadowing issues, because types with the same name\nare defined in various modules, use a namespaced import:\n`import mymodule as MM`\n\n### IMP-5: Qt.include() deprecated\n`Qt.include()` was deprecated in Qt 5.14 and removed from Qt 6\ndocumentation. Use ES module imports or explicit QML imports.\n\n### IMP-6: Duplicate imports\nThe same module imported more than once. Remove the duplicate.\n\n### IMP-7: Qt 5 graphical effects import\n`QtGraphicalEffects` does not exist in Qt 6 and\n`Qt5Compat.GraphicalEffects` is a migration bridge only. Use\n`MultiEffect` from `QtQuick.Effects` (Qt 6.5+), which combines blur,\nshadow and colorization in a single pass.\n\n---\n\n## 2. Attribute Ordering\n\n### ORD-1: QML attribute ordering convention\nWithin each QML object block, attributes must appear in this order:\n\n1. `id`\n2. Property declarations (`property type name`, `required property`)\n3. Signal declarations (`signal name()`)\n4. Property assignments (`width: 100`, `color: \"red\"`)\n5. Attached properties (`Layout.fillWidth`, `Drag.active`)\n6. `states`\n7. `transitions`\n8. Signal handlers (`onClicked`, `Component.onCompleted`)\n9. Child objects (visual first, then non-visual)\n10. JavaScript functions\n\nThis ordering ensures the most intrinsic properties are visible\nfirst. Signal handlers should be grouped by semantic relation,\nand ordered shortest-first as a fallback.\n\nThe linter reports only the first ordering violation per block.\nBlocks with special internal structure (Connections, Behavior,\nanimation types, State, Transition, PropertyChanges) are exempt.\n\n---\n\n## 3. Bindings & Properties\n\n### BND-1: property var\nUse typed properties (`int`, `string`, `color`, etc.) instead of\n`property var`. Typed properties enable `qmlsc` compilation to C++,\neliminate meta-object overhead, and allow `qmllint` type checking.\nMatches qmllint's `prefer-non-var-properties` warning.\n\n### BND-2: Imperative = destroys binding\nAny `property = value` in JavaScript permanently replaces the\ndeclarative binding with a static value. Use `Qt.binding(() => expr)`\nto restore reactivity if needed. The `qt.qml.binding.removal`\nlogging category (Qt 5.10+) is the only runtime diagnostic. qmllint\ndoes NOT detect this.\n\n### BND-3: Qt.binding with old-style function\nUse arrow syntax: `Qt.binding(() => expr)` not\n`Qt.binding(function() { return expr })`. Arrow functions avoid\n`this` context issues inside `Qt.binding()`.\n\n### BND-5: list\u003c> property type\nQML `list` properties have no granular change signals for add, move,\nor remove. Only whole-list replacement triggers notification. Binding\nexpensive operations to list properties causes subtle update bugs.\nConsider a `ListModel` or emit change signals manually.\n\n---\n\n## 4. Layout & Anchoring\n\n### LAY-1: anchors + Layout on same item\nAnchors and `Layout.*` properties conflict. An item managed by a\nLayout must use only `Layout.*` for sizing and positioning.\n\n### LAY-2: Bare width/height inside Layout child\nSetting `width` or `height` directly on a Layout-managed item\nsilently breaks the layout's size negotiation. Use\n`Layout.preferredWidth`, `Layout.fillWidth`, etc.\n\n### LAY-3: Four anchor edges instead of fill\nSetting `anchors.left`, `anchors.right`, `anchors.top`, and\n`anchors.bottom` separately to the `parent`'s values is verbose.\nUse `anchors.fill: parent`.\n\n### LAY-5: Anchoring to parent.parent\nAnchors must only point to the `parent` or sibling items.\n`parent.parent` is a fragile cross-branch reference that breaks as\nsoon as the component is reparented.\n\n### LAY-6: Bare x/y inside Layout child\nLayouts manage positioning. Setting `x:` or `y:` on a layout child\nis ignored by the layout engine and creates confusion.\n\n---\n\n## 5. Loader & Dynamic Creation\n\n### LDR-1: Loader.item without status guard\nWith `asynchronous: true`, `Loader.item` is `null` until\n`status === Loader.Ready`. Binding to `Loader.item.someProp` without\na guard causes `TypeError`. Use optional chaining (`?.`) or gate on\n`Loader.status`.\n\n### LDR-2: Qt.createComponent with string URL\nString-based `Qt.createComponent()` is not type-safe and is not supported\nby QML tooling. Prefer `Component {}` definitions, or the module URI +\ntype name overload of `Qt.createComponent`.\n\n### LDR-3: Qt.createQmlObject\nParses a QML string at runtime on every call. No component caching.\nSlow and error-prone. Use `Loader` or `Component.createObject()`.\n\n### LDR-5: Loader with both source and sourceComponent\nThese are mutually exclusive. Setting both is confusing, as the\nbehavior is unspecified.\n\n---\n\n## 6. ListView & Delegates\n\n### DEL-1: model.roleName without required property\nModern Qt 6 best practice is to declare `required property` for each\nmodel role. Once any required property is declared, the implicit\n`model` context object is no longer injected (but can also be explicitly\nrequested as a `required property`). Required properties\nenable compilation, eliminate `unqualified` warnings and have generally\nbetter tooling support (e.g. in qmlls, qmllint).\n\n### DEL-2: state in delegate with reuseItems\nWith `reuseItems: true`, a delegate is pooled and rebound rather\nthan recreated. Bindings to model roles re-evaluate automatically;\nall other state persists across reuse and bleeds between items.\nReset non-model-derived state in `ListView.onReused`\n(`Component.onCompleted` runs only once, at first creation).\n\n### DEL-3: connect() in Component.onCompleted without receiver\nDirect `connect()` without a receiver creates signal connections that\noutlive delegate destruction, causing `TypeError` when the signal fires\non a destroyed delegate. Use `Connections {}` objects instead, or pass\nan explicit receiver.\n\n### DEL-4: Component.onCompleted with reuseItems\nWith `reuseItems: true`, a pooled delegate is not recreated, so\n`Component.onCompleted` does not fire again when it is reused. Move\nper-item initialisation to `ListView.onReused`. (The related check\nfor a missing `required property int index` needs the delegate\ncontext and is an agent rule, A4.)\n\n---\n\n## 7. States & Transitions\n\n### STA-1: PropertyChanges target: syntax (Qt 6)\nQt 6 prefers `PropertyChanges { myId.property: value }` syntax. The old\n`target: myId; property: value` form still works but should only be used\nwith ui.qml files used with Qt Design Studio.\n\n### STA-2: Transition without from/to\nA `Transition {}` without explicit `from`/`to` fires on every state\nchange, including unintended ones. Use explicit `from`/`to` pairs.\nIf there are multiple rules, a catch-all transition will only be picked\nif no other rule matches either on `from` or `to`.\n\n### STA-3: Top-level states in reusable component\n`states` is a `QQmlListProperty` -- assigning from outside a\ncomponent *adds* to the existing list rather than replacing it,\ncausing conflicts. Wrap internal states in a `StateGroup`. Only\nflagged when the file has `required property` declarations\n(indicating it is a reusable component).\n\n### STA-4: PropertyChanges instead of imperative assignments\nDrive state-dependent property values through declarative PropertyChanges\nblocks rather than assigning properties imperatively (`obj.prop = ...`)\nfrom signal handlers or scripts. Only the declarative form participates in\n`restoreEntryValues`, so original values/bindings are restored when the\nstate is left.\n\n---\n\n## 8. Images\n\n### IMG-1: Image without sourceSize\nWithout `sourceSize`, Qt decodes the full-resolution image into GPU\nmemory. A 4000x3000 photo displayed at 100x75 still allocates ~48MB\nof texture memory. Set `sourceSize` to display dimensions for large\nimages.\n\n### IMG-2: Network Image without asynchronous: true\nImage decoding blocks the UI thread by default. For network sources,\nthe entire download+decode is synchronous without `asynchronous: true`.\n\n---\n\n## 9. Performance & Rendering\n\n### PRF-1: Transparent Rectangle\n`Rectangle { color: \"transparent\" }` creates a scene graph geometry\nnode even when transparent. Use `Item` for grouping -- it generates\nno geometry node. The cost compounds in delegates.\n\n### PRF-2: opacity: 0 without animation\n`opacity: 0` still incurs rendering overhead and retains keyboard\nfocus. `visible: false` skips rendering entirely and removes from\ninput handling. Use `opacity: 0` only during fade animations.\nSuppressed when the file contains opacity animation declarations.\n\n### PRF-3: clip: true\nQt docs: \"Clipping is a visual effect, NOT an optimization.\" Forces a\nseparate scene graph batch (scissor/stencil). Acceptable on ListView\n(many children) but costly on small items.\n\n### PRF-4: font.pixelSize animation\nEvery `font.pixelSize` change triggers full text relayout (glyph\nshaping, line breaking). Use a `scale` transform on the `Text`\nelement for size animations instead.\n\n### PRF-5: Text.RichText\nRichText invokes a full HTML/CSS parser, significantly more expensive\nthan PlainText or StyledText. Use `textFormat: Text.PlainText` unless\nrich formatting is needed.\n\n### PRF-6: layer.enabled\nRenders the subtree to an offscreen FBO, then composites as texture.\nThe layered item cannot be batched with siblings. Multisampling on\nlayers is especially expensive. Enable only during effects/animations.\n\n---\n\n## 10. Style & Conventions\n\n### STY-1: Top-level component missing id: root\nThe QML convention is `id: root` on the top-level component. This\nenables qualified lookup (`root.someProperty`).\n\n### STY-3: Multiple dot-notation for same group\nWhen setting 3+ sub-properties of the same group (e.g.,\n`sourceSize.width`, `sourceSize.height`, `sourceSize...`), use group\nnotation instead: `sourceSize { width: 32; height: 32 }`. Attached\nproperty namespaces (Layout, Component, etc.) are exempt.\n\n### STY-6: id not camelCase\nQML convention is `lowerCamelCase` for ids. Underscore or UPPER ids\nbreak convention.\n\n---\n\n## 11. Signals & Connections\n\n### SIG-1: Connections without explicit target\nDefault target is `parent`, which causes unintended signal handling\nif the parent type changes. Always set `target` explicitly. Set\n`target: null` if the real target is assigned later at runtime.\n\n### SIG-2: Deprecated onFoo: handler syntax\nThe `onFoo:` syntax in `Connections` blocks is deprecated since\nQt 5.15. Use `function onFoo() {}` instead.\n\n### SIG-3: Mixed handler syntax in Connections\nMixing old `onFoo:` handlers with new `function onFoo()` handlers in\nthe same `Connections` block silently ignores the function-based\nhandlers. Use one style consistently.\n\n---\n\n## 12. Error Handling & Security\n\n### ERR-1: Hardcoded HTTP URL\nUnencrypted `http://` URLs expose data in plaintext. Use `https://`\nfor any network endpoint. Localhost and test URLs are excluded.\n\n### ERR-2: Hardcoded Unix paths\n`/tmp/` and other Unix-specific paths do not exist on Windows. Qt\nprovides `QStandardPaths::writableLocation(QStandardPaths::TempLocation)`\nfor cross-platform temporary file access.\n\n---\n\n## 13. JavaScript Quality\n\n### JS-1: var instead of let/const\n`var` has function scope and hoisting, causing subtle bugs. `let` and\n`const` have block scope. Qt coding instructions mandate `let`/`const`.\n\n### JS-2: Loose equality\nLoose equality (`==`/`!=`) performs type coercion, which is almost\nnever desired in QML property comparisons. Use strict equality\n(`===`/`!==`). Matches qmllint's `equality-type-coercion` warning.\n\n### JS-3: Dynamic code execution\nDynamic JS code execution (such as the `eval` function) blocks JIT\ncompilation in QV4 and is a security risk. qmllint flags it. There\nis never a valid use case in QML.\n\n---\n\nCopyright (C) 2026 The Qt Company.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12825,"content_sha256":"3af238df9b983e3c4c71676c35d0bfeb61208d68cd15a3908d71b42e59951ae1"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Qt QML Code Review","type":"text"}]},{"type":"paragraph","content":[{"text":"Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Scope detection","type":"text"}]},{"type":"paragraph","content":[{"text":"Diff scope","type":"text","marks":[{"type":"strong"}]},{"text":" -- \"this commit\", \"these changes\", \"the diff\", \"what I changed\", \"staged changes\", \"before I commit\". The range is ","type":"text"},{"text":"HEAD","type":"text","marks":[{"type":"code_inline"}]},{"text":" (working tree plus index), ","type":"text"},{"text":"--cached","type":"text","marks":[{"type":"code_inline"}]},{"text":" for staged changes only, or ","type":"text"},{"text":"HEAD~1..HEAD","type":"text","marks":[{"type":"code_inline"}]},{"text":" for \"this commit\". Do not load the diff itself: the linter's ","type":"text"},{"text":"--diff","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode (Phase 1) lists the changed files with their changed-line counts and reports only findings on changed lines.","type":"text"}]},{"type":"paragraph","content":[{"text":"Codebase scope","type":"text","marks":[{"type":"strong"}]},{"text":" -- \"review the codebase\", \"audit src/\", or an explicit path with no commit language. Glob ","type":"text"},{"text":"*.qml","type":"text","marks":[{"type":"code_inline"}]},{"text":" under the path.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Token budget rules","type":"text"}]},{"type":"paragraph","content":[{"text":"These rules exist to keep the review cheap. Follow them.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not load the reference files into the main context.","type":"text","marks":[{"type":"strong"}]},{"text":" Agents read ","type":"text"},{"text":"references/agent-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" themselves; you pass its absolute path and the section IDs. Only an inline run (Phase 2) reads it in the main context. Lint messages are self-describing; the checklist is for humans.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass lint results in ","type":"text","marks":[{"type":"strong"}]},{"text":"--summary","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" form","type":"text","marks":[{"type":"strong"}]},{"text":" (grouped by rule, paths relative to a root printed once), never the raw per-line listing or qmllint's JSON.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Point agents at content, never paste it.","type":"text","marks":[{"type":"strong"}]},{"text":" Every token you write into a prompt is output on the orchestrating model, the most expensive tokens in the review. Prompts carry paths, commands and section IDs; the agents load the rules, the files and the diff. In diff scope an agent gets the ","type":"text"},{"text":"git diff -U50 \u003crange> -- \u003cfiles>","type":"text","marks":[{"type":"code_inline"}]},{"text":" command instead of the hunks, and reads other files only to trace a specific symbol.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scale the agent count to the scope","type":"text","marks":[{"type":"strong"}]},{"text":" (see Phase 2). Launch no agents for a small scope and split a large one into shards of about equal line count.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the dedicated review agent where the tool has one.","type":"text","marks":[{"type":"strong"}]},{"text":" If the ","type":"text"},{"text":"qt-development-skills:qml-review-agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" definition shipped with this plugin is available, launch the agents with it (read-only, tools limited to Read, Grep, Glob and read-only Bash); otherwise use the tool's general-purpose agent.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Execution order","type":"text"}]},{"type":"paragraph","content":[{"text":"Never skip a phase.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 1: Linting (Python linter + qmllint)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# codebase scope\npython3 \u003cskill>/references/lint-scripts/qt_qml_lint.py --summary --qmllint \u003cfiles...>\n# diff scope, run inside the repository; no file arguments\npython3 \u003cskill>/references/lint-scripts/qt_qml_lint.py --summary --qmllint --diff=\u003crange>\n# fall back to `python` if python3 is not found","type":"text"}]},{"type":"paragraph","content":[{"text":"Python 3.7+, no dependencies. If Python is unavailable, warn and continue (in diff scope, take the file list from ","type":"text"},{"text":"git diff --stat \u003crange>","type":"text","marks":[{"type":"code_inline"}]},{"text":"). The linter is authoritative; do not second-guess its output. Rule families: IMP, ORD, BND, LAY, LDR, DEL, STA, IMG, PRF, STY, SIG, ERR, JS (48+ checks). Exit code 1 simply means findings exist; 2 means a usage or git error.","type":"text"}]},{"type":"paragraph","content":[{"text":"--qmllint","type":"text","marks":[{"type":"code_inline"}]},{"text":" also runs qmllint and merges its warnings as ","type":"text"},{"text":"QL:\u003cid>","type":"text","marks":[{"type":"code_inline"}]},{"text":" rows; add ","type":"text"},{"text":"--qmllint-args=\"-I \u003cdir>\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" for a build directory with QML modules. The linter looks in ","type":"text"},{"text":"$QT_HOST_PATH/bin","type":"text","marks":[{"type":"code_inline"}]},{"text":", on PATH, in the host bin directory reported by ","type":"text"},{"text":"qtpaths","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"qmake","type":"text","marks":[{"type":"code_inline"}]},{"text":", and in the newest Qt 6 kit of a Qt online installer under ","type":"text"},{"text":"~/Qt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"C:\\Qt","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"/opt/Qt","type":"text","marks":[{"type":"code_inline"}]},{"text":" (","type":"text"},{"text":"\u003cversion>/\u003ckit>/bin","type":"text","marks":[{"type":"code_inline"}]},{"text":", kits such as ","type":"text"},{"text":"gcc_64","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"macos","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"msvc2022_64","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mingw_64","type":"text","marks":[{"type":"code_inline"}]},{"text":"). If it reports ","type":"text"},{"text":"not available","type":"text","marks":[{"type":"code_inline"}]},{"text":" but Qt is installed elsewhere (a custom installer path, or ","type":"text"},{"text":"CMAKE_PREFIX_PATH","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the project's ","type":"text"},{"text":"CMakeCache.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":"), rerun once with ","type":"text"},{"text":"--qmllint=\u003cthat kit>/bin","type":"text","marks":[{"type":"code_inline"}]},{"text":"; it accepts the binary or its bin directory. Do not search further. Copy the printed ","type":"text"},{"text":"qmllint:","type":"text","marks":[{"type":"code_inline"}]},{"text":" status into the report header. qmllint is authoritative for type-level issues, the Python linter for style, ordering and performance patterns.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 2: Deep analysis (inline, or one agent per shard)","type":"text"}]},{"type":"paragraph","content":[{"text":"The seven sections of ","type":"text"},{"text":"references/agent-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" are the analysis. Agents read it themselves; read it yourself only when running inline:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Section","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mission","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bindings & Properties","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Layout & Anchoring","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Component Loading & Lifecycle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ListView & Delegates","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"States, Transitions & Migration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Performance & Code Quality","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"A7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Logic & Wiring","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Choose the fan-out from the scope size. Count the lines of the files in scope (the changed lines in diff scope), taken from the lint summary or ","type":"text"},{"text":"wc -l","type":"text","marks":[{"type":"code_inline"}]},{"text":". A block is about 1,500 lines; the line count alone decides the split, never the number of files.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scope","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Agents","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Up to 1,500 lines","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"0 (inline)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"More than 1,500 lines","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"one per shard, ","type":"text"},{"text":"ceil(lines / 1500)","type":"text","marks":[{"type":"code_inline"}]},{"text":" shards","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Inline.","type":"text","marks":[{"type":"strong"}]},{"text":" A small scope does not repay the cost of agents, which each rebuild the context and roughly double the review's token cost for the same findings. Do the analysis yourself: read ","type":"text"},{"text":"references/agent-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the files (or the diff) with the one Bash call shown under the agent prompt below, and apply A1-A7 in a single pass under the same rules, confidence thresholds and output format. Do not re-report lint rows, and do not run inline above the limit because the code looks simple. Report ","type":"text"},{"text":"Agents: 0 (inline)","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Shards.","type":"text","marks":[{"type":"strong"}]},{"text":" Cut the files, in directory order, into n = ","type":"text"},{"text":"ceil(lines / 1500)","type":"text","marks":[{"type":"code_inline"}]},{"text":" shards of about ","type":"text"},{"text":"lines / n","type":"text","marks":[{"type":"code_inline"}]},{"text":" lines each; how many files a shard holds does not matter. Keep a directory in one shard where it fits, so that components and the files that instantiate them land together; split a larger directory by file. A file larger than a shard gets a shard to itself. Each shard agent applies all seven sections to its shard only and may Grep the whole scope to trace an id or a model role. Run up to six shard agents at a time and the rest in a second wave. Cross-shard checks (ids, model roles) are then best effort; say so in the report header.","type":"text"}]},{"type":"paragraph","content":[{"text":"Launch the agents in parallel, named after their shard (e.g. \"Review shard 2/6: qml/controls\"). Keep each prompt short; it contains only:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The absolute path of ","type":"text"},{"text":"references/agent-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and the section IDs to apply (A1-A7)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The root directory from the lint summary, the files in scope relative to it and, in diff scope, the ","type":"text"},{"text":"git diff -U50 \u003crange> -- \u003cfiles>","type":"text","marks":[{"type":"code_inline"}]},{"text":" command for them","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The lint summary rows (including ","type":"text"},{"text":"QL:","type":"text","marks":[{"type":"code_inline"}]},{"text":" rows) for that agent's own files only (a shard agent never needs the other shards' rows), with the instruction \"do not re-report these lines\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The instruction to load everything with one Bash call before analysing, and to return findings in the format at the top of the rules file. Each tool call re-sends the agent's whole context, so one batched read instead of one Read per file is the single largest saving on large scopes. Both forms print per-file line numbers for the ","type":"text"},{"text":"file:line","type":"text","marks":[{"type":"code_inline"}]},{"text":" locations (","type":"text"},{"text":"cat -n","type":"text","marks":[{"type":"code_inline"}]},{"text":" on several files numbers them as one run, and ","type":"text"},{"text":"git diff","type":"text","marks":[{"type":"code_inline"}]},{"text":" has no line numbers):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# codebase scope\ncd \u003croot> && cat \u003crules> && for f in \u003cfiles...>; do echo \"== $f\"; cat -n \"$f\"; done\n# diff scope; numbers context and added lines with the new-file line\ncd \u003croot> && cat \u003crules> && git diff -U50 \u003crange> -- \u003cfiles> | awk '/^@@/{split($3,a,/[+,]/);n=a[2];h=1;print;next} /^diff /{h=0} h&&/^[ +]/{printf \"%5d %s\\n\",n++,$0;next} {print}'","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Agents are read-only and must never edit files. In tools without agents, run each mission as whatever prompt chain or subprocess the tool supports; the contract is the same.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Phase 3: Consolidation","type":"text"}]},{"type":"paragraph","content":[{"text":"Merge lint and agent findings. Deduplicate on file+line+issue; when duplicates disagree on confidence, keep the block with the highest score and its trace, never average scores. Keep at most 10 investigation targets, and renumber the agents' D-/I- blocks consecutively. Findings below confidence 60 are suppressed. Write the report in the format below.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output format","type":"text"}]},{"type":"paragraph","content":[{"text":"Use exactly this structure. Lint and qmllint findings are grouped by rule with one mitigation each; only deep-analysis findings get a full block, copied verbatim from the agents.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"## QML Code Review Report\n\n**Scope**: [diff: `git diff HEAD~1..HEAD` | files: \u003cpaths>]\n**Files reviewed**: N\n**Issues found**: N (M from lint, K from deep analysis)\n**Agents**: N (shards ... | inline)\n**qmllint**: [ran / not available / failed]\n\n---\n\n### Lint findings\n\n| Rule | Count | Locations | Mitigation |\n|---|---|---|---|\n| IMP-2 Versioned import | 3 | `Main.qml:1,2`, `Panel.qml:1` | Drop the version numbers. |\n| ... | | | |\n\n---\n\n### Deep analysis findings\n\n\u003c[D-NNN] blocks from the agents>\n\n---\n\n### Investigation targets (human verification needed)\n\n\u003c[I-NNN] blocks from the agents, at most 10>\n\n---\n\n### Summary\n\n| Category | Lint | Deep | Investigate | Total |\n|---|---|---|---|---|\n| ... | N | N | N | N |\n| **Total** | **M** | **K** | **I** | **N** |\n\nFindings below confidence 60 are suppressed.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/agent-rules.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- agent missions A1-A7 and the agent output format (read by the agents, not the orchestrator)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/qt-qml-review-checklist.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- rationale for every linter rule (human reference, not loaded during a review)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/lint-scripts/qt_qml_lint.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- single-pass linter, ","type":"text"},{"text":"--summary","type":"text","marks":[{"type":"code_inline"}]},{"text":" for grouped output, ","type":"text"},{"text":"--diff=\u003crange>","type":"text","marks":[{"type":"code_inline"}]},{"text":" for changed lines only, ","type":"text"},{"text":"--qmllint","type":"text","marks":[{"type":"code_inline"}]},{"text":" to merge qmllint, ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for tooling","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"paragraph","content":[{"text":"Copyright (C) 2026 The Qt Company.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"qt-qml-review","author":"@skillopedia","source":{"stars":522,"repo_name":"agent-skills","origin_url":"https://github.com/TheQtCompanyRnD/agent-skills/blob/HEAD/skills/qt-qml-review/SKILL.md","repo_owner":"TheQtCompanyRnD","body_sha256":"c88ccaae48eff046582f582291ff420dde47b85bd4eeb52436c9b47aba96382c","cluster_key":"85af53f8a3e27c5ff1bccbe76ed8c3755fee7fe047027ee19269d750d1c046c3","clean_bundle":{"format":"clean-skill-bundle-v1","source":"TheQtCompanyRnD/agent-skills/skills/qt-qml-review/SKILL.md","text_bytes":95087,"source_commit":"6845f02d41171a6df8b6e6fe225b7f5d1a6f4adf","skills_sh_rank":13651,"attachment_count":4,"text_attachments":4,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/qt-qml-review/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"license":"LicenseRef-Qt-Commercial OR BSD-3-Clause","version":"v1","category":"software-engineering","metadata":{"author":"qt-ai-skills","version":"2.0","category":"review","qt-version":"6.x"},"import_tag":"skills-sh-top20k-2026-11","description":"Invoke when the user asks to review, check, audit, or look over Qt6 QML code -- or suggest before committing. Runs deterministic linting (48+ rules) then deep-analysis agents (scaled to the size of the change) covering bindings, layout, loaders, delegates, states, and performance. Optionally invokes system qmllint for type-level checks. Reports only high-confidence issues (>80/100) with structured mitigations. Read-only -- never modifies code.","compatibility":"Designed for Claude Code, GitHub Copilot, and similar agents.","disable-model-invocation":false}},"renderedAt":1793686663958}

Qt QML Code Review Read-only review of Qt6 QML code: a deterministic Python linter (with qmllint folded in when available) first, then agent-driven deep analysis across seven rule domains, then one consolidated report. Suggest it before the user commits QML code. Scope detection Diff scope -- "this commit", "these changes", "the diff", "what I changed", "staged changes", "before I commit". The range is (working tree plus index), for staged changes only, or for "this commit". Do not load the diff itself: the linter's mode (Phase 1) lists the changed files with their changed-line counts and rep…