feat(vba-extractor): model TempVars keys as cross-form state nodes (closes #50) - #75
Merged
Merged
Conversation
…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.
3 tasks
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 #50.
What
Access's global
TempVarskey-value store is the canonicalmechanism for passing state between forms (
TempVars!IDExpediente = Me.Idin the caller;TempVars("IDExpediente")read in theopened 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/.clsnow emits ONE synthetic placeholder per unique TempVar key
(cross-file stable id via
synthetic:tempvar/<key>prefix; sameclasskind as SQL-table placeholders for consistency) withreferencesedges from each reading/writing procedure. Themetadata.access: 'read'|'write'tag discriminates write fromread; the
metadata.synthesizedBy: 'vba-tempvar'tagdiscriminates TempVars placeholders from SQL-table placeholders
(both
classkind, both heuristic).Why
Impact analysis across
.form.txtfiles (the form UI layer) andtheir data sources (
.sqlqueries,TempVarscross-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
referencesedge hop.Diff
src/extraction/vba-extractor.ts__tests__/extraction-vba.test.tsInside 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
TempVars!clave = x(regexTEMP_VAR_BANG_RE,scanned on the masked line;
!itself survivesmaskStringContentbecause mask only replaces INSIDE string literals).
TempVars("clave")orTempVars("clave") = x(regexTEMP_VAR_PAREN_RE, scanned onthe ORIGINAL unmasked line — same split the SQL_TABLE_RE and
OpenForm literal-form scanners use).
TempVars.Add "clave", value(regexTEMP_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.classplaceholder vs newtempvarNodeKindChose
class, matching the SQL-table precedent (emitSqlTableReferences→
emitReference) andsynthClassNodeIdsdedup shape. Themetadata.synthesizedBy: 'vba-tempvar'tag cleanly discriminatesTempVars references from SQL-table references in the graph even
though both use
classkind. AvoidsNodeKindunion work acrosstypes.ts+ every consumer that filters on kind.Cross-file id stability
Uses synthetic file path
synthetic:tempvar/<key>(mirroringsynthetic:opensFormStub/...) so the same key across N filesmaps 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), NOTemitReference'sdeferred
pendingModuleOrClassSourcere-attribution. TempVarsis 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) andTempVars("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"):
TempVars!MiClave = "valor"→referencesedge from the proc,access: 'write',placeholder
MiClaveofclasskind.Dim x = TempVars("MiClave")→ edge from theSAME placeholder (cross-proc same-node: A writes, B reads).
TempVars("MiClave") = 42→access: 'write'.TempVars.Add "MiClave", "x"→access: 'write';second key produces a second placeholder (atom verifies
distinct placeholders for distinct keys).
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.TempVars(strNombre)→ ZEROreferences, ZERO placeholder nodes. Concatenation
TempVars("a" & "b")similarly silent.a,b) + 3 edges(write-a, write-b, read-a) from the same proc.
emitReference/emitSqlTableReferences): aDoCmd.OpenForm+TempVars!xfile produces ONE form-layout stub + ONE TempVars placeholder,
NOT a single merged node — different
synthesizedBytags keepthem distinct.
Validation
pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #50"→ 8 passed in 778 msregressions; 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 — onehop instead of zero.
Out of scope (intentional, deferred)
TempVars("x").Value = 1write-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 — issuespec enumerates Read/Write/Add only.
are deterministic (so duplicates from N file extractions share
the id); the existing indexer dedup at insert time handles
collapse. No indexer change required.