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

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 (38) hide show
  1. package/lib/dist/bin/codegraph.js +1 -1
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/bin/node-version-check.d.ts +9 -6
  4. package/lib/dist/bin/node-version-check.d.ts.map +1 -1
  5. package/lib/dist/bin/node-version-check.js +25 -10
  6. package/lib/dist/bin/node-version-check.js.map +1 -1
  7. package/lib/dist/db/migrations.d.ts +1 -1
  8. package/lib/dist/db/migrations.d.ts.map +1 -1
  9. package/lib/dist/db/migrations.js +10 -1
  10. package/lib/dist/db/migrations.js.map +1 -1
  11. package/lib/dist/db/queries.d.ts +33 -0
  12. package/lib/dist/db/queries.d.ts.map +1 -1
  13. package/lib/dist/db/queries.js +64 -2
  14. package/lib/dist/db/queries.js.map +1 -1
  15. package/lib/dist/db/schema.sql +1 -0
  16. package/lib/dist/extraction/vba-extractor.d.ts +60 -20
  17. package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
  18. package/lib/dist/extraction/vba-extractor.js +387 -52
  19. package/lib/dist/extraction/vba-extractor.js.map +1 -1
  20. package/lib/dist/extraction/vba-preprocess.d.ts +17 -1
  21. package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
  22. package/lib/dist/extraction/vba-preprocess.js +92 -1
  23. package/lib/dist/extraction/vba-preprocess.js.map +1 -1
  24. package/lib/dist/index.d.ts.map +1 -1
  25. package/lib/dist/index.js +7 -0
  26. package/lib/dist/index.js.map +1 -1
  27. package/lib/dist/resolution/index.d.ts +38 -0
  28. package/lib/dist/resolution/index.d.ts.map +1 -1
  29. package/lib/dist/resolution/index.js +137 -0
  30. package/lib/dist/resolution/index.js.map +1 -1
  31. package/lib/dist/types.d.ts +11 -2
  32. package/lib/dist/types.d.ts.map +1 -1
  33. package/lib/dist/types.js +4 -0
  34. package/lib/dist/types.js.map +1 -1
  35. package/lib/node_modules/.modules.yaml +1 -1
  36. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  37. package/lib/package.json +2 -2
  38. package/package.json +1 -1
@@ -127,12 +127,13 @@ class VbaExtractor {
127
127
  this.nodes.push(this.createFileNode());
128
128
  return this.result(startTime);
129
129
  }
130
- // Pre-process pipeline (per design): join continuations, strip comments,
131
- // then we sweep the joined-but-uncommented source for call sites and
132
- // SQL strings. Comments are gone before regex runs, so no commented
133
- // SQL can match the SQL regex.
130
+ // Pre-process pipeline (per design): join continuations, blank inactive
131
+ // conditional-compilation branches, strip comments, then sweep the
132
+ // joined-but-uncommented source. The conditional preprocessor preserves
133
+ // line count by replacing directives/inactive lines with empty strings.
134
134
  const joined = (0, vba_preprocess_1.joinLineContinuations)(this.source);
135
- const uncommented = (0, vba_preprocess_1.stripVbaComments)(joined);
135
+ const preprocessed = (0, vba_preprocess_1.preprocessConditionalCompilation)(joined);
136
+ const uncommented = (0, vba_preprocess_1.stripVbaComments)(preprocessed);
136
137
  // Create the file node.
137
138
  this.nodes.push(this.createFileNode());
138
139
  // Resolve the file's "kind" — .cls → class, else module (including .bas
@@ -160,6 +161,12 @@ class VbaExtractor {
160
161
  const procs = this.sweepProcedures(uncommented);
161
162
  if (procs.length > 0)
162
163
  hasAnySymbols = true;
164
+ // Sweep: first-class Event / Type / Declare declarations (roadmap #26).
165
+ // This runs before call-site scanning so `RaiseEvent Foo` and calls to a
166
+ // `Declare` can resolve to real local nodes rather than fall through.
167
+ const declarationCount = this.sweepEventsTypesAndDeclares(uncommented);
168
+ if (declarationCount > 0)
169
+ hasAnySymbols = true;
163
170
  // Sweep: Implements (REQ-CODE-5).
164
171
  const implCount = this.sweepImplements(uncommented);
165
172
  if (implCount > 0)
@@ -477,6 +484,159 @@ class VbaExtractor {
477
484
  }
478
485
  return procs;
479
486
  }
