Skip to content

feat(vba-extractor): support bang operator - Me!Ctl, Forms!Form!Ctl (closes #44) - #68

Merged
ardelperal merged 1 commit into
mainfrom
chore/2026-07-03-issue-44-release-origin-main
Jul 4, 2026
Merged

feat(vba-extractor): support bang operator - Me!Ctl, Forms!Form!Ctl (closes #44)#68
ardelperal merged 1 commit into
mainfrom
chore/2026-07-03-issue-44-release-origin-main

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #44.

What

Enables two Access VBA bang-operator idioms that the previous extractor silently dropped:

  • Me!<Control> — the default-collection shortcut for Me.<Control>. Same emission shape (byte-identical referenceName, referenceKind, metadata.synthesizedBy), so the resolver needs no special-casing.
  • Forms!<Form>[!<Ctl>] and Forms("<Form>")!<Ctl> — cross-form UI traffic. Previously doubly-invisible: Forms is in RUNTIME_RECEIVER_BLACKLIST so CALL_RE skipped it, and there was no dedicated scanner.

Why

In a real Dysflow-managed Access project (codegraph-vba issue #44), every cross-form Forms!FormPrincipal!txtEstado.Value = … line produced zero graph edges — so Form_FormPrincipal.cls and the Forms/* reference trail were unrelated after indexing. Same for Me!txtNombre inside form .cls bodies. The resolver layer alone couldn't recover these because the references didn't exist in the first place.

Diff

File Lines
src/extraction/vba-extractor.ts +162 / −13
__tests__/extraction-vba.test.ts +163 / −0
Total +312 / −13

Inside the 400-line review budget → single PR, no chain required.

Design

  • Extend ME_CONTROL_RE from /\bMe\.(…)/gu to /\bMe[.!](…)/gu. The +3 column offset in scanMeControlReferences remains valid — both Me. and Me! are 3-character prefixes.
  • New FORMS_BANG_RE with two alternatives: bang form (Forms!<Form>(?![.\w]), optional !<Ctl>) and paren form (Forms("<Form>")!<Ctl>, required trailing control). Negative lookahead on the bang form is what excludes Forms!FormX.Recordsource (property access, not control access).
  • New scanFormsBang() method beside scanMeControlReferences; one UnresolvedReference per match (metadata.synthesizedBy = 'vba-forms-bang'). No synthetic function node — the form is a real .cls/.form.txt pair the resolver already picks up via vba-form-binding (W4 invariant).
  • Wiring next to scanOpenFormCalls() — both need the original unmasked line because the paren form Forms("X")!Y has the form name inside a string literal.

Test coverage

8 new atoms in __tests__/extraction-vba.test.ts:

  1. Me!txtFoo emits vba-me-control reference to txtFoo
  2. Me!txtFoo and Me.txtFoo produce byte-identical UnresolvedReferences (parity regression)
  3. Forms!FormX!txtY.Value = 1 emits vba-forms-bang reference to FormX
  4. Forms!FormX (no control segment) emits vba-forms-bang to FormX
  5. Forms("FormX")!txtY emits the same vba-forms-bang to FormX as the bang form
  6. Forms!FormX.Foo (post-form property access) does NOT emit
  7. rs!Campo (recordset field, STRETCH SCOPE) emits zero — pinned for future review
  8. Forms!FormX!txtY does NOT synthesize a function node (W4 invariant)

Verification

  • pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #44" → 8 passed in 569 ms
  • pnpm exec vitest run __tests__/extraction-vba.test.ts __tests__/extraction-vba-control-modeling.test.ts __tests__/extraction-vba-form.test.ts __tests__/extraction-vba-enums-consts.test.ts __tests__/extraction-vba-realfixtures.test.ts __tests__/extraction-vba-roadmap-25-26.test.ts227 passed in 5.09 s (zero regressions across the entire VBA test surface)
  • pnpm run build → exit 0 (tsc + asset copy OK)

Out of scope (intentional)

  • rs!Campo (DAO/ADO recordset default-member field access). The runtime-receiver blacklist plus the absence of any Forms/Me prefix means the existing scanners already skip it. Pinned by a test so a future change can be reviewed explicitly against the bang-form scope decision.
  • <Form>::<Ctl> qualified control emission — the bang scanner emits only the form reference today; the control segment is consumed but not emitted. Control emission is the form's responsibility downstream.

Not done

n/a — issue complete in this PR.

…loses #44)

Access VBA uses the bang (!) operator as an alias for default-collection access
(Me controls) and default-member access (Forms collection, Recordsets). Before this
fix only `Me.<Control>` was tracked via `ME_CONTROL_RE`; the bang form was
silently invisible, and `Forms!…` was doubly invisible because `Forms` sits in
`RUNTIME_RECEIVER_BLACKLIST` so `CALL_RE` skipped it too. The cross-form UI
traffic from `Forms!FormX!txtY.Value` never surfaced the form->control binding
the resolver needed.

Changes:

* `ME_CONTROL_RE` regex extended from `/\bMe\.(…)/gu` to `/\bMe[.!](…)/gu`.
  `Me!txtFoo` now produces a byte-identical UnresolvedReference to `Me.txtFoo`
  (same `referenceName`, `referenceKind`, `metadata.synthesizedBy='vba-me-control'`);
  the regression test pins the parity.
* New `FORMS_BANG_RE` scanner added next to `ME_CONTROL_RE` (extends the same
  hole-1 control-modeling references family). Two alternatives:
  `Forms!<FormName>[!<Ctl>]` (bang form, with negative-lookahead `(?![.\w])` to
  drop `Forms!FormX.Foo` as a property access) and `Forms("<FormName>")!<Ctl>`
  (paren form, must have trailing bang control). Bracketed form names
  (`Forms![Mi Formulario]`) follow the #54 bracket-stripping rule.
* New `scanFormsBang()` method — companion to `scanMeControlReferences`; emits
  ONE UnresolvedReference per match (`referenceKind='references'`,
  `metadata.synthesizedBy='vba-forms-bang'`). NO synthetic function node for the
  form (W4 graph-pollution invariant preserved — the form is a real
  `.cls`/`.form.txt` pair the resolver picks up via `vba-form-binding`).
* Wired `scanFormsBang()` into the line scanner right after `scanOpenFormCalls()`
  (both scan the original unmasked line because the paren form
  `Forms("X")!Y` has the form name INSIDE a string literal).
* 8 regression tests added in `__tests__/extraction-vba.test.ts`:
  `Me!txtFoo` parity (byte-identical to `Me.txtFoo`), `Forms!FormX!txtY`,
  `Forms!FormX` alone, `Forms("FormX")!txtY`, `Forms!FormX.Foo` (must NOT emit),
  `rs!Campo` (STRETCH SCOPE, intentionally silent), and the W4 no-synthetic-fn
  guard.

STRETCH SCOPE: recordset field access (`rs!Campo`, DAO/ADO default-member) is
intentionally out of scope and pinned by a test so a future change can be
reviewed explicitly against the bang-form scope decision.
@ardelperal ardelperal added the type:feature New feature label Jul 4, 2026
@ardelperal
ardelperal merged commit 4eafa19 into main Jul 4, 2026
5 checks passed
@ardelperal
ardelperal deleted the chore/2026-07-03-issue-44-release-origin-main branch July 4, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(vba): support the bang operator — Me!Ctl, Forms!Form!Ctl

1 participant