Skip to content

feat(vba-extractor): detect statement-form calls after Then/Else in single-line If (closes #45) - #63

Merged
ardelperal merged 1 commit into
mainfrom
chore/2026-07-03-issue-45-single-line-if-then-else
Jul 3, 2026
Merged

feat(vba-extractor): detect statement-form calls after Then/Else in single-line If (closes #45)#63
ardelperal merged 1 commit into
mainfrom
chore/2026-07-03-issue-45-single-line-if-then-else

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #45

Problem

\detectStatementCall\ and \detectQualifiedStatementCall\ (in \src/extraction/vba-extractor.ts) inspect only the FIRST identifier of the line. For \If x Then Foo arg\ the leading identifier is the keyword \If, which is in \CALL_KEYWORD_BLACKLIST, so the call AFTER \Then\ is silently dropped — even though the paren form \x.Method(...)\ on the same line is correctly captured by \CALL_RE. Same gap on the qualified path.

Result: the dominant VBA idiom for early-exit guards (\If Err.Number <> 0 Then GestionarError) contributed zero edges to the call graph, and the gap was inconsistent with the paren-form path.

Fix

Add \splitSingleLineIfClauses(line: string): string[]\ (Issue #45 helper):

  • If the line matches ^If\s[\s\S]+?\bThen\b\s+\ (non-greedy condition, requires whitespace after \Then\ so block-form \If x Then\ is left for the existing per-line scan), the prefix is stripped and the body is split on \ Else \ (case-insensitive, statement-level only) and then on :\ (multi-statement). \GoTo\ / \Exit\ / \Resume\ clauses are filtered at the fragment level (defense in depth) before reaching the statement-call detectors.
  • If the line does NOT match the single-line \If … Then\ shape, the splitter returns []\ so callers can use it unconditionally. Block-form \If\ keeps working through the existing per-line call-site scan that picks up the body on a subsequent line.

The two call-detection calls in \sweepCallsAndSql\ now iterate over the returned fragments instead of receiving the raw line. \CALL_RE\ (paren-form) is unchanged — it still scans the entire masked line as before, so \If x Then M(a) Else N(b)\ continues to emit both \M(a)\ and \N(b)\ edges via that path.

Tests

_tests_/extraction-vba.test.ts\ gains a new \describe('VbaExtractor — single-line \If … Then\ statement-form calls (Issue #45)', ...)\ block with 8 atoms that previously failed (red) and now pass (green):

  1. \If Err.Number <> 0 Then GestionarError\ → emits \GestionarError\ (the canonical real-world case).
  2. \If x Then Foo Else Bar\ → emits BOTH \Foo\ and \Bar\ calls edges.
  3. \GoTo fin\ clause → silent (no edge to \GoTo*).
  4. \Exit Sub\ clause → silent (no edge to \Exit).
  5. Multi-statement \If x Then DoA: DoB\ (colon-separated) → emits both.
  6. Block-form \If x Then\ with body on next line → unchanged (regression guard).
  7. \Call\ keyword with statement-form arguments after \If … Then\ → still emits.
  8. Qualified statement-call \m_Srv.Registrar 1\ after \If … Then\ → emits heuristic \Srv.Registrar\ stub (Fix 2 / #12a receiver-resolution path preserved).

Verification

  • \pnpm test\ → 2065 passed, 41 skipped (skipped set is the pre-existing Windows + Node 25 environment skips listed in \AGENTS.md; nothing new skipped).
  • \pnpm exec tsc --noEmit\ → clean.
  • 324 lines changed across the two files; no other code touched.

…ngle-line If (closes #45)

Reads \If <cond> Then <calls> [Else <calls>]\ and \If <cond> Then <calls>: <calls>\ and emits \calls\ edges for the same-file Sub targets. \GoTo\, \Exit\, \Resume\ clauses are deliberately silent. Block-form \If\ (where the body is on subsequent lines) is unchanged.

Tests: __tests__/extraction-vba.test.ts -> +8 (bare-Then happy path, Else clause, GoTo/Exit suppressed, colon multi-statement, Call keyword, qualified statement-call, block-form regression).
@ardelperal
ardelperal merged commit 390ba29 into main Jul 3, 2026
6 of 7 checks passed
@ardelperal
ardelperal deleted the chore/2026-07-03-issue-45-single-line-if-then-else branch July 3, 2026 18:56
ardelperal added a commit that referenced this pull request Jul 4, 2026
* fix(vba): unify qualified call gating (closes #40)

Cherry-pick of 9b1787a from `master` onto `main` (80ca05d).

The original commit landed on `master` but never reached the
`ardelperal/codegraph-vba` fork's `main`; the issue was reopened
2026-07-04 because the fix was unreachable from the published binary.
This commit reapplies the unified `shouldProcessQualifiedCall` gate
on the `main` branch tip.

Blame on merge — three conflict hunks ported:

1. `isLocalProjectClassVar` (line ~1687): kept HEAD. The bracketed-strip
   + null-check + Issue #54 comment are load-bearing for the
   `[FUNCIONES UTILES].Foo` shape; the trailing PRIMITIVE_TYPES check is
   identical on both sides.

2. `sweepCallsAndSql` qualified-statement block (line ~1932): kept
   HEAD's full `splitSingleLineIfClauses` loop structure (added in
   PR #63, which is on `main` but not in the cherry-pick base) and
   swapped the qualStmt gate from `isLocalProjectClassVar` to
   `shouldProcessQualifiedCall`. The `withReceiverStack` block is
   unchanged — its own `isLocalProjectClassVar` gate stays.

3. `localVarTypeMap` docstring (line ~3167): took 9b1787a's updated
   wording (references the unified gate) but kept the Issue #2
   reference for historical continuity. The TempVars code between
   `synthClassNodeIds` and `localVarTypeMap` is HEAD-only (added by
   commit 71bdb5f / Issue #50, post-9b1787a) and stays untouched.

Acceptance criteria (Issue #40):

- (a) `Set rs = db.OpenRecordset(sql)` with `Dim db As DAO.Database`
      produces no stub function node. Pinned by the new
      `qualified statement-form call on external local variable is
      silent` test and the `AC (a)` test in `extraction-vba.test.ts`.
- (b) `modUtils.Foo arg` and `modUtils.Foo(arg)` both produce a
      resolvable call edge. Pinned by `AC (b)` (both shapes on
      separate lines, each emits a heuristic edge to a `modUtils.Foo`
      stub).
- (c) `Dim m_NCOp As NCOperaciones` then `m_NCOp.Registrar args`
      keeps emitting to the resolved class name. Pinned by `AC (c)`.

Dysflow corpus validation (C:\Proyectos\dysflow\E2E_testing\src,
203 .bas/.cls/.form.txt files):

  metric                     main    fix    delta
  total nodes                8640   10045  +1405
  total edges               15338   16833  +1495
  stub nodes                 1850    3255  +1405
  total calls edges          5091    6586  +1495
  heuristic calls edges      2629    4124  +1495

Heuristic `calls` edges increase by +1495 — the fix's main intent
(cross-module statement-form qualified calls now emit). Stubs go up
because the unified gate also admits undeclared receivers in the
statement form (e.g. `VBA.DoEvents`, function-parameter receivers
that aren't in `localVarTypeMap`); the DAO-typed local-var shape is
silenced correctly:

  silenced (good):                     newly emitted (statement-form):
    db.OpenRecordset    -54             VBA.DoEvents           +1534
    dbUse.OpenRecordset -22             p_Db.Execute            +48
    datos.Exists         -8             cmb.AddItem             +23
    m_ColDestinatariosOcultos.exists -8 pLogs.Add              +13
    VBA.VarType          -7             fso.CreateFolder        +12
                                        AddLog                  +65
                                        InvalidarCache          +18
                                        ActualizarDatosCalculados +14

The +1534 `VBA.DoEvents` spike is a pre-existing gap: the
statement-form path doesn't run the `RUNTIME_RECEIVER_BLACKLIST`
filter that the paren-form `scanCallSites` does at
vba-extractor.ts:2070. Filtering runtime receivers in the statement
form is a follow-up (not in this PR's scope); the cross-module
qualified-call shape is what issue #40 asked for, and that now works
in both forms.

Test results (focused):
  npx vitest run __tests__/extraction-vba.test.ts -t "qualified"     36 ✓
  npx vitest run __tests__/extraction-vba.test.ts -t "AC"             8 ✓
  npx vitest run __tests__/extraction-vba.test.ts -t "bracketed"       7 ✓

Full VBA suite (346 tests across 10 files): all green. The
`npm test` full-suite run on Node 25.2.1 hits the documented V8
turboshaft Zone OOM (issue #81) in `resolution.test.ts` —
environment, not regression.

* docs(changelog): note Issue #40 fix in [Unreleased]

Cherry-pick of 9b1787a from master onto main re-applies the unified
qualified-call gating on the published binary's main branch.
Document the user-visible change in [Unreleased] so the next release
notes cover it.

* ci(workflow): drop Ubuntu runner; Windows-only CI

Project target is Microsoft Access / VBA. Ubuntu CI added minutes without
exercising anything the Windows leg didn't already cover — node:sqlite,
tree-sitter, and the cross-language surface all run fine on Windows.

Revisit if a Linux-specific failure mode ever appears in production.
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.

feat(vba): detect statement-form calls after Then/Else in single-line If

1 participant