487
+ /** `[visibility] Event <Name>(...)` custom event declaration. */
488
+ static EVENT_DECL_RE = /^\s*((?:Public|Private|Friend)\s+)?Event\s+(\p{L}[\p{L}\p{N}_]*)\b/iu;
489
+ /** `[visibility] Type <Name>` user-defined type block start. */
490
+ static TYPE_START_RE = /^\s*((?:Public|Private|Friend)\s+)?Type\s+(\p{L}[\p{L}\p{N}_]*)\b/iu;
491
+ /** `End Type` user-defined type block end. */
492
+ static TYPE_END_RE = /^\s*End\s+Type\b/iu;
493
+ /** `<MemberName> As <Type>` inside a user-defined type block. */
494
+ static TYPE_MEMBER_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:\([^)]*\))?\s+As\s+(.+?)\s*$/iu;
495
+ /** `[visibility] Declare [PtrSafe] Sub|Function <Name> Lib "dll" [Alias "x"] ...` */
496
+ static DLL_DECLARE_RE = /^\s*((?:Public|Private)\s+)?Declare\s+(PtrSafe\s+)?(Sub|Function)\s+(\p{L}[\p{L}\p{N}_]*)\s+Lib\s+"([^"]+)"(?:\s+Alias\s+"([^"]+)")?/iu;
497
+ /**
498
+ * Roadmap #26 declaration sweep:
499
+ * - Event declarations become `event` nodes and `RaiseEvent` can point to them.
500
+ * - Type...End Type blocks become `type` + `type_member` nodes.
501
+ * - Win32 API Declare statements become `declare` nodes, while still being
502
+ * cached by name so normal call-site scanning can emit `calls` edges.
503
+ */
504
+ sweepEventsTypesAndDeclares(src) {
505
+ const lines = src.split('\n');
506
+ let count = 0;
507
+ let currentType = null;
508
+ for (let i = 0; i < lines.length; i++) {
509
+ const line = lines[i] ?? '';
510
+ const lineNum = i + 1;
511
+ if (currentType) {
512
+ if (VbaExtractor.TYPE_END_RE.test(line)) {
513
+ currentType = null;
514
+ continue;
515
+ }
516
+ const member = VbaExtractor.TYPE_MEMBER_RE.exec(line);
517
+ if (member) {
518
+ const memberName = member[1] ?? '';
519
+ const memberType = (member[2] ?? '').trim();
520
+ if (!memberName)
521
+ continue;
522
+ const memberId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'type_member', memberName, lineNum);
523
+ this.nodes.push({
524
+ id: memberId,
525
+ kind: 'type_member',
526
+ name: memberName,
527
+ qualifiedName: `${currentType.name}.${memberName}`,
528
+ filePath: this.filePath,
529
+ language: 'vba',
530
+ startLine: lineNum,
531
+ endLine: lineNum,
532
+ startColumn: 0,
533
+ endColumn: line.length,
534
+ metadata: { memberType },
535
+ updatedAt: Date.now(),
536
+ });
537
+ this.edges.push({
538
+ source: currentType.id,
539
+ target: memberId,
540
+ kind: 'type-member',
541
+ provenance: 'parser',
542
+ });
543
+ }
544
+ continue;
545
+ }
546
+ const eventDecl = VbaExtractor.EVENT_DECL_RE.exec(line);
547
+ if (eventDecl) {
548
+ const visibility = VbaExtractor.foldVisibility(eventDecl[1] ?? '');
549
+ const name = eventDecl[2] ?? '';
550
+ if (!name)
551
+ continue;
552
+ const eventId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'event', name, lineNum);
553
+ const eventNode = {
554
+ id: eventId,
555
+ kind: 'event',
556
+ name,
557
+ qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
558
+ filePath: this.filePath,
559
+ language: 'vba',
560
+ startLine: lineNum,
561
+ endLine: lineNum,
562
+ startColumn: 0,
563
+ endColumn: line.length,
564
+ visibility,
565
+ updatedAt: Date.now(),
566
+ };
567
+ this.nodes.push(eventNode);
568
+ this.localEvents.set(name.toLowerCase(), eventNode);
569
+ this.pushContainsFromModule(eventId);
570
+ count++;
571
+ continue;
572
+ }
573
+ const typeStart = VbaExtractor.TYPE_START_RE.exec(line);
574
+ if (typeStart) {
575
+ const visibility = VbaExtractor.foldVisibility(typeStart[1] ?? '');
576
+ const name = typeStart[2] ?? '';
577
+ if (!name)
578
+ continue;
579
+ const typeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'type', name, lineNum);
580
+ this.nodes.push({
581
+ id: typeId,
582
+ kind: 'type',
583
+ name,
584
+ qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
585
+ filePath: this.filePath,
586
+ language: 'vba',
587
+ startLine: lineNum,
588
+ endLine: lineNum,
589
+ startColumn: 0,
590
+ endColumn: line.length,
591
+ visibility,
592
+ updatedAt: Date.now(),
593
+ });
594
+ this.pushContainsFromModule(typeId);
595
+ currentType = { id: typeId, name };
596
+ count++;
597
+ continue;
598
+ }
599
+ const declaration = VbaExtractor.DLL_DECLARE_RE.exec(line);
600
+ if (declaration) {
601
+ const visibility = VbaExtractor.foldVisibility(declaration[1] ?? '');
602
+ const ptrSafe = !!declaration[2];
603
+ const declareKind = (declaration[3] ?? '').toLowerCase();
604
+ const name = declaration[4] ?? '';
605
+ const dll = declaration[5] ?? '';
606
+ const aliasName = declaration[6] ?? undefined;
607
+ if (!name)
608
+ continue;
609
+ const declareId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'declare', name, lineNum);
610
+ const declareNode = {
611
+ id: declareId,
612
+ kind: 'declare',
613
+ name,
614
+ qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
615
+ filePath: this.filePath,
616
+ language: 'vba',
617
+ startLine: lineNum,
618
+ endLine: lineNum,
619
+ startColumn: 0,
620
+ endColumn: line.length,
621
+ visibility,
622
+ metadata: {
623
+ dll,
624
+ declareKind,
625
+ ptrSafe,
626
+ ...(aliasName ? { aliasName } : {}),
627
+ },
628
+ updatedAt: Date.now(),
629
+ };
630
+ this.nodes.push(declareNode);
631
+ if (!this.functionNodeByName.has(name)) {
632
+ this.functionNodeByName.set(name, declareNode);
633
+ }
634
+ this.pushContainsFromModule(declareId);
635
+ count++;
636
+ }
637
+ }
638
+ return count;
639
+ }
480
640
  /** Implements regex. */
