@aroman22/codegraph-vba-darwin-arm64 1.3.5 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/lib/dist/extraction/index.d.ts.map +1 -1
  2. package/lib/dist/extraction/index.js +26 -7
  3. package/lib/dist/extraction/index.js.map +1 -1
  4. package/lib/dist/extraction/sql-query-extractor.d.ts +11 -3
  5. package/lib/dist/extraction/sql-query-extractor.d.ts.map +1 -1
  6. package/lib/dist/extraction/sql-query-extractor.js +19 -5
  7. package/lib/dist/extraction/sql-query-extractor.js.map +1 -1
  8. package/lib/dist/extraction/vba-extractor.d.ts +575 -55
  9. package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
  10. package/lib/dist/extraction/vba-extractor.js +1233 -135
  11. package/lib/dist/extraction/vba-extractor.js.map +1 -1
  12. package/lib/dist/extraction/vba-form-extractor.d.ts +127 -0
  13. package/lib/dist/extraction/vba-form-extractor.d.ts.map +1 -1
  14. package/lib/dist/extraction/vba-form-extractor.js +290 -3
  15. package/lib/dist/extraction/vba-form-extractor.js.map +1 -1
  16. package/lib/dist/extraction/vba-preprocess.d.ts +22 -3
  17. package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
  18. package/lib/dist/extraction/vba-preprocess.js +137 -15
  19. package/lib/dist/extraction/vba-preprocess.js.map +1 -1
  20. package/lib/dist/extraction/vba-source.d.ts +58 -0
  21. package/lib/dist/extraction/vba-source.d.ts.map +1 -0
  22. package/lib/dist/extraction/vba-source.js +137 -0
  23. package/lib/dist/extraction/vba-source.js.map +1 -0
  24. package/lib/dist/types.d.ts +2 -2
  25. package/lib/dist/types.d.ts.map +1 -1
  26. package/lib/dist/types.js +6 -0
  27. package/lib/dist/types.js.map +1 -1
  28. package/lib/dist/utils/backtrace-helpers.d.ts +35 -0
  29. package/lib/dist/utils/backtrace-helpers.d.ts.map +1 -0
  30. package/lib/dist/utils/backtrace-helpers.js +129 -0
  31. package/lib/dist/utils/backtrace-helpers.js.map +1 -0
  32. package/lib/dist/utils/sql-impact-helpers.d.ts +38 -0
  33. package/lib/dist/utils/sql-impact-helpers.d.ts.map +1 -0
  34. package/lib/dist/utils/sql-impact-helpers.js +329 -0
  35. package/lib/dist/utils/sql-impact-helpers.js.map +1 -0
  36. package/lib/node_modules/.modules.yaml +2 -2
  37. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  38. package/lib/package.json +2 -2
  39. package/package.json +1 -1
