Skip to content

feat(vba): connect form and report lifecycle events to their layout node - #269

Merged
ardelperal merged 1 commit into
mainfrom
feat/issue-247-form-lifecycle-events
Sep 1, 2026
Merged

ardelperal merged 1 commit into
mainfrom
feat/issue-247-form-lifecycle-events

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #247

What this changes

A form's own lifecycle handlers — Form_Open, Form_Load, Form_Unload, Form_Current, … — produced no edge at all. "What runs when this form opens" was a naming convention the reader had to know, not a graph query.

The control-handler path refuses those names on purpose: a form-level event fires on the form object, not on a control, so routing it there would synthesize a bogus form-instance-control node literally named Form. That refusal stays. The form-level case gets its own target instead: the sibling form-layout / report-layout node.

  • New parseFormLevelEventHandlerName in vba/text-utils.ts — the other half of parseEventHandlerName's decision. It accepts exactly the names the control parser rejects: owner segment exactly Form or Report, suffix a known Access event name.
  • The procedure rule's code-behind block is now a two-way branch on one isFormCodeBehind guard. Form-level handlers emit a form-layout / report-layout stub plus the event-handler edge; control handlers keep their existing form-instance-control stub and edge, byte for byte.
  • The stub carries the same deterministic id VbaFormExtractor.createFormLayoutNode produces for that file — generateNodeId(siblingPath, layoutKind, basename, 1) — so the existing INSERT OR REPLACE convergence applies whichever file is indexed first. The test does not hardcode that id; it runs the real form extractor on the real sibling and compares.
  • The edge keeps kind event-handler and metadata.eventName, and adds metadata.scope: 'form' so consumers can separate form-level from control-level handlers without re-parsing the Sub name.
  • The gate is isAccessEventName, not the Form_ prefix. Form_Load is an event; a class method called Form_Helper is not, and gets nothing.

Measured effect

Both corpora walked file by file through VbaExtractor / VbaFormExtractor, counting edges by kind and nodes by kind (353 files):

  • C:/00repos/codigo/00_EXPEDIENTES/src
  • C:/00repos/codigo/00_GESTION_RIESGOS/src
Metric Before After
event-handler edges 695 808
— of those, control-level (no scope) 695 695
— of those, form-level (scope: 'form') 0 113
form-instance-control nodes 2,485 2,485

The control population is unchanged to the edge, and form-instance-control does not move — no bogus Form control node is created anywhere.

113, not the 115 the issue counted: the corpora hold 115 Sub Form_<Event> declarations, but 2 of them live in 00_EXPEDIENTES/src/forms/frmSplash.cls, which is not named Form_*.cls. The pre-existing basename guard skips that file entirely (it is what keeps service classes such as InformeRiesgoPDFServicio.cls from synthesizing hundreds of spurious stubs), and this change does not touch it. Widening that guard is a separate decision about which files count as code-behind.

Verification

  • npx tsc --noEmit — clean.
  • npx vitest run __tests__/extraction-vba-form-lifecycle-events.test.ts — 16 tests, all pass.
  • pnpm run build — clean.
  • pnpm exec vitest run vba extraction-sql-query sql-query-discovery — 44 files, 685 tests passed, 1 skipped (43 files is what Windows CI runs today; the 44th is the new test file).

The four test files failing on the full pnpm exec vitest run (extraction.test.ts, multi-repo-workspace.test.ts, npm-sdk.test.ts, worktree-detection.test.ts) fail identically on a clean checkout of the base commit — Windows EPERM on temp-directory teardown and packaging checks, unrelated to this change.

New tests

__tests__/extraction-vba-form-lifecycle-events.test.ts, with real fixtures under __tests__/fixtures/vba-form-lifecycle/ (a form and a report code-behind, each with its layout sibling). Real files, real extractors, no mocking. It covers each acceptance criterion: Form_Load / Form_Open / Form_Unload → the form layout node with eventName and scope: 'form'; Report_Open → the report layout node; Form_Helper and Report_Helper → nothing; cmdSave_Click → the same control node id and edge as before, with no scope; no control stub named Form or Report; the basename guard still skips a plain service class; and a code-behind with no exported layout still gets its node.