481
641
  static IMPLEMENTS_RE = /^\s*Implements\s+(\p{L}[\p{L}\p{N}_]*)/iu;
482
642
  /** Edges whose source needs to be set to the module/class id once it exists. */
@@ -615,9 +775,28 @@ class VbaExtractor {
615
775
  this.localVarTypeMap.set(weVarName.toLowerCase(), {
616
776
  outer: formType,
617
777
  qualified: false,
778
+ withEvents: true,
779
+ variableName: weVarName,
618
780
  });
619
781
  }
620
782
  this.emitReference(formType, lineNum, 0, 'vba-withevents');
783
+ const targetId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'class', formType, 0);
784
+ const subscriberEdge = {
785
+ source: this.moduleOrClassNode?.id ?? '',
786
+ target: targetId,
787
+ kind: 'subscribes-event',
788
+ provenance: 'heuristic',
789
+ metadata: {
790
+ synthesizedBy: 'vba-withevents',
791
+ variableName: weVarName || undefined,
792
+ },
793
+ line: lineNum,
794
+ column: 0,
795
+ };
796
+ this.edges.push(subscriberEdge);
797
+ if (!this.moduleOrClassNode) {
798
+ this.pendingModuleOrClassSource.push(subscriberEdge);
799
+ }
621
800
  count++;
622
801
  }
623
802
  }
@@ -637,13 +816,6 @@ class VbaExtractor {
637
816
  static ENUM_MEMBER_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:=|$)/u;
638
817
  /** `[visibility] Const <decls>` — captures visibility (1) and the rest (2). */
639
818
  static CONST_DECL_RE = /^\s*(?:(Public|Private|Friend|Global)\s+)?Const\s+(.+)$/i;
640
- /**
641
- * One declared name inside a `Const` body. A name sits at a declaration
642
- * boundary (start-of-body or after a comma), optionally followed by
643
- * `As <Type>`, then `=`. Run with /g over the CONST_DECL_RE group 2 so
644
- * multi-name lines (`Const A = 1, B = 2`) emit one node per name.
645
- */
646
- static CONST_NAME_RE = /(?:^|,)\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:As\s+[\p{L}\p{N}_.]+\s*)?=/giu;
647
819
  /**
648
820
  * Fold a VBA visibility keyword to the canonical lowercase enum, matching
649
821
  * the procedure convention: `Private` → 'private'; `Public`, `Global`,
@@ -652,7 +824,7 @@ class VbaExtractor {
652
824
  * sweep uses so visibility is consistent across symbol kinds).
653
825
  */
