Delphi-RAG-Lint

drag-lint help

Code-built menu tree and registry drift

Since: 1.33.0-alpha

The menu tree a Delphi program builds in code, read statically, and a check that the registered menu paths still match it.

Read this when a menu is built in code (TMenuItem.Create, Caption :=, Add) rather than in a .dfm and you need to see the real tree, or when a documented menu path may have drifted from the code. drag-lint menu-tree --project <dproj> --db <db> prints the tree: helpers are recognised by what their body does, not by their name, and a caption it cannot evaluate is shown as dynamic with a note instead of being dropped. Needs a fresh project index; a changed file is refused with exit 3.

How to run it

drag-lint menu-tree --project <X.dproj> --db <file.sqlite> [--root <QName>] [--format text|json|paths] [--output <f>] (the menu tree a program builds in code, read statically; exit 0 ran, 2 usage/setup, 3 stale index)

Full flag list: menu-tree in the CLI reference.

drag-lint menu-drift --project <X.dproj> --db <file.sqlite> --registry <features dir> [--format text|json] [--output <f>] (code-built menu tree vs the registry's ide-menu paths: missing / moved / unregistered / notes; exit 0 no drift, 1 drift, 2 usage/setup, 3 stale index)

Full flag list: menu-drift in the CLI reference.

Flags

FlagArgumentMeaningDefault
--project<X.dproj>
--db<file.sqlite>
--root<QName>
--formattext|json|paths
--output<f>
--project<X.dproj>
--db<file.sqlite>
--registry<features dir>
--formattext|json
--output<f>

Needs: index

Details

drag-lint menu-tree recovers, statically, the menu tree a Delphi program BUILDS IN CODE -- TMenuItem.Create + Caption := + Parent.Add / Insert, wrapper helpers, data-driven submenus -- from the project index plus a tree-sitter re-parse of only the routines that build menus. Nothing runs; no IDE is needed.

CLI

drag-lint menu-tree --project <X.dproj> --db <db> [--root <QName>] [--format text|json|paths] [--output <f>]
  • text (default): an indented tree, [kind] caption -> handler (file:line, confidence), then one NOTE line per note.
  • paths: one leaf per line, path<TAB>handler<TAB>file:line<TAB>confidence; external roots follow, each after a ## external <ident> line.
  • json: schema menu-tree/1 -- roots, external, notes; every node has kind, caption, captionKey, handler, source, confidence, children.
  • --root <QName> walks one builder routine (and what it reaches) only.

Exit codes: 0 it ran; 2 usage or setup (no --db, project not in the db, unknown --root); 3 stale index -- a file the walk must read changed since it was indexed, so it refuses and prints the reindex command instead of answering from old code.

What it recognises

ShapeResult
X := TMenuItem.Create(O) (or a TMenuItem descendant, inline var too)a node, unparented until added
X.Caption := Eliteral, 'a' + 'b', Format('pattern', ...) and const identifiers evaluate (certain); ARR[I].Field over a const array of records and a case-of-literals function expand to one node per value (derived); anything else is ONE dynamic node plus a note
X.OnClick := Hthe handler, qualified through the index; a wrapper class built from a handler (W := TWrapper.Create(H); X.OnClick := W.HandleClick) is followed one level
X.Enabled := False, or no OnClick and no childrena header
caption '-'a separator
P.Add(X) / P.Insert(i, X)X under P; MainMenu.Items.Add(X) or AddActionMenu(.., X, ..) makes X a root; a menu-typed field / component / variable that is not a node of this run (a DFM item, a looked-up IDE menu) becomes an external root
a call that passes a node or an anonymous methodthe callee's body is walked with its parameters bound -- this is how helpers such as AddItem(Parent, 'Caption', Handler) are recognised: by what their body does, never by their name
if .. then .. elseboth branches; the same node added on both is one root

A node created with Create(nil) and never added is a root; any other node never added is a note (unparented item).

Builders are found through the index: routines whose body references TMenuItem (or a descendant) and that take no menu-item parameter themselves (those are helpers, walked from their call sites).

Drift against the registry: menu-drift

drag-lint menu-drift --project <X.dproj> --db <db> --registry <features dir> [--format text|json] [--output <f>]

Compares the live tree with the ide-menu surfaces of <features dir>\entries\*.json (and the captions of exemptions.json) by PATH key -- the same caption / path keys featuregen uses (&& -> &, ... dropped, punctuation trimmed, lowercase, segments joined with >).

ClassWhenPrinted with
MISSINGa registered path the code does not build, and its caption is nowhere elserenamed? -> '<caption>' when a live, unregistered item under the same parent shares at least 12 leading key characters
MOVEDa registered path whose caption now lives under another parentnow at <live path>
UNREGISTEREDa built item no surface covers (exact path, or a caption prefix of at least 12 characters under the same parent) and not exempteda skeleton surface line to paste
NOTEdynamic captions, unparented items, an exempted caption that is now a submenu / header / root (the exemption is no longer needed)--

Separators, headers, submenus and external roots are never UNREGISTERED; a registered path that names a live submenu is not MISSING; a registered path under a parent that holds a dynamic item is never MISSING (the dynamic item may be it -- its note says so). Order inside a submenu is not compared.

Summary line: menu-drift: missing=N unregistered=N moved=N notes=N. Exit 0 no drift, 1 drift, 2 usage / setup, 3 stale index. --format json writes menu-drift/1 (missing, moved, unregistered, notes, summary).

The battery runs it on the IDE plugin itself (tests\autotest\run_menu_drift_guard.ps1): positive controls on the fixture (a deleted entry -> UNREGISTERED, a changed caption -> MISSING with renamed?, a changed parent -> MOVED, a renamed helper -> no drift, a caption made dynamic -> a note, a touched unit -> exit 3), then the real run, which passes only on exit 0. The plugin index is a tests\autotest\prepare_battery.ps1 artifact (src\delphi-plugin\_D-RAG\dclDragLintWizard.sqlite).

Confidence

certain (literal caption, direct parenting or a recognised helper), derived (const / case expansion, a callback binding), unknown (a dynamic caption).

Controls

tests\fixtures\menutree holds every shape above; tests\autotest\run_menu_tree_guard.ps1 compares --format paths with the hand-written expected-paths.txt cell by cell, checks the JSON schema, exit 2 without --db and exit 3 on a touched unit. tests\autotest\run_menu_tree_tests.ps1 runs the unit tests, including the renamed-helper control (rename AddItem to PutItem in a copy: the tree does not change).

Limits (v1)

DevExpress TdxBar* items, shortcuts and menu order are not compared; context menus, About-form buttons and tool windows are out of scope. The tree is computed on demand -- there is no persisted menu_items table yet.


All features | CLI reference | IDE menus