Skip to content

fix(vba-extractor): scope Const declarations - proc-local no longer leaks to module scope (closes #52) - #73

Merged
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-52-const-scope
Jul 4, 2026
Merged

fix(vba-extractor): scope Const declarations - proc-local no longer leaks to module scope (closes #52)#73
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-52-const-scope

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #52.

What

Bug fix: sweepEnumsAndConsts was matching CONST_DECL_RE on every
line of a .bas/.cls file with no awareness of the procedure
stack. A Const declared inside a Sub/Function would emit a
module-level constant node with visibility: 'public' and a
contains edge from the module — wrong containment. localConstants
was a single file-wide Map<string, string>, so two procs declaring
the same Const name with different values collided (last write wins)
and could mis-resolve DoCmd.OpenForm targets.

Why

In a real Dysflow-managed Access project, a Const FORM_DESTINO As String = "FormDetalle" declared inside Public Sub Abrir() would
emit a module-level public constant node for FORM_DESTINO — which
breaks graph containment (the constant isn't actually module-scoped)
and pollutes the per-proc resolution cache. Two procs declaring the
same Const name with different values would collide in the cache and
produce wrong OpenForm edges.

Diff

File Lines
src/extraction/vba-extractor.ts +160 / −12
__tests__/extraction-vba.test.ts +147 / −0
Total +307 / −12

Inside the 400-line review budget.

Design

Option (b) from the issue spec — simpler

Per the issue body's recommendation, option (b) was chosen: emit
NO graph node for procedure-local Const, but keep the resolution
value for DoCmd.OpenForm / DoCmd.OpenReport / DoCmd.OpenQuery
argument lookup. Module-level Const behavior is bit-for-bit
unchanged.

Implementation

  • localConstants re-typed from Map<string, string> to
    Map<'module' | string, Map<string, string>>. Inner key is the
    current proc's startLine (a string-coerced number) when inside a
    proc, or the literal 'module' at file scope. New helper
    resolveLocalConst(name) looks up the current-proc bucket first
    and falls back to 'module'; setLocalConstInScope is the
    lazy-bucket writer.
  • sweepEnumsAndConsts and sweepCallsAndSql now share a single
    proc-stack discipline via two parallel fields on the extractor:
    currentProcKey ('module' at file scope, ${startLine} inside a
    proc) and procStack: number[]. Both resets at sweep start.
  • sweepEnumsAndConsts consults currentProcKey per line: when
    inside a proc it populates the per-proc bucket via
    setLocalConstInScope and skips both the constant node emission
    and the pushContainsFromModule edge; at module scope it emits
    exactly as before.
  • PROCEDURE_END_RE promoted from a local const inside
    sweepCallsAndSql to a class-level static so both sweeps share
    one canonical regex. Pure refactor.
  • scanDoCmdOpenCalls and scanDoCmdOpenQuery use the new
    resolveLocalConst(name) for argument resolution — per-proc
    bucket first, module fallback.

Test coverage

6 atoms in __tests__/extraction-vba.test.ts (new describe block
"Issue #52: procedure-local Const scoping"):

  1. Module-level Public Const FORM_EMPLOYEES still emits a constant
    node + module contains edge (regression guard).
  2. Procedure-local Const FORM_DESTINO inside a Sub emits ZERO
    constant nodes (the bug being fixed).
  3. Procedure-local Const still resolves in DoCmd.OpenForm
    (resolution preserved).
  4. Two procs with same-named local consts (Sub A / Sub B, each
    declaring Const TARGET) resolve their own OpenForm targets
    correctly — the proc-local shadowing works.
  5. Mixed-scope consts — module-level + proc-local with the same
    name; module emits one constant node, proc-local resolves the
    shadowed value, only one node exists.
  6. Multi-decl Const FORM_EMPLOYEES = "...", FORM_ORDERS As String = "..." at module scope still emits one node per name
    (regression guard).

Validation

  • pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #52"
    → 6 passed in 24 ms
  • Full VBA suite (6 files): 242 passed in 1.4 s — zero regressions
  • pnpm run build → tsc clean, no TS errors

Out of scope (intentional, per issue spec)

  • Option (a) — emitting a constant node with visibility: 'private' + metadata.scope: 'local' attached to the enclosing
    function via contains. Maintainer's call; option (b) is simpler
    and preserves all user-visible resolution.
  • Moving Const detection INTO sweepCallsAndSql — the line-range
    precompute approach (shared currentProcKey discipline) achieves
    the same scope awareness with a smaller code-surface delta.
  • Variable / parameter scoping — orthogonal issue.

…eaks to module scope (closes #52)

Bug: `sweepEnumsAndConsts` matched `CONST_DECL_RE` on every line of a
`.bas`/`.cls` file with no awareness of the procedure stack. A `Const`
declared inside a `Sub`/`Function` would emit a module-level `constant`
node with `visibility: 'public'` and a `contains` edge from the
module — wrong containment. Additionally `localConstants` was a
single file-wide `Map<string, string>`, so two procs declaring the
same Const name with different values collided (last write wins) and
could mis-resolve `DoCmd.OpenForm` targets.

Fix (option (b) from the issue spec — simpler and loses little):
  - Procedure-local Consts no longer emit a graph node; they keep
    their resolution value for OpenForm/OpenQuery argument lookup.
  - Module-level Consts are bit-for-bit unchanged (visibility fold,
    multi-declaration lines, module → constant `contains` edge).

Changes:

* `localConstants` re-typed from `Map<string, string>` to
  `Map<'module' | string, Map<string, string>>` — the inner key
  is the current proc's startLine (a string-coerced number) when
  inside a proc, or the literal `'module'` at file scope. The new
  helper `resolveLocalConst(name)` looks up the current-proc bucket
  first and falls back to `'module'`; `setLocalConstInScope` is the
  lazy-bucket writer.

* `sweepEnumsAndConsts` and `sweepCallsAndSql` now share a single
  proc-stack discipline. Both maintain `currentProcKey`
  (`'module'` at file scope, `${startLine}` inside a proc) and a
  parallel `procStack: number[]` reset at sweep start. The const
  sweep consults `currentProcKey` per line: when inside a proc it
  populates the per-proc bucket via `setLocalConstInScope` and
  skips both the `constant` node emission and the
  `pushContainsFromModule` edge; when at module scope it emits as
  before.

* `PROCEDURE_END_RE` (used to be a local const inside
  `sweepCallsAndSql`) promoted to a class-level static so both
  sweeps share one canonical regex. Pure refactor — no behavior
  change in `sweepCallsAndSql`.

* `scanDoCmdOpenCalls` and `scanDoCmdOpenQuery` use the new
  `resolveLocalConst(name)` for argument resolution — per-proc
  bucket first, module fallback. The user-visible behavior of
  OpenForm/OpenReport/OpenQuery resolution is preserved for module-
  level consts and now correctly scoped for proc-local consts.

* 6 regression atoms in `__tests__/extraction-vba.test.ts` (new
  describe block "Issue #52: procedure-local Const scoping"):
  1. Module-level `Public Const FORM_EMPLOYEES` still emits a
     `constant` node + module contains edge (regression guard).
  2. Procedure-local `Const FORM_DESTINO` inside a `Sub` emits ZERO
     `constant` nodes (the bug being fixed).
  3. Procedure-local Const still resolves in `DoCmd.OpenForm`
     (resolution preserved).
  4. Two procs with same-named local consts (`Sub A` / `Sub B`,
     each declaring `Const TARGET`) resolve their own `OpenForm`
     targets correctly — the proc-local shadowing works.
  5. Mixed-scope consts — module-level + proc-local with the same
     name; module emits one `constant` node, proc-local resolves
     the shadowed value, only one node exists.
  6. Multi-decl `Const FORM_EMPLOYEES = "...", FORM_ORDERS As
     String = "..."` at module scope still emits one node per name
     (regression guard).

Validation:

* `pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue
  #52"` → 6 passed in 24 ms
* Full VBA suite (6 files): **242 passed** in 1.4 s — zero regressions
* `pnpm run build` → tsc clean, no TS errors

Scope: this is a behavioral fix (no API change). The `constant`
node kind, `vba-name-resolution` synthesizedBy tag, OpenForm dispatch,
and existing module-level Const paths are unchanged. A Dysflow-managed
project that previously emitted N module-level constant nodes for
proc-local Const will now emit fewer — by exactly the number of
proc-local Consts in its `.bas`/`.cls` files.

Out of scope (intentional, per issue spec):

* Option (a) — emitting a `constant` node with `visibility:
  'private'` + `metadata.scope: 'local'` attached to the enclosing
  function via `contains`. Maintainer's call; option (b) is simpler
  and preserves all user-visible resolution.
* Moving Const detection INTO `sweepCallsAndSql` — the line-range
  precompute approach (shared `currentProcKey` discipline) achieves
  the same scope awareness with a smaller code-surface delta.
* Variable / parameter scoping — orthogonal issue.
@ardelperal ardelperal added the bug Something isn't working label Jul 4, 2026
@ardelperal
ardelperal merged commit 2edd436 into main Jul 4, 2026
5 checks passed
@ardelperal
ardelperal deleted the chore/2026-07-04-issue-52-const-scope branch July 4, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(vba): procedure-local Const leaks to module scope (node + OpenForm resolution collisions)

1 participant