654
826
  static foldVisibility(raw) {
655
- return raw.toLowerCase() === 'private' ? 'private' : 'public';
827
+ return raw.trim().toLowerCase() === 'private' ? 'private' : 'public';
656
828
  }
657
829
  /**
658
830
  * Walk the (uncommented, line-joined) source and emit:
@@ -736,12 +908,14 @@ class VbaExtractor {
736
908
  if (constDecl) {
737
909
  const visibility = VbaExtractor.foldVisibility(constDecl[1] ?? '');
738
910
  const body = constDecl[2] ?? '';
739
- VbaExtractor.CONST_NAME_RE.lastIndex = 0;
740
- let cm;
741
- while ((cm = VbaExtractor.CONST_NAME_RE.exec(body)) !== null) {
742
- const constName = cm[1] ?? '';
911
+ const declarations = parseConstDeclarations(body);
912
+ for (const declaration of declarations) {
913
+ const constName = declaration.name;
743
914
  if (!constName)
744
915
  continue;
916
+ if (declaration.value !== null) {
917
+ this.localConstants.set(constName.toLowerCase(), declaration.value);
918
+ }
745
919
  const constId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'constant', constName, lineNum);
746
920
  this.nodes.push({
747
921
  id: constId,
@@ -755,6 +929,7 @@ class VbaExtractor {
755
929
  startColumn: 0,
756
930
  endColumn: line.length,
757
931
  visibility,
932
+ metadata: declaration.value !== null ? { value: declaration.value } : undefined,
758
933
  updatedAt: Date.now(),
759
934
  });
760
935
  this.pushContainsFromModule(constId);
@@ -798,9 +973,8 @@ class VbaExtractor {
798
973
  /** SQL wrapper helpers — order matters because `db.Execute` is a suffix of others. */
799
974
  static SQL_WRAPPERS = [
800
975
  { name: 'DoCmd.RunSQL', re: /\bDoCmd\.RunSQL\s+"((?:[^"]|"")*)"/g },
801
- { name: 'CurrentDb.OpenRecordset', re: /\bCurrentDb\.OpenRecordset\s+"((?:[^"]|"")*)"/g },
802
- { name: 'CurrentDb.Execute', re: /\bCurrentDb\.Execute\s+"((?:[^"]|"")*)"/g },
803
- { name: 'db.Execute', re: /\bdb\.Execute\s+"((?:[^"]|"")*)"/g },
976
+ { name: '*db.OpenRecordset', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.OpenRecordset\s+"((?:[^"]|"")*)"/giu },
977
+ { name: '*db.Execute', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.Execute\s+"((?:[^"]|"")*)"/giu },
804
978
  // Fix 4 (Issue #4): inline-literal forms `getdb().Execute "..."` and
805
979
  // `getdb().OpenRecordset "..."` — the variable form is covered by
806
980
  // SQL_VAR_EXEC_RE but the direct-literal form was missing.
@@ -810,16 +984,15 @@ class VbaExtractor {
810
984
  /**
811
985
  * `DoCmd.OpenForm "<FormName>"` modelling regex — B4 (hueco 6).
812
986
  *
813
- * Real VBA idiom (matches both forms):
987
+ * Real VBA idiom (matches literal and bare-identifier forms):
814
988
  * `DoCmd.OpenForm "MyForm"`
989
+ * `DoCmd.OpenForm FORM_MY_FORM`
815
990
  * `DoCmd.OpenForm "MyForm", acNormal, , , acFormEdit`
816
991
  *
817
- * Captures the form NAME (group 1) so the extractor can synthesize an
818
- * `opens-form` heuristic edge from the calling Sub to a stub for the
819
- * target form. The trailing positional args (`acNormal`, `acFormEdit`,
820
- * etc.) are intentionally NOT captured — the orchestrator's scope decision
821
- * was to cover ONLY `OpenForm` for this commit; `OpenReport`, `OpenQuery`,
822
- * `OpenTable`, … are flagged in the commit body as follow-up work.
992
+ * Captures the first argument (group 1). String literals are unwrapped;
993
+ * bare identifiers resolve against local Const declarations, falling back
994
+ * to the identifier name when unknown. The trailing positional args
995
+ * (`acNormal`, `acFormEdit`, etc.) are intentionally NOT captured.
823
996
  *
824
997
  * Why a separate dispatch: `DoCmd` is in `RUNTIME_RECEIVER_BLACKLIST`
825
998
  * (R4 invariant), so `DoCmd.OpenForm` is intentionally SKIPPED by the
@@ -828,11 +1001,11 @@ class VbaExtractor {
828
1001
  * matches BEFORE the call-site scan and uses its own dispatch to emit
829
1002
  * the `opens-form` edge instead — sharing no logic with CALL_RE.
830
1003
  */
831
- static OPEN_FORM_RE = /\bDoCmd\.OpenForm\s+"([^"]+)"/g;
1004
+ static OPEN_FORM_ARG_RE = /\bDoCmd\.OpenForm\s+("(?:(?:[^"]|"")*)"|\p{L}[\p{L}\p{N}_]*)/gu;
832
1005
  /** SQL assigned to a local variable, e.g. `m_SQL = "SELECT ..." & ...`. */
