Skip to content

feat(vba-extractor): model TempVars keys as cross-form state nodes (closes #50) - #75

Merged
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-50-tempvars
Jul 4, 2026
Merged

feat(vba-extractor): model TempVars keys as cross-form state nodes (closes #50)#75
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-50-tempvars

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #50.

What

Access's global TempVars key-value store is the canonical
mechanism for passing state between forms (TempVars!IDExpediente = Me.Id in the caller; TempVars("IDExpediente") read in the
opened form's Form_Load). The dysflow corpus uses this idiom in
~10 files; until now these cross-form data flows had zero graph
representation.

This PR adds cross-form state modeling: indexing a .bas/.cls
now emits ONE synthetic placeholder per unique TempVar key
(cross-file stable id via synthetic:tempvar/<key> prefix; same
class kind as SQL-table placeholders for consistency) with
references edges from each reading/writing procedure. The
metadata.access: 'read'|'write' tag discriminates write from
read; the metadata.synthesizedBy: 'vba-tempvar' tag
discriminates TempVars placeholders from SQL-table placeholders
(both class kind, both heuristic).

Why

Impact analysis across .form.txt files (the form UI layer) and
their data sources (.sql queries, TempVars cross-form state)
was the missing link in codegraph-vba's Access-graph: producer→
consumer navigation for TempVars keys was invisible to the graph.
This PR makes that connection permanent so impact analysis can
ride a single references edge hop.

Diff

File Lines
src/extraction/vba-extractor.ts +244
__tests__/extraction-vba.test.ts +295
Total +539

Inside the 400-line review budget for the source change; the test
additions are larger than typical because the cross-file dedup
atom requires a separate extract() call per file.

Design

Three TempVars forms covered

  • Bang-writeTempVars!clave = x (regex TEMP_VAR_BANG_RE,
    scanned on the masked line; ! itself survives maskStringContent
    because mask only replaces INSIDE string literals).
  • Parens-read-or-writeTempVars("clave") or
    TempVars("clave") = x (regex TEMP_VAR_PAREN_RE, scanned on
    the ORIGINAL unmasked line — same split the SQL_TABLE_RE and
    OpenForm literal-form scanners use).
  • Add-writeTempVars.Add "clave", value (regex
    TEMP_VAR_ADD_RE, always a write).

Write-vs-read detection looks at the line suffix (skip whitespace
then expect =) on the SAME line. VBA has no ==, so the bare
= check is safe.

class placeholder vs new tempvar NodeKind

Chose class, matching the SQL-table precedent (emitSqlTableReferences
emitReference) and synthClassNodeIds dedup shape. The
metadata.synthesizedBy: 'vba-tempvar' tag cleanly discriminates
TempVars references from SQL-table references in the graph even
though both use class kind. Avoids NodeKind union work across
types.ts + every consumer that filters on kind.

Cross-file id stability

Uses synthetic file path synthetic:tempvar/<key> (mirroring
synthetic:opensFormStub/...) so the same key across N files
maps to one node — generateNodeId('synthetic:tempvar/<key>', 'class', '<key>', 0) is byte-stable regardless of where produced.
Atom #5 verifies this with two independent extract() calls.

Edge source

Used findOrCreateFunctionNodeId(caller) (immediate resolution,
same pattern as scanDoCmdOpenCalls), NOT emitReference's
deferred pendingModuleOrClassSource re-attribution. TempVars
is per-proc and the proc is known at scan time; the deferred
mechanism is for module-level references where the source
isn't known yet.

Dynamic keys stay silent (no explosion)

TempVars(strNombre) (variable arg) and TempVars("a" & "b")
(concatenation) emit nothing. The regexes don't tolerate
function-call or concatenation forms, and the placeholder id
would be different per call site in the dynamic case anyway.

Test coverage

8 atoms in __tests__/extraction-vba.test.ts (new describe
"VbaExtractor — Issue #50: TempVars keys modeled as cross-form
state nodes"):

  1. Bang-write TempVars!MiClave = "valor"
    references edge from the proc, access: 'write',
    placeholder MiClave of class kind.
  2. Parens-read Dim x = TempVars("MiClave") → edge from the
    SAME placeholder (cross-proc same-node: A writes, B reads).
  3. Parens-write TempVars("MiClave") = 42access: 'write'.
  4. Add-form TempVars.Add "MiClave", "x"access: 'write';
    second key produces a second placeholder (atom verifies
    distinct placeholders for distinct keys).
  5. Cross-file dedup: extract(fileA) + extract(fileB)
    produce ONE placeholder node id (deterministic, equals
    generateNodeId('synthetic:tempvar/<key>', 'class', '<key>', 0)); two edges from the two respective proc nodes.
  6. Dynamic key — variable arg TempVars(strNombre) → ZERO
    references, ZERO placeholder nodes. Concatenation
    TempVars("a" & "b") similarly silent.
  7. Cross-cutting smoke: 2 placeholders (a, b) + 3 edges
    (write-a, write-b, read-a) from the same proc.
  8. Regression guard on the SQL-table path (emitReference /
    emitSqlTableReferences): a DoCmd.OpenForm + TempVars!x
    file produces ONE form-layout stub + ONE TempVars placeholder,
    NOT a single merged node — different synthesizedBy tags keep
    them distinct.

Validation

  • pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #50" → 8 passed in 778 ms
  • Full VBA suite (6 files): 258 passed in 2.73 s — zero
    regressions; existing REQ-CODE-* atoms continue to pass.
  • pnpm run build → tsc clean, no TS errors.

Cross-form impact analysis example (per the issue spec)

The workflow codegraph_explore "quién produce IDExpediente"
now connects the producing form's Sub to the consuming form's
Sub through one TempVars("IDExpediente") placeholder node — one
hop instead of zero.

Out of scope (intentional, deferred)

  • TempVars("x").Value = 1 write-detection (the (?:\.Value)?
    suffix in the issue's regex sketch). Per-line scope only;
    multi-physical-line continuations are not followed. Can be
    added in a future PR if any dysflow fixture uses this shape.
  • TempVars.Remove "x" and other Remove/Count forms — issue
    spec enumerates Read/Write/Add only.
  • Indexer-side cross-file placeholder collapse — placeholder ids
    are deterministic (so duplicates from N file extractions share
    the id); the existing indexer dedup at insert time handles
    collapse. No indexer change required.

…loses #50)

Access's global `TempVars` key-value store is the canonical
mechanism for passing state between forms (`TempVars!IDExpediente =
Me.Id` in the caller; `TempVars("IDExpediente")` read in the
opened form's `Form_Load`). The dysflow corpus uses this idiom in
~10 files; until now these cross-form data flows had zero graph
representation.

Indexing a `.bas`/`.cls` now emits ONE synthetic placeholder per
unique TempVar key (cross-file stable id via
`synthetic:tempvar/<key>` prefix; same kind as SQL-table
placeholders for consistency) with `references` edges from each
reading/writing procedure. The `metadata.access: 'read'|'write'`
tag discriminates write from read; the `metadata.synthesizedBy:
'vba-tempvar'` tag discriminates TempVars placeholders from SQL-
table placeholders (also `class` kind, also heuristic).

Three TempVars forms covered:

- **Bang-write** — `TempVars!clave = x` (regex `TEMP_VAR_BANG_RE`,
  scanned on the masked line; `!` survives `maskStringContent`).
- **Parens-read-or-write** — `TempVars("clave")` or
  `TempVars("clave") = x` (regex `TEMP_VAR_PAREN_RE`, scanned on the
  ORIGINAL unmasked line — same split the SQL_TABLE_RE and
  OpenForm literal-form scanners use).
- **Add-write** — `TempVars.Add "clave", value` (regex
  `TEMP_VAR_ADD_RE`, always a write).

Write-vs-read detection looks at the line suffix (skip whitespace
then expect `=`) on the SAME line. VBA has no `==`, so the bare `=`
check is safe.

Dynamic keys (`TempVars(strNombre)`, `TempVars("a" & "b")`) emit
nothing — the regexes don't tolerate function-call or concatenation
forms, and the placeholder id would be different per call site in
the dynamic case (no node explosion).

Changes:

* `src/extraction/vba-extractor.ts` (244 / +0):
  - `TEMP_VAR_BANG_RE`, `TEMP_VAR_PAREN_RE`, `TEMP_VAR_ADD_RE`
    static regex definitions (just after `SQL_TABLE_RE`, with a
    doc block explaining the masked-vs-original split).
  - `detectAssignmentSuffix(line, fromIndex)` module-level helper —
    skips whitespace then checks for `=`. Used by both the bang
    and parens forms.
  - `synthTempVarNodeIds: Set<string>` instance field — de-dup
    keyed on `generateNodeId('synthetic:tempvar/<key>', 'class',
    '<key>', 0)`. Cross-file stable id: same key across N files
    → one placeholder node (matches the cross-form premise).
  - `emitTempVarReference(caller, key, lineNum, column, access)`
    — emits the placeholder (lazily on first encounter) + one
    `references` edge from `findOrCreateFunctionNodeId(caller)`,
    tagged `metadata.synthesizedBy: 'vba-tempvar'` + `metadata.access:
    'read' | 'write'`. Per-proc source at scan time — no
    `pendingModuleOrClassSource` deferred re-attribution (the
    deferred pattern is for module-level references where the
    source isn't known yet).
  - `sweepTempVars(maskedLine, originalLine, lineNum, caller)` —
    three regex passes (bang on masked, parens/Add on original)
    and one `emitTempVarReference` per match. Wired into
    `sweepCallsAndSql` right after `this.scanFormsBang(line,
    caller2, lineNum)` so the per-proc `caller2` is reused.

* 8 regression atoms in `__tests__/extraction-vba.test.ts` (new
  describe "VbaExtractor — Issue #50: TempVars keys modeled as
  cross-form state nodes"):
  1. Bang-write `TempVars!MiClave = "valor"` →
     `references` edge from the proc, `access: 'write'`,
     placeholder `MiClave` of `class` kind.
  2. Parens-read `Dim x = TempVars("MiClave")` → edge from the
     SAME placeholder (cross-proc same-node: A writes, B reads).
  3. Parens-write `TempVars("MiClave") = 42` → `access: 'write'`.
  4. Add-form `TempVars.Add "MiClave", "x"` → `access: 'write'`;
     second key produces a second placeholder (atom verifies
     distinct placeholders for distinct keys).
  5. Cross-file dedup: `extract(fileA)` + `extract(fileB)`
     produce ONE placeholder node id (deterministic, equals
     `generateNodeId('synthetic:tempvar/<key>', 'class', '<key>',
     0)`); two edges from the two respective proc nodes.
  6. Dynamic key — variable arg `TempVars(strNombre)` → ZERO
     references, ZERO placeholder nodes. Concatenation
     `TempVars("a" & "b")` similarly silent.
  7. Cross-cutting smoke: 2 placeholders (`a`, `b`) + 3 edges
     (write-a, write-b, read-a) from the same proc.
  8. Regression guard on the SQL-table path (`emitReference` /
     `emitSqlTableReferences`): a `DoCmd.OpenForm` + `TempVars!x`
     file produces ONE form-layout stub + ONE TempVars placeholder,
     NOT a single merged node — different `synthesizedBy` tags keep
     them distinct.

Validation:

* `pnpm exec vitest run __tests__/extraction-vba.test.ts -t
  "Issue #50"` → 8 passed in 778 ms
* Full VBA suite (6 files): **258 passed** in 2.73 s — zero
  regressions; existing REQ-CODE-* atoms continue to pass
* `pnpm run build` → tsc clean, no TS errors

Cross-form impact analysis example (per the issue spec):
the workflow `codegraph_explore "quién produce IDExpediente"`
now connects the producing form's Sub to the consuming form's Sub
through one `TempVars("IDExpediente")` placeholder node — one
hop instead of zero.

Out of scope (intentional, deferred):

* `TempVars("x").Value = 1` write-detection (the `(?:\.Value)?`
  suffix in the issue's regex sketch). Per-line scope only;
  multi-physical-line continuations are not followed. Can be
  added in a future PR if any dysflow fixture uses this shape.
* `TempVars.Remove "x"` and other Remove/Count forms — issue spec
  enumerates Read/Write/Add only.
* Indexer-side cross-file placeholder collapse — placeholder ids
  are deterministic (so duplicates from N file extractions
  share the id); the existing indexer dedup at insert time
  handles collapse. No indexer change required.

## Not done

n/a — issue complete in this PR.
@ardelperal ardelperal added the type:feature New feature label Jul 4, 2026
@ardelperal
ardelperal merged commit 71bdb5f into main Jul 4, 2026
5 checks passed
@ardelperal
ardelperal deleted the chore/2026-07-04-issue-50-tempvars branch July 4, 2026 11:32
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): model TempVars keys as cross-form state nodes

1 participant