Skip to content

fix(vba): classify unambiguous statement-form calls as calls - #270

Merged
ardelperal merged 4 commits into
mainfrom
fix/issue-265-statement-call-kind
Sep 1, 2026
Merged

ardelperal merged 4 commits into
mainfrom
fix/issue-265-statement-call-kind

Conversation

@ardelperal

@ardelperal ardelperal commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

Closes #265

What changed

An unresolved statement-form Sub call was always pushed as unqualified-ident, the bucket for ambiguous bare-identifier reads. That classification is correct for exactly one of the three statement shapes:

Form Can it be a Const read? referenceKind
Escribir Yes — a bare identifier read unqualified-ident (unchanged)
Call Escribir No — the Call keyword is only valid on a procedure calls
Escribir 1, 2 No — a constant cannot take an argument list calls

detectStatementCall now returns { name, unambiguous }, and the unresolved-reference push site picks calls or unqualified-ident from that flag. Nothing else about the row changes — same fromNodeId, same referenceName, same vba-statement-call-unresolved stamp — so the two kinds can only trade rows with each other.

Resolved calls are untouched: they emit a calls edge and never reach this branch.

One extra piece the issue did not call out

maskStringContent blanks string literals to spaces before the statement detectors see the line, which reduces Escribir "texto" to the identifier plus trailing whitespace — indistinguishable from a bare read. Implementing "an argument list is present" on the space-masked line would therefore have left the single most common statement-call shape in the wrong bucket.

The statement/qualified/With detectors now run on a second, index-identical mask that fills literals with _ instead of spaces, so an argument list stays visible while the literal's contents stay unparseable. Column-sensitive scanners (scanCallSites, scanMeControlReferences, TempVars, DoCmd, Forms-bang) keep the original space mask; maskStringContent's default is unchanged.

Measurement

Two production Access projects (00_EXPEDIENTES, 00_GESTION_RIESGOS), 246 .bas / .cls modules. Baseline is a detached checkout of main at a0345ae.

Extractor output — the committed probe

npm run probe:vba -- "C:/00repos/codigo/00_EXPEDIENTES/src" "C:/00repos/codigo/00_GESTION_RIESGOS/src"
referenceKind Before (main a0345ae) After Δ
calls 7,679 8,976 +1,297
unqualified-ident 1,459 162 −1,297
sum of the two 9,138 9,138 0
references 2,057 2,057 0
member-with 1,488 1,488 0
qualified-call 399 399 0
property-get 215 215 0
property-set 76 76 0

diff between the two probe reports is 8 lines — those two rows and nothing else. Declared procedures 3,840, stub nodes 1,597, every node kind, every edge kind and synthesizer, the top-30 stub targets, SQL tables (69), forms (132) and errors are all identical.

The before column reproduces the figures the orchestrator quoted for current main exactly.

After the resolver — failed vs declined-runtime

These are the numbers the noise-ratio table in docs/vba-reference-kinds.md publishes. Same two projects, indexed separately through the real pipeline.

reference_kind Total (before → after) failed (before → after) failed / total (after)
calls 6,194 → 6,351 693 → 777 ~12%
unqualified-ident 166 → 9 85 → 1 ~11%
combined 6,360 → 6,360 778 → 778 ~12%

Both sums are invariant — rows were reclassified, none created or dropped. Every other kind is identical (member-with 1,401, references 826, qualified-call 380, property-get 215, property-set 76).

Docs

docs/vba-reference-kinds.md:

  • the calls row keeps Call LimpiaBuffer and now also names the argument-list form;
  • the unqualified-ident row states explicitly that a bare identifier with no Call keyword and no arguments stays there because of the Const-read ambiguity, with a short table and the reasons (FR-3.1 const-first disambiguation, and the name-only DAO-enum / intrinsic-constant gates from enh(resolver): DAO enum values + VBA intrinsic constants should be classified as built-ins #188 that key on this kind);
  • the v1.13.0 noise-ratio rows for the two affected kinds are marked superseded and a re-measured section is added, citing both the probe and the indexed run. The 00_VBA_TOOLKIT_BENCH corpus that produced the v1.13.0 numbers is no longer present on disk (its index is empty and it holds no sources), so the re-measurement is on the two Access projects above and says so;
  • stale line-number cross-references to src/types.ts and the two extractor files corrected.

Regression guards

One existing expectation was updated, not weakened: extraction-vba-stub-resolver.test.ts Test 9 pinned MsgBox "hi" and Shell "calc.exe" to unqualified-ident. Their kind is now calls; the assertion this test exists for — all three are declined-runtime — is unchanged.

Verification (on main a0345ae merged in)

  • npx tsc --noEmit — clean
  • pnpm run build — clean
  • pnpm exec vitest run vba extraction-sql-query sql-query-discovery — 48 files passed, 747 tests (47 on main; the new test file makes 48, nothing else moved)

Merging main produced one conflict, in CHANGELOG.md, where two ### Fixes bullets landed on the same line — both kept. src/extraction/vba/calls.ts (#245) and src/extraction/vba/text-utils.ts (#247) auto-merged cleanly; both were re-read after the merge and the measurements above were re-run on the merged tree.

New tests

__tests__/extraction-vba-statement-call-kind.test.ts — 13 tests, real files, no mocking: the detectStatementCall flag itself (including the _-mask case and the trailing-whitespace non-case), the three shapes end-to-end through VbaExtractor, the resolved Call Escribir edge, the FR-3.1 guards, and the declined-runtime guards through a real index.

An unresolved statement-form Sub call was always surfaced as
`unqualified-ident`, the bucket for ambiguous bare-identifier reads.
That is right for exactly one of the three statement shapes.

  Escribir        can be a Const read  -> unqualified-ident (unchanged)
  Call Escribir   Call is only valid on a procedure -> calls
  Escribir 1, 2   a constant takes no argument list -> calls

`detectStatementCall` now reports whether the shape is syntactically
unambiguous, and the unresolved-reference push site picks the kind from
it. Nothing else about the row changes, so the two kinds only trade rows.

The statement detectors also needed a second string mask: the existing
space mask reduces `Escribir "texto"` to the identifier plus blanks,
making an argument list indistinguishable from a bare read. They now run
on an index-identical `_` mask that keeps the literal visible as an
opaque token, while the column-sensitive scanners keep the space mask.

Resolved calls are unaffected. Bare identifiers stay in
`unqualified-ident` so the const-first disambiguation rule (#108, FR-3.1)
and the name-only DAO-enum / intrinsic-constant gates (#188) keep
working, and statement-form built-ins stay `declined-runtime` because the
stdlib gate (#192/#195) already accepts the `calls` kind by name.

Measured on two production Access projects, before -> after:
calls 6,194 -> 6,351 rows (693 -> 777 failed), unqualified-ident
166 -> 9 rows (85 -> 1 failed); combined 6,360 rows / 778 failed, both
unchanged. docs/vba-reference-kinds.md updated with the split and the
re-measured table.

Closes #265
The probe (`npm run probe:vba`) landed on main after this branch started;
it is now the supported way to reproduce the extractor-level half of the
issue #265 measurement, so quote it alongside the resolver-level numbers.
@ardelperal
ardelperal merged commit 8f43e06 into main Sep 1, 2026
5 checks passed
@ardelperal
ardelperal deleted the fix/issue-265-statement-call-kind branch September 1, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(vba): unambiguous statement-form calls are classified as bare identifier reads

1 participant