833
1006
  static SQL_VAR_ASSIGN_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*=\s*(.*)$/iu;
834
1007
  /** SQL wrapper called with a variable, e.g. `getdb().Execute m_SQL`. */
835
- static SQL_VAR_EXEC_RE = /\b(?:getdb\(\)|CurrentDb|db)\.(?:OpenRecordset|Execute)\s*\(?\s*(\p{L}[\p{L}\p{N}_]*)\s*\)?/giu;
1008
+ static SQL_VAR_EXEC_RE = /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.(?:OpenRecordset|Execute)\s*\(?\s*(\p{L}[\p{L}\p{N}_]*)\s*\)?/giu;
836
1009
  /** SQL table-name regex scoped to FROM / INTO / UPDATE. */
837
1010
  static SQL_TABLE_RE = /\b(?:FROM|INTO|UPDATE)\s+(\[?\p{L}[\p{L}\p{N}_]*\]?)/giu;
838
1011
  /**
@@ -994,6 +1167,26 @@ class VbaExtractor {
994
1167
  return false;
995
1168
  return true;
996
1169
  }
1170
+ /**
1171
+ * #12a: resolve the "receiver type" used to build a qualified call-stub's
1172
+ * name/qualifiedName. When `receiverName` is a file-local variable typed
1173
+ * as a candidate project class (`isLocalProjectClassVar`), returns the
1174
+ * RESOLVED CLASS NAME from `localVarTypeMap` (e.g. `m_NCOp` typed
1175
+ * `As NCOperaciones` → `'NCOperaciones'`) so the stub's qualifiedName
1176
+ * matches the real `.cls` method's `${className}.${proc}` shape and the
1177
+ * post-extraction resolver (#12b) can find it via an exact qualifiedName
1178
+ * match. Otherwise returns `receiverName` unchanged — this is the case
1179
+ * for `.bas`-qualified module calls (`modUtils.Foo`), where the receiver
1180
+ * IS already the target module's name and no resolution is needed.
1181
+ */
1182
+ resolveReceiverType(receiverName) {
1183
+ if (this.isLocalProjectClassVar(receiverName)) {
1184
+ const entry = this.localVarTypeMap.get(receiverName.toLowerCase());
1185
+ if (entry)
1186
+ return entry.outer;
1187
+ }
1188
+ return receiverName;
1189
+ }
997
1190
  sweepCallsAndSql(src) {
998
1191
  const lines = src.split('\n');
999
1192
  const procedureStartLines = new Set();
@@ -1049,7 +1242,9 @@ class VbaExtractor {
1049
1242
  // Don't scan call sites on the line that declares the procedure — it
1050
1243
  // would match the proc name itself in `Sub Outer()`.
1051
1244
  if (!procedureStartLines.has(lineNum) && stack.length > 0) {
1052
- this.scanCallSites(callScanLine, stack[stack.length - 1], lineNum);
1245
+ const currentProc = stack[stack.length - 1];
1246
+ this.scanRaiseEvents(callScanLine, currentProc, lineNum);
1247
+ this.scanCallSites(callScanLine, currentProc, lineNum);
1053
1248
  }
1054
1249
  // Hueco 1: capture `Me.<Control>` references (property assignments,
1055
1250
  // read expressions, method calls, anything after `Me.`). Only inside
@@ -1127,6 +1322,26 @@ class VbaExtractor {
1127
1322
  n.endLine = end;
1128
1323
  }
1129
1324
  }
1325
+ static RAISE_EVENT_RE = /\bRaiseEvent\s+(\p{L}[\p{L}\p{N}_]*)\b/giu;
1326
+ scanRaiseEvents(line, from, lineNum) {
1327
+ VbaExtractor.RAISE_EVENT_RE.lastIndex = 0;
1328
+ let m;
1329
+ while ((m = VbaExtractor.RAISE_EVENT_RE.exec(line)) !== null) {
1330
+ const eventName = m[1] ?? '';
1331
+ const eventNode = this.localEvents.get(eventName.toLowerCase());
1332
+ if (!eventNode)
1333
+ continue;
1334
+ this.edges.push({
1335
+ source: this.findOrCreateFunctionNodeId(from),
1336
+ target: eventNode.id,
1337
+ kind: 'raises-event',
1338
+ provenance: 'parser',
1339
+ metadata: { eventName },
1340
+ line: lineNum,
1341
+ column: m.index,
1342
+ });
1343
+ }
1344
+ }
1130
1345
  scanCallSites(line, from, lineNum) {
1131
1346
  VbaExtractor.CALL_RE.lastIndex = 0;
1132
1347
  let m;
@@ -1153,10 +1368,6 @@ class VbaExtractor {
1153
1368
  const col = m.index;
1154
1369
  if (!member) {
1155
1370
  // Bare `Name(...)` — same-file resolution.
1156
- const bucket = this.localProcs.get(receiver);
1157
- const local = bucket?.[0];
1158
- if (!local)
1159
- continue; // unresolvable — silent per spec R4.
1160
1371
  const localFuncNode = this.findFunctionNodeByName(receiver);
1161
1372
  if (!localFuncNode)
1162
1373
  continue;
@@ -1170,7 +1381,13 @@ class VbaExtractor {
1170
1381
  }
1171
1382
  else {
1172
1383
  // Qualified `Receiver.Member(...)` — synthesize the call target.
1173
- const qualified = `${receiver}.${member}`;
1384
+ // #12a: `receiverType` resolves to the real class name when
1385
+ // `receiver` is a declared project-class local var (matching a real
1386
+ // `.cls` method's `${className}.${proc}` qualifiedName shape so the
1387
+ // #12b resolver can find it by exact match); otherwise it's the raw
1388
+ // `receiver` text unchanged (e.g. `.bas`-qualified module calls).
1389
+ const receiverType = this.resolveReceiverType(receiver);
1390
+ const qualified = `${receiverType}.${member}`;
1174
1391
  // Avoid emitting duplicate edges for the same call (within a line).
1175
1392
  const dedupeKey = `${from.name}->${qualified}@${lineNum}`;
1176
1393
  if (this.callDedupe.has(dedupeKey))
@@ -1192,6 +1409,10 @@ class VbaExtractor {
1192
1409
  startColumn: col,
1193
1410
  endColumn: col + qualified.length,
1194
1411
  visibility: 'public',
1412
+ // #12a: tag the stub so the post-extraction resolver (#12b)
1413
+ // can find and repoint it. Mirrors the DoCmd.OpenForm stub
1414
+ // precedent (`emitOpensFormEdge`).
1415
+ metadata: { stub: true },
1195
1416
  updatedAt: Date.now(),
1196
1417
  });
1197
1418
  }
@@ -1200,7 +1421,12 @@ class VbaExtractor {
1200
1421
  target: synthId,
1201
1422
  kind: 'calls',
1202
1423
  provenance: 'heuristic',
1203
- metadata: { synthesizedBy: 'vba-name-resolution' },
1424
+ metadata: {
1425
+ synthesizedBy: 'vba-name-resolution',
1426
+ stub: true,
1427
+ receiverType,
1428
+ member,
1429
+ },
1204
1430
  line: lineNum,
1205
1431
  column: col,
1206
1432
  });
@@ -1351,9 +1577,6 @@ class VbaExtractor {
1351
1577
  return;
1352
1578
  if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(procName))
1353
1579
  return;
1354
- const bucket = this.localProcs.get(procName);
1355
- if (!bucket || !bucket[0])
1356
- return;
1357
1580
  const target = this.findFunctionNodeByName(procName);
1358
1581
  if (!target)
1359
1582
  return;
@@ -1440,7 +1663,15 @@ class VbaExtractor {
1440
1663
  * paren and non-paren form on the same line don't create duplicate edges.
1441
1664
  */
1442
1665
  emitQualifiedStatementCallEdge(caller, receiver, member, lineNum) {
1443
- const qualified = `${receiver}.${member}`;
1666
+ // #12a: the caller already checked `isLocalProjectClassVar(receiver)`
1667
+ // before calling this method, so `resolveReceiverType` always returns
1668
+ // the RESOLVED CLASS NAME here — the stub's name/qualifiedName matches
1669
+ // the real `.cls` method's `${className}.${proc}` shape (e.g.
1670
+ // `m_NCOp` typed `As NCOperaciones` → `NCOperaciones.Registrar`, not
1671
+ // `m_NCOp.Registrar`) so the #12b resolver can find it by exact
1672
+ // qualifiedName match.
1673
+ const receiverType = this.resolveReceiverType(receiver);
1674
+ const qualified = `${receiverType}.${member}`;
1444
1675
  const dedupeKey = `${caller.name}->${qualified}@${lineNum}`;
1445
1676
  if (this.callDedupe.has(dedupeKey))
1446
1677
  return;
@@ -1460,6 +1691,7 @@ class VbaExtractor {
1460
1691
  startColumn: 0,
1461
1692
  endColumn: qualified.length,
1462
1693
  visibility: 'public',
1694
+ metadata: { stub: true },
1463
1695
  updatedAt: Date.now(),
1464
1696
  });
1465
1697
  }
@@ -1468,7 +1700,12 @@ class VbaExtractor {
1468
1700
  target: synthId,
1469
1701
  kind: 'calls',
1470
1702
  provenance: 'heuristic',
1471
- metadata: { synthesizedBy: 'vba-name-resolution' },
1703
+ metadata: {
1704
+ synthesizedBy: 'vba-name-resolution',
1705
+ stub: true,
1706
+ receiverType,
1707
+ member,
1708
+ },
1472
1709
  line: lineNum,
1473
1710
  column: 0,
1474
1711
  });
@@ -1496,19 +1733,20 @@ class VbaExtractor {
1496
1733
  * flagged this as acceptable for B4 — only `OpenForm` is in scope.
1497
1734
  * `OpenReport`, `OpenQuery`, `OpenTable`, … are follow-up work.
1498
1735
  *
1499
- * Scope note: this regex matches ONLY the literal-string form
1500
- * `DoCmd.OpenForm "X"`. Variable-form calls like
1501
- * `DoCmd.OpenForm m_FormName` are intentionally NOT captured here
1502
- * because resolving the variable to a concrete form name would
1503
- * require data-flow analysis that is out of scope.
1736
+ * Scope note: this regex matches literal-string and bare-identifier forms.
1737
+ * Bare identifiers are resolved only through local `Const` declarations;
1738
+ * arbitrary variable data-flow remains intentionally out of scope.
1504
1739
  */
1505
1740
  scanOpenFormCalls(line, caller, lineNum) {
1506
1741
  // Each regex has /g so we MUST reset `lastIndex` before use; cloning
1507
1742
  // the regex is the simplest way to avoid leaking state across lines.
1508
- const localRe = new RegExp(VbaExtractor.OPEN_FORM_RE.source, VbaExtractor.OPEN_FORM_RE.flags);
1743
+ const localRe = new RegExp(VbaExtractor.OPEN_FORM_ARG_RE.source, VbaExtractor.OPEN_FORM_ARG_RE.flags);
1509
1744
  let m;
1510
1745
  while ((m = localRe.exec(line)) !== null) {
1511
- const targetFormName = (m[1] ?? '').trim();
1746
+ const rawArg = (m[1] ?? '').trim();
1747
+ const targetFormName = rawArg.startsWith('"')
1748
+ ? unwrapVbaStringLiteral(rawArg)
1749
+ : (this.localConstants.get(rawArg.toLowerCase()) ?? rawArg);
1512
1750
  if (!targetFormName)
1513
1751
  continue;
1514
1752
  this.emitOpensFormEdge(caller, targetFormName, lineNum, m.index);
@@ -1592,16 +1830,38 @@ class VbaExtractor {
1592
1830
  this.emitSqlTableReferences(sqlString, lineNum, dedupe);
1593
1831
  }
1594
1832
  }
1833
+ /**
1834
+ * #13 fix: `sql = sql & "..."` (self-referential concatenation) must
1835
+ * ACCUMULATE the new fragment onto whatever was already tracked for
1836
+ * `varName`, not overwrite it. Overwriting silently dropped earlier
1837
+ * fragments' tables — typically the initial `FROM <table>` in
1838
+ * `sql = "SELECT * FROM tblA"` followed by `sql = sql & " WHERE x=1"`.
1839
+ *
1840
+ * Detection: the RHS (`m[2]`, trimmed) starts with `<varName> &`,
1841
+ * case-insensitively — matching VBA's case-insensitive identifiers (`Sql`
1842
+ * and `sql` are the same variable). A genuine fresh assignment (RHS does
1843
+ * NOT start with the self-reference) still RESETS tracking — that
1844
+ * behavior is unchanged.
1845
+ */
1595
1846
  trackSqlVariableAssignment(lines, lineIndex, sqlVariables) {
1596
1847
  const line = lines[lineIndex] ?? '';
1597
1848
  const m = VbaExtractor.SQL_VAR_ASSIGN_RE.exec(line);
1598
1849
  if (!m)
1599
1850
  return;
1600
- const varName = (m[1] ?? '').toLowerCase();
1601
- const sqlText = this.collectStringLiteralText(lines, lineIndex);
1602
- if (!sqlText)
1851
+ const rawVarName = m[1] ?? '';
1852
+ const varName = rawVarName.toLowerCase();
1853
+ const rhs = (m[2] ?? '').trim();
1854
+ const newFragment = this.collectStringLiteralText(lines, lineIndex);
1855
+ if (!newFragment)
1603
1856
  return;
1604
- sqlVariables.set(varName, sqlText);
1857
+ const selfRefRe = new RegExp(`^${escapeRegExpLiteral(rawVarName)}\\s*&`, 'i');
1858
+ const existing = sqlVariables.get(varName);
1859
+ if (existing !== undefined && selfRefRe.test(rhs)) {
1860
+ sqlVariables.set(varName, `${existing} ${newFragment}`);
1861
+ }
1862
+ else {
1863
+ sqlVariables.set(varName, newFragment);
1864
+ }
1605
1865
  }
1606
1866
  collectStringLiteralText(lines, startIndex) {
1607
1867
  const fragments = [];
@@ -1683,8 +1943,83 @@ class VbaExtractor {
1683
1943
  * typed as a SIMPLE (non-qualified, non-primitive) identifier emit edges.
1684
1944
  */
1685
1945
  localVarTypeMap = new Map();
1946
+ /** Local constant name (lowercase) → simple literal value for OpenForm resolution. */
1947
+ localConstants = new Map();
1948
+ /** Local event name (lowercase) → event node for `RaiseEvent` edge emission. */
1949
+ localEvents = new Map();
1686
1950
  }
1687
1951
  exports.VbaExtractor = VbaExtractor;
1952
+ /**
1953
+ * #13 helper: escape a variable name for safe interpolation into the
1954
+ * self-reference RegExp built by `trackSqlVariableAssignment`. VBA
1955
+ * identifiers are alphanumeric+underscore only, so in practice nothing here
1956
+ * ever needs escaping — this guards against regex metacharacters anyway
1957
+ * rather than assume the input is always well-formed.
1958
+ */
1959
+ function escapeRegExpLiteral(value) {
1960
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1961
+ }
1962
+ function parseConstDeclarations(body) {
1963
+ const declarations = [];
1964
+ for (const part of splitOutsideVbaStrings(body, ',')) {
1965
+ const m = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:As\s+[^=]+?)?\s*=\s*(.+?)\s*$/iu.exec(part);
1966
+ if (!m)
1967
+ continue;
1968
+ const name = m[1] ?? '';
1969
+ const rawValue = (m[2] ?? '').trim();
1970
+ declarations.push({
1971
+ name,
1972
+ value: rawValue.startsWith('"') ? unwrapVbaStringLiteral(rawValue) : rawValue || null,
1973
+ });
1974
+ }
1975
+ return declarations;
1976
+ }
1977
+ function splitOutsideVbaStrings(value, separator) {
1978
+ const parts = [];
1979
+ let current = '';
1980
+ let inString = false;
1981
+ for (let i = 0; i < value.length; i++) {
1982
+ const ch = value[i];
1983
+ const next = value[i + 1];
1984
+ if (ch === '"') {
1985
+ current += ch;
1986
+ if (inString && next === '"') {
1987
+ current += next;
1988
+ i++;
1989
+ continue;
1990
+ }
1991
+ inString = !inString;
1992
+ continue;
1993
+ }
1994
+ if (!inString && ch === separator) {
1995
+ parts.push(current);
1996
+ current = '';
1997
+ continue;
1998
+ }
1999
+ current += ch;
2000
+ }
2001
+ parts.push(current);
2002
+ return parts;
2003
+ }
2004
+ function unwrapVbaStringLiteral(raw) {
2005
+ const trimmed = raw.trim();
2006
+ if (!trimmed.startsWith('"'))
2007
+ return trimmed;
2008
+ let text = '';
2009
+ for (let i = 1; i < trimmed.length; i++) {
2010
+ const ch = trimmed[i];
2011
+ const next = trimmed[i + 1];
2012
+ if (ch === '"' && next === '"') {
2013
+ text += '"';
2014
+ i++;
2015
+ continue;
2016
+ }
2017
+ if (ch === '"')
2018
+ break;
2019
+ text += ch;
2020
+ }
2021
+ return text;
2022
+ }
1688
2023
  /**
1689
2024
  * Hueco 3 helper: parse an Access event-handler Sub name into its
1690
2025
  * `<ControlName>_<EventName>` components. Returns null when the name does