@@ -94,17 +94,56 @@ export declare class VbaExtractor {
94
94
  * DIM_UNQUAL_RE pair with a prefix-check + global scan that handles
95
95
  * `As New <Type>`, multi-variable `Dim a As Foo, b As Bar`, and all
96
96
  * visibility keywords in one pass.
97
+ * Issue #47: now also accepts `Global` (module-level typed instance) and
98
+ * `Static` (procedure-local retention modifier) so they emit the same
99
+ * `references` edge and `localVarTypeMap` registration as their `Dim`
100
+ * siblings today. The negative lookahead is unchanged: `Const` is still
101
+ * routed to `sweepEnumsAndConsts`.
97
102
  */
98
103
  private static readonly DIM_DECL_PREFIX_RE;
99
104
  /**
100
105
  * Globally scan all `identifier As [New] TypePart1[.TypePart2]` on a
101
106
  * variable declaration line. Run with /g after confirming DIM_DECL_PREFIX_RE.
102
107
  *
103
- * Groups: (1) variable name, (2) type outer part, (3) type inner part (if qualified).
108
+ * Groups: (1) variable name, (2) bracketed outer type, (3) unbracketed
109
+ * outer type, (4) bracketed inner type (if qualified), (5) unbracketed
110
+ * inner type. The variable name is always bare (`Dim` cannot declare a
111
+ * bracketed variable). The TYPE position accepts BOTH bracketed names
112
+ * with spaces (e.g. `[Clase Con Espacios]`) and bare identifiers — the
113
+ * bracketed capture wins when present. Only one of (2)/(3) and one of
114
+ * (4)/(5) is ever populated per match.
104
115
  * `(?:New\s+)?` consumes the VBA auto-instantiation keyword so it is
105
116
  * never captured as the type name (Fix 1).
117
+ *
118
+ * Issue #54: extends the type alternative to accept `[Name With Spaces]`
119
+ * so `Dim x As [Clase Con Espacios]` emits a `references` edge to
120
+ * `Clase Con Espacios` (brackets unwrapped). The unwrap is applied in
121
+ * the sweep loop by picking the bracketed capture group when present.
106
122
  */
107
123
  private static readonly DIM_ALL_VARS_RE;
124
+ /**
125
+ * Bare-declared variable capture for the `Dim|Private|Public|Global|Static`
126
+ * prefix. Captures (1) the variable name. Used to register bare `Dim x`
127
+ * (no `As` clause) and explicit-primitive `Dim x As Long|String|...`
128
+ * declarations into `localVarTypeMap` so the type tracking is consistent
129
+ * across all three Dim shapes:
130
+ *
131
+ * `Dim x` → outer = 'variant' (VBA default)
132
+ * `Dim x As Variant` → outer = 'variant' (PRIMITIVE_TYPES member)
133
+ * `Dim x As Long` → outer = 'long' (PRIMITIVE_TYPES member)
134
+ * `Dim x As Foo` → outer = 'foo' (project class — non-primitive)
135
+ *
136
+ * Antigravity audit Task 3: the previous `DIM_ALL_VARS_RE` only matched
137
+ * the `... As <Type>` form, so a bare `Dim x` was invisible to
138
+ * `isLocalProjectClassVar` / `scanCallSites` and `x.Method(1)` produced
139
+ * a dead-end `calls` edge to a stub named `x.Method` that no resolver
140
+ * could repoint. Registering bare Dim with `outer = 'variant'` closes
141
+ * the gate, so `scanCallSites` skips ONLY when the receiver is mapped
142
+ * as a primitive — leaving the "undeclared receiver → stub → resolver
143
+ * repoints" path intact for cross-module qualified calls like
144
+ * `modUtils.Foo(1)` (`modUtils` is not in `localVarTypeMap`).
145
+ */
146
+ private static readonly BARE_DIM_VAR_RE;
108
147
  /**
109
148
  * VBA primitive type names — skipped when emitted as Dim targets so
110
149
  * we don't pollute the graph with `As Long` / `As String` references.
@@ -114,7 +153,7 @@ export declare class VbaExtractor {
114
153
  * pattern is ever captured as a type name it is silently skipped.
115
154
  */
116
155
  private static readonly PRIMITIVE_TYPES;
117
- /** `WithEvents m_X As Form_Foo` — Dim/Private/Public prefix is optional. */
156
+ /** `WithEvents m_X As Form_Foo` — Dim/Private/Public/Global/Static prefix is optional. */
118
157
  private static readonly WITHEVENTS_RE;
119
158
  private sweepDimsAndWithEvents;
120
159
  /** `[visibility] Enum <Name>` — opens an enum block. */
@@ -130,6 +169,15 @@ export declare class VbaExtractor {
130
169
  private static readonly ENUM_MEMBER_RE;
131
170
  /** `[visibility] Const <decls>` — captures visibility (1) and the rest (2). */
132
171
  private static readonly CONST_DECL_RE;
172
+ /**
173
+ * Issue #52: shared `End Sub` / `End Function` / `End Property` marker.
174
+ * Promoted from a local regex in `sweepCallsAndSql` so `sweepEnumsAndConsts`
175
+ * can walk the same proc boundaries and decide Const scope per line.
176
+ * The `(?:^|:\s*)` prefix tolerates colon-separated single-line procs
177
+ * (`Public Sub X(): ... : End Sub`) so the proc stack pops on the same
178
+ * physical line.
179
+ */
180
+ private static readonly PROCEDURE_END_RE;
133
181
  /**
134
182
  * Fold a VBA visibility keyword to the canonical lowercase enum, matching
135
183
  * the procedure convention: `Private` → 'private'; `Public`, `Global`,
@@ -165,9 +213,18 @@ export declare class VbaExtractor {
165
213
  * - While inside a procedure, scan the line for call-site patterns and
166
214
  * SQL-wrapper patterns.
167
215
  *
168
- * Call-site regex: `(?<!\w)([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?\s*\(`
169
- * captures either `Name(...)` (same-file candidate) or `Receiver.Member(...)`
170
- * (qualified emit a synthetic node + heuristic edge).
216
+ * Call-site regex captures either `Name(...)` (same-file candidate) or
217
+ * `Receiver.Member(...)` (qualified emit a synthetic node + heuristic
218
+ * edge). The receiver AND member alternatives accept BOTH the bare form
219
+ * (`Foo`) and the VBA bracketed form (`[Foo Bar]`) — bracketed captures
220
+ * win when present. Only one of (1)/(2) and one of (3)/(4) is ever
221
+ * populated per match. Brackets are stripped by the regex itself (the
222
+ * capture groups hold the inner content), so callers receive unwrapped
223
+ * identifiers and the `${name}.${proc}` stub shape stays canonical.
224
+ *
225
+ * Issue #54: the bracketed alternative was previously absent, so
226
+ * `[FUNCIONES UTILES].FormatearFecha(fecha)` (a real Dysflow-exported
227
+ * idiom for modules with spaces in their names) was silently dropped.
171
228
  */
172
229
  private static readonly CALL_RE;
173
230
  /** SQL wrapper helpers — order matters because `db.Execute` is a suffix of others. */
@@ -193,30 +250,169 @@ export declare class VbaExtractor {
193
250
  * the `opens-form` edge instead — sharing no logic with CALL_RE.
194
251
  */
195
252
  private static readonly OPEN_FORM_ARG_RE;
253
+ /**
254
+ * Issue #48: `DoCmd.OpenReport "<ReportName>"` modelling regex — sibling
255
+ * of `OPEN_FORM_ARG_RE` (hueco 6 expanded). Same literal-or-bare-id argument
256
+ * capture (group 1) and same trailing positional-args drop. The dispatch
257
+ * table `DOCMD_OPEN_DISPATCH` (below) carries the per-method metadata so
258
+ * OpenForm and OpenReport share the same scan/emit pipeline while their
259
+ * edge kinds (`opens-form` vs `opens-report`), stub node kinds
260
+ * (`form-layout` vs `report-layout`), synthetic file-path prefixes, and
261
+ * qualifiedName prefixes (`Form_<Name>` vs `Report_<Name>`) stay
262
+ * distinct.
263
+ */
264
+ private static readonly OPEN_REPORT_ARG_RE;
265
+ /**
266
+ * Issue #48 dispatch table — shared literal-or-Const argument resolution
267
+ * for `DoCmd.OpenForm` and `DoCmd.OpenReport`. Each entry is everything
268
+ * `scanDoCmdOpenCalls` + `emitOpensStubEdge` need to share the pipeline
269
+ * between methods while keeping the per-method names distinct.
270
+ *
271
+ * OpenQuery is intentionally NOT in this dispatch — it emits an
272
+ * `UnresolvedReference` (not a stub + edge), resolution to the REAL
273
+ * `query` node emitted by `SqlQueryExtractor`. See
274
+ * `OPEN_QUERY_ARG_RE` + `scanDoCmdOpenQuery`.
275
+ *
276
+ * Why a separate dispatch: `DoCmd` is in `RUNTIME_RECEIVER_BLACKLIST`
277
+ * (R4 invariant), so ALL of these methods are intentionally SKIPPED by
278
+ * the generic `CALL_RE` path that would otherwise emit a junk `calls`
279
+ * edge to a synthetic `function` node for `DoCmd.OpenX`. This dispatch
280
+ * matches BEFORE the call-site scan and uses its own emission path.
281
+ */
282
+ private static readonly DOCMD_OPEN_DISPATCH;
283
+ /**
284
+ * Issue #48: `DoCmd.OpenQuery "<QueryName>"` modelling regex. Emits an
285
+ * `UnresolvedReference` (NOT a stub + edge) so the resolver binds to the
286
+ * REAL `query` node `SqlQueryExtractor` emits for `queries/<Name>.sql`
287
+ * — the same shape as `vba-me-control` and `vba-forms-bang`. The query
288
+ * may not yet exist in the index when the .bas is parsed; the resolver
289
+ * does the binding when the .sql is later indexed.
290
+ *
291
+ * Argument shape: identical to OpenForm/OpenReport — literal `"..."` or
292
+ * bare identifier resolved against local `Const` declarations, falling
293
+ * back to the bare identifier when unknown.
294
+ */
295
+ private static readonly OPEN_QUERY_ARG_RE;
196
296
  /** SQL assigned to a local variable, e.g. `m_SQL = "SELECT ..." & ...`. */
197
297
  private static readonly SQL_VAR_ASSIGN_RE;
198
298
  /** SQL wrapper called with a variable, e.g. `getdb().Execute m_SQL`. */
199
299
  private static readonly SQL_VAR_EXEC_RE;
200
- /** SQL table-name regex scoped to FROM / INTO / UPDATE. */
300
+ /**
301
+ * Issue #42: `DoCmd.RunSQL <identifier>` (variable form) — the dominant
302
+ * Access idiom for executing a dynamically-built SQL string. Today only
303
+ * the literal form `DoCmd.RunSQL "DELETE FROM X"` is tracked via the
304
+ * `SQL_WRAPPERS` regex at line 1108; the variable form silently dropped
305
+ * table impact for every procedure that builds SQL in a string and runs
306
+ * it through `DoCmd.RunSQL`.
307
+ *
308
+ * This regex is the DoCmd.RunSQL analogue of `SQL_VAR_EXEC_RE` above and
309
+ * is iterated by `scanSqlInLine` (lines 2051+). When a match is found,
310
+ * the captured identifier is resolved against `sqlVariables` (populated
311
+ * by `trackSqlVariableAssignment` with `&`-accumulate semantics — Issue
312
+ * #13) and the resulting SQL string drives `emitSqlTableReferences`.
313
+ *
314
+ * The optional `(?:\(\))?` + `\s*\(?` shape lets the regex match both
315
+ * the parenthesised form `DoCmd.RunSQL(strSQL)` and the no-paren form
316
+ * `DoCmd.RunSQL strSQL` that the existing SQL_WRAPPERS literal regex
317
+ * does not cover. The captured identifier is the only thing we need —
318
+ * we DO NOT try to parse what the variable points at; that's the
319
+ * existing `sqlVariables` map's job.
320
+ */
321
+ private static readonly SQL_VAR_DOCMD_RUNSQL_RE;
322
+ /**
323
+ * SQL table-name regex scoped to the clauses that introduce a table
324
+ * reference: `FROM <t>`, `JOIN <t>`, `INTO <t>`, `UPDATE <t>`. Adding
325
+ * `JOIN` lets the scanner pick up tables from joined fragments that
326
+ * arrive via `&`-concatenated wrapper literals (e.g.
327
+ * `db.Execute "FROM A" & " JOIN B"`); without it the second literal's
328
+ * table was silently dropped even though the wrapper regex now matches
329
+ * the chain.
330
+ *
331
+ * The captured table name is an optional bracketed/unbracketed schema
332
+ * prefix followed by a `.`, then a bracketed-or-bare identifier — so
333
+ * `FROM dbo.tblCustomers` and `FROM [My Schema].[My Table]` come
334
+ * through as one composite reference. Without the prefix the regex
335
+ * still matches a single identifier byte-identical to the old shape.
336
+ * Brackets in the captured composite are stripped by
337
+ * `emitSqlTableReferences` (`replace(/[\[\]]/g, '')`), so the public
338
+ * node name is the unwrapped form `dbo.tblCustomers` /
339
+ * `My Schema.My Table` — matching how plain `[Order Details]` is also
340
+ * unwrapped to `Order Details`. The identifier class
341
+ * `\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*` (same as the saved-queries
342
+ * `TABLE_RE` in `sql-query-extractor.ts`) ensures bracketed names
343
+ * with spaces — `[Order Details]`, `[My Schema]`, `[My Table]` —
344
+ * are captured whole.
345
+ */
201
346
  private static readonly SQL_TABLE_RE;
202
347
  /**
203
- * `Me.<ControlName>` reference capturehole 1 of VBA control-modeling.
348
+ * Issue #50: TempVarsAccess's global key-value store for cross-form
349
+ * state. We model each STATIC-LITERAL key as a synthetic `class` placeholder
350
+ * (same NodeKind as SQL table refs, see `emitReference`) and emit one
351
+ * `references` edge per reading/writing procedure with
352
+ * `metadata.synthesizedBy: 'vba-tempvar'` and `metadata.access` ∈
353
+ * `{'read','write'}` (the user-facing enum; lowercase single-tokens).
354
+ *
355
+ * Three scanner surfaces cover the four real idioms:
356
+ * - `TEMP_VAR_BANG_RE` — `TempVars!clave` (no parens; write or read)
357
+ * - `TEMP_VAR_PAREN_RE` — `TempVars("clave")` (parens; write or read)
358
+ * - `TEMP_VAR_ADD_RE` — `TempVars.Add "clave", v` (always a write)
359
+ *
360
+ * Bang vs paren split by line-source: the bang form has no string literals
361
+ * in scope, so we scan the MASKED line (`maskStringContent` replaces
362
+ * `"…"` content with spaces — same line source the call-site / With
363
+ * event scanners consume). The paren and Add forms have their key INSIDE
364
+ * a `"…"` literal that gets blanked by the masker, so those regexes scan
365
+ * the ORIGINAL (unmasked) line — same split the SQL_TABLE_RE/OpenForm
366
+ * literal scanners already use.
367
+ *
368
+ * Dynamic-key forms — `TempVars(strNombre)`,
369
+ * `TempVars("clave" & suffix)` — are silently unmatched by all three
370
+ * regexes (none of them tolerate a function-call or `&` arg). REQ-CODE-4
371
+ * "unresolvable is silent" applies: these stay silent by design to
372
+ * prevent placeholder-node explosion.
373
+ */
374
+ private static readonly TEMP_VAR_BANG_RE;
375
+ /**
376
+ * Issue #50 (cont.):
377
+ * `TempVars("clave")` / `TempVars( "clave" )` capture. Scanned
378
+ * over the original (unmasked) line — the literal lives INSIDE a
379
+ * string and would be stripped by `maskStringContent`.
380
+ */
381
+ private static readonly TEMP_VAR_PAREN_RE;
382
+ /**
383
+ * Issue #50 (cont.):
384
+ * `TempVars.Add "clave", value` capture. Always a write (the
385
+ * `.Add` method inserts/updates the entry). Scanned over the
386
+ * original (unmasked) line for the same string-literal reason as
387
+ * `TEMP_VAR_PAREN_RE`.
388
+ */
389
+ private static readonly TEMP_VAR_ADD_RE;
390
+ /**
391
+ * `Me.<ControlName>` / `Me!<ControlName>` reference capture — hole 1
392
+ * of VBA control-modeling. Extended by Issue #44 to accept the bang
393
+ * form (the default-collection shortcut Access VBA inherits from VB).
204
394
  *
205
- * Real VBA idiom:
206
- * `Me.lblTitulo.Caption = "Hello"` ← property assignment
207
- * `Me.txtDescripcion.Value = "World"` ← property assignment
208
- * `Me.ComandoGrabar.Enabled = True` ← property assignment
395
+ * Real VBA idioms:
396
+ * `Me.lblTitulo.Caption = "Hello"` ← dot form, property assignment
397
+ * `Me.txtDescripcion.Value = "World"` ← dot form, property assignment
398
+ * `Me.ComandoGrabar.Enabled = True` ← dot form, property assignment
399
+ * `Me!txtNombre = "Hello"` ← BANG form (default collection)
400
+ * `Me!txtEstado.Value = 1` ← BANG form, then property
209
401
  * `If Nz(Me.MotivoBorrado, "") = "" Then` ← read in expression
210
402
  *
211
403
  * The existing call-site scanner (CALL_RE) only fires on `Name(`
212
404
  * (paren form) and `Me` is in its keyword blacklist anyway, so
213
405
  * `Me.<Control>` references are silently invisible. This regex matches
214
- * the FIRST identifier after `Me.` regardless of what follows (a
215
- * property, an index, an assignment, a call argument, etc.) so the
406
+ * the FIRST identifier after `Me.` or `Me!` regardless of what follows
407
+ * (a property, an index, an assignment, a call argument, etc.) so the
216
408
  * form → control binding is surfaced as an UnresolvedReference for the
217
409
  * resolver to pick up later. Subsequent segments (`.Caption`, `.Value`,
218
410
  * `.Enabled`) are intentionally NOT captured — they are properties of
219
- * the control, not new symbols.
411
+ * the control, not new symbols. The bang (`!`) is the default-collection
412
+ * shortcut: `Me!txtFoo` is semantically identical to `Me.txtFoo` and
413
+ * produces a byte-identical UnresolvedReference (same `referenceName`,
414
+ * `referenceKind`, and `metadata.synthesizedBy`) — the regression test
415
+ * in `__tests__/extraction-vba.test.ts` pins this parity.
220
416
  *
221
417
  * Provenance: `metadata.synthesizedBy = 'vba-me-control'`. Mirrors the
222
418
  * `vba-form-binding` (form→sibling-`.cls`) and `vba-name-resolution`
@@ -224,6 +420,84 @@ export declare class VbaExtractor {
224
420
  * in `src/types.ts`.
225
421
  */
226
422
  private static readonly ME_CONTROL_RE;
423
+ /**
424
+ * Issue #44: `Forms!<FormName>` / `Forms("<FormName>")!<Ctl>` cross-form
425
+ * reference capture — companion to `ME_CONTROL_RE` above. Access VBA's
426
+ * default-collection shortcut for cross-form control access, captured
427
+ * BEFORE the generic call-site scan and emitted as its own
428
+ * `UnresolvedReference` family.
429
+ *
430
+ * Real VBA idioms:
431
+ * `Forms!FormX!txtY.Value = 1` — bang form, with control segment
432
+ * `Forms!FormX.Recordsource = "..."` — bang form, trailing property access
433
+ * `Set f = Forms!FormX` — bang form, no control segment
434
+ * `Forms("FormX")!txtY.Value = 1` — paren form, with control
435
+ * `Forms![Mi Formulario]!txtY` — bracketed form name (#54 mirror)
436
+ *
437
+ * Why a dedicated scanner (mirroring `OPEN_FORM_ARG_RE` / B4 / DoCmd.OpenForm):
438
+ * `Forms` is in `RUNTIME_RECEIVER_BLACKLIST`, so the generic CALL_RE
439
+ * path silently skips both `Forms!X` and `Forms("X")!Y`. Without this
440
+ * scanner, cross-form UI traffic from `Forms!FormX!txtY.Value` would
441
+ * never surface the form → control binding that the resolver needs to
442
+ * glue form `form-layout` nodes to control `form-instance-control`
443
+ * nodes. The dedicated dispatch fires BEFORE the call-site scan and
444
+ * uses its own emission path; the runtime-blacklist constraint is
445
+ * preserved (we intentionally do not rewrite CALL_RE).
446
+ *
447
+ * What this scanner emits per match:
448
+ * - ONE `UnresolvedReference` whose `referenceName` is the form's
449
+ * identifier (stripped of any surrounding quote or bracket
450
+ * decoration), tagged `metadata.synthesizedBy = 'vba-forms-bang'`
451
+ * and `referenceKind = 'references'`. The control segment (if
452
+ * present) is consumed by the regex so `Forms!FormX!txtY.Value` is
453
+ * captured as a single match, but the control name is NOT emitted
454
+ * as its own reference — control emission is the form's
455
+ * responsibility downstream.
456
+ * - NO synthetic `function` node (W4 graph-pollution invariant — the
457
+ * form is a real `.cls` / `.form.txt` pair the resolver already
458
+ * picks up via the `vba-form-binding` path). Without this guard the
459
+ * bang form would synthesize one stub per cross-form reference
460
+ * site, identical to what `bumpShapes` from #43 audited out.
461
+ *
462
+ * What this scanner does NOT match (intentional, pinned by tests):
463
+ * - `Forms!FormX.Foo` — the trailing `.Foo` IS a property
464
+ * access on the form (e.g. `Recordsource`), NOT a control access.
465
+ * The bang alternative carries a `(?![.\w])` negative-lookahead
466
+ * after the form identifier to drop this shape. The "W4
467
+ * no-synthetic-fn" test pins both halves of this contract.
468
+ * - `rs!Campo` — recordset field access (DAO/ADO
469
+ * default-member field read) is explicitly out of scope for the
470
+ * current change. The runtime-receiver blacklist plus the absence
471
+ * of any `Forms`/`Me` prefix means the existing scanners already
472
+ * skip it cleanly; the "STRETCH SCOPE" test pins that behaviour
473
+ * so a future change to bring recordset bangs in is reviewed
474
+ * explicitly against the bang-form scope decision.
475
+ *
476
+ * Operates on the ORIGINAL (unmasked) line — the paren form
477
+ * `Forms("FormX")!txtY` has the form name INSIDE a string literal, so
478
+ * masking string content would destroy the form identifier. Same
479
+ * unmasked-line constraint as `scanOpenFormCalls`.
480
+ */
481
+ private static readonly FORMS_BANG_RE;
482
+ /**
483
+ * Issue #46: scan `Set <var> = New <Type>[.<Inner>]` lines — the dominant
484
+ * VBA late-instantiation idiom. Run inside `sweepCallsAndSql`'s proc-stack
485
+ * loop so the surrounding procedure is known. For each match:
486
+ * - register `<var>` in `localVarTypeMap` with `outer=<Type>`,
487
+ * `qualified=<hasInner>`, `assignedWithSet=true` so the PR #61 refined
488
+ * gate lets subsequent `<var>.Member ...` qualified calls resolve via
489
+ * the resolved class name;
490
+ * - emit a `references` edge from the module/class node to a synthetic
491
+ * node named `<Type>`, tagged `synthesizedBy: 'vba-set-new'`.
492
+ *
493
+ * Groups: (1) variable name, (2) outer type, (3) optional inner type.
494
+ * Operates on the MASKED line (string-literal content already replaced
495
+ * with spaces) so `Set x = New Foo` inside a string literal never matches.
496
+ */
497
+ private static readonly SET_NEW_RE;
498
+ /** Issue #43: track the receiver for `With <expr>` / `End With` blocks. */
499
+ private static readonly WITH_START_RE;
500
+ private static readonly WITH_END_RE;
227
501
  /** Keywords we never want to match as call receivers. */
228
502
  private static readonly CALL_KEYWORD_BLACKLIST;
229
503
  /**
@@ -253,6 +527,15 @@ export declare class VbaExtractor {
253
527
  * qualified, non-primitive) identifier — a candidate project-defined class.
254
528
  * Qualified types (e.g. `DAO.Recordset`) and primitives (`String`, `Long`)
255
529
  * return false so runtime/DAO calls are suppressed.
530
+ *
531
+ * Issue #54 (defensive): brackets are stripped from the lookup key, so a
532
+ * caller that forgets to unwrap a bracketed name still finds the
533
+ * corresponding entry. This is a no-op when the name is already bare —
534
+ * the unwrap pattern only matches an opening `[` at the start and a
535
+ * closing `]` at the end. Today's call sites (`scanCallSites`,
536
+ * `detectQualifiedStatementCall`) already unwrap in the regex captures,
537
+ * so this defensive strip is a belt-and-braces guard for any future
538
+ * caller that forgets.
256
539
  */
257
540
  private isLocalProjectClassVar;
258
541
  /**
@@ -268,6 +551,8 @@ export declare class VbaExtractor {
268
551
  * IS already the target module's name and no resolution is needed.
269
552
  */
270
553
  private resolveReceiverType;
554
+ private normalizeWithReceiver;
555
+ private detectWithMemberCall;
271
556
  private sweepCallsAndSql;
272
557
  private static readonly RAISE_EVENT_RE;
273
558
  private scanRaiseEvents;
@@ -277,16 +562,21 @@ export declare class VbaExtractor {
277
562
  private callDedupe;
278
563
  private synthFunctionNodeIds;
279
564
  /**
280
- * B4 (hueco 6): cache of stub `form-layout` node ids we've already emitted
281
- * for a given target form name in this file. Avoids emitting duplicate
282
- * stubs when `DoCmd.OpenForm "FormTest"` shows up N times across N calls.
283
- * Keyed by the lowercased form name so `FormTest` / `formtest` collapse.
565
+ * B4 (hueco 6) extended by Issue #48: cache of stub node ids we've already
566
+ * emitted for a given (method, target name) pair in this file. Avoids
567
+ * emitting duplicate stubs when `DoCmd.OpenForm "FormTest"` or
568
+ * `DoCmd.OpenReport "InformeMensual"` shows up N times across N calls.
569
+ * Keyed by `${cacheKey}:${lowerName}` so the OpenForm and OpenReport
570
+ * de-dup buckets stay disjoint — `OpenForm:Form1` ≠ `OpenReport:Form1`.
571
+ * The name part is lowercased so `FormTest` / `formtest` collapse.
284
572
  */
285
- private opensFormStubIdsByName;
573
+ private opensStubIdsByKey;
286
574
  /**
287
- * Hueco 1: scan a line for `Me.<ControlName>` patterns and emit one
288
- * UnresolvedReference per occurrence, tagged
289
- * `metadata.synthesizedBy: 'vba-me-control'`.
575
+ * Hueco 1: scan a line for `Me.<ControlName>` / `Me!<ControlName>`
576
+ * patterns and emit one UnresolvedReference per occurrence, tagged
577
+ * `metadata.synthesizedBy: 'vba-me-control'`. Issue #44 extended
578
+ * `ME_CONTROL_RE` from `Me\.` to `Me[.!]` so the bang form (default-
579
+ * collection shortcut) is captured byte-identically to the dot form.
290
580
  *
291
581
  * Operates on the masked `callScanLine` (string-literal content already
292
582
  * replaced with spaces) so `Me.X` inside a string literal is not falsely
@@ -297,10 +587,76 @@ export declare class VbaExtractor {
297
587
  *
298
588
  * `fromNodeId` is the current procedure's function node — that's the
299
589
  * "owner" of the reference (the Sub body that wrote `Me.lblTitulo = …`).
590
+ * The +3 column offset remains correct under Issue #44's regex change
591
+ * because both `Me.` and `Me!` are 3-character prefixes.
300
592
  */
301
593
  private scanMeControlReferences;
594
+ /**
595
+ * Issue #44: scan a line for cross-form bang references
596
+ * (`Forms!<FormName>[!<Ctl>]` and `Forms("<FormName>")!<Ctl>`) and
597
+ * emit ONE UnresolvedReference per match with `metadata.synthesizedBy
598
+ * = 'vba-forms-bang'`. Companion to `scanMeControlReferences` above;
599
+ * shares the emission shape (a single `UnresolvedReference` per match,
600
+ * `referenceKind: 'references'`), keeping the W4 invariant that we
601
+ * synthesize NO `function` node for forms.
602
+ *
603
+ * Operates on the ORIGINAL (unmasked) line — the paren form
604
+ * `Forms("FormX")!txtY` carries the form name INSIDE a string literal
605
+ * and would be destroyed by `maskStringContent`. Mirrors
606
+ * `scanOpenFormCalls`'s unmasked-line constraint.
607
+ *
608
+ * Regex alternatives (see `FORMS_BANG_RE`):
609
+ * 1. `Forms!<FormName>` (bare or `[bracketed]`, NOT followed by `.X`)
610
+ * — bang form, may have a trailing `!<Ctl>[.<Prop>]` that is
611
+ * consumed but NOT emitted.
612
+ * 2. `Forms("<FormName>")!<Ctl>` (quoted or bare/bracketed form arg)
613
+ * — paren form, ALWAYS with a control segment.
614
+ *
615
+ * Stripping: the form's identifier is unwrapped of `"` quotes (string
616
+ * literals) and `[…]` brackets (Issue #54 reserved-identifier shape) so
617
+ * the public `referenceName` is the bare form name. Bracketed forms
618
+ * (`Forms![Mi Formulario]`) follow the same strip rules as the paren
619
+ * form so the resolver sees `Mi Formulario` either way.
620
+ */
621
+ private scanFormsBang;
302
622
  private findOrCreateFunctionNodeId;
303
623
  private findFunctionNodeByName;
624
+ /**
625
+ * Issue #45: split a single-line VBA `If <cond> Then <body>` into one or
626
+ * more statement-clause fragments that the existing `detectStatementCall`
627
+ * and `detectQualifiedStatementCall` detectors can process. Handles:
628
+ *
629
+ * - `If x Then Foo` → `['Foo']`
630
+ * - `If x Then Foo Else Bar` → `['Foo', 'Bar']`
631
+ * - `If x Then DoA: DoB` → `['DoA', 'DoB']` (colon-separated multi-statement)
632
+ * - `If x Then Foo Else A: B` → `['Foo', 'A', 'B']`
633
+ * - `If x Then GoTo fin` → `[]` (GoTo clause filtered out)
634
+ * - `If x Then Exit Sub` → `[]` (Exit clause filtered out)
635
+ *
636
+ * When the line does NOT match a single-line `If … Then` shape — for
637
+ * instance a block-form `If x Then` whose body lives on subsequent
638
+ * lines — the splitter returns `[<line>]` (the original input) so
639
+ * callers can use this method unconditionally and let the existing
640
+ * per-line scan pick up the body on a separate line.
641
+ *
642
+ * `GoTo`, `Exit`, and `Resume` clauses are filtered at the fragment
643
+ * level (defense in depth): even though `emitStatementCallEdge` already
644
+ * drops these via the `CALL_KEYWORD_BLACKLIST`, filtering here prevents
645
+ * any chance of `detectStatementCall`'s generic identifier extractor
646
+ * matching a substring (e.g. an identifier like `GoToFinishingTouches`)
647
+ * as a side effect of a richer clause where the keyword happens to be
648
+ * the leading token.
649
+ *
650
+ * `line` is the string-literal-masked scan line. The mask makes global
651
+ * `:` splitting safe: real VBA colons never appear inside string
652
+ * literals (already masked to spaces) and never inside expressions
653
+ * inside parens at the source level (a colon ends a statement in VBA,
654
+ * so it cannot appear inside a parenthesised argument list either).
655
+ * The `Else` keyword is a VBA statement-level separator and is
656
+ * forbidden inside parens or expressions, so splitting on
657
+ * `\s+Else\s+` does not need paren tracking either.
658
+ */
659
+ private splitSingleLineIfClauses;
304
660
  /**
305
661
  * H1: detect a statement-form Sub call.
306
662
  *
@@ -351,51 +707,112 @@ export declare class VbaExtractor {
351
707
  */
352
708
  private emitQualifiedStatementCallEdge;
353
709
  /**
354
- * B4 (hueco 6): scan one line of VBA source for `DoCmd.OpenForm "X"`
355
- * calls. For each match, emit:
356
- * - a stub `form-layout` node for the target form (cached by name so
357
- * the same form referenced from N sites emits exactly ONE stub),
358
- * - an `opens-form` heuristic edge from the calling Sub to that stub.
710
+ * B4 (hueco 6) extended by Issue #48: scan one line of VBA source for
711
+ * `DoCmd.OpenX "Target"` calls where X ∈ {Form, Report} (see the
712
+ * `DOCMD_OPEN_DISPATCH` table). For each match, emit:
713
+ * - a stub node (form-layout / report-layout) for the target, cached
714
+ * per-(method, name) so the same target referenced from N sites
715
+ * emits exactly ONE stub,
716
+ * - an `opens-form` / `opens-report` heuristic edge from the calling
717
+ * Sub to that stub.
359
718
  *
360
719
  * Both endpoints are pushed into `this.nodes` / `this.edges`, so the
361
720
  * per-file edge filter at `index.ts:insertedIds.has(source) &&
362
721
  * insertedIds.has(target)` passes the edge naturally without any
363
722
  * exemption to the filter.
364
723
  *
365
- * Why a stub and not a direct lookup: the target form lives in a
366
- * DIFFERENT file (its own `.form.txt`), and the extractor doesn't have
367
- * DB access at parse time. The stub's synthetic file path
368
- * (`synthetic:opensFormStub/<FormName>.form.txt`) guarantees a
369
- * deterministic node id so re-indexes collapse to the same stub.
370
- * When the consumer's `.form.txt` is later indexed, the real
371
- * `form-layout` node carries a different id (it uses the real file
372
- * path); the stub and the real coexist harmlessly. The orchestrator
373
- * flagged this as acceptable for B4 only `OpenForm` is in scope.
374
- * `OpenReport`, `OpenQuery`, `OpenTable`, … are follow-up work.
375
- *
376
- * Scope note: this regex matches literal-string and bare-identifier forms.
377
- * Bare identifiers are resolved only through local `Const` declarations;
378
- * arbitrary variable data-flow remains intentionally out of scope.
379
- */
380
- private scanOpenFormCalls;
381
- /**
382
- * B4 (hueco 6): emit a stub `form-layout` node for `targetFormName`
383
- * (cached so duplicates collapse) and an `opens-form` heuristic edge
724
+ * Why a stub and not a direct lookup: the target form/report lives in a
725
+ * DIFFERENT file (its own `.form.txt` / `.report.txt`), and the extractor
726
+ * doesn't have DB access at parse time. The stub's synthetic file path
727
+ * (`synthetic:opensFormStub/<Name>.form.txt` /
728
+ * `synthetic:opensReportStub/<Name>.form.txt`) guarantees a deterministic
729
+ * node id so re-indexes collapse to the same stub. When the consumer's
730
+ * `.form.txt` / `.report.txt` is later indexed, the real
731
+ * `form-layout` / `report-layout` node carries a different id (it uses
732
+ * the real file path); the stub and the real coexist harmlessly.
733
+ *
734
+ * Why a separate dispatch from CALL_RE: `DoCmd` is in
735
+ * `RUNTIME_RECEIVER_BLACKLIST` (R4 invariant), so `DoCmd.OpenForm` /
736
+ * `DoCmd.OpenReport` are intentionally SKIPPED by the generic CALL_RE
737
+ * path that would otherwise emit a junk `calls` edge to a synthetic
738
+ * `function` node for `DoCmd.OpenX`. The dispatch below matches BEFORE
739
+ * the call-site scan and uses its own emission path.
740
+ *
741
+ * Scope note: literal-string and bare-identifier argument forms are
742
+ * supported. Bare identifiers resolve only through local `Const`
743
+ * declarations; arbitrary variable data-flow remains intentionally
744
+ * out of scope.
745
+ */
746
+ private scanDoCmdOpenCalls;
747
+ /**
748
+ * B4 (hueco 6) extended by Issue #48: emit a stub `form-layout` /
749
+ * `report-layout` node for `targetName` (cached per dispatch entry so
750
+ * duplicates collapse and OpenForm/OpenReport de-dup buckets stay
751
+ * disjoint) and a single `opens-form` / `opens-report` heuristic edge
384
752
  * from `caller` to that stub.
385
753
  *
386
754
  * The edge carries:
387
- * - `kind: 'opens-form'` new cross-file edge kind
388
- * - `provenance: 'heuristic'` — synthesized, not parsed
389
- * - `metadata.targetFormName` — the captured literal
390
- * - `metadata.synthesizedBy: 'vba-opens-form'` — distinguishes this
391
- * synthesis from the dim/sql/event-handler families
755
+ * - `kind` — dispatch-specific (`opens-form` / `opens-report`)
756
+ * - `provenance: 'heuristic'` — synthesized, not parsed
757
+ * - `metadata.<dispatchTargetKey>` (e.g. `targetFormName`) — the resolved name
758
+ * - `metadata.synthesizedBy` — dispatch-specific (`vba-opens-form` /
759
+ * `vba-opens-report`); distinguishes this synthesis from the
760
+ * dim/sql/event-handler families
392
761
  *
393
762
  * The stub's `metadata.stub: true` flag lets downstream UI render
394
- * unresolved references distinctly (e.g. with a dashed border) and
395
- * gives later re-resolution pass a hook for collapse. The stub is
763
+ * stubs distinctly (e.g. with a dashed border) and gives later
764
+ * re-resolution pass a hook for collapse. The stub is
396
765
  * line-independent (`line = 0`) so re-indexes produce identical ids.
397
766
  */
398
- private emitOpensFormEdge;
767
+ private emitOpensStubEdge;
768
+ /**
769
+ * Issue #48: scan one line of VBA source for `DoCmd.OpenQuery "X"` calls.
770
+ * Each match emits ONE `UnresolvedReference` (NOT a stub + edge) so the
771
+ * resolver binds to the REAL `query` node that `SqlQueryExtractor`
772
+ * produces for `queries/<Name>.sql` (dysflow exports every saved QueryDef
773
+ * + `queries.json` manifest). Falls back to silent when the .sql is not
774
+ * yet in the index — the resolver does the binding when it's later
775
+ * indexed, exactly like `vba-me-control` and `vba-forms-bang`.
776
+ *
777
+ * Companion to `scanDoCmdOpenCalls` but intentionally NOT in the
778
+ * dispatch table — OpenQuery's emission shape (`UnresolvedReference`)
779
+ * is structurally different from OpenForm/OpenReport's (synthetic node
780
+ * + heuristic edge). The two pipelines share the literal-vs-Const
781
+ * argument resolution pattern via `localConstants.get(...)` but emit
782
+ * via two different branches of `unresolvedReferences` vs
783
+ * `nodes`/`edges`.
784
+ *
785
+ * UnresolvedReference shape (per Issue #48 spec — must match
786
+ * SqlQueryExtractor's query node name exactly):
787
+ * - `referenceName` = resolved query name
788
+ * - `referenceKind: 'references'` = same kind the resolver binds
789
+ * - `metadata.synthesizedBy: 'vba-opens-query'`
790
+ * - NO synthetic `function` node (W4 graph-pollution invariant — the
791
+ * real `query` node already exists in the index once `.sql` is
792
+ * processed, and creating stubs would compete with the binding).
793
+ */
794
+ private scanDoCmdOpenQuery;
795
+ /**
796
+ * Regex matching the chained `& "..."` literals that may follow a
797
+ * wrapper's first literal on the same physical line. Captures the
798
+ * literal CONTENT (group 1); the surrounding `&` and quotes are
799
+ * structural, not data. VBA allows whitespace around `&` and around
800
+ * the inner quotes — handled with `\s*`. The `((?:[^"]|"")*)` body
801
+ * mirrors the wrapper regex so a `""` inside a chained literal still
802
+ * decodes to a single `"`.
803
+ *
804
+ * Cross-physical-line concat via `_` continuation is OUT OF SCOPE for
805
+ * v1 (deferred; see commit message).
806
+ */
807
+ private static readonly SQL_WRAPPER_CHAIN_RE;
808
+ /**
809
+ * Given the text that follows a SQL wrapper's first literal on the same
810
+ * physical line, return the contents of every `& "..."` chained literal
811
+ * in source order. Operates per-physical-line only — VBA `_` line
812
+ * continuation across physical lines is handled separately by
813
+ * `collectStringLiteralText` for the variable-assignment path.
814
+ */
815
+ private collectSqlWrapperChain;
399
816
  private scanSqlInLine;
400
817
  /**
401
818
  * #13 fix: `sql = sql & "..."` (self-referential concatenation) must
@@ -419,6 +836,64 @@ export declare class VbaExtractor {
419
836
  */
420
837
  private emitReference;
421
838
  private synthClassNodeIds;
839
+ /**
840
+ * Issue #50: TempVars placeholder-node de-dup cache. Keys are the
841
+ * deterministic node ids produced by `emitTempVarReference` — those
842
+ * ids intentionally ignore `this.filePath` (use a synthetic
843
+ * `synthetic:tempvar/<key>` path instead) so the SAME key referenced
844
+ * from Form_A.cls AND Form_B.cls collapses to ONE placeholder node.
845
+ * Cross-file id stability is the cross-form state premise that makes
846
+ * `codegraph_explore` connect producer ⇄ consumer in one hop.
847
+ */
848
+ private synthTempVarNodeIds;
849
+ /**
850
+ * Issue #50: emit one TempVar reading/writing site. Per call:
851
+ * - placeholder `class` node keyed on the synthetic
852
+ * `synthetic:tempvar/<key>` file path so cross-file extraction
853
+ * calls collapse to one node per key,
854
+ * - `references` edge from the calling `function` node
855
+ * (via `findOrCreateFunctionNodeId` — same access pattern as
856
+ * `scanDoCmdOpenCalls` / `scanDoCmdOpenQuery`) carrying
857
+ * `metadata.synthesizedBy: 'vba-tempvar'` AND
858
+ * `metadata.access: 'read' | 'write'`.
859
+ *
860
+ * Reuses the synthetic-`class`-placeholder shape established by
861
+ * `emitReference` for SQL tables, events, Dim types, etc. — so every
862
+ * synthesized `references` edge in the file already carries the same
863
+ * `kind: 'class'` target, and downstream consumers (UI, resolvers,
864
+ * search queries) filter on `metadata.synthesizedBy` rather than
865
+ * NodeKind anyway. The `metadata.synthesizedBy` tag cleanly distinguishes
866
+ * TempVars refs from SQL-table refs inside the same NodeKind bucket.
867
+ *
868
+ * Skips when stack is empty (the writer/reader is module-level code
869
+ * — REQ-CODE-4 "unresolvable/runtime reference is silent"). The
870
+ * spec wires this per-proc only; the module-level shape would need
871
+ * a different emission (no procedure source) and is out of scope.
872
+ */
873
+ private emitTempVarReference;
874
+ /**
875
+ * Issue #50: scan one line of VBA source for TempVars access sites and
876
+ * emit one `references` edge per site. Three regex runs:
877
+ * - bang form `TempVars!x` over the MASKED line (`!` itself never
878
+ * lives inside a string literal, so masked == unmasked here — using
879
+ * the masked line is conservative against false positives in
880
+ * concatenated string content),
881
+ * - paren form `TempVars("x")` over the ORIGINAL line (the literal is
882
+ * inside a string — masked would strip the key),
883
+ * - Add form `TempVars.Add "x", v` over the ORIGINAL line (same reason),
884
+ * always classified as a write.
885
+ *
886
+ * Each match classifies access by looking at the LINE SUFFIX (after the
887
+ * closing paren / bang-key) on the SAME line: if `=` (and not the
888
+ * nonexistent `==`) is the next non-whitespace character, it's a write.
889
+ * VBA has no `==` so a bare `=` suffix check is safe.
890
+ *
891
+ * The caller parameter comes from `stack[stack.length - 1]` in
892
+ * `sweepCallsAndSql`. Pass `undefined` for module-level access — we
893
+ * drop the edge (per REQ-CODE-4 spirit, runtime references have no
894
+ * static source to anchor against).
895
+ */
896
+ private sweepTempVars;
422
897
  /**
423
898
  * Fix 2 (Issue #2): maps `variableName.toLowerCase()` → declared type info.
424
899
  * Built by `sweepDimsAndWithEvents`; consulted by `sweepCallsAndSql` to gate
@@ -426,8 +901,53 @@ export declare class VbaExtractor {
426
901
  * typed as a SIMPLE (non-qualified, non-primitive) identifier emit edges.
427
902
  */
428
903
  private localVarTypeMap;
429
- /** Local constant name (lowercase) → simple literal value for OpenForm resolution. */
904
+ /**
905
+ * Issue #52: Const resolution buckets, scoped per procedure. Key is
906
+ * `'module'` for module-level Consts, or the procedure's `startLine`
907
+ * (stringified) for proc-local Consts. Each bucket maps the lowercase
908
+ * constant name to its simple-literal value (used by `DoCmd.OpenForm` /
909
+ * `OpenReport` / `OpenQuery` argument resolution via `resolveLocalConst`).
910
+ *
911
+ * Two procs declaring the same Const name with different values stay
912
+ * isolated (each in its own bucket); reads look up the current proc's
913
+ * bucket first and fall back to the module bucket. The bucket-per-proc
914
+ * model was chosen over a single file-wide Map (the pre-fix shape) so
915
+ * `DoCmd.OpenForm FORM_DESTINO` resolves to the proc-local value, not
916
+ * whichever was written last.
917
+ */
430
918
  private localConstants;
919
+ /**
920
+ * Issue #52: shared lookup helper for `scanDoCmdOpenCalls` and
921
+ * `scanDoCmdOpenQuery`. The current scope is the procedure whose
922
+ * `startLine` is on top of `procStack` (or `'module'` when the stack is
923
+ * empty). Per-proc bucket first; module bucket is the fallback.
924
+ */
925
+ private resolveLocalConst;
926
+ /**
927
+ * Issue #52: shared writer. `scopeKey` is `'module'` or the procedure's
928
+ * startLine-as-string. Creates the bucket lazily so callers do not have
929
+ * to pre-allocate per proc. Returns the bucket the value was written to
930
+ * (mostly useful for tests; production code ignores it).
931
+ */
932
+ private setLocalConstInScope;
933
+ /**
934
+ * Issue #52: the current Const-lookup scope. `'module'` when no procedure
935
+ * is open, otherwise the top-of-stack proc's `startLine` as a string.
936
+ * Both `sweepEnumsAndConsts` (to decide whether to emit a `constant`
937
+ * node) and `sweepCallsAndSql` (to drive OpenForm/OpenQuery resolution)
938
+ * keep this in sync with their per-line stack walk by pushing/popping
939
+ * `procStack` and writing the new top's key here.
940
+ */
941
+ private currentProcKey;
942
+ /**
943
+ * Issue #52: per-extraction proc-stack shared between `sweepEnumsAndConsts`
944
+ * and `sweepCallsAndSql`. Each sweep clears it at the start so the file's
945
+ * mid-proc structural state never leaks across sweeps. Holds the
946
+ * `startLine` (1-based, matches `ProcInfo.startLine`) of every procedure
947
+ * whose body the sweep has not yet emitted `End Sub`/`End Function`/
948
+ * `End Property` for.
949
+ */
950
+ private procStack;
431
951
  /** Local event name (lowercase) → event node for `RaiseEvent` edge emission. */
432
952
  private localEvents;
433
953
  }