feat(vba): emit variable nodes for module-level state - #277
Merged
Merged
Conversation
Module-level Public/Private/Global/Dim declarations populated the type map and emitted type-reference edges, but never a node, so shared state was the largest coupling channel the graph could not see: "who reads gblConn?" had no answer at all. Both halves of T9 land together. A variable node on its own is a symbol nobody can trace, which is the partial-coverage trap the plan warns about, so nodes and read/write references ship in the same change. Design decisions: - Nodes are emitted ONLY when the declaration is module-level (currentVarTypeProcKey === 'module'). Procedure locals stay out: every local of every procedure is the node-explosion failure mode, and the def-use frontier is deliberately uncovered. - Dim and Static in a declarations section mean module-private in VBA, so visibility folds Public/Global to public and everything else to private. foldVisibility() is not reused: it folds unknown keywords to public and would claim every `Dim gblFoo` is exported. - Read/write references are gated strictly to names already registered as module-level variables of the SAME file. The sweep never looks for identifiers that might be variables; any looser rule produces thousands of references to names that merely collide. - Direction reuses isDirectAssignment() from the control sweep, which is exported rather than duplicated. A leading `Set ` is stripped from the prefix first so `Set gblConn = New Foo` reads as the write it is; that is input normalisation, not a second predicate. - References are de-duplicated per (procedure, variable, direction), so a procedure that reads a global twenty times reports one read and, if it also assigns it, one write. - Shadowing keeps the #205 rule holding for cases the type map alone cannot answer: its bare-Dim path yields to an outer declaration of the same name, and it never sees parameters. A dedicated per-procedure name registry records both, so a procedure that declares its own `codigo` reports no access to the module-level `codigo`. - Resolution is file-scoped. Names like m_count and strSQL are declared privately in dozens of modules; a global name match would invent a coupling between modules that never reference each other, so a miss stays a miss. The two pre-existing behaviours on the same lines are unchanged and pinned by tests: a qualified `As DAO.Database` declaration still emits its type reference exactly once, and a WithEvents field still emits its subscribes-event edge. Closes #251 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d
ardelperal
force-pushed
the
feat/issue-251
branch
from
September 2, 2026 06:05
67e9a80 to
31a4f3e
Compare
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.
What changed
Module-level VBA state is now in the graph, both as symbols and as relationships.
Nodes. Every module-level
Public/Private/Global/Dim/Staticdeclaration — includingWithEventsfields — emits onevariablenode withqualifiedName: '<Module>.<name>', a visibility folded from the declaration keyword,metadata: { declaredType, isArray, isWithEvents, isConst: false }, and acontainsedge from the module/class node via the existing pending-source mechanism. Procedure locals emit nothing.References. Inside a procedure body, an identifier that matches one of that same file's module-level variable names emits an
UnresolvedReferencetaggedsynthesizedBy: 'vba-module-var',property-setfor a write andproperty-getfor a read. The direction comes from the control sweep's existingisDirectAssignmentpredicate, which is exported rather than duplicated; a leadingSetis stripped from the prefix before the call soSet gblConn = New Fooreads as the write it is.Resolution. These references bind only to a
variablenode in their own file and never fall through to global name matching —m_countandstrSQLare declared privately in dozens of modules, and a global match would invent couplings between modules that never reference each other.Decisions worth reviewing
foldVisibility. That helper folds unrecognised keywords topublic, which would claim everyDim gblFoois exported. In a declarations sectionDimandStaticmean module-private; onlyPublicandGlobalexport.Me.<Control>sweep already makes.localVarTypeMap. The type map's bare-Dimpath yields to an outer declaration of the same name, and it never sees parameters at all, so both cases would have read as unshadowed. Parameters are parsed off the signature line, which also closes the shadow hole forSub Guardar(ByVal codigo As String)..basnow gets a module node. Module-level variables count towardhasAnySymbols, exactly as module-levelConstalready did.Tested
__tests__/extraction-vba-module-variables.test.ts— 26 tests, one per acceptance criterion that is expressible as a unit test, plus the two regression pins:Public gblConn As DAO.Database→ onevariablenode +contains, and theDAOreferencesedge still emitted exactly onceDim x As Longinside aSub→ no node, no referencePublic WithEvents m_Form As Form_X→ node withisWithEvents: true,subscribes-eventedge unchangedx = gblConn→property-get;gblConn = NothingandSet gblConn = New Foo→property-setisArray, multi-variable lines, re-declaration, string-literal names,other.gblConnmember access, and file-scoped resolutionnpx tsc --noEmitis clean.npx vitest run: 3261 passed, 21 failed, 59 skipped.Not verified
EPERMtemp-directory teardown quirk (worktree-detection,multi-repo-workspace, and twoextraction.test.tssubmodule cases — all fail inafterEachrmSync, with their assertions passing). The other 2 arenpm-sdkcases that resolve a real installed platform bundle on this machine instead of their fixture. No VBA extraction or resolution suite fails. I did not run amainbaseline to prove this, because the shared stash and sibling worktrees made a clean checkout unsafe here — the evidence is the failure sites, which are all in files this change does not touch.nodesByKind['variable'] ≈ 2,900, the drop inproceduresWithNoOutgoing, and the 15-reference precision spot-check all need the Access corpus, which is not checked in here. Someone with the corpus should run theT0probe before this is called done. The mechanism theproceduresWithNoOutgoingcriterion depends on is in place: the probe counts a procedure as a leaf only when it has zero edges and zero unresolved references, and a procedure that only touches globals now has references.Closes #251
🤖 Generated with Claude Code
https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d