Skip to content

feat(extract): add QML support and Qt C++/QML bridge (#1716)#1748

Open
Hamza-Masuadi wants to merge 4 commits into
Graphify-Labs:v8from
Hamza-Masuadi:feat/qml-support
Open

feat(extract): add QML support and Qt C++/QML bridge (#1716)#1748
Hamza-Masuadi wants to merge 4 commits into
Graphify-Labs:v8from
Hamza-Masuadi:feat/qml-support

Conversation

@Hamza-Masuadi

@Hamza-Masuadi Hamza-Masuadi commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Adds Qt/QML support to graphify's deterministic AST extraction layer, covering the full scope of #1716:

  1. QML grammar support — new tree-sitter-qmljs extractor for .qml files
  2. Cross-language C++ ↔ QML bindingsQML_NAMED_ELEMENT / qmlRegisterType aliases resolve onto real C++ class nodes
  3. Qt macro handlingsignals:, slots:, Q_PROPERTY, and bare Q_OBJECT/Q_GADGET parsing fixes

QML extractor (graphify/extractors/qml.py)

Walks the QML object tree and models:

  • Real objects vs grouped bindings (uppercase type vs lowercase property group)
  • File root as a reusable component named after the file stem
  • id: bindings, properties, functions, signals, inline components, enums
  • Behavior on <prop> { ... } and ui_object_array children
  • Same-file references/calls (with member-call receiver validation via func_owner)
  • Cross-file component instantiation via sourceless stubs + existing stub-rewire
  • Import statements as imports_from edges

Wired into _DISPATCH, LANGUAGE_EXTRACTORS, CODE_EXTENSIONS, and _LANG_FAMILY.

C++ / Qt bridge (extractors/engine.py + extract.py)

  • signals: / slots: bodyless prototypes become tagged signal / slot nodes
  • Q_PROPERTY(type name READ … WRITE … NOTIFY …) emits a property node plus accessor edges
  • QML_NAMED_ELEMENT("Alias") and qmlRegisterType<T>(…, "Alias") feed _resolve_cpp_qml_aliases
  • Bare Q_OBJECT/Q_GADGET get a semicolon preprocess so following signals:/slots: sections parse cleanly
  • Macro noise (Q_OBJECT, QML_NAMED_ELEMENT, …) is suppressed as field nodes

tree-sitter-qmljs dependency

tree-sitter-qmljs is not on PyPI, so pip install graphifyy[qml] cannot resolve the extra on its own (and PyPI rejects git-URL deps in published metadata). Left out of the all extra for the same reason.

Test plan

  • 17 unit tests in tests/test_qml.py (components, bindings, Behavior, ids, stubs, builtins, member-call disambiguation, bare refs, arrays, imports, inline components, missing grammar)
  • 9 unit tests in tests/test_cpp_qml_bridge.py (named-element alias, bare QML_ELEMENT, ambiguous alias guard, qmlRegisterType, signals, slots, Q_PROPERTY accessors, macro-noise suppression, build survival)
  • Related C++/QML suites green; ruff clean on touched files
  • Smoke-tested against a real Qt/QML app (~450 .qml files): zero extraction crashes; signal nodes and alias bridging verified

QML files declare a tree of typed objects (Item { id: root; Text { ... } }).
The new extractor walks that tree with tree-sitter-qmljs, distinguishing real
child objects from grouped-property-binding blocks (e.g. `anchors { ... }`)
by QML's own uppercase/lowercase type-name convention, and models:

- the file's root object as its own reusable component (named after the
  file stem, since that's how QML engines expose it to other files)
- id: bindings as their own nodes for reference resolution
- properties, functions, signals, inline components, and enum decls
- Behavior on <prop> { ... } as a real anonymous child object
- ui_object_array children (e.g. `states: [ State { ... } ]`)
- id/property-alias/event-handler references and same-file JS calls
- cross-file component instantiation via sourceless stub nodes, resolved
  onto the real definition by graphify's existing corpus-wide rewire pass
- import statements (module and relative-path) as imports_from edges

Member-call resolution (`x.method()`) checks that `method` is actually
defined on the node `x` resolves to via a func_owner map, not just that some
same-named function exists anywhere in the file -- func_table is a flat,
whole-file dict, so without this check two same-named methods on different
objects/components would let one shadow the other and produce a wrong
`calls` edge. A bare function reference with no call parens (a valid QML
idiom, e.g. `onClicked: doSomething`) now also resolves to a `references`
edge instead of being silently dropped. Anonymous same-typed siblings that
start on the same source line (e.g. a compact `states: [State{}, State{}]`
one-liner) are disambiguated by column, not just line, so they no longer
collapse into a single node.

tree-sitter-qmljs's PyPI release has a broken Python binding build (missing
src/scanner.c, so the extension fails to import) -- fixed upstream at
yuja/tree-sitter-qmljs#29, pending merge. The dev dependency group pins a
patched fork in the meantime; the `qml` extra tracks the eventual PyPI fix.

Verified against all 452 .qml files in a real ~450k-line Qt/QML app with
zero extraction errors (23k nodes, 35k edges).

Co-authored-by: Cursor <[email protected]>
tree-sitter-qmljs is not published on PyPI under any name, so
pip install graphifyy[qml] can never resolve this extra on its own
(and PyPI disallows direct git-URL deps in published metadata, so this
can't be fixed from our end). Left out of the `all` extra for the same
reason -- users who want QML support install tree-sitter-qmljs from
git themselves.

For our own tests/CI, pin the dev group directly to
yuja/tree-sitter-qmljs@565872b, which has the external-scanner-missing
fix (yuja/tree-sitter-qmljs#29) merged and generated parser sources
refreshed on top of it.

Co-authored-by: Cursor <[email protected]>
@Hamza-Masuadi

Copy link
Copy Markdown
Author

Update on the tree-sitter-qmljs dependency (the qml extra used by extract_qml()):

  • The Python binding had a build bug (missing src/scanner.c), which blocked this extra from working at all. Filed and got the fix merged upstream: fix: include external scanner in Python binding build yuja/tree-sitter-qmljs#29 (merged as 073d361e, with generated sources refreshed at 565872b).
  • pyproject.toml's qml dev dependency now pins directly to yuja/tree-sitter-qmljs@565872b — verified it builds cleanly and all QML tests pass against it.
  • tree-sitter-qmljs has never been published to PyPI, so pip install graphifyy[qml] can't resolve this extra on its own (and it's excluded from the all extra for the same reason — see the pyproject.toml comment for details). Filed Publish tree-sitter-qmljs to PyPI yuja/tree-sitter-qmljs#30 requesting a PyPI release to fix that properly.
  • Until that lands, this PR ships as-is: QML support works, but users need to pip install git+https://github.com/yuja/tree-sitter-qmljs by hand before installing graphifyy[qml].

Close out the remaining Graphify-Labs#1716 scope on top of the QML extractor:
tag signals:/slots:, model Q_PROPERTY READ/WRITE/NOTIFY, and resolve
QML_NAMED_ELEMENT / qmlRegisterType aliases onto real C++ class nodes.

Co-authored-by: Cursor <[email protected]>
@Hamza-Masuadi Hamza-Masuadi changed the title feat(extract): add QML language support feat(extract): add QML support and Qt C++/QML bridge (#1716) Jul 9, 2026
Skip bridge tests that need QML when tree-sitter-qmljs is missing,
pick the correct alias string for qmlRegisterUncreatableType, and
only remappable QML type stubs (origin_file) in the alias resolver.

Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant