@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.
- package/lib/dist/bin/codegraph.js +1 -1
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/bin/node-version-check.d.ts +9 -6
- package/lib/dist/bin/node-version-check.d.ts.map +1 -1
- package/lib/dist/bin/node-version-check.js +25 -10
- package/lib/dist/bin/node-version-check.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +10 -1
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +33 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +64 -2
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +1 -0
- package/lib/dist/extraction/vba-extractor.d.ts +60 -20
- package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vba-extractor.js +387 -52
- package/lib/dist/extraction/vba-extractor.js.map +1 -1
- package/lib/dist/extraction/vba-preprocess.d.ts +17 -1
- package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
- package/lib/dist/extraction/vba-preprocess.js +92 -1
- package/lib/dist/extraction/vba-preprocess.js.map +1 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +7 -0
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +38 -0
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +137 -0
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/types.d.ts +11 -2
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +4 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/node_modules/.modules.yaml +1 -1
- package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
- package/lib/package.json +2 -2
- 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,
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
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
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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: '
|
|
802
|
-
{ name: '
|
|
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
|
|
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
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
* etc.) are intentionally NOT captured
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
1601
|
-
const
|
|
1602
|
-
|
|
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
|
-
|
|
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
|