feat(vba): connect form and report lifecycle events to their layout node - #269
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-controlnode literally namedForm. That refusal stays. The form-level case gets its own target instead: the siblingform-layout/report-layoutnode.parseFormLevelEventHandlerNameinvba/text-utils.ts— the other half ofparseEventHandlerName's decision. It accepts exactly the names the control parser rejects: owner segment exactlyFormorReport, suffix a known Access event name.procedurerule's code-behind block is now a two-way branch on oneisFormCodeBehindguard. Form-level handlers emit aform-layout/report-layoutstub plus theevent-handleredge; control handlers keep their existingform-instance-controlstub and edge, byte for byte.VbaFormExtractor.createFormLayoutNodeproduces for that file —generateNodeId(siblingPath, layoutKind, basename, 1)— so the existingINSERT OR REPLACEconvergence 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.event-handlerandmetadata.eventName, and addsmetadata.scope: 'form'so consumers can separate form-level from control-level handlers without re-parsing the Sub name.isAccessEventName, not theForm_prefix.Form_Loadis an event; a class method calledForm_Helperis 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/srcC:/00repos/codigo/00_GESTION_RIESGOS/srcevent-handleredgesscope)scope: 'form')form-instance-controlnodesThe control population is unchanged to the edge, and
form-instance-controldoes not move — no bogusFormcontrol node is created anywhere.113, not the 115 the issue counted: the corpora hold 115
Sub Form_<Event>declarations, but 2 of them live in00_EXPEDIENTES/src/forms/frmSplash.cls, which is not namedForm_*.cls. The pre-existing basename guard skips that file entirely (it is what keeps service classes such asInformeRiesgoPDFServicio.clsfrom 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 — WindowsEPERMon 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 witheventNameandscope: 'form';Report_Open→ the report layout node;Form_HelperandReport_Helper→ nothing;cmdSave_Click→ the same control node id and edge as before, with noscope; no control stub namedFormorReport; 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
extraction-vba-event-whitelist.test.ts— the fixture declaresForm_Loadalongside two control handlers, so it now sees threeevent-handleredges instead of two. The assertion that matters is kept and sharpened: exactly one of the three carriesscope: 'form', and theform-instance-controlcount stays at two.Form_Loadis still not a control handler.extraction-vba-control-modeling.test.ts(hueco 5) —searchNodesalready reports anyfunctionwith an outgoingevent-handleredge as kindevent-handlerrather thanfunction(db/queries.ts).Form_Loadis now one of those, so the filter accepts both kinds. The assertion under test — that thequalifiedNamecarries the owning form's prefix — is unchanged.vba-open-object-stub-resolution.test.ts— the fixture wroteForm_FormNC.clsandForm_FormNC.form.txtinto 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.clsfor.form.txton 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, andDoCmd.OpenFormstub resolution — which refuses to repoint when two layout candidates answer to the same name — silently loses a real navigation edge. The.clsnow shares its directory with its layout; ordering within the pair is still deterministic (.clssorts before.form.txt), so the layoutFirst/layoutLast coverage is intact.One design decision worth flagging
A
Form_X.clswhoseForm_X.form.txtwas never exported still gets the synthesized layout node. This is where most of the corpus benefit comes from —00_GESTION_RIESGOSships 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 aDoCmd.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.