Three existing tests move with the behaviour

  1. extraction-vba-event-whitelist.test.ts — the fixture declares Form_Load alongside two control handlers, so it now sees three event-handler edges instead of two. The assertion that matters is kept and sharpened: exactly one of the three carries scope: 'form', and the form-instance-control count stays at two. Form_Load is still not a control handler.

  2. extraction-vba-control-modeling.test.ts (hueco 5) — searchNodes already reports any function with an outgoing event-handler edge as kind event-handler rather than function (db/queries.ts). Form_Load is now one of those, so the filter accepts both kinds. The assertion under test — that the qualifiedName carries the owning form's prefix — is unchanged.

  3. vba-open-object-stub-resolution.test.ts — the fixture wrote Form_FormNC.cls and Form_FormNC.form.txt into different numbered directories. Those directories exist only to control index order; splitting the pair across two of them describes a project that cannot exist, because the extractor has always derived the layout path by swapping .cls for .form.txt on the same path, and a Dysflow export always writes the pair as siblings. With them apart, the code-behind's stub is an orphan that never converges, and DoCmd.OpenForm stub resolution — which refuses to repoint when two layout candidates answer to the same name — silently loses a real navigation edge. The .cls now shares its directory with its layout; ordering within the pair is still deterministic (.cls sorts before .form.txt), so the layoutFirst/layoutLast coverage is intact.

One design decision worth flagging

A Form_X.cls whose Form_X.form.txt was never exported still gets the synthesized layout node. This is where most of the corpus benefit comes from — 00_GESTION_RIESGOS ships 110 form code-behind classes but only 65 layouts — and dropping the node would drop the handler edge with it, since that node is the only node those forms have. It also normalizes to the same Access object identity a DoCmd.OpenForm "X" target does, so the navigation stub can resolve onto it. This mirrors the control branch's long-standing behaviour, where a handler whose control is missing from the sibling still gets its stub. AC #7 of the new test pins it.

Nothing in the issue went unimplemented. No version bump, no tag, no publish.

A form's own lifecycle handlers (Form_Open, Form_Load, Form_Unload, ...)
produced no edge at all. The control-handler path deliberately refuses
them: a form-level event fires on the form object, not on a control, so
routing it there would synthesize a bogus form-instance-control node
literally named "Form".

Keep that refusal and give the form-level case its own target. When the
owner segment is exactly `Form` or `Report` and the suffix is a known
Access event name, emit an `event-handler` edge to the sibling
form-layout / report-layout node, carrying metadata.scope: 'form' so a
consumer can separate the two populations without re-parsing the Sub
name. The local stub uses the same deterministic id VbaFormExtractor
produces for that file, so INSERT OR REPLACE converges whichever file is
indexed first.

The gate is isAccessEventName, not the `Form_` prefix: `Form_Load` is an
event, a class method called `Form_Helper` is not.

On the EXPEDIENTES + GESTION_RIESGOS corpora: event-handler edges
695 -> 808 (+113 form-level), form-instance-control nodes unchanged at
2,485.

Two existing tests move with the behaviour: the event whitelist now
expects the third (form-level) edge while pinning the control-node count
at two, and the hueco-5 search test accepts the `event-handler` kind
searchNodes reports for any function with an outgoing event-handler
edge. The DoCmd stub-resolution fixture now places Form_FormNC.cls
beside its own .form.txt, which is how a Dysflow export always lays them
out and what the extractor's sibling-path derivation has always assumed.

Closes #247
@ardelperal
ardelperal merged commit eac0e0d into main Sep 1, 2026
5 checks passed
@ardelperal
ardelperal deleted the feat/issue-247-form-lifecycle-events branch September 1, 2026 18:20
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.

feat(vba): connect form and report lifecycle events to their layout node

1 participant