@aroman22/codegraph-vba-darwin-arm64 1.5.1 → 1.6.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.
- package/bin/codegraph +5 -0
- package/lib/dist/bin/codegraph.d.ts +1 -1
- package/lib/dist/bin/codegraph.d.ts.map +1 -1
- package/lib/dist/bin/codegraph.js +307 -81
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/bin/command-supervision.d.ts +38 -1
- package/lib/dist/bin/command-supervision.d.ts.map +1 -1
- package/lib/dist/bin/command-supervision.js +22 -3
- package/lib/dist/bin/command-supervision.js.map +1 -1
- package/lib/dist/db/index.d.ts +84 -5
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +209 -12
- package/lib/dist/db/index.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 +80 -3
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +127 -4
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +317 -7
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +43 -1
- package/lib/dist/db/wal-valve.d.ts +106 -0
- package/lib/dist/db/wal-valve.d.ts.map +1 -0
- package/lib/dist/db/wal-valve.js +208 -0
- package/lib/dist/db/wal-valve.js.map +1 -0
- package/lib/dist/directory.d.ts +9 -5
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +210 -19
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
- package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/cfml-extractor.js +494 -0
- package/lib/dist/extraction/cfml-extractor.js.map +1 -0
- package/lib/dist/extraction/grammars.d.ts +25 -1
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +177 -23
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +47 -2
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +568 -51
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/arkts.d.ts +3 -0
- package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
- package/lib/dist/extraction/languages/arkts.js +127 -0
- package/lib/dist/extraction/languages/arkts.js.map +1 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts +98 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +563 -1
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
- package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfquery.js +28 -0
- package/lib/dist/extraction/languages/cfquery.js.map +1 -0
- package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
- package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfscript.js +73 -0
- package/lib/dist/extraction/languages/cfscript.js.map +1 -0
- package/lib/dist/extraction/languages/cobol.d.ts +33 -0
- package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cobol.js +499 -0
- package/lib/dist/extraction/languages/cobol.js.map +1 -0
- package/lib/dist/extraction/languages/erlang.d.ts +3 -0
- package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/lib/dist/extraction/languages/erlang.js +350 -0
- package/lib/dist/extraction/languages/erlang.js.map +1 -0
- package/lib/dist/extraction/languages/index.d.ts.map +1 -1
- package/lib/dist/extraction/languages/index.js +18 -0
- package/lib/dist/extraction/languages/index.js.map +1 -1
- package/lib/dist/extraction/languages/nix.d.ts +3 -0
- package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
- package/lib/dist/extraction/languages/nix.js +294 -0
- package/lib/dist/extraction/languages/nix.js.map +1 -0
- package/lib/dist/extraction/languages/solidity.d.ts +3 -0
- package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/lib/dist/extraction/languages/solidity.js +293 -0
- package/lib/dist/extraction/languages/solidity.js.map +1 -0
- package/lib/dist/extraction/languages/terraform.d.ts +3 -0
- package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/lib/dist/extraction/languages/terraform.js +641 -0
- package/lib/dist/extraction/languages/terraform.js.map +1 -0
- package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
- package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/lib/dist/extraction/languages/vbnet.js +141 -0
- package/lib/dist/extraction/languages/vbnet.js.map +1 -0
- package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/mybatis-extractor.js +140 -38
- package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
- package/lib/dist/extraction/parse-pool.d.ts +28 -1
- package/lib/dist/extraction/parse-pool.d.ts.map +1 -1
- package/lib/dist/extraction/parse-pool.js +76 -7
- package/lib/dist/extraction/parse-pool.js.map +1 -1
- package/lib/dist/extraction/parse-worker.js +11 -4
- package/lib/dist/extraction/parse-worker.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
- package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +20 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +60 -1
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +905 -20
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/vba/call-sweep.d.ts +3 -0
- package/lib/dist/extraction/vba/call-sweep.d.ts.map +1 -0
- package/lib/dist/extraction/vba/call-sweep.js +206 -0
- package/lib/dist/extraction/vba/call-sweep.js.map +1 -0
- package/lib/dist/extraction/vba/calls.d.ts +55 -0
- package/lib/dist/extraction/vba/calls.d.ts.map +1 -0
- package/lib/dist/extraction/vba/calls.js +439 -0
- package/lib/dist/extraction/vba/calls.js.map +1 -0
- package/lib/dist/extraction/vba/constants.d.ts +42 -0
- package/lib/dist/extraction/vba/constants.d.ts.map +1 -0
- package/lib/dist/extraction/vba/constants.js +112 -0
- package/lib/dist/extraction/vba/constants.js.map +1 -0
- package/lib/dist/extraction/vba/context.d.ts +175 -0
- package/lib/dist/extraction/vba/context.d.ts.map +1 -0
- package/lib/dist/extraction/vba/context.js +268 -0
- package/lib/dist/extraction/vba/context.js.map +1 -0
- package/lib/dist/extraction/vba/controls.d.ts +24 -0
- package/lib/dist/extraction/vba/controls.d.ts.map +1 -0
- package/lib/dist/extraction/vba/controls.js +89 -0
- package/lib/dist/extraction/vba/controls.js.map +1 -0
- package/lib/dist/extraction/vba/declarations.d.ts +3 -0
- package/lib/dist/extraction/vba/declarations.d.ts.map +1 -0
- package/lib/dist/extraction/vba/declarations.js +154 -0
- package/lib/dist/extraction/vba/declarations.js.map +1 -0
- package/lib/dist/extraction/vba/dims.d.ts +3 -0
- package/lib/dist/extraction/vba/dims.d.ts.map +1 -0
- package/lib/dist/extraction/vba/dims.js +186 -0
- package/lib/dist/extraction/vba/dims.js.map +1 -0
- package/lib/dist/extraction/vba/docmd.d.ts +16 -0
- package/lib/dist/extraction/vba/docmd.d.ts.map +1 -0
- package/lib/dist/extraction/vba/docmd.js +179 -0
- package/lib/dist/extraction/vba/docmd.js.map +1 -0
- package/lib/dist/extraction/vba/enums-consts.d.ts +15 -0
- package/lib/dist/extraction/vba/enums-consts.d.ts.map +1 -0
- package/lib/dist/extraction/vba/enums-consts.js +178 -0
- package/lib/dist/extraction/vba/enums-consts.js.map +1 -0
- package/lib/dist/extraction/vba/implements.d.ts +3 -0
- package/lib/dist/extraction/vba/implements.d.ts.map +1 -0
- package/lib/dist/extraction/vba/implements.js +52 -0
- package/lib/dist/extraction/vba/implements.js.map +1 -0
- package/lib/dist/extraction/vba/procedures.d.ts +3 -0
- package/lib/dist/extraction/vba/procedures.d.ts.map +1 -0
- package/lib/dist/extraction/vba/procedures.js +239 -0
- package/lib/dist/extraction/vba/procedures.js.map +1 -0
- package/lib/dist/extraction/vba/sql-wrapper.d.ts +17 -0
- package/lib/dist/extraction/vba/sql-wrapper.d.ts.map +1 -0
- package/lib/dist/extraction/vba/sql-wrapper.js +212 -0
- package/lib/dist/extraction/vba/sql-wrapper.js.map +1 -0
- package/lib/dist/extraction/vba/tempvars.d.ts +10 -0
- package/lib/dist/extraction/vba/tempvars.d.ts.map +1 -0
- package/lib/dist/extraction/vba/tempvars.js +138 -0
- package/lib/dist/extraction/vba/tempvars.js.map +1 -0
- package/lib/dist/extraction/vba/text-utils.d.ts +73 -0
- package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -0
- package/lib/dist/extraction/vba/text-utils.js +195 -0
- package/lib/dist/extraction/vba/text-utils.js.map +1 -0
- package/lib/dist/extraction/vba-extractor.d.ts +3 -951
- package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vba-extractor.js +46 -2934
- package/lib/dist/extraction/vba-extractor.js.map +1 -1
- package/lib/dist/extraction/vba-preprocess.d.ts +1 -1
- package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
- package/lib/dist/extraction/vba-preprocess.js +300 -88
- package/lib/dist/extraction/vba-preprocess.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/lib/dist/graph/traversal.d.ts.map +1 -1
- package/lib/dist/graph/traversal.js +76 -17
- package/lib/dist/graph/traversal.js.map +1 -1
- package/lib/dist/index.d.ts +81 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +467 -10
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts +42 -0
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +97 -2
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/mcp/daemon.d.ts +35 -3
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +101 -9
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.js +2 -1
- package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/lib/dist/mcp/early-ppid.d.ts +26 -0
- package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
- package/lib/dist/mcp/early-ppid.js +29 -0
- package/lib/dist/mcp/early-ppid.js.map +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +29 -4
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/lib/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/lib/dist/mcp/liveness-watchdog.js +73 -13
- package/lib/dist/mcp/liveness-watchdog.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +30 -4
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/query-pool.d.ts +14 -0
- package/lib/dist/mcp/query-pool.d.ts.map +1 -1
- package/lib/dist/mcp/query-pool.js +18 -0
- package/lib/dist/mcp/query-pool.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +14 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +28 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/startup-handshake.d.ts +44 -0
- package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/lib/dist/mcp/startup-handshake.js +73 -0
- package/lib/dist/mcp/startup-handshake.js.map +1 -0
- package/lib/dist/mcp/tools.d.ts +22 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +289 -23
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/mcp/transport.d.ts.map +1 -1
- package/lib/dist/mcp/transport.js +18 -0
- package/lib/dist/mcp/transport.js.map +1 -1
- package/lib/dist/project-config.d.ts +44 -0
- package/lib/dist/project-config.d.ts.map +1 -1
- package/lib/dist/project-config.js +151 -2
- package/lib/dist/project-config.js.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.js +170 -146
- package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +1109 -97
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/cooperative-yield.d.ts +32 -0
- package/lib/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/lib/dist/resolution/cooperative-yield.js +42 -0
- package/lib/dist/resolution/cooperative-yield.js.map +1 -0
- package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
- package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/cics.js +90 -0
- package/lib/dist/resolution/frameworks/cics.js.map +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +6 -0
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/java.js +14 -6
- package/lib/dist/resolution/frameworks/java.js.map +1 -1
- package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/terraform.js +277 -0
- package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
- package/lib/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/goframe-synthesizer.js +8 -3
- package/lib/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +7 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +129 -4
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +85 -2
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +535 -58
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +22 -3
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +613 -39
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/strip-comments.d.ts +1 -1
- package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
- package/lib/dist/resolution/strip-comments.js +49 -0
- package/lib/dist/resolution/strip-comments.js.map +1 -1
- package/lib/dist/resolution/types.d.ts +29 -0
- package/lib/dist/resolution/types.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.d.ts +10 -0
- package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.js +142 -4
- package/lib/dist/resolution/workspace-packages.js.map +1 -1
- package/lib/dist/search/identifier-segments.d.ts +60 -0
- package/lib/dist/search/identifier-segments.d.ts.map +1 -0
- package/lib/dist/search/identifier-segments.js +176 -0
- package/lib/dist/search/identifier-segments.js.map +1 -0
- package/lib/dist/sync/git-hooks.d.ts.map +1 -1
- package/lib/dist/sync/git-hooks.js +2 -0
- package/lib/dist/sync/git-hooks.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +10 -5
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +51 -14
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/sync/worktree.d.ts +9 -0
- package/lib/dist/sync/worktree.d.ts.map +1 -1
- package/lib/dist/sync/worktree.js +45 -0
- package/lib/dist/sync/worktree.js.map +1 -1
- package/lib/dist/types.d.ts +25 -2
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +10 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/dist/upgrade/index.d.ts +32 -0
- package/lib/dist/upgrade/index.d.ts.map +1 -1
- package/lib/dist/upgrade/index.js +163 -14
- package/lib/dist/upgrade/index.js.map +1 -1
- package/lib/dist/upgrade/remove-binary.d.ts +87 -0
- package/lib/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/lib/dist/upgrade/remove-binary.js +289 -0
- package/lib/dist/upgrade/remove-binary.js.map +1 -0
- package/lib/dist/upgrade/update-check.d.ts +92 -0
- package/lib/dist/upgrade/update-check.d.ts.map +1 -0
- package/lib/dist/upgrade/update-check.js +258 -0
- package/lib/dist/upgrade/update-check.js.map +1 -0
- package/lib/node_modules/.modules.yaml +1 -1
- package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/lib/dist/reasoning/config.d.ts +0 -45
- package/lib/dist/reasoning/config.d.ts.map +0 -1
- package/lib/dist/reasoning/config.js +0 -171
- package/lib/dist/reasoning/config.js.map +0 -1
- package/lib/dist/reasoning/credentials.d.ts +0 -5
- package/lib/dist/reasoning/credentials.d.ts.map +0 -1
- package/lib/dist/reasoning/credentials.js +0 -83
- package/lib/dist/reasoning/credentials.js.map +0 -1
- package/lib/dist/reasoning/login.d.ts +0 -21
- package/lib/dist/reasoning/login.d.ts.map +0 -1
- package/lib/dist/reasoning/login.js +0 -85
- package/lib/dist/reasoning/login.js.map +0 -1
- package/lib/dist/reasoning/reasoner.d.ts +0 -43
- package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
- package/lib/dist/reasoning/reasoner.js +0 -308
- package/lib/dist/reasoning/reasoner.js.map +0 -1
|
@@ -61,62 +61,33 @@ exports.VbaExtractor = void 0;
|
|
|
61
61
|
* Rejects `.form.txt` / `.report.txt` (REQ-CODE-9 — the form UI extractor
|
|
62
62
|
* owns those). `Option ...` directives are inert — `stripVbaComments()`
|
|
63
63
|
* drops them.
|
|
64
|
+
*
|
|
65
|
+
* This class is a THIN ORCHESTRATOR: it owns the input, drives the file-level
|
|
66
|
+
* node creation, and sequences the per-concern sweeps. All shared extraction
|
|
67
|
+
* state (node/edge accumulators, the `functionNodeByName` / `localVarTypeMap`
|
|
68
|
+
* maps, `moduleOrClassNode`) and the sweeps themselves live on
|
|
69
|
+
* `VbaExtractorContext` under `src/extraction/vba/`.
|
|
64
70
|
*/
|
|
65
71
|
const path = __importStar(require("path"));
|
|
66
72
|
const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
|
|
67
73
|
const vba_preprocess_1 = require("./vba-preprocess");
|
|
74
|
+
const context_1 = require("./vba/context");
|
|
75
|
+
const procedures_1 = require("./vba/procedures");
|
|
76
|
+
const declarations_1 = require("./vba/declarations");
|
|
77
|
+
const dims_1 = require("./vba/dims");
|
|
78
|
+
const enums_consts_1 = require("./vba/enums-consts");
|
|
79
|
+
const implements_1 = require("./vba/implements");
|
|
80
|
+
const call_sweep_1 = require("./vba/call-sweep");
|
|
68
81
|
class VbaExtractor {
|
|
69
82
|
filePath;
|
|
70
83
|
source;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
unresolvedReferences = [];
|
|
75
|
-
moduleOrClassNode = null;
|
|
76
|
-
/**
|
|
77
|
-
* Class-name prefix for `qualifiedName` composition.
|
|
78
|
-
*
|
|
79
|
-
* B3 (hueco 5): for `.cls` files every function node's `qualifiedName`
|
|
80
|
-
* is composed as `${className}.${procName}` (e.g. `Form_TestForm.Form_Load`)
|
|
81
|
-
* so cross-class callers can disambiguate which form each `Form_Load`
|
|
82
|
-
* belongs to. For `.bas` files this is `null` — module-scoped procs
|
|
83
|
-
* keep their bare-name `qualifiedName` (e.g. `DoThing`), preserving
|
|
84
|
-
* the pre-hueco-5 behavior and every existing qualifiedName-based
|
|
85
|
-
* assertion in `extraction-vba.test.ts`.
|
|
86
|
-
*
|
|
87
|
-
* Resolved once per `extract()` from `isCls` + `vbName` (both already
|
|
88
|
-
* computed for `createModuleOrClassNode`); `sweepProcedures` reads it
|
|
89
|
-
* to set `ProcInfo.qualifiedName` and the function node's qualifiedName.
|
|
90
|
-
*/
|
|
91
|
-
classNamePrefix = null;
|
|
92
|
-
/**
|
|
93
|
-
* Map of procedure name (within this file) → list of ProcInfo. The list
|
|
94
|
-
* (rather than a single value) is needed because VBA allows multiple
|
|
95
|
-
* Property accessors with the same name: `Property Get Foo`, `Property
|
|
96
|
-
* Let Foo`, and `Property Set Foo` all share the name `Foo`. Audit W6
|
|
97
|
-
* (June 2026): the previous Map<string, ProcInfo> kept only the last
|
|
98
|
-
* accessor, breaking same-file call resolution and emitting the wrong
|
|
99
|
-
* `findFunctionNodeByName` match.
|
|
100
|
-
*/
|
|
101
|
-
localProcs = new Map();
|
|
102
|
-
/**
|
|
103
|
-
* Cache: procedure name → first matching function node emitted for that
|
|
104
|
-
* name. Audit S2 (June 2026): the previous implementation did an O(n)
|
|
105
|
-
* linear scan of all nodes for every call site; with W6's multimap
|
|
106
|
-
* the same-name collision can return multiple matches but bare-name
|
|
107
|
-
* call resolution only needs the first.
|
|
108
|
-
*/
|
|
109
|
-
functionNodeByName = new Map();
|
|
110
|
-
/**
|
|
111
|
-
* Cache: startLine → function node emitted for that line. Used by Fix 1
|
|
112
|
-
* (June 2026): when Property Get/Let/Set share a name, we need to find
|
|
113
|
-
* the SPECIFIC accessor's node by its declaration line rather than relying
|
|
114
|
-
* on `functionNodeByName` which only stores the first accessor.
|
|
115
|
-
*/
|
|
116
|
-
functionNodeByStartLine = new Map();
|
|
117
|
-
constructor(filePath, source) {
|
|
84
|
+
ctx;
|
|
85
|
+
vbaTargets;
|
|
86
|
+
constructor(filePath, source, vbaTargets) {
|
|
118
87
|
this.filePath = filePath;
|
|
119
88
|
this.source = source;
|
|
89
|
+
this.vbaTargets = vbaTargets;
|
|
90
|
+
this.ctx = new context_1.VbaExtractorContext(filePath);
|
|
120
91
|
}
|
|
121
92
|
extract() {
|
|
122
93
|
const startTime = Date.now();
|
|
@@ -124,18 +95,18 @@ class VbaExtractor {
|
|
|
124
95
|
// REQ-CODE-9: hand form/report files off to VbaFormExtractor. If
|
|
125
96
|
// somehow routed here, emit just a file node and return.
|
|
126
97
|
if (this.isFormOrReportFile()) {
|
|
127
|
-
this.nodes.push(this.createFileNode());
|
|
98
|
+
this.ctx.nodes.push(this.createFileNode());
|
|
128
99
|
return this.result(startTime);
|
|
129
100
|
}
|
|
130
|
-
// Pre-process pipeline (per design):
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const uncommented = (0, vba_preprocess_1.
|
|
101
|
+
// Pre-process pipeline (per design): strip comments first to prevent
|
|
102
|
+
// comments from blocking or causing false line continuations, then join
|
|
103
|
+
// continuations, and blank inactive conditional-compilation branches.
|
|
104
|
+
// Line count is preserved by all stages.
|
|
105
|
+
const cleanComments = (0, vba_preprocess_1.stripVbaComments)(this.source);
|
|
106
|
+
const joined = (0, vba_preprocess_1.joinLineContinuations)(cleanComments);
|
|
107
|
+
const uncommented = (0, vba_preprocess_1.preprocessConditionalCompilation)(joined, this.vbaTargets);
|
|
137
108
|
// Create the file node.
|
|
138
|
-
this.nodes.push(this.createFileNode());
|
|
109
|
+
this.ctx.nodes.push(this.createFileNode());
|
|
139
110
|
// Resolve the file's "kind" — .cls → class, else module (including .bas
|
|
140
111
|
// and the legacy .frm/.dsr stubs).
|
|
141
112
|
const isCls = this.filePath.toLowerCase().endsWith('.cls');
|
|
@@ -152,68 +123,68 @@ class VbaExtractor {
|
|
|
152
123
|
.basename(this.filePath)
|
|
153
124
|
.replace(/\.[^.]+$/, '');
|
|
154
125
|
const className = isCls ? (vbName ?? fallbackName) : null;
|
|
155
|
-
this.classNamePrefix = className;
|
|
126
|
+
this.ctx.classNamePrefix = className;
|
|
156
127
|
// Module/class node — created lazily so a file containing only
|
|
157
128
|
// Option directives (REQ-CODE-10: "emits nothing") doesn't carry a
|
|
158
129
|
// module node with no symbols attached.
|
|
159
130
|
let hasAnySymbols = false;
|
|
160
131
|
// Sweep: procedures → function nodes + contains edges.
|
|
161
|
-
const procs =
|
|
132
|
+
const procs = (0, procedures_1.sweepProcedures)(this.ctx, uncommented);
|
|
162
133
|
if (procs.length > 0)
|
|
163
134
|
hasAnySymbols = true;
|
|
164
135
|
// Sweep: first-class Event / Type / Declare declarations (roadmap #26).
|
|
165
136
|
// This runs before call-site scanning so `RaiseEvent Foo` and calls to a
|
|
166
137
|
// `Declare` can resolve to real local nodes rather than fall through.
|
|
167
|
-
const declarationCount =
|
|
138
|
+
const declarationCount = (0, declarations_1.sweepEventsTypesAndDeclares)(this.ctx, uncommented);
|
|
168
139
|
if (declarationCount > 0)
|
|
169
140
|
hasAnySymbols = true;
|
|
170
141
|
// Sweep: Implements (REQ-CODE-5).
|
|
171
|
-
const implCount =
|
|
142
|
+
const implCount = (0, implements_1.sweepImplements)(this.ctx, uncommented);
|
|
172
143
|
if (implCount > 0)
|
|
173
144
|
hasAnySymbols = true;
|
|
174
145
|
// Sweep: qualified Dim (REQ-CODE-6) and WithEvents (REQ-CODE-7).
|
|
175
|
-
const dimCount =
|
|
146
|
+
const dimCount = (0, dims_1.sweepDimsAndWithEvents)(this.ctx, uncommented);
|
|
176
147
|
if (dimCount > 0)
|
|
177
148
|
hasAnySymbols = true;
|
|
178
149
|
// Sweep: Enum/Const declarations (REQ-CODE-12, REQ-CODE-13). Dysflow
|
|
179
150
|
// exports the full module text, so a constants module carries its
|
|
180
151
|
// `Const` lines and `Enum ... End Enum` blocks verbatim — these are the
|
|
181
152
|
// project's domain dictionary and must be in the graph.
|
|
182
|
-
const enumConstCount =
|
|
153
|
+
const enumConstCount = (0, enums_consts_1.sweepEnumsAndConsts)(this.ctx, uncommented);
|
|
183
154
|
if (enumConstCount > 0)
|
|
184
155
|
hasAnySymbols = true;
|
|
185
156
|
// Sweep: call sites (REQ-CODE-4) and SQL-in-strings (REQ-CODE-8).
|
|
186
157
|
// Both walk the same per-line view; combining them in one pass is
|
|
187
158
|
// simpler and keeps line tracking consistent.
|
|
188
|
-
|
|
159
|
+
(0, call_sweep_1.sweepCallsAndSql)(this.ctx, uncommented);
|
|
189
160
|
// Create the module/class node ONLY when the file has actual symbols
|
|
190
161
|
// (REQ-CODE-10 — a file with ONLY Option directives emits zero symbol
|
|
191
162
|
// nodes. A file with Enum/Const but no procedures DOES emit a module
|
|
192
163
|
// node, since those are real symbols — see REQ-CODE-12/13).
|
|
193
164
|
if (hasAnySymbols) {
|
|
194
|
-
this.moduleOrClassNode = this.createModuleOrClassNode(isCls, vbName);
|
|
195
|
-
this.nodes.push(this.moduleOrClassNode);
|
|
165
|
+
this.ctx.moduleOrClassNode = this.createModuleOrClassNode(isCls, vbName);
|
|
166
|
+
this.ctx.nodes.push(this.ctx.moduleOrClassNode);
|
|
196
167
|
// Re-attribute contains/implements/reference edges whose source was
|
|
197
168
|
// held in `pendingModuleOrClassSource` — see below. Since we now
|
|
198
169
|
// know the module id, redirect those edges to it.
|
|
199
|
-
for (const edge of this.pendingModuleOrClassSource) {
|
|
200
|
-
edge.source = this.moduleOrClassNode.id;
|
|
170
|
+
for (const edge of this.ctx.pendingModuleOrClassSource) {
|
|
171
|
+
edge.source = this.ctx.moduleOrClassNode.id;
|
|
201
172
|
}
|
|
202
|
-
this.pendingModuleOrClassSource.length = 0;
|
|
173
|
+
this.ctx.pendingModuleOrClassSource.length = 0;
|
|
203
174
|
// Sub New marker on the class node (REQ-CODE-3).
|
|
204
175
|
if (isCls) {
|
|
205
176
|
const hasNew = procs.some((p) => p.name === 'New');
|
|
206
177
|
if (hasNew) {
|
|
207
|
-
const md = (this.moduleOrClassNode.metadata ?? {});
|
|
178
|
+
const md = (this.ctx.moduleOrClassNode.metadata ?? {});
|
|
208
179
|
md.hasClassInitializer = true;
|
|
209
180
|
md.initializerName = 'New';
|
|
210
|
-
this.moduleOrClassNode.metadata = md;
|
|
181
|
+
this.ctx.moduleOrClassNode.metadata = md;
|
|
211
182
|
}
|
|
212
183
|
}
|
|
213
184
|
}
|
|
214
185
|
}
|
|
215
186
|
catch (error) {
|
|
216
|
-
this.errors.push({
|
|
187
|
+
this.ctx.errors.push({
|
|
217
188
|
message: `VBA extraction error: ${error instanceof Error ? error.message : String(error)}`,
|
|
218
189
|
filePath: this.filePath,
|
|
219
190
|
severity: 'error',
|
|
@@ -227,10 +198,10 @@ class VbaExtractor {
|
|
|
227
198
|
// ---------------------------------------------------------------------------
|
|
228
199
|
result(startTime) {
|
|
229
200
|
return {
|
|
230
|
-
nodes: this.nodes,
|
|
231
|
-
edges: this.edges,
|
|
232
|
-
unresolvedReferences: this.unresolvedReferences,
|
|
233
|
-
errors: this.errors,
|
|
201
|
+
nodes: this.ctx.nodes,
|
|
202
|
+
edges: this.ctx.edges,
|
|
203
|
+
unresolvedReferences: this.ctx.unresolvedReferences,
|
|
204
|
+
errors: this.ctx.errors,
|
|
234
205
|
durationMs: Date.now() - startTime,
|
|
235
206
|
};
|
|
236
207
|
}
|
|
@@ -300,2865 +271,6 @@ class VbaExtractor {
|
|
|
300
271
|
updatedAt: Date.now(),
|
|
301
272
|
};
|
|
302
273
|
}
|
|
303
|
-
/** Sub/Function/Property regex — captures visibility prefix, kind, and name. */
|
|
304
|
-
static PROC_RE = /^\s*((?:Public|Private|Friend|Static)\s+)?(?:Static\s+)?(Sub|Function|Property(?:\s+(?:Get|Let|Set))?)\s+(\p{L}[\p{L}\p{N}_]*)/iu;
|
|
305
|
-
/**
|
|
306
|
-
* Walk the (uncommented, line-joined) source and emit one `function` node
|
|
307
|
-
* per `Sub` / `Function` / `Property` declaration. Also records the proc
|
|
308
|
-
* in `localProcs` so call-site resolution can distinguish same-file from
|
|
309
|
-
* cross-module calls.
|
|
310
|
-
*/
|
|
311
|
-
sweepProcedures(src) {
|
|
312
|
-
const procs = [];
|
|
313
|
-
const lines = src.split('\n');
|
|
314
|
-
for (let i = 0; i < lines.length; i++) {
|
|
315
|
-
const line = lines[i] ?? '';
|
|
316
|
-
const m = VbaExtractor.PROC_RE.exec(line);
|
|
317
|
-
if (!m)
|
|
318
|
-
continue;
|
|
319
|
-
const visibilityRaw = (m[1] ?? '').trim();
|
|
320
|
-
const kindRaw = (m[2] ?? '').trim().toLowerCase();
|
|
321
|
-
const name = m[3] ?? '';
|
|
322
|
-
if (!name)
|
|
323
|
-
continue;
|
|
324
|
-
const lineNum = i + 1;
|
|
325
|
-
// Normalize the visibility. VBA's "Static" keyword is a storage
|
|
326
|
-
// specifier, not visibility, so we treat bare-static declarations as
|
|
327
|
-
// 'Public' (the default) per spec R1. "Friend" is not in the Node
|
|
328
|
-
// visibility enum; treat it as 'Public' since it's the closest
|
|
329
|
-
// broader-than-private modifier.
|
|
330
|
-
let visibility;
|
|
331
|
-
switch (visibilityRaw.toLowerCase()) {
|
|
332
|
-
case 'private':
|
|
333
|
-
visibility = 'private';
|
|
334
|
-
break;
|
|
335
|
-
case 'public':
|
|
336
|
-
case 'static':
|
|
337
|
-
case 'friend':
|
|
338
|
-
case '':
|
|
339
|
-
default:
|
|
340
|
-
visibility = 'public';
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
const kind = kindRaw.startsWith('sub')
|
|
344
|
-
? 'sub'
|
|
345
|
-
: kindRaw.startsWith('function')
|
|
346
|
-
? 'function'
|
|
347
|
-
: 'property';
|
|
348
|
-
const proc = {
|
|
349
|
-
name,
|
|
350
|
-
// B3 (hueco 5): when this file is a `.cls`, prefix the
|
|
351
|
-
// qualifiedName with the resolved class name so cross-class
|
|
352
|
-
// queries (e.g. `Form_Load`) match only the owning class.
|
|
353
|
-
// `.bas` files leave `qualifiedName === name`.
|
|
354
|
-
qualifiedName: this.classNamePrefix
|
|
355
|
-
? `${this.classNamePrefix}.${name}`
|
|
356
|
-
: name,
|
|
357
|
-
kind,
|
|
358
|
-
visibility,
|
|
359
|
-
startLine: lineNum,
|
|
360
|
-
};
|
|
361
|
-
procs.push(proc);
|
|
362
|
-
const bucket = this.localProcs.get(name);
|
|
363
|
-
if (bucket)
|
|
364
|
-
bucket.push(proc);
|
|
365
|
-
else
|
|
366
|
-
this.localProcs.set(name, [proc]);
|
|
367
|
-
const nodeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'function', name, lineNum);
|
|
368
|
-
const fnNode = {
|
|
369
|
-
id: nodeId,
|
|
370
|
-
kind: 'function',
|
|
371
|
-
name,
|
|
372
|
-
// B3 (hueco 5): same prefix rule as the ProcInfo above — class
|
|
373
|
-
// methods get `${className}.${name}`, module-level Subs keep
|
|
374
|
-
// their bare name.
|
|
375
|
-
qualifiedName: this.classNamePrefix
|
|
376
|
-
? `${this.classNamePrefix}.${name}`
|
|
377
|
-
: name,
|
|
378
|
-
filePath: this.filePath,
|
|
379
|
-
language: 'vba',
|
|
380
|
-
startLine: lineNum,
|
|
381
|
-
endLine: lineNum,
|
|
382
|
-
startColumn: 0,
|
|
383
|
-
endColumn: line.length,
|
|
384
|
-
visibility,
|
|
385
|
-
updatedAt: Date.now(),
|
|
386
|
-
};
|
|
387
|
-
this.nodes.push(fnNode);
|
|
388
|
-
// Cache the first node emitted for this name — `findFunctionNodeByName`
|
|
389
|
-
// (audit S2) becomes O(1) instead of O(n) per call site.
|
|
390
|
-
if (!this.functionNodeByName.has(name)) {
|
|
391
|
-
this.functionNodeByName.set(name, fnNode);
|
|
392
|
-
}
|
|
393
|
-
// Fix 1: also index by startLine so Property Get/Let/Set with the same
|
|
394
|
-
// name can each be found by their exact declaration line.
|
|
395
|
-
this.functionNodeByStartLine.set(lineNum, fnNode);
|
|
396
|
-
// Hueco 3: synthesize the event-handler edge for Access naming
|
|
397
|
-
// convention `<ControlName>_<EventName>` (e.g. `ComandoAltaPM_Click`,
|
|
398
|
-
// `MotivoBorrado_AfterUpdate`). The `<X>_<Y>` shape is split on the
|
|
399
|
-
// LAST underscore so multi-word events like `BeforeDelConfirm` parse
|
|
400
|
-
// correctly. Form-level events (`Form_Load`, `Form_Open`,
|
|
401
|
-
// `Form_Unload`, …) are NOT control handlers — they fire on the
|
|
402
|
-
// form object itself, not on a control — so they are skipped here
|
|
403
|
-
// (the form module node is the conceptual source for those).
|
|
404
|
-
//
|
|
405
|
-
// Scope guard: only emit when this .cls looks like a form code-
|
|
406
|
-
// behind (`Form_*.cls`). Without this guard, regular service classes
|
|
407
|
-
// whose methods happen to have underscores in their names
|
|
408
|
-
// (e.g. `InformeRiesgoPDFServicio.cls` declares
|
|
409
|
-
// `GenerarHTML_Principal`, `GetEstilosCSS_PDF`,
|
|
410
|
-
// `Class_Initialize`) would synthesize ~550 spurious
|
|
411
|
-
// `form-instance-control` stubs in real Dysflow projects. The
|
|
412
|
-
// `Form_` prefix is the canonical Access code-behind naming
|
|
413
|
-
// convention and matches the .form.txt siblings' basename.
|
|
414
|
-
//
|
|
415
|
-
// Cross-file synthesis caveat: the edge's source is a
|
|
416
|
-
// `form-instance-control` node that lives in the sibling .form.txt,
|
|
417
|
-
// not this .cls file. Two consequences:
|
|
418
|
-
// 1. We also emit a STUB form-instance-control node locally with
|
|
419
|
-
// the deterministic id so the per-file edge filter
|
|
420
|
-
// (`insertedIds.has(source)`) accepts the edge. When the
|
|
421
|
-
// sibling .form.txt is later indexed, VbaFormExtractor emits
|
|
422
|
-
// the real form-instance-control node with the same id; the
|
|
423
|
-
// `INSERT OR REPLACE` semantics in queries.ts:insertNode
|
|
424
|
-
// overwrite the stub with the real one (preserving the
|
|
425
|
-
// metadata.controlType, filePath, line range, etc.).
|
|
426
|
-
// 2. When the .form.txt is processed FIRST (alphabetically
|
|
427
|
-
// unlikely but possible), the real node exists in the DB
|
|
428
|
-
// before this edge is committed; insertEdges's DB-level
|
|
429
|
-
// endpoint check passes the edge naturally without the stub.
|
|
430
|
-
// Either order converges on the same final state.
|
|
431
|
-
// See vba-form-extractor.ts:findControlName for the matching real
|
|
432
|
-
// form-instance-control node emission.
|
|
433
|
-
const handler = parseEventHandlerName(name);
|
|
434
|
-
// Prefix-driven sibling binding (issue #41). Both `Form_*.cls` and
|
|
435
|
-
// `Report_*.cls` Dysflow code-behind files share the same code path;
|
|
436
|
-
// only the sibling extension differs (`.form.txt` vs `.report.txt`).
|
|
437
|
-
// The check is on the BASENAME prefix so a class called
|
|
438
|
-
// `FormularioVentas.cls` or `ReportingHelper.cls` (no trailing
|
|
439
|
-
// underscore) does not match — the trailing `_` is the discriminator.
|
|
440
|
-
// Any other `.cls` (e.g. `InformeRiesgoPDFServicio.cls` with methods
|
|
441
|
-
// like `GenerarHTML_Principal`) gets `codeBehindExt === null` and is
|
|
442
|
-
// skipped, preserving the original Form_-only guard's behaviour for
|
|
443
|
-
// non-form classes.
|
|
444
|
-
const basename = path.basename(this.filePath).toLowerCase();
|
|
445
|
-
const codeBehindExt = basename.startsWith('report_')
|
|
446
|
-
? '.report.txt'
|
|
447
|
-
: basename.startsWith('form_')
|
|
448
|
-
? '.form.txt'
|
|
449
|
-
: null;
|
|
450
|
-
const isFormCodeBehind = codeBehindExt !== null;
|
|
451
|
-
if (handler && isFormCodeBehind) {
|
|
452
|
-
const siblingPath = this.filePath.replace(/\.cls$/i, codeBehindExt);
|
|
453
|
-
const controlNodeId = (0, tree_sitter_helpers_1.generateNodeId)(siblingPath, 'form-instance-control', handler.controlName, 0);
|
|
454
|
-
// Stub form-instance-control: local so the per-file edge filter
|
|
455
|
-
// passes the event-handler edge. Overwritten by the real node
|
|
456
|
-
// emitted from the sibling .form.txt (or .report.txt) at index time
|
|
457
|
-
// (same id, same schema, INSERT OR REPLACE). No metadata.controlType
|
|
458
|
-
// here — the sibling side carries the real control type.
|
|
459
|
-
this.nodes.push({
|
|
460
|
-
id: controlNodeId,
|
|
461
|
-
kind: 'form-instance-control',
|
|
462
|
-
name: handler.controlName,
|
|
463
|
-
qualifiedName: `${siblingPath}::${handler.controlName}`,
|
|
464
|
-
filePath: siblingPath,
|
|
465
|
-
language: 'vba',
|
|
466
|
-
startLine: 0,
|
|
467
|
-
endLine: 0,
|
|
468
|
-
startColumn: 0,
|
|
469
|
-
endColumn: 0,
|
|
470
|
-
updatedAt: Date.now(),
|
|
471
|
-
});
|
|
472
|
-
this.edges.push({
|
|
473
|
-
source: nodeId,
|
|
474
|
-
target: controlNodeId,
|
|
475
|
-
kind: 'event-handler',
|
|
476
|
-
provenance: 'heuristic',
|
|
477
|
-
metadata: { eventName: handler.eventName },
|
|
478
|
-
line: lineNum,
|
|
479
|
-
column: 0,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
if (this.moduleOrClassNode) {
|
|
483
|
-
this.edges.push({
|
|
484
|
-
source: this.moduleOrClassNode.id,
|
|
485
|
-
target: nodeId,
|
|
486
|
-
kind: 'contains',
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
490
|
-
// Module/class node is created lazily (see extract). Hold the edge in
|
|
491
|
-
// pending so its source can be rewritten once the module exists.
|
|
492
|
-
const edge = {
|
|
493
|
-
source: '',
|
|
494
|
-
target: nodeId,
|
|
495
|
-
kind: 'contains',
|
|
496
|
-
};
|
|
497
|
-
this.edges.push(edge);
|
|
498
|
-
this.pendingModuleOrClassSource.push(edge);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return procs;
|
|
502
|
-
}
|
|
503
|
-
/** `[visibility] Event <Name>(...)` custom event declaration. */
|
|
504
|
-
static EVENT_DECL_RE = /^\s*((?:Public|Private|Friend)\s+)?Event\s+(\p{L}[\p{L}\p{N}_]*)\b/iu;
|
|
505
|
-
/** `[visibility] Type <Name>` user-defined type block start. */
|
|
506
|
-
static TYPE_START_RE = /^\s*((?:Public|Private|Friend)\s+)?Type\s+(\p{L}[\p{L}\p{N}_]*)\b/iu;
|
|
507
|
-
/** `End Type` user-defined type block end. */
|
|
508
|
-
static TYPE_END_RE = /^\s*End\s+Type\b/iu;
|
|
509
|
-
/** `<MemberName> As <Type>` inside a user-defined type block. */
|
|
510
|
-
static TYPE_MEMBER_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:\([^)]*\))?\s+As\s+(.+?)\s*$/iu;
|
|
511
|
-
/** `[visibility] Declare [PtrSafe] Sub|Function <Name> Lib "dll" [Alias "x"] ...` */
|
|
512
|
-
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;
|
|
513
|
-
/**
|
|
514
|
-
* Roadmap #26 declaration sweep:
|
|
515
|
-
* - Event declarations become `event` nodes and `RaiseEvent` can point to them.
|
|
516
|
-
* - Type...End Type blocks become `type` + `type_member` nodes.
|
|
517
|
-
* - Win32 API Declare statements become `declare` nodes, while still being
|
|
518
|
-
* cached by name so normal call-site scanning can emit `calls` edges.
|
|
519
|
-
*/
|
|
520
|
-
sweepEventsTypesAndDeclares(src) {
|
|
521
|
-
const lines = src.split('\n');
|
|
522
|
-
let count = 0;
|
|
523
|
-
let currentType = null;
|
|
524
|
-
for (let i = 0; i < lines.length; i++) {
|
|
525
|
-
const line = lines[i] ?? '';
|
|
526
|
-
const lineNum = i + 1;
|
|
527
|
-
if (currentType) {
|
|
528
|
-
if (VbaExtractor.TYPE_END_RE.test(line)) {
|
|
529
|
-
currentType = null;
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const member = VbaExtractor.TYPE_MEMBER_RE.exec(line);
|
|
533
|
-
if (member) {
|
|
534
|
-
const memberName = member[1] ?? '';
|
|
535
|
-
const memberType = (member[2] ?? '').trim();
|
|
536
|
-
if (!memberName)
|
|
537
|
-
continue;
|
|
538
|
-
const memberId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'type_member', memberName, lineNum);
|
|
539
|
-
this.nodes.push({
|
|
540
|
-
id: memberId,
|
|
541
|
-
kind: 'type_member',
|
|
542
|
-
name: memberName,
|
|
543
|
-
qualifiedName: `${currentType.name}.${memberName}`,
|
|
544
|
-
filePath: this.filePath,
|
|
545
|
-
language: 'vba',
|
|
546
|
-
startLine: lineNum,
|
|
547
|
-
endLine: lineNum,
|
|
548
|
-
startColumn: 0,
|
|
549
|
-
endColumn: line.length,
|
|
550
|
-
metadata: { memberType },
|
|
551
|
-
updatedAt: Date.now(),
|
|
552
|
-
});
|
|
553
|
-
this.edges.push({
|
|
554
|
-
source: currentType.id,
|
|
555
|
-
target: memberId,
|
|
556
|
-
kind: 'type-member',
|
|
557
|
-
provenance: 'parser',
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
const eventDecl = VbaExtractor.EVENT_DECL_RE.exec(line);
|
|
563
|
-
if (eventDecl) {
|
|
564
|
-
const visibility = VbaExtractor.foldVisibility(eventDecl[1] ?? '');
|
|
565
|
-
const name = eventDecl[2] ?? '';
|
|
566
|
-
if (!name)
|
|
567
|
-
continue;
|
|
568
|
-
const eventId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'event', name, lineNum);
|
|
569
|
-
const eventNode = {
|
|
570
|
-
id: eventId,
|
|
571
|
-
kind: 'event',
|
|
572
|
-
name,
|
|
573
|
-
signature: line.trim(),
|
|
574
|
-
qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
|
|
575
|
-
filePath: this.filePath,
|
|
576
|
-
language: 'vba',
|
|
577
|
-
startLine: lineNum,
|
|
578
|
-
endLine: lineNum,
|
|
579
|
-
startColumn: 0,
|
|
580
|
-
endColumn: line.length,
|
|
581
|
-
visibility,
|
|
582
|
-
updatedAt: Date.now(),
|
|
583
|
-
};
|
|
584
|
-
this.nodes.push(eventNode);
|
|
585
|
-
this.localEvents.set(name.toLowerCase(), eventNode);
|
|
586
|
-
this.pushContainsFromModule(eventId);
|
|
587
|
-
count++;
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
const typeStart = VbaExtractor.TYPE_START_RE.exec(line);
|
|
591
|
-
if (typeStart) {
|
|
592
|
-
const visibility = VbaExtractor.foldVisibility(typeStart[1] ?? '');
|
|
593
|
-
const name = typeStart[2] ?? '';
|
|
594
|
-
if (!name)
|
|
595
|
-
continue;
|
|
596
|
-
const typeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'type', name, lineNum);
|
|
597
|
-
this.nodes.push({
|
|
598
|
-
id: typeId,
|
|
599
|
-
kind: 'type',
|
|
600
|
-
name,
|
|
601
|
-
qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
|
|
602
|
-
filePath: this.filePath,
|
|
603
|
-
language: 'vba',
|
|
604
|
-
startLine: lineNum,
|
|
605
|
-
endLine: lineNum,
|
|
606
|
-
startColumn: 0,
|
|
607
|
-
endColumn: line.length,
|
|
608
|
-
visibility,
|
|
609
|
-
updatedAt: Date.now(),
|
|
610
|
-
});
|
|
611
|
-
this.pushContainsFromModule(typeId);
|
|
612
|
-
currentType = { id: typeId, name };
|
|
613
|
-
count++;
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
const declaration = VbaExtractor.DLL_DECLARE_RE.exec(line);
|
|
617
|
-
if (declaration) {
|
|
618
|
-
const visibility = VbaExtractor.foldVisibility(declaration[1] ?? '');
|
|
619
|
-
const ptrSafe = !!declaration[2];
|
|
620
|
-
const declareKind = (declaration[3] ?? '').toLowerCase();
|
|
621
|
-
const name = declaration[4] ?? '';
|
|
622
|
-
const dll = declaration[5] ?? '';
|
|
623
|
-
const aliasName = declaration[6] ?? undefined;
|
|
624
|
-
if (!name)
|
|
625
|
-
continue;
|
|
626
|
-
const declareId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'declare', name, lineNum);
|
|
627
|
-
const declareNode = {
|
|
628
|
-
id: declareId,
|
|
629
|
-
kind: 'declare',
|
|
630
|
-
name,
|
|
631
|
-
qualifiedName: this.classNamePrefix ? `${this.classNamePrefix}.${name}` : name,
|
|
632
|
-
filePath: this.filePath,
|
|
633
|
-
language: 'vba',
|
|
634
|
-
startLine: lineNum,
|
|
635
|
-
endLine: lineNum,
|
|
636
|
-
startColumn: 0,
|
|
637
|
-
endColumn: line.length,
|
|
638
|
-
visibility,
|
|
639
|
-
metadata: {
|
|
640
|
-
isDeclare: true,
|
|
641
|
-
dll,
|
|
642
|
-
declareKind,
|
|
643
|
-
ptrSafe,
|
|
644
|
-
...(aliasName ? { aliasName } : {}),
|
|
645
|
-
},
|
|
646
|
-
updatedAt: Date.now(),
|
|
647
|
-
};
|
|
648
|
-
this.nodes.push(declareNode);
|
|
649
|
-
if (!this.functionNodeByName.has(name)) {
|
|
650
|
-
this.functionNodeByName.set(name, declareNode);
|
|
651
|
-
}
|
|
652
|
-
this.pushContainsFromModule(declareId);
|
|
653
|
-
count++;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
return count;
|
|
657
|
-
}
|
|
658
|
-
/** Implements regex. */
|
|
659
|
-
static IMPLEMENTS_RE = /^\s*Implements\s+(\p{L}[\p{L}\p{N}_]*)/iu;
|
|
660
|
-
/** Edges whose source needs to be set to the module/class id once it exists. */
|
|
661
|
-
pendingModuleOrClassSource = [];
|
|
662
|
-
sweepImplements(src) {
|
|
663
|
-
const lines = src.split('\n');
|
|
664
|
-
let count = 0;
|
|
665
|
-
for (let i = 0; i < lines.length; i++) {
|
|
666
|
-
const line = lines[i] ?? '';
|
|
667
|
-
const m = VbaExtractor.IMPLEMENTS_RE.exec(line);
|
|
668
|
-
if (!m)
|
|
669
|
-
continue;
|
|
670
|
-
const name = m[1] ?? '';
|
|
671
|
-
if (!name)
|
|
672
|
-
continue;
|
|
673
|
-
const lineNum = i + 1;
|
|
674
|
-
const targetId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'interface', name, lineNum);
|
|
675
|
-
this.nodes.push({
|
|
676
|
-
id: targetId,
|
|
677
|
-
kind: 'interface',
|
|
678
|
-
name,
|
|
679
|
-
qualifiedName: name,
|
|
680
|
-
filePath: this.filePath,
|
|
681
|
-
language: 'vba',
|
|
682
|
-
startLine: lineNum,
|
|
683
|
-
endLine: lineNum,
|
|
684
|
-
startColumn: 0,
|
|
685
|
-
endColumn: line.length,
|
|
686
|
-
updatedAt: Date.now(),
|
|
687
|
-
});
|
|
688
|
-
const edge = {
|
|
689
|
-
source: '', // placeholder; rewritten after module node exists
|
|
690
|
-
target: targetId,
|
|
691
|
-
kind: 'implements',
|
|
692
|
-
// S4 fix: `Implements IFoo` is a static, source-declared fact —
|
|
693
|
-
// not a guess. Use the `parser` provenance (generalizes
|
|
694
|
-
// `tree-sitter` for non-tree-sitter extractors like our regex
|
|
695
|
-
// sweepers) instead of `heuristic`, which is reserved for
|
|
696
|
-
// guessed/inferred edges.
|
|
697
|
-
provenance: 'parser',
|
|
698
|
-
line: lineNum,
|
|
699
|
-
column: 0,
|
|
700
|
-
};
|
|
701
|
-
this.edges.push(edge);
|
|
702
|
-
this.pendingModuleOrClassSource.push(edge);
|
|
703
|
-
count++;
|
|
704
|
-
}
|
|
705
|
-
return count;
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Check that a line is a variable declaration and NOT a Sub/Function/
|
|
709
|
-
* Property/Const/WithEvents header (those have their own sweeps).
|
|
710
|
-
* Fix 1+3 (Issues #1,#3): replaced the old single-match DIM_QUAL_RE /
|
|
711
|
-
* DIM_UNQUAL_RE pair with a prefix-check + global scan that handles
|
|
712
|
-
* `As New <Type>`, multi-variable `Dim a As Foo, b As Bar`, and all
|
|
713
|
-
* visibility keywords in one pass.
|
|
714
|
-
* Issue #47: now also accepts `Global` (module-level typed instance) and
|
|
715
|
-
* `Static` (procedure-local retention modifier) so they emit the same
|
|
716
|
-
* `references` edge and `localVarTypeMap` registration as their `Dim`
|
|
717
|
-
* siblings today. The negative lookahead is unchanged: `Const` is still
|
|
718
|
-
* routed to `sweepEnumsAndConsts`.
|
|
719
|
-
*/
|
|
720
|
-
static DIM_DECL_PREFIX_RE = /^\s*(?:Dim|Private|Public|Global|Static)\s+(?!(?:Function|Sub|Property|Const|WithEvents)\b)/i;
|
|
721
|
-
/**
|
|
722
|
-
* Globally scan all `identifier As [New] TypePart1[.TypePart2]` on a
|
|
723
|
-
* variable declaration line. Run with /g after confirming DIM_DECL_PREFIX_RE.
|
|
724
|
-
*
|
|
725
|
-
* Groups: (1) variable name, (2) bracketed outer type, (3) unbracketed
|
|
726
|
-
* outer type, (4) bracketed inner type (if qualified), (5) unbracketed
|
|
727
|
-
* inner type. The variable name is always bare (`Dim` cannot declare a
|
|
728
|
-
* bracketed variable). The TYPE position accepts BOTH bracketed names
|
|
729
|
-
* with spaces (e.g. `[Clase Con Espacios]`) and bare identifiers — the
|
|
730
|
-
* bracketed capture wins when present. Only one of (2)/(3) and one of
|
|
731
|
-
* (4)/(5) is ever populated per match.
|
|
732
|
-
* `(?:New\s+)?` consumes the VBA auto-instantiation keyword so it is
|
|
733
|
-
* never captured as the type name (Fix 1).
|
|
734
|
-
*
|
|
735
|
-
* Issue #54: extends the type alternative to accept `[Name With Spaces]`
|
|
736
|
-
* so `Dim x As [Clase Con Espacios]` emits a `references` edge to
|
|
737
|
-
* `Clase Con Espacios` (brackets unwrapped). The unwrap is applied in
|
|
738
|
-
* the sweep loop by picking the bracketed capture group when present.
|
|
739
|
-
*/
|
|
740
|
-
static DIM_ALL_VARS_RE = /\b(\p{L}[\p{L}\p{N}_]*)\s+As\s+(?:New\s+)?(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*))(?:\.(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*)))?/giu;
|
|
741
|
-
/**
|
|
742
|
-
* Bare-declared variable capture for the `Dim|Private|Public|Global|Static`
|
|
743
|
-
* prefix. Captures (1) the variable name. Used to register bare `Dim x`
|
|
744
|
-
* (no `As` clause) and explicit-primitive `Dim x As Long|String|...`
|
|
745
|
-
* declarations into `localVarTypeMap` so the type tracking is consistent
|
|
746
|
-
* across all three Dim shapes:
|
|
747
|
-
*
|
|
748
|
-
* `Dim x` → outer = 'variant' (VBA default)
|
|
749
|
-
* `Dim x As Variant` → outer = 'variant' (PRIMITIVE_TYPES member)
|
|
750
|
-
* `Dim x As Long` → outer = 'long' (PRIMITIVE_TYPES member)
|
|
751
|
-
* `Dim x As Foo` → outer = 'foo' (project class — non-primitive)
|
|
752
|
-
*
|
|
753
|
-
* Antigravity audit Task 3: the previous `DIM_ALL_VARS_RE` only matched
|
|
754
|
-
* the `... As <Type>` form, so a bare `Dim x` was invisible to
|
|
755
|
-
* `isLocalProjectClassVar` / `scanCallSites` and `x.Method(1)` produced
|
|
756
|
-
* a dead-end `calls` edge to a stub named `x.Method` that no resolver
|
|
757
|
-
* could repoint. Registering bare Dim with `outer = 'variant'` closes
|
|
758
|
-
* the gate, so `scanCallSites` skips ONLY when the receiver is mapped
|
|
759
|
-
* as a primitive — leaving the "undeclared receiver → stub → resolver
|
|
760
|
-
* repoints" path intact for cross-module qualified calls like
|
|
761
|
-
* `modUtils.Foo(1)` (`modUtils` is not in `localVarTypeMap`).
|
|
762
|
-
*/
|
|
763
|
-
static BARE_DIM_VAR_RE = /^\s*(?:Dim|Private|Public|Global|Static)\s+(\p{L}[\p{L}\p{N}_]*)\s*(?:,|$|\b)/iu;
|
|
764
|
-
/**
|
|
765
|
-
* VBA primitive type names — skipped when emitted as Dim targets so
|
|
766
|
-
* we don't pollute the graph with `As Long` / `As String` references.
|
|
767
|
-
* Fix 4: all entries are LOWERCASE and the lookup lowercases the
|
|
768
|
-
* captured type name so `As long`, `As LONG`, `As Long` all match.
|
|
769
|
-
* Fix 1 (Issue #1): added `'new'` as a backstop so that if the `As New`
|
|
770
|
-
* pattern is ever captured as a type name it is silently skipped.
|
|
771
|
-
*/
|
|
772
|
-
static PRIMITIVE_TYPES = new Set([
|
|
773
|
-
'long', 'integer', 'short', 'byte', 'single', 'double', 'currency',
|
|
774
|
-
'string', 'boolean', 'date', 'variant', 'object', 'error',
|
|
775
|
-
'empty', 'null', 'longptr', 'longlong', 'new',
|
|
776
|
-
]);
|
|
777
|
-
/** `WithEvents m_X As Form_Foo` — Dim/Private/Public/Global/Static prefix is optional. */
|
|
778
|
-
static WITHEVENTS_RE = /^\s*(?:(?:Dim|Private|Public|Global|Static)\s+)?WithEvents\s+\p{L}[\p{L}\p{N}_]*\s+As\s+(\p{L}[\p{L}\p{N}_]*)/iu;
|
|
779
|
-
sweepDimsAndWithEvents(src) {
|
|
780
|
-
const lines = src.split('\n');
|
|
781
|
-
let count = 0;
|
|
782
|
-
for (let i = 0; i < lines.length; i++) {
|
|
783
|
-
const line = lines[i] ?? '';
|
|
784
|
-
const lineNum = i + 1;
|
|
785
|
-
// Fix 1 + Fix 3 (Issues #1, #3): replace the old single-match
|
|
786
|
-
// DIM_QUAL_RE / DIM_UNQUAL_RE pair with a global scan that handles
|
|
787
|
-
// `As New <Type>`, multi-variable `Dim a As Foo, b As Bar`, and
|
|
788
|
-
// qualified `Dim x As Foo.Bar` in a single pass.
|
|
789
|
-
if (VbaExtractor.DIM_DECL_PREFIX_RE.test(line)) {
|
|
790
|
-
VbaExtractor.DIM_ALL_VARS_RE.lastIndex = 0;
|
|
791
|
-
let m;
|
|
792
|
-
while ((m = VbaExtractor.DIM_ALL_VARS_RE.exec(line)) !== null) {
|
|
793
|
-
const varName = m[1] ?? '';
|
|
794
|
-
// Issue #54: DIM_ALL_VARS_RE groups (2) and (3) are alternative
|
|
795
|
-
// captures for the same outer-type position — the bracketed
|
|
796
|
-
// alternative wins when present, the bare one otherwise. The
|
|
797
|
-
// captured value is already unwrapped (the `[...]` is consumed
|
|
798
|
-
// by the regex, group (2) holds the inner content). Same shape
|
|
799
|
-
// for the inner type at groups (4)/(5).
|
|
800
|
-
const outerType = m[2] ?? m[3] ?? '';
|
|
801
|
-
const innerType = m[4] ?? m[5] ?? '';
|
|
802
|
-
// Fix 2 (Issue #2): populate the local var type map so that
|
|
803
|
-
// `sweepCallsAndSql` can gate qualified statement-form calls.
|
|
804
|
-
if (varName && outerType) {
|
|
805
|
-
this.localVarTypeMap.set(varName.toLowerCase(), {
|
|
806
|
-
outer: outerType,
|
|
807
|
-
qualified: !!innerType,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
if (innerType) {
|
|
811
|
-
// Qualified form (`Dim x As Foo.Bar`) — emit reference to the
|
|
812
|
-
// outer type `Foo` (same behaviour as the old DIM_QUAL_RE path).
|
|
813
|
-
if (outerType) {
|
|
814
|
-
this.emitReference(outerType, lineNum, 0, 'vba-name-resolution');
|
|
815
|
-
count++;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
// Unqualified form (`Dim x As SomeType`, `Dim x As New SomeType`)
|
|
820
|
-
// — emit reference only when the type is not a primitive or keyword.
|
|
821
|
-
if (outerType && !VbaExtractor.PRIMITIVE_TYPES.has(outerType.toLowerCase())) {
|
|
822
|
-
this.emitReference(outerType, lineNum, 0, 'vba-name-resolution');
|
|
823
|
-
count++;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
// Antigravity audit Task 3: bare `Dim x` (no `As` clause) and
|
|
828
|
-
// explicit-primitive `Dim x As <primitive>` declarations must
|
|
829
|
-
// still register `x` in `localVarTypeMap` so the qualified-call
|
|
830
|
-
// site scan in `scanCallSites` can gate the dead-end stub that
|
|
831
|
-
// no resolver could ever repoint.
|
|
832
|
-
//
|
|
833
|
-
// Captures the FIRST variable name only. Multi-variable bare
|
|
834
|
-
// Dim (e.g. `Dim a, b, c` without an `As` clause) is rare in
|
|
835
|
-
// real Dysflow fixtures and is intentionally NOT tracked here —
|
|
836
|
-
// those bare variables fall back to the undeclared-receiver
|
|
837
|
-
// path, which is the conservative choice. Skip if the typed-form
|
|
838
|
-
// loop already populated the entry.
|
|
839
|
-
const bm = VbaExtractor.BARE_DIM_VAR_RE.exec(line);
|
|
840
|
-
if (bm) {
|
|
841
|
-
const varName = bm[1] ?? '';
|
|
842
|
-
if (varName) {
|
|
843
|
-
const key = varName.toLowerCase();
|
|
844
|
-
if (!this.localVarTypeMap.has(key)) {
|
|
845
|
-
// Look for an `As <Type>` continuation on the same line so the
|
|
846
|
-
// outer type matches the existing typed-form behaviour. If
|
|
847
|
-
// absent, the variable is implicit `Variant` per VBA semantics.
|
|
848
|
-
const asRe = /\bAs\s+(\p{L}[\p{L}\p{N}_]*)/iu;
|
|
849
|
-
const asMatch = asRe.exec(line);
|
|
850
|
-
const outer = asMatch ? (asMatch[1] ?? '').toLowerCase() : 'variant';
|
|
851
|
-
this.localVarTypeMap.set(key, {
|
|
852
|
-
outer,
|
|
853
|
-
qualified: false,
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
// WithEvents declarations — handled by their own regex; also populate
|
|
860
|
-
// the local var type map for completeness.
|
|
861
|
-
const weMatch = VbaExtractor.WITHEVENTS_RE.exec(line);
|
|
862
|
-
if (weMatch) {
|
|
863
|
-
const formType = weMatch[1] ?? '';
|
|
864
|
-
if (formType) {
|
|
865
|
-
// Extract the variable name from the WithEvents line for the map.
|
|
866
|
-
const weVarM = /^\s*(?:(?:Dim|Private|Public|Global|Static)\s+)?WithEvents\s+(\p{L}[\p{L}\p{N}_]*)/iu.exec(line);
|
|
867
|
-
const weVarName = weVarM?.[1] ?? '';
|
|
868
|
-
if (weVarName) {
|
|
869
|
-
this.localVarTypeMap.set(weVarName.toLowerCase(), {
|
|
870
|
-
outer: formType,
|
|
871
|
-
qualified: false,
|
|
872
|
-
withEvents: true,
|
|
873
|
-
variableName: weVarName,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
this.emitReference(formType, lineNum, 0, 'vba-withevents');
|
|
877
|
-
const targetId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'class', formType, 0);
|
|
878
|
-
const subscriberEdge = {
|
|
879
|
-
source: this.moduleOrClassNode?.id ?? '',
|
|
880
|
-
target: targetId,
|
|
881
|
-
kind: 'subscribes-event',
|
|
882
|
-
provenance: 'heuristic',
|
|
883
|
-
metadata: {
|
|
884
|
-
synthesizedBy: 'vba-withevents',
|
|
885
|
-
variableName: weVarName || undefined,
|
|
886
|
-
},
|
|
887
|
-
line: lineNum,
|
|
888
|
-
column: 0,
|
|
889
|
-
};
|
|
890
|
-
this.edges.push(subscriberEdge);
|
|
891
|
-
if (!this.moduleOrClassNode) {
|
|
892
|
-
this.pendingModuleOrClassSource.push(subscriberEdge);
|
|
893
|
-
}
|
|
894
|
-
count++;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return count;
|
|
899
|
-
}
|
|
900
|
-
/** `[visibility] Enum <Name>` — opens an enum block. */
|
|
901
|
-
static ENUM_START_RE = /^\s*(?:(Public|Private|Friend|Global)\s+)?Enum\s+(\p{L}[\p{L}\p{N}_]*)/iu;
|
|
902
|
-
/** `End Enum` — closes the current enum block. */
|
|
903
|
-
static ENUM_END_RE = /^\s*End\s+Enum\b/i;
|
|
904
|
-
/**
|
|
905
|
-
* An enum member line: a leading identifier optionally followed by `=
|
|
906
|
-
* <value>`. Runs only inside an open Enum block, on the (already
|
|
907
|
-
* comment-stripped) source, so a trailing `'comment` never reaches here.
|
|
908
|
-
* `\p{L}` covers accented member names (e.g. `Sí`).
|
|
909
|
-
*/
|
|
910
|
-
static ENUM_MEMBER_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:=|$)/u;
|
|
911
|
-
/** `[visibility] Const <decls>` — captures visibility (1) and the rest (2). */
|
|
912
|
-
static CONST_DECL_RE = /^\s*(?:(Public|Private|Friend|Global)\s+)?Const\s+(.+)$/i;
|
|
913
|
-
/**
|
|
914
|
-
* Issue #52: shared `End Sub` / `End Function` / `End Property` marker.
|
|
915
|
-
* Promoted from a local regex in `sweepCallsAndSql` so `sweepEnumsAndConsts`
|
|
916
|
-
* can walk the same proc boundaries and decide Const scope per line.
|
|
917
|
-
* The `(?:^|:\s*)` prefix tolerates colon-separated single-line procs
|
|
918
|
-
* (`Public Sub X(): ... : End Sub`) so the proc stack pops on the same
|
|
919
|
-
* physical line.
|
|
920
|
-
*/
|
|
921
|
-
static PROCEDURE_END_RE = /(?:^|:\s*)End\s+(?:Sub|Function|Property)\b/i;
|
|
922
|
-
/**
|
|
923
|
-
* Fold a VBA visibility keyword to the canonical lowercase enum, matching
|
|
924
|
-
* the procedure convention: `Private` → 'private'; `Public`, `Global`,
|
|
925
|
-
* `Friend`, or none → 'public' (VBA's default module-level `Const`/`Enum`
|
|
926
|
-
* is Private, but we follow the same broader-than-private fold the proc
|
|
927
|
-
* sweep uses so visibility is consistent across symbol kinds).
|
|
928
|
-
*/
|
|
929
|
-
static foldVisibility(raw) {
|
|
930
|
-
return raw.trim().toLowerCase() === 'private' ? 'private' : 'public';
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Walk the (uncommented, line-joined) source and emit:
|
|
934
|
-
* - one `enum` node per `Enum <Name>` block, with one `enum_member` node
|
|
935
|
-
* per member and a `contains` edge enum→member;
|
|
936
|
-
* - one `constant` node per name declared on a `Const` line (multi-name
|
|
937
|
-
* lines emit one node per name);
|
|
938
|
-
* - a `contains` edge from the module/class node to each enum and constant
|
|
939
|
-
* (held in `pendingModuleOrClassSource` until the module node exists).
|
|
940
|
-
*
|
|
941
|
-
* Returns the number of top-level symbols (enums + constants) emitted so
|
|
942
|
-
* the caller can flip `hasAnySymbols`.
|
|
943
|
-
*/
|
|
944
|
-
sweepEnumsAndConsts(src) {
|
|
945
|
-
const lines = src.split('\n');
|
|
946
|
-
let count = 0;
|
|
947
|
-
let currentEnum = null;
|
|
948
|
-
// Issue #52: reset the shared scope stack + lookup key so leftover
|
|
949
|
-
// state from a previous extract() (impossible in production but
|
|
950
|
-
// possible in unit tests that construct a fresh extractor and run
|
|
951
|
-
// twice) never leaks across sweeps. The walk below updates both
|
|
952
|
-
// every iteration; `sweepCallsAndSql` resets again at its own
|
|
953
|
-
// start, before any OpenForm/OpenQuery reader consults them.
|
|
954
|
-
this.procStack.length = 0;
|
|
955
|
-
this.currentProcKey = 'module';
|
|
956
|
-
for (let i = 0; i < lines.length; i++) {
|
|
957
|
-
const line = lines[i] ?? '';
|
|
958
|
-
const lineNum = i + 1;
|
|
959
|
-
// Issue #52: track proc scope so Const declarations on this line
|
|
960
|
-
// can decide whether they belong to the module (currentProcKey
|
|
961
|
-
// === 'module') or to the top-most procedure (write the per-proc
|
|
962
|
-
// bucket, skip the module-level `constant` node emission).
|
|
963
|
-
//
|
|
964
|
-
// PROC_RE cannot overlap with CONST_DECL_RE on the same physical
|
|
965
|
-
// line (different leading keywords), so it is safe to advance the
|
|
966
|
-
// stack here and then fall through to the rest of the body.
|
|
967
|
-
const procStart = VbaExtractor.PROC_RE.exec(line);
|
|
968
|
-
if (procStart) {
|
|
969
|
-
this.procStack.push(lineNum);
|
|
970
|
-
this.currentProcKey = String(lineNum);
|
|
971
|
-
}
|
|
972
|
-
else if (VbaExtractor.PROCEDURE_END_RE.test(line) && this.procStack.length > 0) {
|
|
973
|
-
this.procStack.pop();
|
|
974
|
-
this.currentProcKey =
|
|
975
|
-
this.procStack.length > 0
|
|
976
|
-
? String(this.procStack[this.procStack.length - 1])
|
|
977
|
-
: 'module';
|
|
978
|
-
}
|
|
979
|
-
if (currentEnum) {
|
|
980
|
-
if (VbaExtractor.ENUM_END_RE.test(line)) {
|
|
981
|
-
currentEnum = null;
|
|
982
|
-
continue;
|
|
983
|
-
}
|
|
984
|
-
const mm = VbaExtractor.ENUM_MEMBER_RE.exec(line);
|
|
985
|
-
if (mm) {
|
|
986
|
-
const memberName = mm[1] ?? '';
|
|
987
|
-
if (memberName) {
|
|
988
|
-
const memberId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'enum_member', memberName, lineNum);
|
|
989
|
-
this.nodes.push({
|
|
990
|
-
id: memberId,
|
|
991
|
-
kind: 'enum_member',
|
|
992
|
-
name: memberName,
|
|
993
|
-
qualifiedName: `${currentEnum.name}.${memberName}`,
|
|
994
|
-
filePath: this.filePath,
|
|
995
|
-
language: 'vba',
|
|
996
|
-
startLine: lineNum,
|
|
997
|
-
endLine: lineNum,
|
|
998
|
-
startColumn: 0,
|
|
999
|
-
endColumn: line.length,
|
|
1000
|
-
updatedAt: Date.now(),
|
|
1001
|
-
});
|
|
1002
|
-
// enum → member: source is known, emit directly (not pending).
|
|
1003
|
-
this.edges.push({
|
|
1004
|
-
source: currentEnum.id,
|
|
1005
|
-
target: memberId,
|
|
1006
|
-
kind: 'contains',
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
const enumStart = VbaExtractor.ENUM_START_RE.exec(line);
|
|
1013
|
-
if (enumStart) {
|
|
1014
|
-
const visibility = VbaExtractor.foldVisibility(enumStart[1] ?? '');
|
|
1015
|
-
const name = enumStart[2] ?? '';
|
|
1016
|
-
if (!name)
|
|
1017
|
-
continue;
|
|
1018
|
-
const enumId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'enum', name, lineNum);
|
|
1019
|
-
this.nodes.push({
|
|
1020
|
-
id: enumId,
|
|
1021
|
-
kind: 'enum',
|
|
1022
|
-
name,
|
|
1023
|
-
qualifiedName: name,
|
|
1024
|
-
filePath: this.filePath,
|
|
1025
|
-
language: 'vba',
|
|
1026
|
-
startLine: lineNum,
|
|
1027
|
-
endLine: lineNum,
|
|
1028
|
-
startColumn: 0,
|
|
1029
|
-
endColumn: line.length,
|
|
1030
|
-
visibility,
|
|
1031
|
-
updatedAt: Date.now(),
|
|
1032
|
-
});
|
|
1033
|
-
this.pushContainsFromModule(enumId);
|
|
1034
|
-
currentEnum = { id: enumId, name };
|
|
1035
|
-
count++;
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
const constDecl = VbaExtractor.CONST_DECL_RE.exec(line);
|
|
1039
|
-
if (constDecl) {
|
|
1040
|
-
const visibility = VbaExtractor.foldVisibility(constDecl[1] ?? '');
|
|
1041
|
-
const body = constDecl[2] ?? '';
|
|
1042
|
-
const declarations = parseConstDeclarations(body);
|
|
1043
|
-
for (const declaration of declarations) {
|
|
1044
|
-
const constName = declaration.name;
|
|
1045
|
-
if (!constName)
|
|
1046
|
-
continue;
|
|
1047
|
-
// Issue #52: every Const line (module-level or proc-local)
|
|
1048
|
-
// writes into a per-scope resolution bucket so
|
|
1049
|
-
// `DoCmd.OpenForm FORM_X` later resolves correctly; module-level
|
|
1050
|
-
// Consts additionally emit a `constant` graph node + the
|
|
1051
|
-
// module→constant `contains` edge. Proc-local Consts skip both
|
|
1052
|
-
// (the const is not a module symbol, so the wrong-containment
|
|
1053
|
-
// node + edge the pre-fix code emitted are gone), but the
|
|
1054
|
-
// per-proc bucket keeps OpenForm/OpenQuery argument
|
|
1055
|
-
// resolution working exactly as before.
|
|
1056
|
-
if (declaration.value !== null) {
|
|
1057
|
-
this.setLocalConstInScope(this.currentProcKey, constName, declaration.value);
|
|
1058
|
-
}
|
|
1059
|
-
if (this.procStack.length > 0)
|
|
1060
|
-
continue;
|
|
1061
|
-
const constId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'constant', constName, lineNum);
|
|
1062
|
-
this.nodes.push({
|
|
1063
|
-
id: constId,
|
|
1064
|
-
kind: 'constant',
|
|
1065
|
-
name: constName,
|
|
1066
|
-
qualifiedName: constName,
|
|
1067
|
-
filePath: this.filePath,
|
|
1068
|
-
language: 'vba',
|
|
1069
|
-
startLine: lineNum,
|
|
1070
|
-
endLine: lineNum,
|
|
1071
|
-
startColumn: 0,
|
|
1072
|
-
endColumn: line.length,
|
|
1073
|
-
visibility,
|
|
1074
|
-
metadata: declaration.value !== null ? { value: declaration.value } : undefined,
|
|
1075
|
-
updatedAt: Date.now(),
|
|
1076
|
-
});
|
|
1077
|
-
this.pushContainsFromModule(constId);
|
|
1078
|
-
count++;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return count;
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Emit a `contains` edge from the (lazily-created) module/class node to
|
|
1086
|
-
* `targetId`. Mirrors the pending-source pattern the procedure and
|
|
1087
|
-
* implements sweeps use: the module node doesn't exist yet, so the edge's
|
|
1088
|
-
* source is rewritten once `extract()` creates it.
|
|
1089
|
-
*/
|
|
1090
|
-
pushContainsFromModule(targetId) {
|
|
1091
|
-
if (this.moduleOrClassNode) {
|
|
1092
|
-
this.edges.push({
|
|
1093
|
-
source: this.moduleOrClassNode.id,
|
|
1094
|
-
target: targetId,
|
|
1095
|
-
kind: 'contains',
|
|
1096
|
-
});
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
const edge = { source: '', target: targetId, kind: 'contains' };
|
|
1100
|
-
this.edges.push(edge);
|
|
1101
|
-
this.pendingModuleOrClassSource.push(edge);
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Walk the (uncommented) source per line. For each line:
|
|
1105
|
-
* - If it starts a `Sub`/`Function`/`Property`, push the proc onto a stack.
|
|
1106
|
-
* - If it ends one (`End Sub` / `End Function` / `End Property`), pop.
|
|
1107
|
-
* - While inside a procedure, scan the line for call-site patterns and
|
|
1108
|
-
* SQL-wrapper patterns.
|
|
1109
|
-
*
|
|
1110
|
-
* Call-site regex captures either `Name(...)` (same-file candidate) or
|
|
1111
|
-
* `Receiver.Member(...)` (qualified — emit a synthetic node + heuristic
|
|
1112
|
-
* edge). The receiver AND member alternatives accept BOTH the bare form
|
|
1113
|
-
* (`Foo`) and the VBA bracketed form (`[Foo Bar]`) — bracketed captures
|
|
1114
|
-
* win when present. Only one of (1)/(2) and one of (3)/(4) is ever
|
|
1115
|
-
* populated per match. Brackets are stripped by the regex itself (the
|
|
1116
|
-
* capture groups hold the inner content), so callers receive unwrapped
|
|
1117
|
-
* identifiers and the `${name}.${proc}` stub shape stays canonical.
|
|
1118
|
-
*
|
|
1119
|
-
* Issue #54: the bracketed alternative was previously absent, so
|
|
1120
|
-
* `[FUNCIONES UTILES].FormatearFecha(fecha)` (a real Dysflow-exported
|
|
1121
|
-
* idiom for modules with spaces in their names) was silently dropped.
|
|
1122
|
-
*/
|
|
1123
|
-
static CALL_RE = /(?<![\w.])(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*))(?:\.(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*)))?\s*\(/gu;
|
|
1124
|
-
/** SQL wrapper helpers — order matters because `db.Execute` is a suffix of others. */
|
|
1125
|
-
static SQL_WRAPPERS = [
|
|
1126
|
-
{ name: 'DoCmd.RunSQL', re: /\bDoCmd\.RunSQL\s+"((?:[^"]|"")*)"/g },
|
|
1127
|
-
{ name: '*db.OpenRecordset', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.OpenRecordset\s+"((?:[^"]|"")*)"/giu },
|
|
1128
|
-
{ name: '*db.Execute', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.Execute\s+"((?:[^"]|"")*)"/giu },
|
|
1129
|
-
// Fix 4 (Issue #4): inline-literal forms `getdb().Execute "..."` and
|
|
1130
|
-
// `getdb().OpenRecordset "..."` — the variable form is covered by
|
|
1131
|
-
// SQL_VAR_EXEC_RE but the direct-literal form was missing.
|
|
1132
|
-
{ name: 'getdb().Execute', re: /\bgetdb\(\)\.Execute\s+"((?:[^"]|"")*)"/g },
|
|
1133
|
-
{ name: 'getdb().OpenRecordset', re: /\bgetdb\(\)\.OpenRecordset\s+"((?:[^"]|"")*)"/g },
|
|
1134
|
-
];
|
|
1135
|
-
/**
|
|
1136
|
-
* `DoCmd.OpenForm "<FormName>"` modelling regex — B4 (hueco 6).
|
|
1137
|
-
*
|
|
1138
|
-
* Real VBA idiom (matches literal and bare-identifier forms):
|
|
1139
|
-
* `DoCmd.OpenForm "MyForm"`
|
|
1140
|
-
* `DoCmd.OpenForm FORM_MY_FORM`
|
|
1141
|
-
* `DoCmd.OpenForm "MyForm", acNormal, , , acFormEdit`
|
|
1142
|
-
*
|
|
1143
|
-
* Captures the first argument (group 1). String literals are unwrapped;
|
|
1144
|
-
* bare identifiers resolve against local Const declarations, falling back
|
|
1145
|
-
* to the identifier name when unknown. The trailing positional args
|
|
1146
|
-
* (`acNormal`, `acFormEdit`, etc.) are intentionally NOT captured.
|
|
1147
|
-
*
|
|
1148
|
-
* Why a separate dispatch: `DoCmd` is in `RUNTIME_RECEIVER_BLACKLIST`
|
|
1149
|
-
* (R4 invariant), so `DoCmd.OpenForm` is intentionally SKIPPED by the
|
|
1150
|
-
* generic `CALL_RE` path that would otherwise emit a junk `calls` edge
|
|
1151
|
-
* to a synthetic `function` node for `DoCmd.OpenForm`. This regex
|
|
1152
|
-
* matches BEFORE the call-site scan and uses its own dispatch to emit
|
|
1153
|
-
* the `opens-form` edge instead — sharing no logic with CALL_RE.
|
|
1154
|
-
*/
|
|
1155
|
-
static OPEN_FORM_ARG_RE = /\bDoCmd\.OpenForm\s+("(?:(?:[^"]|"")*)"|\p{L}[\p{L}\p{N}_]*)/gu;
|
|
1156
|
-
/**
|
|
1157
|
-
* Issue #48: `DoCmd.OpenReport "<ReportName>"` modelling regex — sibling
|
|
1158
|
-
* of `OPEN_FORM_ARG_RE` (hueco 6 expanded). Same literal-or-bare-id argument
|
|
1159
|
-
* capture (group 1) and same trailing positional-args drop. The dispatch
|
|
1160
|
-
* table `DOCMD_OPEN_DISPATCH` (below) carries the per-method metadata so
|
|
1161
|
-
* OpenForm and OpenReport share the same scan/emit pipeline while their
|
|
1162
|
-
* edge kinds (`opens-form` vs `opens-report`), stub node kinds
|
|
1163
|
-
* (`form-layout` vs `report-layout`), synthetic file-path prefixes, and
|
|
1164
|
-
* qualifiedName prefixes (`Form_<Name>` vs `Report_<Name>`) stay
|
|
1165
|
-
* distinct.
|
|
1166
|
-
*/
|
|
1167
|
-
static OPEN_REPORT_ARG_RE = /\bDoCmd\.OpenReport\s+("(?:(?:[^"]|"")*)"|\p{L}[\p{L}\p{N}_]*)/gu;
|
|
1168
|
-
/**
|
|
1169
|
-
* Issue #48 dispatch table — shared literal-or-Const argument resolution
|
|
1170
|
-
* for `DoCmd.OpenForm` and `DoCmd.OpenReport`. Each entry is everything
|
|
1171
|
-
* `scanDoCmdOpenCalls` + `emitOpensStubEdge` need to share the pipeline
|
|
1172
|
-
* between methods while keeping the per-method names distinct.
|
|
1173
|
-
*
|
|
1174
|
-
* OpenQuery is intentionally NOT in this dispatch — it emits an
|
|
1175
|
-
* `UnresolvedReference` (not a stub + edge), resolution to the REAL
|
|
1176
|
-
* `query` node emitted by `SqlQueryExtractor`. See
|
|
1177
|
-
* `OPEN_QUERY_ARG_RE` + `scanDoCmdOpenQuery`.
|
|
1178
|
-
*
|
|
1179
|
-
* Why a separate dispatch: `DoCmd` is in `RUNTIME_RECEIVER_BLACKLIST`
|
|
1180
|
-
* (R4 invariant), so ALL of these methods are intentionally SKIPPED by
|
|
1181
|
-
* the generic `CALL_RE` path that would otherwise emit a junk `calls`
|
|
1182
|
-
* edge to a synthetic `function` node for `DoCmd.OpenX`. This dispatch
|
|
1183
|
-
* matches BEFORE the call-site scan and uses its own emission path.
|
|
1184
|
-
*/
|
|
1185
|
-
static DOCMD_OPEN_DISPATCH = [
|
|
1186
|
-
{
|
|
1187
|
-
method: 'OpenForm',
|
|
1188
|
-
re: VbaExtractor.OPEN_FORM_ARG_RE,
|
|
1189
|
-
edgeKind: 'opens-form',
|
|
1190
|
-
stubKind: 'form-layout',
|
|
1191
|
-
syntheticPrefix: 'synthetic:opensFormStub',
|
|
1192
|
-
syntheticExtension: '.form.txt',
|
|
1193
|
-
moduleNamePrefix: 'Form_',
|
|
1194
|
-
cacheKey: 'OpenForm',
|
|
1195
|
-
metadataTargetKey: 'targetFormName',
|
|
1196
|
-
synthesizedBy: 'vba-opens-form',
|
|
1197
|
-
},
|
|
1198
|
-
{
|
|
1199
|
-
method: 'OpenReport',
|
|
1200
|
-
re: VbaExtractor.OPEN_REPORT_ARG_RE,
|
|
1201
|
-
edgeKind: 'opens-report',
|
|
1202
|
-
stubKind: 'report-layout',
|
|
1203
|
-
syntheticPrefix: 'synthetic:opensReportStub',
|
|
1204
|
-
syntheticExtension: '.report.txt',
|
|
1205
|
-
moduleNamePrefix: 'Report_',
|
|
1206
|
-
cacheKey: 'OpenReport',
|
|
1207
|
-
metadataTargetKey: 'targetReportName',
|
|
1208
|
-
synthesizedBy: 'vba-opens-report',
|
|
1209
|
-
},
|
|
1210
|
-
];
|
|
1211
|
-
/**
|
|
1212
|
-
* Issue #48: `DoCmd.OpenQuery "<QueryName>"` modelling regex. Emits an
|
|
1213
|
-
* `UnresolvedReference` (NOT a stub + edge) so the resolver binds to the
|
|
1214
|
-
* REAL `query` node `SqlQueryExtractor` emits for `queries/<Name>.sql`
|
|
1215
|
-
* — the same shape as `vba-me-control` and `vba-forms-bang`. The query
|
|
1216
|
-
* may not yet exist in the index when the .bas is parsed; the resolver
|
|
1217
|
-
* does the binding when the .sql is later indexed.
|
|
1218
|
-
*
|
|
1219
|
-
* Argument shape: identical to OpenForm/OpenReport — literal `"..."` or
|
|
1220
|
-
* bare identifier resolved against local `Const` declarations, falling
|
|
1221
|
-
* back to the bare identifier when unknown.
|
|
1222
|
-
*/
|
|
1223
|
-
static OPEN_QUERY_ARG_RE = /\bDoCmd\.OpenQuery\s+("(?:(?:[^"]|"")*)"|\p{L}[\p{L}\p{N}_]*)/gu;
|
|
1224
|
-
/** SQL assigned to a local variable, e.g. `m_SQL = "SELECT ..." & ...`. */
|
|
1225
|
-
static SQL_VAR_ASSIGN_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*=\s*(.*)$/iu;
|
|
1226
|
-
/** SQL wrapper called with a variable, e.g. `getdb().Execute m_SQL`. */
|
|
1227
|
-
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;
|
|
1228
|
-
/**
|
|
1229
|
-
* Issue #42: `DoCmd.RunSQL <identifier>` (variable form) — the dominant
|
|
1230
|
-
* Access idiom for executing a dynamically-built SQL string. Today only
|
|
1231
|
-
* the literal form `DoCmd.RunSQL "DELETE FROM X"` is tracked via the
|
|
1232
|
-
* `SQL_WRAPPERS` regex at line 1108; the variable form silently dropped
|
|
1233
|
-
* table impact for every procedure that builds SQL in a string and runs
|
|
1234
|
-
* it through `DoCmd.RunSQL`.
|
|
1235
|
-
*
|
|
1236
|
-
* This regex is the DoCmd.RunSQL analogue of `SQL_VAR_EXEC_RE` above and
|
|
1237
|
-
* is iterated by `scanSqlInLine` (lines 2051+). When a match is found,
|
|
1238
|
-
* the captured identifier is resolved against `sqlVariables` (populated
|
|
1239
|
-
* by `trackSqlVariableAssignment` with `&`-accumulate semantics — Issue
|
|
1240
|
-
* #13) and the resulting SQL string drives `emitSqlTableReferences`.
|
|
1241
|
-
*
|
|
1242
|
-
* The optional `(?:\(\))?` + `\s*\(?` shape lets the regex match both
|
|
1243
|
-
* the parenthesised form `DoCmd.RunSQL(strSQL)` and the no-paren form
|
|
1244
|
-
* `DoCmd.RunSQL strSQL` that the existing SQL_WRAPPERS literal regex
|
|
1245
|
-
* does not cover. The captured identifier is the only thing we need —
|
|
1246
|
-
* we DO NOT try to parse what the variable points at; that's the
|
|
1247
|
-
* existing `sqlVariables` map's job.
|
|
1248
|
-
*/
|
|
1249
|
-
static SQL_VAR_DOCMD_RUNSQL_RE = /\bDoCmd\.RunSQL\s*\(?\s*(\p{L}[\p{L}\p{N}_]*)\s*\)?/giu;
|
|
1250
|
-
/**
|
|
1251
|
-
* SQL table-name regex scoped to the clauses that introduce a table
|
|
1252
|
-
* reference: `FROM <t>`, `JOIN <t>`, `INTO <t>`, `UPDATE <t>`. Adding
|
|
1253
|
-
* `JOIN` lets the scanner pick up tables from joined fragments that
|
|
1254
|
-
* arrive via `&`-concatenated wrapper literals (e.g.
|
|
1255
|
-
* `db.Execute "FROM A" & " JOIN B"`); without it the second literal's
|
|
1256
|
-
* table was silently dropped even though the wrapper regex now matches
|
|
1257
|
-
* the chain.
|
|
1258
|
-
*
|
|
1259
|
-
* The captured table name is an optional bracketed/unbracketed schema
|
|
1260
|
-
* prefix followed by a `.`, then a bracketed-or-bare identifier — so
|
|
1261
|
-
* `FROM dbo.tblCustomers` and `FROM [My Schema].[My Table]` come
|
|
1262
|
-
* through as one composite reference. Without the prefix the regex
|
|
1263
|
-
* still matches a single identifier byte-identical to the old shape.
|
|
1264
|
-
* Brackets in the captured composite are stripped by
|
|
1265
|
-
* `emitSqlTableReferences` (`replace(/[\[\]]/g, '')`), so the public
|
|
1266
|
-
* node name is the unwrapped form `dbo.tblCustomers` /
|
|
1267
|
-
* `My Schema.My Table` — matching how plain `[Order Details]` is also
|
|
1268
|
-
* unwrapped to `Order Details`. The identifier class
|
|
1269
|
-
* `\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*` (same as the saved-queries
|
|
1270
|
-
* `TABLE_RE` in `sql-query-extractor.ts`) ensures bracketed names
|
|
1271
|
-
* with spaces — `[Order Details]`, `[My Schema]`, `[My Table]` —
|
|
1272
|
-
* are captured whole.
|
|
1273
|
-
*/
|
|
1274
|
-
static SQL_TABLE_RE = /\b(?:FROM|JOIN|INTO|UPDATE)\s+((?:(?:\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*)\.)?(?:\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*))/giu;
|
|
1275
|
-
/**
|
|
1276
|
-
* Issue #50: TempVars — Access's global key-value store for cross-form
|
|
1277
|
-
* state. We model each STATIC-LITERAL key as a synthetic `class` placeholder
|
|
1278
|
-
* (same NodeKind as SQL table refs, see `emitReference`) and emit one
|
|
1279
|
-
* `references` edge per reading/writing procedure with
|
|
1280
|
-
* `metadata.synthesizedBy: 'vba-tempvar'` and `metadata.access` ∈
|
|
1281
|
-
* `{'read','write'}` (the user-facing enum; lowercase single-tokens).
|
|
1282
|
-
*
|
|
1283
|
-
* Three scanner surfaces cover the four real idioms:
|
|
1284
|
-
* - `TEMP_VAR_BANG_RE` — `TempVars!clave` (no parens; write or read)
|
|
1285
|
-
* - `TEMP_VAR_PAREN_RE` — `TempVars("clave")` (parens; write or read)
|
|
1286
|
-
* - `TEMP_VAR_ADD_RE` — `TempVars.Add "clave", v` (always a write)
|
|
1287
|
-
*
|
|
1288
|
-
* Bang vs paren split by line-source: the bang form has no string literals
|
|
1289
|
-
* in scope, so we scan the MASKED line (`maskStringContent` replaces
|
|
1290
|
-
* `"…"` content with spaces — same line source the call-site / With
|
|
1291
|
-
* event scanners consume). The paren and Add forms have their key INSIDE
|
|
1292
|
-
* a `"…"` literal that gets blanked by the masker, so those regexes scan
|
|
1293
|
-
* the ORIGINAL (unmasked) line — same split the SQL_TABLE_RE/OpenForm
|
|
1294
|
-
* literal scanners already use.
|
|
1295
|
-
*
|
|
1296
|
-
* Dynamic-key forms — `TempVars(strNombre)`,
|
|
1297
|
-
* `TempVars("clave" & suffix)` — are silently unmatched by all three
|
|
1298
|
-
* regexes (none of them tolerate a function-call or `&` arg). REQ-CODE-4
|
|
1299
|
-
* "unresolvable is silent" applies: these stay silent by design to
|
|
1300
|
-
* prevent placeholder-node explosion.
|
|
1301
|
-
*/
|
|
1302
|
-
static TEMP_VAR_BANG_RE = /\bTempVars!\s*(\p{L}[\p{L}\p{N}_]*)/gu;
|
|
1303
|
-
/**
|
|
1304
|
-
* Issue #50 (cont.):
|
|
1305
|
-
* `TempVars("clave")` / `TempVars( "clave" )` capture. Scanned
|
|
1306
|
-
* over the original (unmasked) line — the literal lives INSIDE a
|
|
1307
|
-
* string and would be stripped by `maskStringContent`.
|
|
1308
|
-
*/
|
|
1309
|
-
static TEMP_VAR_PAREN_RE = /\bTempVars\s*\(\s*"([^"]+)"\s*\)/gu;
|
|
1310
|
-
/**
|
|
1311
|
-
* Issue #50 (cont.):
|
|
1312
|
-
* `TempVars.Add "clave", value` capture. Always a write (the
|
|
1313
|
-
* `.Add` method inserts/updates the entry). Scanned over the
|
|
1314
|
-
* original (unmasked) line for the same string-literal reason as
|
|
1315
|
-
* `TEMP_VAR_PAREN_RE`.
|
|
1316
|
-
*/
|
|
1317
|
-
static TEMP_VAR_ADD_RE = /\bTempVars\.Add\s+"([^"]+)"\s*,/gi;
|
|
1318
|
-
/**
|
|
1319
|
-
* `Me.<ControlName>` / `Me!<ControlName>` reference capture — hole 1
|
|
1320
|
-
* of VBA control-modeling. Extended by Issue #44 to accept the bang
|
|
1321
|
-
* form (the default-collection shortcut Access VBA inherits from VB).
|
|
1322
|
-
*
|
|
1323
|
-
* Real VBA idioms:
|
|
1324
|
-
* `Me.lblTitulo.Caption = "Hello"` ← dot form, property assignment
|
|
1325
|
-
* `Me.txtDescripcion.Value = "World"` ← dot form, property assignment
|
|
1326
|
-
* `Me.ComandoGrabar.Enabled = True` ← dot form, property assignment
|
|
1327
|
-
* `Me!txtNombre = "Hello"` ← BANG form (default collection)
|
|
1328
|
-
* `Me!txtEstado.Value = 1` ← BANG form, then property
|
|
1329
|
-
* `If Nz(Me.MotivoBorrado, "") = "" Then` ← read in expression
|
|
1330
|
-
*
|
|
1331
|
-
* The existing call-site scanner (CALL_RE) only fires on `Name(`
|
|
1332
|
-
* (paren form) and `Me` is in its keyword blacklist anyway, so
|
|
1333
|
-
* `Me.<Control>` references are silently invisible. This regex matches
|
|
1334
|
-
* the FIRST identifier after `Me.` or `Me!` regardless of what follows
|
|
1335
|
-
* (a property, an index, an assignment, a call argument, etc.) so the
|
|
1336
|
-
* form → control binding is surfaced as an UnresolvedReference for the
|
|
1337
|
-
* resolver to pick up later. Subsequent segments (`.Caption`, `.Value`,
|
|
1338
|
-
* `.Enabled`) are intentionally NOT captured — they are properties of
|
|
1339
|
-
* the control, not new symbols. The bang (`!`) is the default-collection
|
|
1340
|
-
* shortcut: `Me!txtFoo` is semantically identical to `Me.txtFoo` and
|
|
1341
|
-
* produces a byte-identical UnresolvedReference (same `referenceName`,
|
|
1342
|
-
* `referenceKind`, and `metadata.synthesizedBy`) — the regression test
|
|
1343
|
-
* in `__tests__/extraction-vba.test.ts` pins this parity.
|
|
1344
|
-
*
|
|
1345
|
-
* Provenance: `metadata.synthesizedBy = 'vba-me-control'`. Mirrors the
|
|
1346
|
-
* `vba-form-binding` (form→sibling-`.cls`) and `vba-name-resolution`
|
|
1347
|
-
* (qualified Dim) patterns already documented on `UnresolvedReference.metadata`
|
|
1348
|
-
* in `src/types.ts`.
|
|
1349
|
-
*/
|
|
1350
|
-
static ME_CONTROL_RE = /\bMe[.!](\p{L}[\p{L}\p{N}_]*)/gu;
|
|
1351
|
-
/**
|
|
1352
|
-
* Issue #44: `Forms!<FormName>` / `Forms("<FormName>")!<Ctl>` cross-form
|
|
1353
|
-
* reference capture — companion to `ME_CONTROL_RE` above. Access VBA's
|
|
1354
|
-
* default-collection shortcut for cross-form control access, captured
|
|
1355
|
-
* BEFORE the generic call-site scan and emitted as its own
|
|
1356
|
-
* `UnresolvedReference` family.
|
|
1357
|
-
*
|
|
1358
|
-
* Real VBA idioms:
|
|
1359
|
-
* `Forms!FormX!txtY.Value = 1` — bang form, with control segment
|
|
1360
|
-
* `Forms!FormX.Recordsource = "..."` — bang form, trailing property access
|
|
1361
|
-
* `Set f = Forms!FormX` — bang form, no control segment
|
|
1362
|
-
* `Forms("FormX")!txtY.Value = 1` — paren form, with control
|
|
1363
|
-
* `Forms![Mi Formulario]!txtY` — bracketed form name (#54 mirror)
|
|
1364
|
-
*
|
|
1365
|
-
* Why a dedicated scanner (mirroring `OPEN_FORM_ARG_RE` / B4 / DoCmd.OpenForm):
|
|
1366
|
-
* `Forms` is in `RUNTIME_RECEIVER_BLACKLIST`, so the generic CALL_RE
|
|
1367
|
-
* path silently skips both `Forms!X` and `Forms("X")!Y`. Without this
|
|
1368
|
-
* scanner, cross-form UI traffic from `Forms!FormX!txtY.Value` would
|
|
1369
|
-
* never surface the form → control binding that the resolver needs to
|
|
1370
|
-
* glue form `form-layout` nodes to control `form-instance-control`
|
|
1371
|
-
* nodes. The dedicated dispatch fires BEFORE the call-site scan and
|
|
1372
|
-
* uses its own emission path; the runtime-blacklist constraint is
|
|
1373
|
-
* preserved (we intentionally do not rewrite CALL_RE).
|
|
1374
|
-
*
|
|
1375
|
-
* What this scanner emits per match:
|
|
1376
|
-
* - ONE `UnresolvedReference` whose `referenceName` is the form's
|
|
1377
|
-
* identifier (stripped of any surrounding quote or bracket
|
|
1378
|
-
* decoration), tagged `metadata.synthesizedBy = 'vba-forms-bang'`
|
|
1379
|
-
* and `referenceKind = 'references'`. The control segment (if
|
|
1380
|
-
* present) is consumed by the regex so `Forms!FormX!txtY.Value` is
|
|
1381
|
-
* captured as a single match, but the control name is NOT emitted
|
|
1382
|
-
* as its own reference — control emission is the form's
|
|
1383
|
-
* responsibility downstream.
|
|
1384
|
-
* - NO synthetic `function` node (W4 graph-pollution invariant — the
|
|
1385
|
-
* form is a real `.cls` / `.form.txt` pair the resolver already
|
|
1386
|
-
* picks up via the `vba-form-binding` path). Without this guard the
|
|
1387
|
-
* bang form would synthesize one stub per cross-form reference
|
|
1388
|
-
* site, identical to what `bumpShapes` from #43 audited out.
|
|
1389
|
-
*
|
|
1390
|
-
* What this scanner does NOT match (intentional, pinned by tests):
|
|
1391
|
-
* - `Forms!FormX.Foo` — the trailing `.Foo` IS a property
|
|
1392
|
-
* access on the form (e.g. `Recordsource`), NOT a control access.
|
|
1393
|
-
* The bang alternative carries a `(?![.\w])` negative-lookahead
|
|
1394
|
-
* after the form identifier to drop this shape. The "W4
|
|
1395
|
-
* no-synthetic-fn" test pins both halves of this contract.
|
|
1396
|
-
* - `rs!Campo` — recordset field access (DAO/ADO
|
|
1397
|
-
* default-member field read) is explicitly out of scope for the
|
|
1398
|
-
* current change. The runtime-receiver blacklist plus the absence
|
|
1399
|
-
* of any `Forms`/`Me` prefix means the existing scanners already
|
|
1400
|
-
* skip it cleanly; the "STRETCH SCOPE" test pins that behaviour
|
|
1401
|
-
* so a future change to bring recordset bangs in is reviewed
|
|
1402
|
-
* explicitly against the bang-form scope decision.
|
|
1403
|
-
*
|
|
1404
|
-
* Operates on the ORIGINAL (unmasked) line — the paren form
|
|
1405
|
-
* `Forms("FormX")!txtY` has the form name INSIDE a string literal, so
|
|
1406
|
-
* masking string content would destroy the form identifier. Same
|
|
1407
|
-
* unmasked-line constraint as `scanOpenFormCalls`.
|
|
1408
|
-
*/
|
|
1409
|
-
static FORMS_BANG_RE = /\b(?:Forms!(\p{L}[\p{L}\p{N}_]*|\[[^\]]+\])(?![.\w])|Forms\(\s*(?:"((?:[^"]|"")*)"|(\p{L}[\p{L}\p{N}_]*|\[[^\]]+\]))\s*\)\s*!\s*(?:\p{L}[\p{L}\p{N}_]*|\[[^\]]+\]))/gu;
|
|
1410
|
-
/**
|
|
1411
|
-
* Issue #46: scan `Set <var> = New <Type>[.<Inner>]` lines — the dominant
|
|
1412
|
-
* VBA late-instantiation idiom. Run inside `sweepCallsAndSql`'s proc-stack
|
|
1413
|
-
* loop so the surrounding procedure is known. For each match:
|
|
1414
|
-
* - register `<var>` in `localVarTypeMap` with `outer=<Type>`,
|
|
1415
|
-
* `qualified=<hasInner>`, `assignedWithSet=true` so the PR #61 refined
|
|
1416
|
-
* gate lets subsequent `<var>.Member ...` qualified calls resolve via
|
|
1417
|
-
* the resolved class name;
|
|
1418
|
-
* - emit a `references` edge from the module/class node to a synthetic
|
|
1419
|
-
* node named `<Type>`, tagged `synthesizedBy: 'vba-set-new'`.
|
|
1420
|
-
*
|
|
1421
|
-
* Groups: (1) variable name, (2) outer type, (3) optional inner type.
|
|
1422
|
-
* Operates on the MASKED line (string-literal content already replaced
|
|
1423
|
-
* with spaces) so `Set x = New Foo` inside a string literal never matches.
|
|
1424
|
-
*/
|
|
1425
|
-
static SET_NEW_RE = /\bSet\s+(\p{L}[\p{L}\p{N}_]*)\s*=\s*New\s+(\p{L}[\p{L}\p{N}_]*)(?:\.(\p{L}[\p{L}\p{N}_]*))?/iu;
|
|
1426
|
-
/** Issue #43: track the receiver for `With <expr>` / `End With` blocks. */
|
|
1427
|
-
static WITH_START_RE = /^\s*With\b\s+(.+?)\s*$/iu;
|
|
1428
|
-
static WITH_END_RE = /^\s*End\s+With\b/iu;
|
|
1429
|
-
/** Keywords we never want to match as call receivers. */
|
|
1430
|
-
static CALL_KEYWORD_BLACKLIST = new Set([
|
|
1431
|
-
'If',
|
|
1432
|
-
'For',
|
|
1433
|
-
'While',
|
|
1434
|
-
'Do',
|
|
1435
|
-
'Select',
|
|
1436
|
-
'Case',
|
|
1437
|
-
'Then',
|
|
1438
|
-
'Else',
|
|
1439
|
-
'ElseIf',
|
|
1440
|
-
'With',
|
|
1441
|
-
'Loop',
|
|
1442
|
-
'Wend',
|
|
1443
|
-
'End',
|
|
1444
|
-
'Return',
|
|
1445
|
-
'Dim',
|
|
1446
|
-
'Set',
|
|
1447
|
-
'Let',
|
|
1448
|
-
'Const',
|
|
1449
|
-
'ReDim',
|
|
1450
|
-
'Static',
|
|
1451
|
-
'Public',
|
|
1452
|
-
'Private',
|
|
1453
|
-
'Friend',
|
|
1454
|
-
'Sub',
|
|
1455
|
-
'Function',
|
|
1456
|
-
'Property',
|
|
1457
|
-
'Class',
|
|
1458
|
-
'Module',
|
|
1459
|
-
'Option',
|
|
1460
|
-
'On',
|
|
1461
|
-
'Error',
|
|
1462
|
-
'Resume',
|
|
1463
|
-
'Exit',
|
|
1464
|
-
'New',
|
|
1465
|
-
'Call',
|
|
1466
|
-
'Rem',
|
|
1467
|
-
'LBound',
|
|
1468
|
-
'UBound',
|
|
1469
|
-
'Me',
|
|
1470
|
-
'Nothing',
|
|
1471
|
-
]);
|
|
1472
|
-
/**
|
|
1473
|
-
* Access runtime objects and singletons. Calls on these receivers are
|
|
1474
|
-
* real VBA calls but the targets are NOT user-defined modules or
|
|
1475
|
-
* classes — they're Access/DAO/ADO runtime types. Synthesizing a
|
|
1476
|
-
* `function` node for each would pollute the graph with ~20+ junk
|
|
1477
|
-
* nodes per real-world file (audit W4, June 2026). Skip synthesis
|
|
1478
|
-
* for any receiver or member in this set.
|
|
1479
|
-
*
|
|
1480
|
-
* Note: `DoCmd.RunSQL`, `DoCmd.OpenForm`, etc. still get SQL/edge
|
|
1481
|
-
* tracking via the dedicated `SQL_WRAPPERS` regex path (REQ-CODE-8),
|
|
1482
|
-
* which fires BEFORE this scan and uses its own dispatch — so
|
|
1483
|
-
* blacklisting DoCmd here doesn't lose the SQL-flow edges.
|
|
1484
|
-
*/
|
|
1485
|
-
static RUNTIME_RECEIVER_BLACKLIST = new Set([
|
|
1486
|
-
// Form / page references
|
|
1487
|
-
'Screen',
|
|
1488
|
-
// Access application singletons
|
|
1489
|
-
'Application',
|
|
1490
|
-
'DoCmd',
|
|
1491
|
-
'SysCmd',
|
|
1492
|
-
// VBA debugging intrinsic — Debug.Print / Debug.Assert
|
|
1493
|
-
'Debug',
|
|
1494
|
-
// Access object collections
|
|
1495
|
-
'Forms',
|
|
1496
|
-
'Reports',
|
|
1497
|
-
'Modules',
|
|
1498
|
-
'References',
|
|
1499
|
-
'CommandBars',
|
|
1500
|
-
// Error-handling intrinsic
|
|
1501
|
-
'Err',
|
|
1502
|
-
// Late-binding factories (return IDispatch — not user code)
|
|
1503
|
-
'CreateObject',
|
|
1504
|
-
'GetObject',
|
|
1505
|
-
// DAO/ADO recordset field collection access (e.g. rcdDatos.Fields("ID"))
|
|
1506
|
-
'Fields',
|
|
1507
|
-
]);
|
|
1508
|
-
/**
|
|
1509
|
-
* Fix 2 (Issue #2): replace string-literal content with spaces so that
|
|
1510
|
-
* call-site patterns inside `"..."` spans are invisible to CALL_RE and
|
|
1511
|
-
* the statement-form detectors. Column positions are preserved (each
|
|
1512
|
-
* character is replaced 1-for-1) so any col-based metadata stays correct.
|
|
1513
|
-
*/
|
|
1514
|
-
static maskStringContent(line) {
|
|
1515
|
-
let result = '';
|
|
1516
|
-
let i = 0;
|
|
1517
|
-
while (i < line.length) {
|
|
1518
|
-
const ch = line[i];
|
|
1519
|
-
if (ch === '"') {
|
|
1520
|
-
result += ' '; // opening quote masked
|
|
1521
|
-
i++;
|
|
1522
|
-
while (i < line.length) {
|
|
1523
|
-
const c = line[i];
|
|
1524
|
-
if (c === '"' && line[i + 1] === '"') {
|
|
1525
|
-
result += ' '; // doubled-quote escape masked (2 chars)
|
|
1526
|
-
i += 2;
|
|
1527
|
-
}
|
|
1528
|
-
else if (c === '"') {
|
|
1529
|
-
result += ' '; // closing quote masked
|
|
1530
|
-
i++;
|
|
1531
|
-
break;
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
result += ' '; // string content → space
|
|
1535
|
-
i++;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
else {
|
|
1540
|
-
result += ch;
|
|
1541
|
-
i++;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return result;
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Fix 2 (Issue #2): return true iff `receiverName` is a file-local variable
|
|
1548
|
-
* (appears in `localVarTypeMap`) whose declared type is a SIMPLE (non-
|
|
1549
|
-
* qualified, non-primitive) identifier — a candidate project-defined class.
|
|
1550
|
-
* Qualified types (e.g. `DAO.Recordset`) and primitives (`String`, `Long`)
|
|
1551
|
-
* return false so runtime/DAO calls are suppressed.
|
|
1552
|
-
*
|
|
1553
|
-
* Issue #54 (defensive): brackets are stripped from the lookup key, so a
|
|
1554
|
-
* caller that forgets to unwrap a bracketed name still finds the
|
|
1555
|
-
* corresponding entry. This is a no-op when the name is already bare —
|
|
1556
|
-
* the unwrap pattern only matches an opening `[` at the start and a
|
|
1557
|
-
* closing `]` at the end. Today's call sites (`scanCallSites`,
|
|
1558
|
-
* `detectQualifiedStatementCall`) already unwrap in the regex captures,
|
|
1559
|
-
* so this defensive strip is a belt-and-braces guard for any future
|
|
1560
|
-
* caller that forgets.
|
|
1561
|
-
*/
|
|
1562
|
-
isLocalProjectClassVar(receiverName) {
|
|
1563
|
-
// Issue #54: strip a single leading `[` and/or trailing `]` if present.
|
|
1564
|
-
const key = receiverName.replace(/^\[|\]$/g, '').toLowerCase();
|
|
1565
|
-
const entry = this.localVarTypeMap.get(key);
|
|
1566
|
-
if (!entry)
|
|
1567
|
-
return false; // not declared in this file → silent
|
|
1568
|
-
if (entry.qualified)
|
|
1569
|
-
return false; // DAO.Recordset etc. → silent
|
|
1570
|
-
if (VbaExtractor.PRIMITIVE_TYPES.has(entry.outer.toLowerCase()))
|
|
1571
|
-
return false;
|
|
1572
|
-
return true;
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Qualified-call eligibility is intentionally shared by paren-form
|
|
1576
|
-
* (`Receiver.Member(...)`) and statement-form (`Receiver.Member args`) scans:
|
|
1577
|
-
* project-class locals are processed after type resolution, declared
|
|
1578
|
-
* primitive/external locals are silent, and undeclared receivers remain
|
|
1579
|
-
* candidate module names.
|
|
1580
|
-
*/
|
|
1581
|
-
shouldProcessQualifiedCall(receiverName) {
|
|
1582
|
-
if (this.isLocalProjectClassVar(receiverName))
|
|
1583
|
-
return true;
|
|
1584
|
-
return !this.localVarTypeMap.has(receiverName.toLowerCase());
|
|
1585
|
-
}
|
|
1586
|
-
/**
|
|
1587
|
-
* #12a: resolve the "receiver type" used to build a qualified call-stub's
|
|
1588
|
-
* name/qualifiedName. When `receiverName` is a file-local variable typed
|
|
1589
|
-
* as a candidate project class (`isLocalProjectClassVar`), returns the
|
|
1590
|
-
* RESOLVED CLASS NAME from `localVarTypeMap` (e.g. `m_NCOp` typed
|
|
1591
|
-
* `As NCOperaciones` → `'NCOperaciones'`) so the stub's qualifiedName
|
|
1592
|
-
* matches the real `.cls` method's `${className}.${proc}` shape and the
|
|
1593
|
-
* post-extraction resolver (#12b) can find it via an exact qualifiedName
|
|
1594
|
-
* match. Otherwise returns `receiverName` unchanged — this is the case
|
|
1595
|
-
* for `.bas`-qualified module calls (`modUtils.Foo`), where the receiver
|
|
1596
|
-
* IS already the target module's name and no resolution is needed.
|
|
1597
|
-
*/
|
|
1598
|
-
resolveReceiverType(receiverName) {
|
|
1599
|
-
if (this.isLocalProjectClassVar(receiverName)) {
|
|
1600
|
-
const key = receiverName.replace(/^\[|\]$/g, '').toLowerCase();
|
|
1601
|
-
const entry = this.localVarTypeMap.get(key);
|
|
1602
|
-
if (entry)
|
|
1603
|
-
return entry.outer;
|
|
1604
|
-
}
|
|
1605
|
-
return receiverName;
|
|
1606
|
-
}
|
|
1607
|
-
normalizeWithReceiver(expr) {
|
|
1608
|
-
let receiver = expr.trim();
|
|
1609
|
-
if (!receiver)
|
|
1610
|
-
return null;
|
|
1611
|
-
if (/^Call\s/i.test(receiver))
|
|
1612
|
-
receiver = receiver.replace(/^Call\s+/i, '').trimStart();
|
|
1613
|
-
if (receiver.startsWith('[')) {
|
|
1614
|
-
const m = /^\[([^\]]+)\]/u.exec(receiver);
|
|
1615
|
-
if (!m)
|
|
1616
|
-
return null;
|
|
1617
|
-
receiver = m[1] ?? '';
|
|
1618
|
-
}
|
|
1619
|
-
else {
|
|
1620
|
-
const m = /^(\p{L}[\p{L}\p{N}_]*)/u.exec(receiver);
|
|
1621
|
-
if (!m)
|
|
1622
|
-
return null;
|
|
1623
|
-
receiver = m[1] ?? '';
|
|
1624
|
-
}
|
|
1625
|
-
if (!receiver)
|
|
1626
|
-
return null;
|
|
1627
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(receiver))
|
|
1628
|
-
return null;
|
|
1629
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(receiver))
|
|
1630
|
-
return null;
|
|
1631
|
-
return receiver;
|
|
1632
|
-
}
|
|
1633
|
-
detectWithMemberCall(line) {
|
|
1634
|
-
let trimmed = line.trimStart();
|
|
1635
|
-
if (!trimmed)
|
|
1636
|
-
return null;
|
|
1637
|
-
if (/^Call\s/i.test(trimmed))
|
|
1638
|
-
trimmed = trimmed.replace(/^Call\s+/i, '').trimStart();
|
|
1639
|
-
if (trimmed.startsWith("'") || /^Rem(\s|$)/i.test(trimmed))
|
|
1640
|
-
return null;
|
|
1641
|
-
if (!trimmed.startsWith('.'))
|
|
1642
|
-
return null;
|
|
1643
|
-
const memberRest = trimmed.slice(1);
|
|
1644
|
-
const memberM = /^(\p{L}[\p{L}\p{N}_]*)/u.exec(memberRest);
|
|
1645
|
-
if (!memberM)
|
|
1646
|
-
return null;
|
|
1647
|
-
const member = memberM[1] ?? '';
|
|
1648
|
-
const afterMember = memberRest.slice(member.length);
|
|
1649
|
-
if (afterMember.length > 0) {
|
|
1650
|
-
const ch = afterMember.charAt(0);
|
|
1651
|
-
if (ch !== '(' && ch !== ' ' && ch !== '\t')
|
|
1652
|
-
return null;
|
|
1653
|
-
const argsText = afterMember.trimStart();
|
|
1654
|
-
if (argsText.startsWith('='))
|
|
1655
|
-
return null;
|
|
1656
|
-
}
|
|
1657
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(member))
|
|
1658
|
-
return null;
|
|
1659
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(member))
|
|
1660
|
-
return null;
|
|
1661
|
-
return { member };
|
|
1662
|
-
}
|
|
1663
|
-
sweepCallsAndSql(src) {
|
|
1664
|
-
const lines = src.split('\n');
|
|
1665
|
-
const procedureStartLines = new Set();
|
|
1666
|
-
const sqlTargetsThisFile = new Set();
|
|
1667
|
-
// Issue #52: reset the shared scope state before the per-line walk
|
|
1668
|
-
// begins. `sweepEnumsAndConsts` already populated `procStack` /
|
|
1669
|
-
// `currentProcKey` during its own walk; clearing here guarantees the
|
|
1670
|
-
// `scanDoCmdOpenCalls` / `scanDoCmdOpenQuery` reads (which consult
|
|
1671
|
-
// `currentProcKey` per call-site) start in module scope and follow
|
|
1672
|
-
// the same push/pop discipline as the existing `stack` array below.
|
|
1673
|
-
this.procStack.length = 0;
|
|
1674
|
-
this.currentProcKey = 'module';
|
|
1675
|
-
// Walk the source once, emitting call edges and SQL edges per line and
|
|
1676
|
-
// tracking the current procedure stack. The previous implementation
|
|
1677
|
-
// did this in two passes; audit S1 (June 2026) flagged the first pass
|
|
1678
|
-
// as dead code (its `procStack` was never read after the loop). One
|
|
1679
|
-
// pass suffices.
|
|
1680
|
-
const stack = [];
|
|
1681
|
-
const withReceiverStack = [];
|
|
1682
|
-
const sqlVariables = new Map();
|
|
1683
|
-
// C2 fix: track each procedure's `endLine` (the line containing the
|
|
1684
|
-
// matching `End Sub`/`End Function`/`End Property`) keyed by its
|
|
1685
|
-
// `startLine`. After the loop, we update every function node's
|
|
1686
|
-
// `endLine` so `codegraph_explore` returns the full body span —
|
|
1687
|
-
// not just the signature line.
|
|
1688
|
-
const procEndLines = new Map();
|
|
1689
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1690
|
-
const line = lines[i] ?? '';
|
|
1691
|
-
const lineNum = i + 1;
|
|
1692
|
-
const procStart = VbaExtractor.PROC_RE.exec(line);
|
|
1693
|
-
if (procStart) {
|
|
1694
|
-
// Issue #52: mirror the proc push into the shared
|
|
1695
|
-
// `procStack` + `currentProcKey` so Const reads in this same
|
|
1696
|
-
// sweep see the same scope as the const writes did (during
|
|
1697
|
-
// `sweepEnumsAndConsts`). Uses the same `startLine` key used
|
|
1698
|
-
// for the `localConstants` bucket.
|
|
1699
|
-
const procStartLine = lineNum;
|
|
1700
|
-
this.procStack.push(procStartLine);
|
|
1701
|
-
this.currentProcKey = String(procStartLine);
|
|
1702
|
-
const name = procStart[3] ?? '';
|
|
1703
|
-
const bucket = this.localProcs.get(name);
|
|
1704
|
-
if (bucket) {
|
|
1705
|
-
// Fix 1: select the ProcInfo whose startLine matches the current
|
|
1706
|
-
// line, not always bucket[0]. When Property Get/Let/Set share a
|
|
1707
|
-
// name, all three exist in the bucket; pushing bucket[0] every time
|
|
1708
|
-
// meant Let/Set bodies erroneously attributed to the Get's ProcInfo
|
|
1709
|
-
// (wrong endLine and wrong caller source on call edges).
|
|
1710
|
-
const proc = bucket.find((p) => p.startLine === lineNum) ?? bucket[0];
|
|
1711
|
-
if (proc)
|
|
1712
|
-
stack.push(proc);
|
|
1713
|
-
}
|
|
1714
|
-
procedureStartLines.add(lineNum);
|
|
1715
|
-
}
|
|
1716
|
-
else if (VbaExtractor.PROCEDURE_END_RE.test(line) && stack.length > 0) {
|
|
1717
|
-
const ending = stack.pop();
|
|
1718
|
-
// Issue #52: mirror the pop into the shared scope state.
|
|
1719
|
-
this.procStack.pop();
|
|
1720
|
-
this.currentProcKey =
|
|
1721
|
-
this.procStack.length > 0
|
|
1722
|
-
? String(this.procStack[this.procStack.length - 1])
|
|
1723
|
-
: 'module';
|
|
1724
|
-
procEndLines.set(ending.startLine, lineNum);
|
|
1725
|
-
continue;
|
|
1726
|
-
}
|
|
1727
|
-
// Fix 2 (Issue #2): mask string-literal content before call scanning so
|
|
1728
|
-
// patterns like `modHelper.BuildQuery(` inside a string argument are not
|
|
1729
|
-
// mistakenly treated as call sites. SQL scanning still uses the original
|
|
1730
|
-
// line because SQL lives INSIDE string literals.
|
|
1731
|
-
const callScanLine = VbaExtractor.maskStringContent(line);
|
|
1732
|
-
if (stack.length > 0 && VbaExtractor.WITH_END_RE.test(callScanLine)) {
|
|
1733
|
-
withReceiverStack.pop();
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
if (stack.length > 0 && !procedureStartLines.has(lineNum)) {
|
|
1737
|
-
const withStart = VbaExtractor.WITH_START_RE.exec(callScanLine);
|
|
1738
|
-
if (withStart) {
|
|
1739
|
-
const receiver = this.normalizeWithReceiver(withStart[1] ?? '');
|
|
1740
|
-
if (receiver)
|
|
1741
|
-
withReceiverStack.push(receiver);
|
|
1742
|
-
continue;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
// Don't scan call sites on the line that declares the procedure — it
|
|
1746
|
-
// would match the proc name itself in `Sub Outer()`.
|
|
1747
|
-
if (!procedureStartLines.has(lineNum) && stack.length > 0) {
|
|
1748
|
-
const currentProc = stack[stack.length - 1];
|
|
1749
|
-
this.scanRaiseEvents(callScanLine, currentProc, lineNum);
|
|
1750
|
-
this.scanCallSites(callScanLine, currentProc, lineNum);
|
|
1751
|
-
}
|
|
1752
|
-
// Hueco 1: capture `Me.<Control>` references (property assignments,
|
|
1753
|
-
// read expressions, method calls, anything after `Me.`). Only inside
|
|
1754
|
-
// procedures because `Me` is only meaningful inside a form's class
|
|
1755
|
-
// module. Skipped on the proc-declaration line for the same reason as
|
|
1756
|
-
// the call-site scan above.
|
|
1757
|
-
if (!procedureStartLines.has(lineNum) && stack.length > 0) {
|
|
1758
|
-
this.scanMeControlReferences(callScanLine, stack[stack.length - 1], lineNum);
|
|
1759
|
-
}
|
|
1760
|
-
// SQL wrappers — only inside a procedure (don't pollute module scope
|
|
1761
|
-
// with a stray string literal). Use the ORIGINAL line — SQL is inside
|
|
1762
|
-
// string literals, so the masked line would strip the SQL content.
|
|
1763
|
-
if (stack.length > 0) {
|
|
1764
|
-
this.trackSqlVariableAssignment(lines, i, sqlVariables);
|
|
1765
|
-
this.scanSqlInLine(line, lineNum, sqlTargetsThisFile, sqlVariables);
|
|
1766
|
-
}
|
|
1767
|
-
// H1 fix: detect statement-form Sub calls (no parens, no `Call`
|
|
1768
|
-
// keyword). Audit H1 (June 2026): the parens-only CALL_RE made the
|
|
1769
|
-
// dominant VBA idiom invisible — `EstablecerDatos m_Error` on the
|
|
1770
|
-
// Form_FormNCAuditoriaMotivoEliminado.cls fixture contributed
|
|
1771
|
-
// nothing to the call graph. Walked here so we share the proc stack
|
|
1772
|
-
// already maintained by this loop.
|
|
1773
|
-
//
|
|
1774
|
-
// Issue #45: the statement-call detector used to inspect only the
|
|
1775
|
-
// FIRST identifier of the line, so for `If x Then Foo` it returned
|
|
1776
|
-
// the keyword `If` (which is then dropped by
|
|
1777
|
-
// `emitStatementCallEdge`'s BLACKLIST check) — the actual `Foo` call
|
|
1778
|
-
// after `Then` was silently invisible. Same gap existed on the
|
|
1779
|
-
// `Else`/multi-statement (`:`) and qualified paths. The fix is to
|
|
1780
|
-
// split the line into one or more statement clauses via
|
|
1781
|
-
// `splitSingleLineIfClauses` (which handles `If … Then <body>`,
|
|
1782
|
-
// `Else <body>`, and colon-separated multi-statements) and run the
|
|
1783
|
-
// detectors per clause. Lines that don't match the single-line If
|
|
1784
|
-
// shape pass through unchanged as `[<line>]` so block-form `If`
|
|
1785
|
-
// (where the body lives on subsequent lines) keeps working through
|
|
1786
|
-
// the existing per-line scan that picks up the body line.
|
|
1787
|
-
if (stack.length > 0 && !procedureStartLines.has(lineNum)) {
|
|
1788
|
-
// Issue #46: `Set x = New <Type>[.<Inner>]` late-instantiation.
|
|
1789
|
-
// Run BEFORE the call-site scan so a later `<x>.Member ...` line
|
|
1790
|
-
// finds `x` already registered in `localVarTypeMap` and the PR #61
|
|
1791
|
-
// refined gate lets the qualified call resolve to `<Type>.Member`.
|
|
1792
|
-
const setNew = VbaExtractor.SET_NEW_RE.exec(callScanLine);
|
|
1793
|
-
if (setNew) {
|
|
1794
|
-
const varName = setNew[1] ?? '';
|
|
1795
|
-
const outerType = setNew[2] ?? '';
|
|
1796
|
-
const innerType = setNew[3] ?? '';
|
|
1797
|
-
if (varName && outerType) {
|
|
1798
|
-
// Skip primitives defensively — `Set x = New Long` is nonsense
|
|
1799
|
-
// in practice but the gate is cheap and consistent with the
|
|
1800
|
-
// Dim sweep's PRIMITIVE_TYPES guard.
|
|
1801
|
-
if (!VbaExtractor.PRIMITIVE_TYPES.has(outerType.toLowerCase())) {
|
|
1802
|
-
this.localVarTypeMap.set(varName.toLowerCase(), {
|
|
1803
|
-
outer: outerType,
|
|
1804
|
-
// Mirror `Dim x As Foo.Bar`: qualified `Set rs = New
|
|
1805
|
-
// DAO.Recordset` registers `qualified: true` so the PR #61
|
|
1806
|
-
// gate keeps downstream `rs.Method` calls silent (DAO is
|
|
1807
|
-
// a runtime / external library, not a project class).
|
|
1808
|
-
qualified: !!innerType,
|
|
1809
|
-
assignedWithSet: true,
|
|
1810
|
-
variableName: varName,
|
|
1811
|
-
});
|
|
1812
|
-
this.emitReference(outerType, lineNum, 0, 'vba-set-new');
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
const clauseLines = this.splitSingleLineIfClauses(callScanLine);
|
|
1817
|
-
for (const clauseLine of clauseLines) {
|
|
1818
|
-
const stmtCall = this.detectStatementCall(clauseLine);
|
|
1819
|
-
if (stmtCall) {
|
|
1820
|
-
const caller = stack[stack.length - 1];
|
|
1821
|
-
this.emitStatementCallEdge(caller, stmtCall, lineNum);
|
|
1822
|
-
}
|
|
1823
|
-
// Fix 7 + Fix 2 + Issue #40: qualified statement-form calls
|
|
1824
|
-
// (`Receiver.Member args`) — the dominant cross-object call shape in
|
|
1825
|
-
// real Dysflow fixtures. `CALL_RE` only matches the paren form; this
|
|
1826
|
-
// path covers the no-paren statement form and emits a heuristic
|
|
1827
|
-
// `calls` edge through the unified `shouldProcessQualifiedCall`
|
|
1828
|
-
// gate (shared with the paren form): declared project-class locals
|
|
1829
|
-
// process, declared primitive/external locals stay silent, and
|
|
1830
|
-
// undeclared receivers remain module-name candidates for the
|
|
1831
|
-
// post-extraction resolver.
|
|
1832
|
-
const qualStmt = this.detectQualifiedStatementCall(clauseLine);
|
|
1833
|
-
if (qualStmt) {
|
|
1834
|
-
const caller = stack[stack.length - 1];
|
|
1835
|
-
if (this.shouldProcessQualifiedCall(qualStmt.receiver)) {
|
|
1836
|
-
this.emitQualifiedStatementCallEdge(caller, qualStmt.receiver, qualStmt.member, lineNum);
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
const withReceiver = withReceiverStack[withReceiverStack.length - 1];
|
|
1840
|
-
if (withReceiver) {
|
|
1841
|
-
const withCall = this.detectWithMemberCall(clauseLine);
|
|
1842
|
-
if (withCall && this.isLocalProjectClassVar(withReceiver)) {
|
|
1843
|
-
const caller = stack[stack.length - 1];
|
|
1844
|
-
this.emitQualifiedStatementCallEdge(caller, withReceiver, withCall.member, lineNum);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
// B4 (hueco 6): `DoCmd.OpenForm "FormName"` modelling.
|
|
1849
|
-
//
|
|
1850
|
-
// The literal form name lives INSIDE a string literal, so we
|
|
1851
|
-
// scan the ORIGINAL (unmasked) line — `callScanLine` has its
|
|
1852
|
-
// string content replaced with spaces. The receiver is the same
|
|
1853
|
-
// proc-stack frame so we attribute the edge to the calling Sub,
|
|
1854
|
-
// not the file-level module.
|
|
1855
|
-
//
|
|
1856
|
-
// Cross-file edge filter note: the synthesized stub node is
|
|
1857
|
-
// emitted locally (same file's extraction result), so the
|
|
1858
|
-
// per-file filter at `index.ts:insertedIds.has(source/target)`
|
|
1859
|
-
// passes the edge naturally — no exemption to the filter is
|
|
1860
|
-
// required. The real form-layout node, when VbaFormExtractor
|
|
1861
|
-
// later processes the matching .form.txt file, gets a DIFFERENT
|
|
1862
|
-
// node id (it uses the real .form.txt path); the stub and the
|
|
1863
|
-
// real node coexist harmlessly — the edge from THIS file still
|
|
1864
|
-
// references the stub. Future work can collapse the stub once
|
|
1865
|
-
// the indexer learns to re-resolve cross-file edges to the real
|
|
1866
|
-
// node id (the same pattern the cross-file incoming-edges
|
|
1867
|
-
// snapshot already uses at `index.ts:getCrossFileIncomingEdges`).
|
|
1868
|
-
const caller2 = stack[stack.length - 1];
|
|
1869
|
-
// Issue #48: shared OpenForm/OpenReport dispatch via
|
|
1870
|
-
// `scanDoCmdOpenCalls` (formerly `scanOpenFormCalls`, refactored to
|
|
1871
|
-
// iterate the `DOCMD_OPEN_DISPATCH` table — OpenForm behavior is
|
|
1872
|
-
// byte-identical to pre-#48). The OpenQuery scanner emits an
|
|
1873
|
-
// `UnresolvedReference` and stays separate from the dispatch since
|
|
1874
|
-
// its emission shape (no stub + edge) differs.
|
|
1875
|
-
this.scanDoCmdOpenCalls(line, caller2, lineNum);
|
|
1876
|
-
this.scanDoCmdOpenQuery(line, caller2, lineNum);
|
|
1877
|
-
// Issue #44: cross-form bang references (`Forms!X` / `Forms("X")!Y`).
|
|
1878
|
-
// Same line context as `scanDoCmdOpenCalls` — the form name lives in
|
|
1879
|
-
// a string literal in the paren form, so we MUST scan the unmasked
|
|
1880
|
-
// line. The scanner is independent of `scanCallSites` because
|
|
1881
|
-
// `Forms` is in `RUNTIME_RECEIVER_BLACKLIST` and would otherwise be
|
|
1882
|
-
// dropped by the generic path.
|
|
1883
|
-
this.scanFormsBang(line, caller2, lineNum);
|
|
1884
|
-
// Issue #50: cross-form TempVars key accesses.
|
|
1885
|
-
// Sibling of `scanFormsBang` — TempVars is Access's second global
|
|
1886
|
-
// cross-form state surface (alongside Forms/DoCmd.OpenForm). Each
|
|
1887
|
-
// static-literal key reference emits one `references` edge to a
|
|
1888
|
-
// synthetic `class` placeholder, tagged `synthesizedBy: 'vba-tempvar'`
|
|
1889
|
-
// and `access: 'read' | 'write'`. We need BOTH line sources: bang
|
|
1890
|
-
// form scans the masked line, paren + Add forms scan the original
|
|
1891
|
-
// (literal lives inside `"…"`).
|
|
1892
|
-
this.sweepTempVars(callScanLine, line, lineNum, caller2);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
// Apply endLine to every emitted function node keyed by its startLine.
|
|
1896
|
-
// Functions without a recorded endLine (e.g. malformed VBA without an
|
|
1897
|
-
// `End`) keep their `endLine = startLine` from sweepProcedures —
|
|
1898
|
-
// which is the correct "single line" representation.
|
|
1899
|
-
for (const n of this.nodes) {
|
|
1900
|
-
if (n.kind !== 'function')
|
|
1901
|
-
continue;
|
|
1902
|
-
const end = procEndLines.get(n.startLine);
|
|
1903
|
-
if (end !== undefined)
|
|
1904
|
-
n.endLine = end;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
static RAISE_EVENT_RE = /\bRaiseEvent\s+(\p{L}[\p{L}\p{N}_]*)\b/giu;
|
|
1908
|
-
scanRaiseEvents(line, from, lineNum) {
|
|
1909
|
-
VbaExtractor.RAISE_EVENT_RE.lastIndex = 0;
|
|
1910
|
-
let m;
|
|
1911
|
-
while ((m = VbaExtractor.RAISE_EVENT_RE.exec(line)) !== null) {
|
|
1912
|
-
const eventName = m[1] ?? '';
|
|
1913
|
-
const eventNode = this.localEvents.get(eventName.toLowerCase());
|
|
1914
|
-
if (!eventNode)
|
|
1915
|
-
continue;
|
|
1916
|
-
this.edges.push({
|
|
1917
|
-
source: this.findOrCreateFunctionNodeId(from),
|
|
1918
|
-
target: eventNode.id,
|
|
1919
|
-
kind: 'raises-event',
|
|
1920
|
-
provenance: 'parser',
|
|
1921
|
-
metadata: { eventName },
|
|
1922
|
-
line: lineNum,
|
|
1923
|
-
column: m.index,
|
|
1924
|
-
});
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
scanCallSites(line, from, lineNum) {
|
|
1928
|
-
VbaExtractor.CALL_RE.lastIndex = 0;
|
|
1929
|
-
let m;
|
|
1930
|
-
while ((m = VbaExtractor.CALL_RE.exec(line)) !== null) {
|
|
1931
|
-
// Issue #54: CALL_RE groups (1)/(2) are alternative captures for the
|
|
1932
|
-
// receiver position, (3)/(4) for the member position. The bracketed
|
|
1933
|
-
// alternative wins when present; the captured value is already
|
|
1934
|
-
// unwrapped by the regex (it captures only the inner content, not
|
|
1935
|
-
// the surrounding `[...]`). Result: a `[FUNCIONES UTILES]` receiver
|
|
1936
|
-
// surfaces here as `FUNCIONES UTILES` so the downstream blacklist
|
|
1937
|
-
// checks and `resolveReceiverType` lookup see the bare identifier.
|
|
1938
|
-
const receiver = m[1] ?? m[2] ?? '';
|
|
1939
|
-
const member = m[3] ?? m[4] ?? '';
|
|
1940
|
-
if (!receiver)
|
|
1941
|
-
continue;
|
|
1942
|
-
// Skip VBA control-flow keywords.
|
|
1943
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(receiver))
|
|
1944
|
-
continue;
|
|
1945
|
-
if (member && VbaExtractor.CALL_KEYWORD_BLACKLIST.has(member))
|
|
1946
|
-
continue;
|
|
1947
|
-
// Skip Access runtime objects — `Me`, `DoCmd`, `Application`, etc.
|
|
1948
|
-
// These calls are real but the targets are NOT user code; emitting
|
|
1949
|
-
// synthetic function nodes for them pollutes the graph (audit W4).
|
|
1950
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(receiver))
|
|
1951
|
-
continue;
|
|
1952
|
-
if (member && VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(member))
|
|
1953
|
-
continue;
|
|
1954
|
-
// Skip the receiver when it equals the containing procedure (self-call).
|
|
1955
|
-
if (receiver === from.name && !member)
|
|
1956
|
-
continue;
|
|
1957
|
-
const col = m.index;
|
|
1958
|
-
if (!member) {
|
|
1959
|
-
// Bare `Name(...)` — same-file resolution.
|
|
1960
|
-
const localFuncNode = this.findFunctionNodeByName(receiver);
|
|
1961
|
-
if (!localFuncNode)
|
|
1962
|
-
continue;
|
|
1963
|
-
this.edges.push({
|
|
1964
|
-
source: this.findOrCreateFunctionNodeId(from),
|
|
1965
|
-
target: localFuncNode.id,
|
|
1966
|
-
kind: 'calls',
|
|
1967
|
-
line: lineNum,
|
|
1968
|
-
column: col,
|
|
1969
|
-
});
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1972
|
-
// Qualified `Receiver.Member(...)` — synthesize the call target only
|
|
1973
|
-
// for project-class local variables or undeclared module candidates.
|
|
1974
|
-
if (!this.shouldProcessQualifiedCall(receiver))
|
|
1975
|
-
continue;
|
|
1976
|
-
// #12a: `receiverType` resolves to the real class name when
|
|
1977
|
-
// `receiver` is a declared project-class local var (matching a real
|
|
1978
|
-
// `.cls` method's `${className}.${proc}` qualifiedName shape so the
|
|
1979
|
-
// #12b resolver can find it by exact match); otherwise it's the raw
|
|
1980
|
-
// `receiver` text unchanged (e.g. `.bas`-qualified module calls).
|
|
1981
|
-
//
|
|
1982
|
-
// Antigravity audit Task 3 (refined gate): if `receiver` is a
|
|
1983
|
-
// file-local variable declared as a PRIMITIVE (Variant, Object,
|
|
1984
|
-
// Empty, Null, LongPtr, LongLong, New, Long, String, ...), skip
|
|
1985
|
-
// emission. The previous behaviour emitted a heuristic `calls`
|
|
1986
|
-
// edge to a stub named `<receiver>.<member>` that no resolver
|
|
1987
|
-
// could ever repoint (Variant can hold anything, including
|
|
1988
|
-
// runtime singletons; the stub is dead-end graph pollution).
|
|
1989
|
-
//
|
|
1990
|
-
// This refined gate does NOT regress cross-module qualified
|
|
1991
|
-
// calls like `modUtils.Foo(1)` because `modUtils` is never
|
|
1992
|
-
// declared as a file-local variable and therefore is NOT in
|
|
1993
|
-
// `localVarTypeMap` — `localVarTypeMap.has(receiver.toLowerCase())`
|
|
1994
|
-
// returns false and the gate is skipped. The "stub emitted for
|
|
1995
|
-
// undeclared receivers → resolver repoints if a real module
|
|
1996
|
-
// exists" behaviour is preserved.
|
|
1997
|
-
const recvEntry = this.localVarTypeMap.get(receiver.toLowerCase());
|
|
1998
|
-
if (recvEntry && VbaExtractor.PRIMITIVE_TYPES.has(recvEntry.outer.toLowerCase())) {
|
|
1999
|
-
continue;
|
|
2000
|
-
}
|
|
2001
|
-
const receiverType = this.resolveReceiverType(receiver);
|
|
2002
|
-
const qualified = `${receiverType}.${member}`;
|
|
2003
|
-
// Avoid emitting duplicate edges for the same call (within a line).
|
|
2004
|
-
const dedupeKey = `${from.name}->${qualified}@${lineNum}`;
|
|
2005
|
-
if (this.callDedupe.has(dedupeKey))
|
|
2006
|
-
continue;
|
|
2007
|
-
this.callDedupe.add(dedupeKey);
|
|
2008
|
-
const synthId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'function', qualified, lineNum);
|
|
2009
|
-
// Only add the synthetic function node once per (file, qualified, line).
|
|
2010
|
-
if (!this.synthFunctionNodeIds.has(synthId)) {
|
|
2011
|
-
this.synthFunctionNodeIds.add(synthId);
|
|
2012
|
-
this.nodes.push({
|
|
2013
|
-
id: synthId,
|
|
2014
|
-
kind: 'function',
|
|
2015
|
-
name: qualified,
|
|
2016
|
-
qualifiedName: qualified,
|
|
2017
|
-
filePath: this.filePath,
|
|
2018
|
-
language: 'vba',
|
|
2019
|
-
startLine: lineNum,
|
|
2020
|
-
endLine: lineNum,
|
|
2021
|
-
startColumn: col,
|
|
2022
|
-
endColumn: col + qualified.length,
|
|
2023
|
-
visibility: 'public',
|
|
2024
|
-
// #12a: tag the stub so the post-extraction resolver (#12b)
|
|
2025
|
-
// can find and repoint it. Mirrors the DoCmd.OpenForm stub
|
|
2026
|
-
// precedent (`emitOpensFormEdge`).
|
|
2027
|
-
metadata: { stub: true },
|
|
2028
|
-
updatedAt: Date.now(),
|
|
2029
|
-
});
|
|
2030
|
-
}
|
|
2031
|
-
this.edges.push({
|
|
2032
|
-
source: this.findOrCreateFunctionNodeId(from),
|
|
2033
|
-
target: synthId,
|
|
2034
|
-
kind: 'calls',
|
|
2035
|
-
provenance: 'heuristic',
|
|
2036
|
-
metadata: {
|
|
2037
|
-
synthesizedBy: 'vba-name-resolution',
|
|
2038
|
-
stub: true,
|
|
2039
|
-
receiverType,
|
|
2040
|
-
member,
|
|
2041
|
-
},
|
|
2042
|
-
line: lineNum,
|
|
2043
|
-
column: col,
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
/** Cache so we don't re-emit the same proc function node per call site. */
|
|
2049
|
-
procNodeIdCache = new Map();
|
|
2050
|
-
callDedupe = new Set();
|
|
2051
|
-
synthFunctionNodeIds = new Set();
|
|
2052
|
-
/**
|
|
2053
|
-
* B4 (hueco 6) extended by Issue #48: cache of stub node ids we've already
|
|
2054
|
-
* emitted for a given (method, target name) pair in this file. Avoids
|
|
2055
|
-
* emitting duplicate stubs when `DoCmd.OpenForm "FormTest"` or
|
|
2056
|
-
* `DoCmd.OpenReport "InformeMensual"` shows up N times across N calls.
|
|
2057
|
-
* Keyed by `${cacheKey}:${lowerName}` so the OpenForm and OpenReport
|
|
2058
|
-
* de-dup buckets stay disjoint — `OpenForm:Form1` ≠ `OpenReport:Form1`.
|
|
2059
|
-
* The name part is lowercased so `FormTest` / `formtest` collapse.
|
|
2060
|
-
*/
|
|
2061
|
-
opensStubIdsByKey = new Map();
|
|
2062
|
-
/**
|
|
2063
|
-
* Hueco 1: scan a line for `Me.<ControlName>` / `Me!<ControlName>`
|
|
2064
|
-
* patterns and emit one UnresolvedReference per occurrence, tagged
|
|
2065
|
-
* `metadata.synthesizedBy: 'vba-me-control'`. Issue #44 extended
|
|
2066
|
-
* `ME_CONTROL_RE` from `Me\.` to `Me[.!]` so the bang form (default-
|
|
2067
|
-
* collection shortcut) is captured byte-identically to the dot form.
|
|
2068
|
-
*
|
|
2069
|
-
* Operates on the masked `callScanLine` (string-literal content already
|
|
2070
|
-
* replaced with spaces) so `Me.X` inside a string literal is not falsely
|
|
2071
|
-
* captured. Per-site emission — every `Me.lblTitulo` reference site
|
|
2072
|
-
* produces its own UnresolvedReference carrying the line/column; the
|
|
2073
|
-
* resolver fans them out into multiple `references` edges at index time
|
|
2074
|
-
* once the matching `form-instance-control` node exists.
|
|
2075
|
-
*
|
|
2076
|
-
* `fromNodeId` is the current procedure's function node — that's the
|
|
2077
|
-
* "owner" of the reference (the Sub body that wrote `Me.lblTitulo = …`).
|
|
2078
|
-
* The +3 column offset remains correct under Issue #44's regex change
|
|
2079
|
-
* because both `Me.` and `Me!` are 3-character prefixes.
|
|
2080
|
-
*/
|
|
2081
|
-
scanMeControlReferences(line, from, lineNum) {
|
|
2082
|
-
VbaExtractor.ME_CONTROL_RE.lastIndex = 0;
|
|
2083
|
-
let m;
|
|
2084
|
-
while ((m = VbaExtractor.ME_CONTROL_RE.exec(line)) !== null) {
|
|
2085
|
-
const controlName = m[1] ?? '';
|
|
2086
|
-
if (!controlName)
|
|
2087
|
-
continue;
|
|
2088
|
-
this.unresolvedReferences.push({
|
|
2089
|
-
fromNodeId: this.findOrCreateFunctionNodeId(from),
|
|
2090
|
-
referenceName: controlName,
|
|
2091
|
-
referenceKind: 'references',
|
|
2092
|
-
line: lineNum,
|
|
2093
|
-
column: m.index + 3, // +3 to skip the `Me.` / `Me!` prefix
|
|
2094
|
-
filePath: this.filePath,
|
|
2095
|
-
language: 'vba',
|
|
2096
|
-
metadata: { synthesizedBy: 'vba-me-control' },
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
/**
|
|
2101
|
-
* Issue #44: scan a line for cross-form bang references
|
|
2102
|
-
* (`Forms!<FormName>[!<Ctl>]` and `Forms("<FormName>")!<Ctl>`) and
|
|
2103
|
-
* emit ONE UnresolvedReference per match with `metadata.synthesizedBy
|
|
2104
|
-
* = 'vba-forms-bang'`. Companion to `scanMeControlReferences` above;
|
|
2105
|
-
* shares the emission shape (a single `UnresolvedReference` per match,
|
|
2106
|
-
* `referenceKind: 'references'`), keeping the W4 invariant that we
|
|
2107
|
-
* synthesize NO `function` node for forms.
|
|
2108
|
-
*
|
|
2109
|
-
* Operates on the ORIGINAL (unmasked) line — the paren form
|
|
2110
|
-
* `Forms("FormX")!txtY` carries the form name INSIDE a string literal
|
|
2111
|
-
* and would be destroyed by `maskStringContent`. Mirrors
|
|
2112
|
-
* `scanOpenFormCalls`'s unmasked-line constraint.
|
|
2113
|
-
*
|
|
2114
|
-
* Regex alternatives (see `FORMS_BANG_RE`):
|
|
2115
|
-
* 1. `Forms!<FormName>` (bare or `[bracketed]`, NOT followed by `.X`)
|
|
2116
|
-
* — bang form, may have a trailing `!<Ctl>[.<Prop>]` that is
|
|
2117
|
-
* consumed but NOT emitted.
|
|
2118
|
-
* 2. `Forms("<FormName>")!<Ctl>` (quoted or bare/bracketed form arg)
|
|
2119
|
-
* — paren form, ALWAYS with a control segment.
|
|
2120
|
-
*
|
|
2121
|
-
* Stripping: the form's identifier is unwrapped of `"` quotes (string
|
|
2122
|
-
* literals) and `[…]` brackets (Issue #54 reserved-identifier shape) so
|
|
2123
|
-
* the public `referenceName` is the bare form name. Bracketed forms
|
|
2124
|
-
* (`Forms![Mi Formulario]`) follow the same strip rules as the paren
|
|
2125
|
-
* form so the resolver sees `Mi Formulario` either way.
|
|
2126
|
-
*/
|
|
2127
|
-
scanFormsBang(line, from, lineNum) {
|
|
2128
|
-
VbaExtractor.FORMS_BANG_RE.lastIndex = 0;
|
|
2129
|
-
let m;
|
|
2130
|
-
while ((m = VbaExtractor.FORMS_BANG_RE.exec(line)) !== null) {
|
|
2131
|
-
// Group 1: bang form name (bare or `[bracketed]`); group 2: paren
|
|
2132
|
-
// form name in `"quotes"`; group 3: paren form name bare or
|
|
2133
|
-
// `[bracketed]`. Take whichever the regex alternative produced and
|
|
2134
|
-
// strip the surrounding decoration so the public referenceName is
|
|
2135
|
-
// the bare form identifier the resolver will compare against the
|
|
2136
|
-
// form's `form-layout` node name.
|
|
2137
|
-
const raw = m[1] ?? m[2] ?? m[3] ?? '';
|
|
2138
|
-
if (!raw)
|
|
2139
|
-
continue;
|
|
2140
|
-
const formName = raw
|
|
2141
|
-
.replace(/^"|"$/g, '') // strip surrounding string-literal quotes
|
|
2142
|
-
.replace(/^\[|\]$/g, ''); // strip surrounding bracket decoration (#54)
|
|
2143
|
-
if (!formName)
|
|
2144
|
-
continue;
|
|
2145
|
-
this.unresolvedReferences.push({
|
|
2146
|
-
fromNodeId: this.findOrCreateFunctionNodeId(from),
|
|
2147
|
-
referenceName: formName,
|
|
2148
|
-
referenceKind: 'references',
|
|
2149
|
-
line: lineNum,
|
|
2150
|
-
column: m.index, // start of the `Forms` keyword; the form name's column can be reconstructed by the UI if it cares
|
|
2151
|
-
filePath: this.filePath,
|
|
2152
|
-
language: 'vba',
|
|
2153
|
-
metadata: { synthesizedBy: 'vba-forms-bang' },
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
findOrCreateFunctionNodeId(proc) {
|
|
2158
|
-
// Fix 1: key the cache by `name:startLine` so Property Get/Let/Set
|
|
2159
|
-
// accessors with the same name each resolve to their own node.
|
|
2160
|
-
const cacheKey = `${proc.name}:${proc.startLine}`;
|
|
2161
|
-
const cached = this.procNodeIdCache.get(cacheKey);
|
|
2162
|
-
if (cached)
|
|
2163
|
-
return cached;
|
|
2164
|
-
// Prefer the startLine-indexed lookup (O(1), correct for all three
|
|
2165
|
-
// Property accessors). Fall back to the name-indexed cache for procs
|
|
2166
|
-
// whose startLine wasn't recorded (should not happen in practice).
|
|
2167
|
-
const fn = this.functionNodeByStartLine.get(proc.startLine) ??
|
|
2168
|
-
this.findFunctionNodeByName(proc.name);
|
|
2169
|
-
if (fn) {
|
|
2170
|
-
this.procNodeIdCache.set(cacheKey, fn.id);
|
|
2171
|
-
return fn.id;
|
|
2172
|
-
}
|
|
2173
|
-
// Fallback: synthesize a node id matching generateNodeId's input shape.
|
|
2174
|
-
const id = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'function', proc.name, proc.startLine);
|
|
2175
|
-
this.procNodeIdCache.set(cacheKey, id);
|
|
2176
|
-
return id;
|
|
2177
|
-
}
|
|
2178
|
-
findFunctionNodeByName(name) {
|
|
2179
|
-
// O(1) via the cache populated as function nodes are emitted in
|
|
2180
|
-
// `sweepProcedures`. Audit S2 (June 2026): the previous
|
|
2181
|
-
// `this.nodes.find(...)` was an O(n) linear scan per call site —
|
|
2182
|
-
// meaningful on real .cls files with hundreds of procedures.
|
|
2183
|
-
return this.functionNodeByName.get(name);
|
|
2184
|
-
}
|
|
2185
|
-
/**
|
|
2186
|
-
* Issue #45: split a single-line VBA `If <cond> Then <body>` into one or
|
|
2187
|
-
* more statement-clause fragments that the existing `detectStatementCall`
|
|
2188
|
-
* and `detectQualifiedStatementCall` detectors can process. Handles:
|
|
2189
|
-
*
|
|
2190
|
-
* - `If x Then Foo` → `['Foo']`
|
|
2191
|
-
* - `If x Then Foo Else Bar` → `['Foo', 'Bar']`
|
|
2192
|
-
* - `If x Then DoA: DoB` → `['DoA', 'DoB']` (colon-separated multi-statement)
|
|
2193
|
-
* - `If x Then Foo Else A: B` → `['Foo', 'A', 'B']`
|
|
2194
|
-
* - `If x Then GoTo fin` → `[]` (GoTo clause filtered out)
|
|
2195
|
-
* - `If x Then Exit Sub` → `[]` (Exit clause filtered out)
|
|
2196
|
-
*
|
|
2197
|
-
* When the line does NOT match a single-line `If … Then` shape — for
|
|
2198
|
-
* instance a block-form `If x Then` whose body lives on subsequent
|
|
2199
|
-
* lines — the splitter returns `[<line>]` (the original input) so
|
|
2200
|
-
* callers can use this method unconditionally and let the existing
|
|
2201
|
-
* per-line scan pick up the body on a separate line.
|
|
2202
|
-
*
|
|
2203
|
-
* `GoTo`, `Exit`, and `Resume` clauses are filtered at the fragment
|
|
2204
|
-
* level (defense in depth): even though `emitStatementCallEdge` already
|
|
2205
|
-
* drops these via the `CALL_KEYWORD_BLACKLIST`, filtering here prevents
|
|
2206
|
-
* any chance of `detectStatementCall`'s generic identifier extractor
|
|
2207
|
-
* matching a substring (e.g. an identifier like `GoToFinishingTouches`)
|
|
2208
|
-
* as a side effect of a richer clause where the keyword happens to be
|
|
2209
|
-
* the leading token.
|
|
2210
|
-
*
|
|
2211
|
-
* `line` is the string-literal-masked scan line. The mask makes global
|
|
2212
|
-
* `:` splitting safe: real VBA colons never appear inside string
|
|
2213
|
-
* literals (already masked to spaces) and never inside expressions
|
|
2214
|
-
* inside parens at the source level (a colon ends a statement in VBA,
|
|
2215
|
-
* so it cannot appear inside a parenthesised argument list either).
|
|
2216
|
-
* The `Else` keyword is a VBA statement-level separator and is
|
|
2217
|
-
* forbidden inside parens or expressions, so splitting on
|
|
2218
|
-
* `\s+Else\s+` does not need paren tracking either.
|
|
2219
|
-
*/
|
|
2220
|
-
splitSingleLineIfClauses(line) {
|
|
2221
|
-
const trimmed = line.trimStart();
|
|
2222
|
-
if (!trimmed)
|
|
2223
|
-
return [];
|
|
2224
|
-
// Match `If <cond> Then <body>` with a non-greedy condition. Requiring
|
|
2225
|
-
// at least one whitespace character after `Then` ensures the block
|
|
2226
|
-
// form `If x Then` (with the body on subsequent lines) is left alone
|
|
2227
|
-
// for the existing per-line call-site scan to handle on the body line.
|
|
2228
|
-
const ifThenRe = /^If\s[\s\S]+?\bThen\b\s+/i;
|
|
2229
|
-
const m = ifThenRe.exec(trimmed);
|
|
2230
|
-
if (!m) {
|
|
2231
|
-
// Not a single-line `If … Then` — preserve the original line so the
|
|
2232
|
-
// existing detection path picks it up unchanged.
|
|
2233
|
-
return [line];
|
|
2234
|
-
}
|
|
2235
|
-
const body = trimmed.slice(m[0].length);
|
|
2236
|
-
// Split on top-level `Else` (case-insensitive; word-bounded). The
|
|
2237
|
-
// statement-level-only nature of `Else` in VBA means the regex split
|
|
2238
|
-
// is safe without paren tracking on the masked-line invariant.
|
|
2239
|
-
const elseClauses = body.split(/\s+Else\s+/i);
|
|
2240
|
-
const clauses = [];
|
|
2241
|
-
for (const elseClause of elseClauses) {
|
|
2242
|
-
// Split each Else-clause on `:` for multi-statement single-line
|
|
2243
|
-
// `If` bodies. VBA expressions never contain `:`, so a global
|
|
2244
|
-
// split is correct on a masked line.
|
|
2245
|
-
const subStatements = elseClause.split(':');
|
|
2246
|
-
for (const sub of subStatements) {
|
|
2247
|
-
const t = sub.trim();
|
|
2248
|
-
if (!t)
|
|
2249
|
-
continue;
|
|
2250
|
-
// Defense in depth: GoTo / Exit / Resume are VBA control-flow
|
|
2251
|
-
// statements, not Sub calls — drop them before they reach the
|
|
2252
|
-
// statement-call detectors. (The existing
|
|
2253
|
-
// `CALL_KEYWORD_BLACKLIST` check in `emitStatementCallEdge`
|
|
2254
|
-
// also covers this; the fragment-level filter keeps the two
|
|
2255
|
-
// intent statements aligned and protects against any future
|
|
2256
|
-
// detector refactor that might relax the BLACKLIST check.)
|
|
2257
|
-
if (/^(?:GoTo|Exit|Resume)\b/i.test(t))
|
|
2258
|
-
continue;
|
|
2259
|
-
clauses.push(t);
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
return clauses;
|
|
2263
|
-
}
|
|
2264
|
-
/**
|
|
2265
|
-
* H1: detect a statement-form Sub call.
|
|
2266
|
-
*
|
|
2267
|
-
* Real VBA idioms:
|
|
2268
|
-
* `MySub` — bare no-argument statement call
|
|
2269
|
-
* `MySub arg1, Nz(x, 0)` — bare statement call
|
|
2270
|
-
* `Call MySub arg1, Nz(x, 0)` — Call keyword, no parens around call
|
|
2271
|
-
* `MySub(arg1, arg2)` — parens call (handled by CALL_RE)
|
|
2272
|
-
* `Call MySub(arg1, arg2)` — Call keyword + parens (also CALL_RE)
|
|
2273
|
-
*
|
|
2274
|
-
* Returns the called proc name if the line matches the statement form
|
|
2275
|
-
* AND the proc name is in localProcs (same-file resolution). Returns
|
|
2276
|
-
* null for declarations, assignments, comments, keyword lines, etc.
|
|
2277
|
-
*
|
|
2278
|
-
* The `Call` keyword form is handled by stripping `Call ` and
|
|
2279
|
-
* running the same logic on the remainder — they're structurally the
|
|
2280
|
-
* same call after the keyword.
|
|
2281
|
-
*/
|
|
2282
|
-
detectStatementCall(line) {
|
|
2283
|
-
let trimmed = line.trimStart();
|
|
2284
|
-
if (!trimmed)
|
|
2285
|
-
return null;
|
|
2286
|
-
// Strip `Call ` keyword if present — same call shape after.
|
|
2287
|
-
if (/^Call\s/i.test(trimmed)) {
|
|
2288
|
-
trimmed = trimmed.replace(/^Call\s+/i, '');
|
|
2289
|
-
}
|
|
2290
|
-
// Skip comment lines.
|
|
2291
|
-
if (trimmed.startsWith("'") || trimmed.startsWith('Rem '))
|
|
2292
|
-
return null;
|
|
2293
|
-
// Skip declarations: Dim/Private/Public/Static/Global/Const/ReDim.
|
|
2294
|
-
if (/^(Dim|Private|Public|Static|Global|Const|ReDim)\s/i.test(trimmed))
|
|
2295
|
-
return null;
|
|
2296
|
-
// Extract the leading identifier.
|
|
2297
|
-
const m = /^(\p{L}[\p{L}\p{N}_]*)/u.exec(trimmed);
|
|
2298
|
-
if (!m)
|
|
2299
|
-
return null;
|
|
2300
|
-
const procName = m[1] ?? '';
|
|
2301
|
-
const rest = trimmed.slice(procName.length);
|
|
2302
|
-
// `MySub(...)` is parens-form and already handled by CALL_RE. Parentheses
|
|
2303
|
-
// are valid later in statement-form argument expressions (`MySub Nz(x, 0)`),
|
|
2304
|
-
// so only the character immediately after the proc name distinguishes the
|
|
2305
|
-
// call form.
|
|
2306
|
-
if (rest.startsWith('('))
|
|
2307
|
-
return null;
|
|
2308
|
-
// Bare `MySub` is a valid no-argument statement-form Sub call.
|
|
2309
|
-
if (rest.length === 0)
|
|
2310
|
-
return procName;
|
|
2311
|
-
const nextCh = trimmed.charAt(procName.length);
|
|
2312
|
-
if (nextCh !== ' ' && nextCh !== '\t')
|
|
2313
|
-
return null;
|
|
2314
|
-
const args = rest.trimStart();
|
|
2315
|
-
// Skip leading-identifier assignments (`X = ...`). Do not reject `=` inside
|
|
2316
|
-
// argument expressions because named arguments use `:=` and comparisons can
|
|
2317
|
-
// appear in expressions.
|
|
2318
|
-
if (args.startsWith('='))
|
|
2319
|
-
return null;
|
|
2320
|
-
return procName;
|
|
2321
|
-
}
|
|
2322
|
-
/**
|
|
2323
|
-
* H1: emit a same-file `calls` edge for a statement-form Sub call.
|
|
2324
|
-
* `caller` is the current procedure (from the stack); `procName` is
|
|
2325
|
-
* the called procedure name. Emits a plain `calls` edge to the
|
|
2326
|
-
* already-emitted function node.
|
|
2327
|
-
*/
|
|
2328
|
-
emitStatementCallEdge(caller, procName, lineNum) {
|
|
2329
|
-
if (procName === caller.name)
|
|
2330
|
-
return; // skip self-call
|
|
2331
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(procName))
|
|
2332
|
-
return;
|
|
2333
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(procName))
|
|
2334
|
-
return;
|
|
2335
|
-
const target = this.findFunctionNodeByName(procName);
|
|
2336
|
-
if (!target)
|
|
2337
|
-
return;
|
|
2338
|
-
this.edges.push({
|
|
2339
|
-
source: this.findOrCreateFunctionNodeId(caller),
|
|
2340
|
-
target: target.id,
|
|
2341
|
-
kind: 'calls',
|
|
2342
|
-
line: lineNum,
|
|
2343
|
-
column: 0,
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Fix 7: detect a qualified statement-form call — `Receiver.Member <args>`
|
|
2348
|
-
* where `Receiver.Member` is NOT followed by `(`.
|
|
2349
|
-
*
|
|
2350
|
-
* Real VBA idioms this covers:
|
|
2351
|
-
* `m_NCOp.Registrar m_ARAlInicio, p_Error`
|
|
2352
|
-
* `m_Obj.Init`
|
|
2353
|
-
*
|
|
2354
|
-
* These are distinct from the paren form (`m_NCOp.Registrar(...)`) which is
|
|
2355
|
-
* already handled by `CALL_RE`. Returns `{receiver, member}` or `null`.
|
|
2356
|
-
*
|
|
2357
|
-
* Property assignments (`Receiver.Prop = value`) are excluded via the `=`
|
|
2358
|
-
* guard — consistent with `detectStatementCall`'s same-file assignment skip.
|
|
2359
|
-
* Blacklisted receivers (runtime objects, keywords) are excluded too.
|
|
2360
|
-
*/
|
|
2361
|
-
detectQualifiedStatementCall(line) {
|
|
2362
|
-
let trimmed = line.trimStart();
|
|
2363
|
-
if (!trimmed)
|
|
2364
|
-
return null;
|
|
2365
|
-
// Strip `Call` keyword — same call shape after it.
|
|
2366
|
-
if (/^Call\s/i.test(trimmed))
|
|
2367
|
-
trimmed = trimmed.replace(/^Call\s+/i, '');
|
|
2368
|
-
// Skip comment lines.
|
|
2369
|
-
if (trimmed.startsWith("'") || /^Rem(\s|$)/i.test(trimmed))
|
|
2370
|
-
return null;
|
|
2371
|
-
// Skip declarations.
|
|
2372
|
-
if (/^(Dim|Private|Public|Static|Global|Const|ReDim)\s/i.test(trimmed))
|
|
2373
|
-
return null;
|
|
2374
|
-
// Issue #54: the receiver alternative accepts BOTH the bare form
|
|
2375
|
-
// (`Foo`) and the VBA bracketed form (`[Foo Bar]`). The bracketed
|
|
2376
|
-
// alternative wins when present; the captured value is already
|
|
2377
|
-
// unwrapped by the regex (it captures only the inner content, not
|
|
2378
|
-
// the surrounding `[...]`). The same shape applies to the member.
|
|
2379
|
-
const receiverM = /^(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*))/u.exec(trimmed);
|
|
2380
|
-
if (!receiverM)
|
|
2381
|
-
return null;
|
|
2382
|
-
const receiver = receiverM[1] ?? receiverM[2] ?? '';
|
|
2383
|
-
const rest = trimmed.slice(receiverM[0].length);
|
|
2384
|
-
// Must have a dot separator.
|
|
2385
|
-
if (!rest.startsWith('.'))
|
|
2386
|
-
return null;
|
|
2387
|
-
// Extract member identifier.
|
|
2388
|
-
const memberRest = rest.slice(1); // skip the dot
|
|
2389
|
-
const memberM = /^(?:\[([^\]]+)\]|(\p{L}[\p{L}\p{N}_]*))/u.exec(memberRest);
|
|
2390
|
-
if (!memberM)
|
|
2391
|
-
return null;
|
|
2392
|
-
const member = memberM[1] ?? memberM[2] ?? '';
|
|
2393
|
-
const afterMember = memberRest.slice(memberM[0].length);
|
|
2394
|
-
// Must NOT be followed by `(` — the paren form is handled by CALL_RE.
|
|
2395
|
-
if (afterMember.startsWith('('))
|
|
2396
|
-
return null;
|
|
2397
|
-
// Must be followed by space/tab (args present) OR end of line (no args).
|
|
2398
|
-
if (afterMember.length > 0) {
|
|
2399
|
-
const ch = afterMember.charAt(0);
|
|
2400
|
-
if (ch !== ' ' && ch !== '\t')
|
|
2401
|
-
return null;
|
|
2402
|
-
// Skip property assignments: `Receiver.Prop = value`.
|
|
2403
|
-
const argsText = afterMember.trimStart();
|
|
2404
|
-
if (argsText.startsWith('='))
|
|
2405
|
-
return null;
|
|
2406
|
-
}
|
|
2407
|
-
// Respect the keyword and runtime blacklists.
|
|
2408
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(receiver))
|
|
2409
|
-
return null;
|
|
2410
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(receiver))
|
|
2411
|
-
return null;
|
|
2412
|
-
if (VbaExtractor.CALL_KEYWORD_BLACKLIST.has(member))
|
|
2413
|
-
return null;
|
|
2414
|
-
if (VbaExtractor.RUNTIME_RECEIVER_BLACKLIST.has(member))
|
|
2415
|
-
return null;
|
|
2416
|
-
return { receiver, member };
|
|
2417
|
-
}
|
|
2418
|
-
/**
|
|
2419
|
-
* Fix 7: emit a heuristic `calls` edge for a qualified statement-form call.
|
|
2420
|
-
* Same shape as the qualified-paren path in `scanCallSites` — reuses the
|
|
2421
|
-
* same `callDedupe` / `synthFunctionNodeIds` sets for de-duplication so a
|
|
2422
|
-
* paren and non-paren form on the same line don't create duplicate edges.
|
|
2423
|
-
*/
|
|
2424
|
-
emitQualifiedStatementCallEdge(caller, receiver, member, lineNum) {
|
|
2425
|
-
// Qualified-call eligibility is checked before this method. Project-class
|
|
2426
|
-
// local receivers resolve to their class name (for exact `.cls` method
|
|
2427
|
-
// matching), while undeclared receivers stay as raw module-name candidates.
|
|
2428
|
-
const receiverType = this.resolveReceiverType(receiver);
|
|
2429
|
-
const qualified = `${receiverType}.${member}`;
|
|
2430
|
-
const dedupeKey = `${caller.name}->${qualified}@${lineNum}`;
|
|
2431
|
-
if (this.callDedupe.has(dedupeKey))
|
|
2432
|
-
return;
|
|
2433
|
-
this.callDedupe.add(dedupeKey);
|
|
2434
|
-
const synthId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'function', qualified, lineNum);
|
|
2435
|
-
if (!this.synthFunctionNodeIds.has(synthId)) {
|
|
2436
|
-
this.synthFunctionNodeIds.add(synthId);
|
|
2437
|
-
this.nodes.push({
|
|
2438
|
-
id: synthId,
|
|
2439
|
-
kind: 'function',
|
|
2440
|
-
name: qualified,
|
|
2441
|
-
qualifiedName: qualified,
|
|
2442
|
-
filePath: this.filePath,
|
|
2443
|
-
language: 'vba',
|
|
2444
|
-
startLine: lineNum,
|
|
2445
|
-
endLine: lineNum,
|
|
2446
|
-
startColumn: 0,
|
|
2447
|
-
endColumn: qualified.length,
|
|
2448
|
-
visibility: 'public',
|
|
2449
|
-
metadata: { stub: true },
|
|
2450
|
-
updatedAt: Date.now(),
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
this.edges.push({
|
|
2454
|
-
source: this.findOrCreateFunctionNodeId(caller),
|
|
2455
|
-
target: synthId,
|
|
2456
|
-
kind: 'calls',
|
|
2457
|
-
provenance: 'heuristic',
|
|
2458
|
-
metadata: {
|
|
2459
|
-
synthesizedBy: 'vba-name-resolution',
|
|
2460
|
-
stub: true,
|
|
2461
|
-
receiverType,
|
|
2462
|
-
member,
|
|
2463
|
-
},
|
|
2464
|
-
line: lineNum,
|
|
2465
|
-
column: 0,
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
/**
|
|
2469
|
-
* B4 (hueco 6) extended by Issue #48: scan one line of VBA source for
|
|
2470
|
-
* `DoCmd.OpenX "Target"` calls where X ∈ {Form, Report} (see the
|
|
2471
|
-
* `DOCMD_OPEN_DISPATCH` table). For each match, emit:
|
|
2472
|
-
* - a stub node (form-layout / report-layout) for the target, cached
|
|
2473
|
-
* per-(method, name) so the same target referenced from N sites
|
|
2474
|
-
* emits exactly ONE stub,
|
|
2475
|
-
* - an `opens-form` / `opens-report` heuristic edge from the calling
|
|
2476
|
-
* Sub to that stub.
|
|
2477
|
-
*
|
|
2478
|
-
* Both endpoints are pushed into `this.nodes` / `this.edges`, so the
|
|
2479
|
-
* per-file edge filter at `index.ts:insertedIds.has(source) &&
|
|
2480
|
-
* insertedIds.has(target)` passes the edge naturally without any
|
|
2481
|
-
* exemption to the filter.
|
|
2482
|
-
*
|
|
2483
|
-
* Why a stub and not a direct lookup: the target form/report lives in a
|
|
2484
|
-
* DIFFERENT file (its own `.form.txt` / `.report.txt`), and the extractor
|
|
2485
|
-
* doesn't have DB access at parse time. The stub's synthetic file path
|
|
2486
|
-
* (`synthetic:opensFormStub/<Name>.form.txt` /
|
|
2487
|
-
* `synthetic:opensReportStub/<Name>.form.txt`) guarantees a deterministic
|
|
2488
|
-
* node id so re-indexes collapse to the same stub. When the consumer's
|
|
2489
|
-
* `.form.txt` / `.report.txt` is later indexed, the real
|
|
2490
|
-
* `form-layout` / `report-layout` node carries a different id (it uses
|
|
2491
|
-
* the real file path); the stub and the real coexist harmlessly.
|
|
2492
|
-
*
|
|
2493
|
-
* Why a separate dispatch from CALL_RE: `DoCmd` is in
|
|
2494
|
-
* `RUNTIME_RECEIVER_BLACKLIST` (R4 invariant), so `DoCmd.OpenForm` /
|
|
2495
|
-
* `DoCmd.OpenReport` are intentionally SKIPPED by the generic CALL_RE
|
|
2496
|
-
* path that would otherwise emit a junk `calls` edge to a synthetic
|
|
2497
|
-
* `function` node for `DoCmd.OpenX`. The dispatch below matches BEFORE
|
|
2498
|
-
* the call-site scan and uses its own emission path.
|
|
2499
|
-
*
|
|
2500
|
-
* Scope note: literal-string and bare-identifier argument forms are
|
|
2501
|
-
* supported. Bare identifiers resolve only through local `Const`
|
|
2502
|
-
* declarations; arbitrary variable data-flow remains intentionally
|
|
2503
|
-
* out of scope.
|
|
2504
|
-
*/
|
|
2505
|
-
scanDoCmdOpenCalls(line, caller, lineNum) {
|
|
2506
|
-
for (const dispatch of VbaExtractor.DOCMD_OPEN_DISPATCH) {
|
|
2507
|
-
// Each regex has /g so we MUST reset `lastIndex` before use; cloning
|
|
2508
|
-
// the regex is the simplest way to avoid leaking state across lines
|
|
2509
|
-
// AND across dispatch iterations.
|
|
2510
|
-
const localRe = new RegExp(dispatch.re.source, dispatch.re.flags);
|
|
2511
|
-
let m;
|
|
2512
|
-
while ((m = localRe.exec(line)) !== null) {
|
|
2513
|
-
const rawArg = (m[1] ?? '').trim();
|
|
2514
|
-
// Issue #52: const lookup is now per-proc-bucket with module
|
|
2515
|
-
// fallback (see `resolveLocalConst`). Two procs declaring the
|
|
2516
|
-
// same Const name with different values no longer collide —
|
|
2517
|
-
// each call site uses the value visible at its own scope.
|
|
2518
|
-
const targetName = rawArg.startsWith('"')
|
|
2519
|
-
? unwrapVbaStringLiteral(rawArg)
|
|
2520
|
-
: (this.resolveLocalConst(rawArg) ?? rawArg);
|
|
2521
|
-
if (!targetName)
|
|
2522
|
-
continue;
|
|
2523
|
-
this.emitOpensStubEdge(dispatch, caller, targetName, lineNum, m.index);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
/**
|
|
2528
|
-
* B4 (hueco 6) extended by Issue #48: emit a stub `form-layout` /
|
|
2529
|
-
* `report-layout` node for `targetName` (cached per dispatch entry so
|
|
2530
|
-
* duplicates collapse and OpenForm/OpenReport de-dup buckets stay
|
|
2531
|
-
* disjoint) and a single `opens-form` / `opens-report` heuristic edge
|
|
2532
|
-
* from `caller` to that stub.
|
|
2533
|
-
*
|
|
2534
|
-
* The edge carries:
|
|
2535
|
-
* - `kind` — dispatch-specific (`opens-form` / `opens-report`)
|
|
2536
|
-
* - `provenance: 'heuristic'` — synthesized, not parsed
|
|
2537
|
-
* - `metadata.<dispatchTargetKey>` (e.g. `targetFormName`) — the resolved name
|
|
2538
|
-
* - `metadata.synthesizedBy` — dispatch-specific (`vba-opens-form` /
|
|
2539
|
-
* `vba-opens-report`); distinguishes this synthesis from the
|
|
2540
|
-
* dim/sql/event-handler families
|
|
2541
|
-
*
|
|
2542
|
-
* The stub's `metadata.stub: true` flag lets downstream UI render
|
|
2543
|
-
* stubs distinctly (e.g. with a dashed border) and gives later
|
|
2544
|
-
* re-resolution pass a hook for collapse. The stub is
|
|
2545
|
-
* line-independent (`line = 0`) so re-indexes produce identical ids.
|
|
2546
|
-
*/
|
|
2547
|
-
emitOpensStubEdge(dispatch, caller, targetName, lineNum, column) {
|
|
2548
|
-
const key = `${dispatch.cacheKey}:${targetName.toLowerCase()}`;
|
|
2549
|
-
let stubId = this.opensStubIdsByKey.get(key);
|
|
2550
|
-
if (!stubId) {
|
|
2551
|
-
// Synthetic file path keeps the stub's id deterministic AND
|
|
2552
|
-
// disambiguates it from any real `.form.txt` / `.report.txt`
|
|
2553
|
-
// indexed later. The directory prefix (`synthetic:opensFormStub/`
|
|
2554
|
-
// or `synthetic:opensReportStub/`) is intentionally not a real
|
|
2555
|
-
// filesystem path — it just namespaces the id space. The file
|
|
2556
|
-
// extension (`syntheticExtension`) DOES mirror the real form/report
|
|
2557
|
-
// file extension so a reader of the synthetic path can tell the
|
|
2558
|
-
// stub's intent at a glance.
|
|
2559
|
-
const syntheticFilePath = `${dispatch.syntheticPrefix}/${targetName}${dispatch.syntheticExtension}`;
|
|
2560
|
-
stubId = (0, tree_sitter_helpers_1.generateNodeId)(syntheticFilePath, dispatch.stubKind, targetName, 0);
|
|
2561
|
-
this.opensStubIdsByKey.set(key, stubId);
|
|
2562
|
-
this.nodes.push({
|
|
2563
|
-
id: stubId,
|
|
2564
|
-
kind: dispatch.stubKind,
|
|
2565
|
-
name: targetName,
|
|
2566
|
-
// Convention: form module names in Access are `Form_<Name>` and
|
|
2567
|
-
// report module names are `Report_<Name>`. We follow the same
|
|
2568
|
-
// convention in the synthetic stub's qualifiedName so cross-file
|
|
2569
|
-
// lookups can find it consistently.
|
|
2570
|
-
qualifiedName: `${dispatch.moduleNamePrefix}${targetName}`,
|
|
2571
|
-
filePath: syntheticFilePath,
|
|
2572
|
-
language: 'vba',
|
|
2573
|
-
startLine: lineNum,
|
|
2574
|
-
endLine: lineNum,
|
|
2575
|
-
startColumn: 0,
|
|
2576
|
-
endColumn: 0,
|
|
2577
|
-
metadata: { stub: true },
|
|
2578
|
-
updatedAt: Date.now(),
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
this.edges.push({
|
|
2582
|
-
source: this.findOrCreateFunctionNodeId(caller),
|
|
2583
|
-
target: stubId,
|
|
2584
|
-
kind: dispatch.edgeKind,
|
|
2585
|
-
provenance: 'heuristic',
|
|
2586
|
-
metadata: {
|
|
2587
|
-
synthesizedBy: dispatch.synthesizedBy,
|
|
2588
|
-
[dispatch.metadataTargetKey]: targetName,
|
|
2589
|
-
},
|
|
2590
|
-
line: lineNum,
|
|
2591
|
-
column,
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
/**
|
|
2595
|
-
* Issue #48: scan one line of VBA source for `DoCmd.OpenQuery "X"` calls.
|
|
2596
|
-
* Each match emits ONE `UnresolvedReference` (NOT a stub + edge) so the
|
|
2597
|
-
* resolver binds to the REAL `query` node that `SqlQueryExtractor`
|
|
2598
|
-
* produces for `queries/<Name>.sql` (dysflow exports every saved QueryDef
|
|
2599
|
-
* + `queries.json` manifest). Falls back to silent when the .sql is not
|
|
2600
|
-
* yet in the index — the resolver does the binding when it's later
|
|
2601
|
-
* indexed, exactly like `vba-me-control` and `vba-forms-bang`.
|
|
2602
|
-
*
|
|
2603
|
-
* Companion to `scanDoCmdOpenCalls` but intentionally NOT in the
|
|
2604
|
-
* dispatch table — OpenQuery's emission shape (`UnresolvedReference`)
|
|
2605
|
-
* is structurally different from OpenForm/OpenReport's (synthetic node
|
|
2606
|
-
* + heuristic edge). The two pipelines share the literal-vs-Const
|
|
2607
|
-
* argument resolution pattern via `localConstants.get(...)` but emit
|
|
2608
|
-
* via two different branches of `unresolvedReferences` vs
|
|
2609
|
-
* `nodes`/`edges`.
|
|
2610
|
-
*
|
|
2611
|
-
* UnresolvedReference shape (per Issue #48 spec — must match
|
|
2612
|
-
* SqlQueryExtractor's query node name exactly):
|
|
2613
|
-
* - `referenceName` = resolved query name
|
|
2614
|
-
* - `referenceKind: 'references'` = same kind the resolver binds
|
|
2615
|
-
* - `metadata.synthesizedBy: 'vba-opens-query'`
|
|
2616
|
-
* - NO synthetic `function` node (W4 graph-pollution invariant — the
|
|
2617
|
-
* real `query` node already exists in the index once `.sql` is
|
|
2618
|
-
* processed, and creating stubs would compete with the binding).
|
|
2619
|
-
*/
|
|
2620
|
-
scanDoCmdOpenQuery(line, caller, lineNum) {
|
|
2621
|
-
const localRe = new RegExp(VbaExtractor.OPEN_QUERY_ARG_RE.source, VbaExtractor.OPEN_QUERY_ARG_RE.flags);
|
|
2622
|
-
let m;
|
|
2623
|
-
while ((m = localRe.exec(line)) !== null) {
|
|
2624
|
-
const rawArg = (m[1] ?? '').trim();
|
|
2625
|
-
// Issue #52: same per-proc-with-module-fallback lookup as
|
|
2626
|
-
// `scanDoCmdOpenCalls` — proc-local consts resolve to their own
|
|
2627
|
-
// values, so a `DoCmd.OpenQuery LOCAL_QUERY` inside `Sub X()`
|
|
2628
|
-
// points at the correct query even when another proc declares a
|
|
2629
|
-
// different `LOCAL_QUERY` (pre-fix this was whichever wrote
|
|
2630
|
-
// the file-wide map last).
|
|
2631
|
-
const targetName = rawArg.startsWith('"')
|
|
2632
|
-
? unwrapVbaStringLiteral(rawArg)
|
|
2633
|
-
: (this.resolveLocalConst(rawArg) ?? rawArg);
|
|
2634
|
-
if (!targetName)
|
|
2635
|
-
continue;
|
|
2636
|
-
this.unresolvedReferences.push({
|
|
2637
|
-
fromNodeId: this.findOrCreateFunctionNodeId(caller),
|
|
2638
|
-
referenceName: targetName,
|
|
2639
|
-
referenceKind: 'references',
|
|
2640
|
-
line: lineNum,
|
|
2641
|
-
column: m.index,
|
|
2642
|
-
filePath: this.filePath,
|
|
2643
|
-
language: 'vba',
|
|
2644
|
-
metadata: { synthesizedBy: 'vba-opens-query' },
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
/**
|
|
2649
|
-
* Regex matching the chained `& "..."` literals that may follow a
|
|
2650
|
-
* wrapper's first literal on the same physical line. Captures the
|
|
2651
|
-
* literal CONTENT (group 1); the surrounding `&` and quotes are
|
|
2652
|
-
* structural, not data. VBA allows whitespace around `&` and around
|
|
2653
|
-
* the inner quotes — handled with `\s*`. The `((?:[^"]|"")*)` body
|
|
2654
|
-
* mirrors the wrapper regex so a `""` inside a chained literal still
|
|
2655
|
-
* decodes to a single `"`.
|
|
2656
|
-
*
|
|
2657
|
-
* Cross-physical-line concat via `_` continuation is OUT OF SCOPE for
|
|
2658
|
-
* v1 (deferred; see commit message).
|
|
2659
|
-
*/
|
|
2660
|
-
static SQL_WRAPPER_CHAIN_RE = /&\s*"((?:[^"]|"")*)"/g;
|
|
2661
|
-
/**
|
|
2662
|
-
* Given the text that follows a SQL wrapper's first literal on the same
|
|
2663
|
-
* physical line, return the contents of every `& "..."` chained literal
|
|
2664
|
-
* in source order. Operates per-physical-line only — VBA `_` line
|
|
2665
|
-
* continuation across physical lines is handled separately by
|
|
2666
|
-
* `collectStringLiteralText` for the variable-assignment path.
|
|
2667
|
-
*/
|
|
2668
|
-
collectSqlWrapperChain(rest) {
|
|
2669
|
-
const out = [];
|
|
2670
|
-
const re = new RegExp(VbaExtractor.SQL_WRAPPER_CHAIN_RE.source, VbaExtractor.SQL_WRAPPER_CHAIN_RE.flags);
|
|
2671
|
-
let m;
|
|
2672
|
-
while ((m = re.exec(rest)) !== null) {
|
|
2673
|
-
out.push(m[1] ?? '');
|
|
2674
|
-
}
|
|
2675
|
-
return out;
|
|
2676
|
-
}
|
|
2677
|
-
scanSqlInLine(line, lineNum, dedupe, sqlVariables) {
|
|
2678
|
-
for (const { re } of VbaExtractor.SQL_WRAPPERS) {
|
|
2679
|
-
// Each wrapper regex is stateful (has /g); reset before use.
|
|
2680
|
-
const localRe = new RegExp(re.source, re.flags);
|
|
2681
|
-
let m;
|
|
2682
|
-
while ((m = localRe.exec(line)) !== null) {
|
|
2683
|
-
const firstLiteral = m[1] ?? '';
|
|
2684
|
-
// After the wrapper regex consumes up to and including the closing
|
|
2685
|
-
// `"` of the first literal, walk the rest of the line for any
|
|
2686
|
-
// `& "..."` chains and concatenate every literal's content. Joining
|
|
2687
|
-
// with a space (mirrors `collectStringLiteralText`) keeps adjacent
|
|
2688
|
-
// `FROM tblA` & `FROM tblB` separated so `SQL_TABLE_RE` finds both.
|
|
2689
|
-
const rest = line.slice(m.index + m[0].length);
|
|
2690
|
-
const chain = this.collectSqlWrapperChain(rest);
|
|
2691
|
-
const joined = [firstLiteral, ...chain].join(' ');
|
|
2692
|
-
this.emitSqlTableReferences(joined, lineNum, dedupe);
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
const localRe = new RegExp(VbaExtractor.SQL_VAR_EXEC_RE.source, VbaExtractor.SQL_VAR_EXEC_RE.flags);
|
|
2696
|
-
let vm;
|
|
2697
|
-
while ((vm = localRe.exec(line)) !== null) {
|
|
2698
|
-
const varName = (vm[1] ?? '').toLowerCase();
|
|
2699
|
-
const sqlString = sqlVariables.get(varName);
|
|
2700
|
-
if (!sqlString)
|
|
2701
|
-
continue;
|
|
2702
|
-
this.emitSqlTableReferences(sqlString, lineNum, dedupe);
|
|
2703
|
-
}
|
|
2704
|
-
// Issue #42: `DoCmd.RunSQL <identifier>` (variable form). Mirrors the
|
|
2705
|
-
// SQL_VAR_EXEC_RE path above but for the Access-style `DoCmd.RunSQL`
|
|
2706
|
-
// idiom — the dominant pattern in real-world VBA modules. Resolve the
|
|
2707
|
-
// captured identifier against `sqlVariables` (populated by
|
|
2708
|
-
// `trackSqlVariableAssignment` with `&`-accumulate semantics, Issue
|
|
2709
|
-
// #13) and feed the resolved SQL string into `emitSqlTableReferences`.
|
|
2710
|
-
// Unresolved identifiers (no row in the map) are silently skipped —
|
|
2711
|
-
// same graceful-no-op contract as SQL_VAR_EXEC_RE.
|
|
2712
|
-
const docmdLocalRe = new RegExp(VbaExtractor.SQL_VAR_DOCMD_RUNSQL_RE.source, VbaExtractor.SQL_VAR_DOCMD_RUNSQL_RE.flags);
|
|
2713
|
-
let dm;
|
|
2714
|
-
while ((dm = docmdLocalRe.exec(line)) !== null) {
|
|
2715
|
-
const varName = (dm[1] ?? '').toLowerCase();
|
|
2716
|
-
const sqlString = sqlVariables.get(varName);
|
|
2717
|
-
if (!sqlString)
|
|
2718
|
-
continue;
|
|
2719
|
-
this.emitSqlTableReferences(sqlString, lineNum, dedupe);
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
/**
|
|
2723
|
-
* #13 fix: `sql = sql & "..."` (self-referential concatenation) must
|
|
2724
|
-
* ACCUMULATE the new fragment onto whatever was already tracked for
|
|
2725
|
-
* `varName`, not overwrite it. Overwriting silently dropped earlier
|
|
2726
|
-
* fragments' tables — typically the initial `FROM <table>` in
|
|
2727
|
-
* `sql = "SELECT * FROM tblA"` followed by `sql = sql & " WHERE x=1"`.
|
|
2728
|
-
*
|
|
2729
|
-
* Detection: the RHS (`m[2]`, trimmed) starts with `<varName> &`,
|
|
2730
|
-
* case-insensitively — matching VBA's case-insensitive identifiers (`Sql`
|
|
2731
|
-
* and `sql` are the same variable). A genuine fresh assignment (RHS does
|
|
2732
|
-
* NOT start with the self-reference) still RESETS tracking — that
|
|
2733
|
-
* behavior is unchanged.
|
|
2734
|
-
*/
|
|
2735
|
-
trackSqlVariableAssignment(lines, lineIndex, sqlVariables) {
|
|
2736
|
-
const line = lines[lineIndex] ?? '';
|
|
2737
|
-
const m = VbaExtractor.SQL_VAR_ASSIGN_RE.exec(line);
|
|
2738
|
-
if (!m)
|
|
2739
|
-
return;
|
|
2740
|
-
const rawVarName = m[1] ?? '';
|
|
2741
|
-
const varName = rawVarName.toLowerCase();
|
|
2742
|
-
const rhs = (m[2] ?? '').trim();
|
|
2743
|
-
const newFragment = this.collectStringLiteralText(lines, lineIndex);
|
|
2744
|
-
if (!newFragment)
|
|
2745
|
-
return;
|
|
2746
|
-
const selfRefRe = new RegExp(`^${escapeRegExpLiteral(rawVarName)}\\s*&`, 'i');
|
|
2747
|
-
const existing = sqlVariables.get(varName);
|
|
2748
|
-
if (existing !== undefined && selfRefRe.test(rhs)) {
|
|
2749
|
-
sqlVariables.set(varName, `${existing} ${newFragment}`);
|
|
2750
|
-
}
|
|
2751
|
-
else {
|
|
2752
|
-
sqlVariables.set(varName, newFragment);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
collectStringLiteralText(lines, startIndex) {
|
|
2756
|
-
const fragments = [];
|
|
2757
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
2758
|
-
const line = lines[i] ?? '';
|
|
2759
|
-
for (const lit of (0, vba_preprocess_1.extractStringLiterals)(line)) {
|
|
2760
|
-
fragments.push(lit.text);
|
|
2761
|
-
}
|
|
2762
|
-
if (!line.trimEnd().endsWith('&'))
|
|
2763
|
-
break;
|
|
2764
|
-
}
|
|
2765
|
-
return fragments.join(' ');
|
|
2766
|
-
}
|
|
2767
|
-
emitSqlTableReferences(sqlString, lineNum, dedupe) {
|
|
2768
|
-
// Scan the SQL string for FROM/INTO/UPDATE <table>.
|
|
2769
|
-
// Preserve the source regex's `/u` flag (Unicode property classes)
|
|
2770
|
-
// — hardcoding `'gi'` here would silently break non-ASCII identifiers.
|
|
2771
|
-
const tableRe = new RegExp(VbaExtractor.SQL_TABLE_RE.source, VbaExtractor.SQL_TABLE_RE.flags);
|
|
2772
|
-
let tm;
|
|
2773
|
-
while ((tm = tableRe.exec(sqlString)) !== null) {
|
|
2774
|
-
let table = (tm[1] ?? '').replace(/[\[\]]/g, '');
|
|
2775
|
-
if (!table)
|
|
2776
|
-
continue;
|
|
2777
|
-
const key = `${lineNum}:${table}`;
|
|
2778
|
-
if (dedupe.has(key))
|
|
2779
|
-
continue;
|
|
2780
|
-
dedupe.add(key);
|
|
2781
|
-
this.emitReference(table, lineNum, 0, 'vba-sql-table');
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
/**
|
|
2785
|
-
* Emit a `references` edge from the file's module/class node to a synthetic
|
|
2786
|
-
* node named `targetName`. Used by Dim, WithEvents, Implements, and SQL.
|
|
2787
|
-
*/
|
|
2788
|
-
emitReference(targetName, lineNum, column, synthesizedBy) {
|
|
2789
|
-
if (!targetName)
|
|
2790
|
-
return;
|
|
2791
|
-
// Fix 5: key the synthetic node id on (filePath, kind, name) WITHOUT
|
|
2792
|
-
// lineNum so the same type/table referenced on N different lines produces
|
|
2793
|
-
// exactly ONE node. The edge carries the per-site `line`/`column`.
|
|
2794
|
-
const targetId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'class', // placeholder kind; cross-file resolution will re-type at lookup
|
|
2795
|
-
targetName, 0);
|
|
2796
|
-
if (this.synthClassNodeIds.has(targetId)) {
|
|
2797
|
-
// Node already emitted for this name — only add the edge below.
|
|
2798
|
-
}
|
|
2799
|
-
else {
|
|
2800
|
-
this.synthClassNodeIds.add(targetId);
|
|
2801
|
-
this.nodes.push({
|
|
2802
|
-
id: targetId,
|
|
2803
|
-
kind: 'class',
|
|
2804
|
-
name: targetName,
|
|
2805
|
-
qualifiedName: targetName,
|
|
2806
|
-
filePath: this.filePath,
|
|
2807
|
-
language: 'vba',
|
|
2808
|
-
startLine: lineNum,
|
|
2809
|
-
endLine: lineNum,
|
|
2810
|
-
startColumn: column,
|
|
2811
|
-
endColumn: column + targetName.length,
|
|
2812
|
-
updatedAt: Date.now(),
|
|
2813
|
-
});
|
|
2814
|
-
}
|
|
2815
|
-
const edge = {
|
|
2816
|
-
source: '', // rewritten after module node exists
|
|
2817
|
-
target: targetId,
|
|
2818
|
-
kind: 'references',
|
|
2819
|
-
provenance: 'heuristic',
|
|
2820
|
-
metadata: { synthesizedBy },
|
|
2821
|
-
line: lineNum,
|
|
2822
|
-
column,
|
|
2823
|
-
};
|
|
2824
|
-
this.edges.push(edge);
|
|
2825
|
-
this.pendingModuleOrClassSource.push(edge);
|
|
2826
|
-
}
|
|
2827
|
-
synthClassNodeIds = new Set();
|
|
2828
|
-
/**
|
|
2829
|
-
* Issue #50: TempVars placeholder-node de-dup cache. Keys are the
|
|
2830
|
-
* deterministic node ids produced by `emitTempVarReference` — those
|
|
2831
|
-
* ids intentionally ignore `this.filePath` (use a synthetic
|
|
2832
|
-
* `synthetic:tempvar/<key>` path instead) so the SAME key referenced
|
|
2833
|
-
* from Form_A.cls AND Form_B.cls collapses to ONE placeholder node.
|
|
2834
|
-
* Cross-file id stability is the cross-form state premise that makes
|
|
2835
|
-
* `codegraph_explore` connect producer ⇄ consumer in one hop.
|
|
2836
|
-
*/
|
|
2837
|
-
synthTempVarNodeIds = new Set();
|
|
2838
|
-
/**
|
|
2839
|
-
* Issue #50: emit one TempVar reading/writing site. Per call:
|
|
2840
|
-
* - placeholder `class` node keyed on the synthetic
|
|
2841
|
-
* `synthetic:tempvar/<key>` file path so cross-file extraction
|
|
2842
|
-
* calls collapse to one node per key,
|
|
2843
|
-
* - `references` edge from the calling `function` node
|
|
2844
|
-
* (via `findOrCreateFunctionNodeId` — same access pattern as
|
|
2845
|
-
* `scanDoCmdOpenCalls` / `scanDoCmdOpenQuery`) carrying
|
|
2846
|
-
* `metadata.synthesizedBy: 'vba-tempvar'` AND
|
|
2847
|
-
* `metadata.access: 'read' | 'write'`.
|
|
2848
|
-
*
|
|
2849
|
-
* Reuses the synthetic-`class`-placeholder shape established by
|
|
2850
|
-
* `emitReference` for SQL tables, events, Dim types, etc. — so every
|
|
2851
|
-
* synthesized `references` edge in the file already carries the same
|
|
2852
|
-
* `kind: 'class'` target, and downstream consumers (UI, resolvers,
|
|
2853
|
-
* search queries) filter on `metadata.synthesizedBy` rather than
|
|
2854
|
-
* NodeKind anyway. The `metadata.synthesizedBy` tag cleanly distinguishes
|
|
2855
|
-
* TempVars refs from SQL-table refs inside the same NodeKind bucket.
|
|
2856
|
-
*
|
|
2857
|
-
* Skips when stack is empty (the writer/reader is module-level code
|
|
2858
|
-
* — REQ-CODE-4 "unresolvable/runtime reference is silent"). The
|
|
2859
|
-
* spec wires this per-proc only; the module-level shape would need
|
|
2860
|
-
* a different emission (no procedure source) and is out of scope.
|
|
2861
|
-
*/
|
|
2862
|
-
emitTempVarReference(caller, key, lineNum, column, access) {
|
|
2863
|
-
if (!caller)
|
|
2864
|
-
return;
|
|
2865
|
-
if (!key)
|
|
2866
|
-
return;
|
|
2867
|
-
const syntheticFilePath = `synthetic:tempvar/${key}`;
|
|
2868
|
-
const targetId = (0, tree_sitter_helpers_1.generateNodeId)(syntheticFilePath, 'class', // placeholder kind — see SQL_TABLE_RE emitReference for precedent
|
|
2869
|
-
key, 0);
|
|
2870
|
-
if (!this.synthTempVarNodeIds.has(targetId)) {
|
|
2871
|
-
this.synthTempVarNodeIds.add(targetId);
|
|
2872
|
-
this.nodes.push({
|
|
2873
|
-
id: targetId,
|
|
2874
|
-
kind: 'class',
|
|
2875
|
-
name: key,
|
|
2876
|
-
qualifiedName: key,
|
|
2877
|
-
filePath: syntheticFilePath,
|
|
2878
|
-
language: 'vba',
|
|
2879
|
-
startLine: lineNum,
|
|
2880
|
-
endLine: lineNum,
|
|
2881
|
-
startColumn: column,
|
|
2882
|
-
endColumn: column + key.length,
|
|
2883
|
-
updatedAt: Date.now(),
|
|
2884
|
-
});
|
|
2885
|
-
}
|
|
2886
|
-
this.edges.push({
|
|
2887
|
-
source: this.findOrCreateFunctionNodeId(caller),
|
|
2888
|
-
target: targetId,
|
|
2889
|
-
kind: 'references',
|
|
2890
|
-
provenance: 'heuristic',
|
|
2891
|
-
metadata: {
|
|
2892
|
-
synthesizedBy: 'vba-tempvar',
|
|
2893
|
-
// User-facing enum: lowercase single-token string values.
|
|
2894
|
-
access,
|
|
2895
|
-
},
|
|
2896
|
-
line: lineNum,
|
|
2897
|
-
column,
|
|
2898
|
-
});
|
|
2899
|
-
}
|
|
2900
|
-
/**
|
|
2901
|
-
* Issue #50: scan one line of VBA source for TempVars access sites and
|
|
2902
|
-
* emit one `references` edge per site. Three regex runs:
|
|
2903
|
-
* - bang form `TempVars!x` over the MASKED line (`!` itself never
|
|
2904
|
-
* lives inside a string literal, so masked == unmasked here — using
|
|
2905
|
-
* the masked line is conservative against false positives in
|
|
2906
|
-
* concatenated string content),
|
|
2907
|
-
* - paren form `TempVars("x")` over the ORIGINAL line (the literal is
|
|
2908
|
-
* inside a string — masked would strip the key),
|
|
2909
|
-
* - Add form `TempVars.Add "x", v` over the ORIGINAL line (same reason),
|
|
2910
|
-
* always classified as a write.
|
|
2911
|
-
*
|
|
2912
|
-
* Each match classifies access by looking at the LINE SUFFIX (after the
|
|
2913
|
-
* closing paren / bang-key) on the SAME line: if `=` (and not the
|
|
2914
|
-
* nonexistent `==`) is the next non-whitespace character, it's a write.
|
|
2915
|
-
* VBA has no `==` so a bare `=` suffix check is safe.
|
|
2916
|
-
*
|
|
2917
|
-
* The caller parameter comes from `stack[stack.length - 1]` in
|
|
2918
|
-
* `sweepCallsAndSql`. Pass `undefined` for module-level access — we
|
|
2919
|
-
* drop the edge (per REQ-CODE-4 spirit, runtime references have no
|
|
2920
|
-
* static source to anchor against).
|
|
2921
|
-
*/
|
|
2922
|
-
sweepTempVars(maskedLine, originalLine, lineNum, caller) {
|
|
2923
|
-
// 1) Bang form — masked line. No string-literal interaction.
|
|
2924
|
-
const bangRe = new RegExp(VbaExtractor.TEMP_VAR_BANG_RE.source, VbaExtractor.TEMP_VAR_BANG_RE.flags);
|
|
2925
|
-
let bm;
|
|
2926
|
-
while ((bm = bangRe.exec(maskedLine)) !== null) {
|
|
2927
|
-
const key = bm[1] ?? '';
|
|
2928
|
-
if (!key)
|
|
2929
|
-
continue;
|
|
2930
|
-
const access = detectAssignmentSuffix(maskedLine, bm.index + bm[0].length)
|
|
2931
|
-
? 'write'
|
|
2932
|
-
: 'read';
|
|
2933
|
-
this.emitTempVarReference(caller, key, lineNum, bm.index, access);
|
|
2934
|
-
}
|
|
2935
|
-
// 2) Paren form — original line. The literal survives only here.
|
|
2936
|
-
const parenRe = new RegExp(VbaExtractor.TEMP_VAR_PAREN_RE.source, VbaExtractor.TEMP_VAR_PAREN_RE.flags);
|
|
2937
|
-
let pm;
|
|
2938
|
-
while ((pm = parenRe.exec(originalLine)) !== null) {
|
|
2939
|
-
const key = pm[1] ?? '';
|
|
2940
|
-
if (!key)
|
|
2941
|
-
continue;
|
|
2942
|
-
const access = detectAssignmentSuffix(originalLine, pm.index + pm[0].length)
|
|
2943
|
-
? 'write'
|
|
2944
|
-
: 'read';
|
|
2945
|
-
this.emitTempVarReference(caller, key, lineNum, pm.index, access);
|
|
2946
|
-
}
|
|
2947
|
-
// 3) Add form — original line. Always a write.
|
|
2948
|
-
const addRe = new RegExp(VbaExtractor.TEMP_VAR_ADD_RE.source, VbaExtractor.TEMP_VAR_ADD_RE.flags);
|
|
2949
|
-
let am;
|
|
2950
|
-
while ((am = addRe.exec(originalLine)) !== null) {
|
|
2951
|
-
const key = am[1] ?? '';
|
|
2952
|
-
if (!key)
|
|
2953
|
-
continue;
|
|
2954
|
-
this.emitTempVarReference(caller, key, lineNum, am.index, 'write');
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
/**
|
|
2958
|
-
* Maps `variableName.toLowerCase()` → declared type info.
|
|
2959
|
-
* Built by `sweepDimsAndWithEvents`; consulted by the unified qualified-call
|
|
2960
|
-
* gate (`shouldProcessQualifiedCall`) so declared project-class locals emit
|
|
2961
|
-
* edges, declared primitive/external locals stay silent, and undeclared
|
|
2962
|
-
* receivers remain module-name candidates for the resolver (Fix 2 / Issue #2).
|
|
2963
|
-
*/
|
|
2964
|
-
localVarTypeMap = new Map();
|
|
2965
|
-
/**
|
|
2966
|
-
* Issue #52: Const resolution buckets, scoped per procedure. Key is
|
|
2967
|
-
* `'module'` for module-level Consts, or the procedure's `startLine`
|
|
2968
|
-
* (stringified) for proc-local Consts. Each bucket maps the lowercase
|
|
2969
|
-
* constant name to its simple-literal value (used by `DoCmd.OpenForm` /
|
|
2970
|
-
* `OpenReport` / `OpenQuery` argument resolution via `resolveLocalConst`).
|
|
2971
|
-
*
|
|
2972
|
-
* Two procs declaring the same Const name with different values stay
|
|
2973
|
-
* isolated (each in its own bucket); reads look up the current proc's
|
|
2974
|
-
* bucket first and fall back to the module bucket. The bucket-per-proc
|
|
2975
|
-
* model was chosen over a single file-wide Map (the pre-fix shape) so
|
|
2976
|
-
* `DoCmd.OpenForm FORM_DESTINO` resolves to the proc-local value, not
|
|
2977
|
-
* whichever was written last.
|
|
2978
|
-
*/
|
|
2979
|
-
localConstants = new Map();
|
|
2980
|
-
/**
|
|
2981
|
-
* Issue #52: shared lookup helper for `scanDoCmdOpenCalls` and
|
|
2982
|
-
* `scanDoCmdOpenQuery`. The current scope is the procedure whose
|
|
2983
|
-
* `startLine` is on top of `procStack` (or `'module'` when the stack is
|
|
2984
|
-
* empty). Per-proc bucket first; module bucket is the fallback.
|
|
2985
|
-
*/
|
|
2986
|
-
resolveLocalConst(name) {
|
|
2987
|
-
const lower = name.toLowerCase();
|
|
2988
|
-
const procBucket = this.localConstants.get(this.currentProcKey);
|
|
2989
|
-
if (procBucket) {
|
|
2990
|
-
const v = procBucket.get(lower);
|
|
2991
|
-
if (v !== undefined)
|
|
2992
|
-
return v;
|
|
2993
|
-
}
|
|
2994
|
-
const moduleBucket = this.localConstants.get('module');
|
|
2995
|
-
return moduleBucket?.get(lower);
|
|
2996
|
-
}
|
|
2997
|
-
/**
|
|
2998
|
-
* Issue #52: shared writer. `scopeKey` is `'module'` or the procedure's
|
|
2999
|
-
* startLine-as-string. Creates the bucket lazily so callers do not have
|
|
3000
|
-
* to pre-allocate per proc. Returns the bucket the value was written to
|
|
3001
|
-
* (mostly useful for tests; production code ignores it).
|
|
3002
|
-
*/
|
|
3003
|
-
setLocalConstInScope(scopeKey, name, value) {
|
|
3004
|
-
let bucket = this.localConstants.get(scopeKey);
|
|
3005
|
-
if (!bucket) {
|
|
3006
|
-
bucket = new Map();
|
|
3007
|
-
this.localConstants.set(scopeKey, bucket);
|
|
3008
|
-
}
|
|
3009
|
-
bucket.set(name.toLowerCase(), value);
|
|
3010
|
-
return bucket;
|
|
3011
|
-
}
|
|
3012
|
-
/**
|
|
3013
|
-
* Issue #52: the current Const-lookup scope. `'module'` when no procedure
|
|
3014
|
-
* is open, otherwise the top-of-stack proc's `startLine` as a string.
|
|
3015
|
-
* Both `sweepEnumsAndConsts` (to decide whether to emit a `constant`
|
|
3016
|
-
* node) and `sweepCallsAndSql` (to drive OpenForm/OpenQuery resolution)
|
|
3017
|
-
* keep this in sync with their per-line stack walk by pushing/popping
|
|
3018
|
-
* `procStack` and writing the new top's key here.
|
|
3019
|
-
*/
|
|
3020
|
-
currentProcKey = 'module';
|
|
3021
|
-
/**
|
|
3022
|
-
* Issue #52: per-extraction proc-stack shared between `sweepEnumsAndConsts`
|
|
3023
|
-
* and `sweepCallsAndSql`. Each sweep clears it at the start so the file's
|
|
3024
|
-
* mid-proc structural state never leaks across sweeps. Holds the
|
|
3025
|
-
* `startLine` (1-based, matches `ProcInfo.startLine`) of every procedure
|
|
3026
|
-
* whose body the sweep has not yet emitted `End Sub`/`End Function`/
|
|
3027
|
-
* `End Property` for.
|
|
3028
|
-
*/
|
|
3029
|
-
procStack = [];
|
|
3030
|
-
/** Local event name (lowercase) → event node for `RaiseEvent` edge emission. */
|
|
3031
|
-
localEvents = new Map();
|
|
3032
274
|
}
|
|
3033
275
|
exports.VbaExtractor = VbaExtractor;
|
|
3034
|
-
/**
|
|
3035
|
-
* #13 helper: escape a variable name for safe interpolation into the
|
|
3036
|
-
* self-reference RegExp built by `trackSqlVariableAssignment`. VBA
|
|
3037
|
-
* identifiers are alphanumeric+underscore only, so in practice nothing here
|
|
3038
|
-
* ever needs escaping — this guards against regex metacharacters anyway
|
|
3039
|
-
* rather than assume the input is always well-formed.
|
|
3040
|
-
*/
|
|
3041
|
-
function escapeRegExpLiteral(value) {
|
|
3042
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3043
|
-
}
|
|
3044
|
-
function parseConstDeclarations(body) {
|
|
3045
|
-
const declarations = [];
|
|
3046
|
-
for (const part of splitOutsideVbaStrings(body, ',')) {
|
|
3047
|
-
const m = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*(?:As\s+[^=]+?)?\s*=\s*(.+?)\s*$/iu.exec(part);
|
|
3048
|
-
if (!m)
|
|
3049
|
-
continue;
|
|
3050
|
-
const name = m[1] ?? '';
|
|
3051
|
-
const rawValue = (m[2] ?? '').trim();
|
|
3052
|
-
declarations.push({
|
|
3053
|
-
name,
|
|
3054
|
-
value: rawValue.startsWith('"') ? unwrapVbaStringLiteral(rawValue) : rawValue || null,
|
|
3055
|
-
});
|
|
3056
|
-
}
|
|
3057
|
-
return declarations;
|
|
3058
|
-
}
|
|
3059
|
-
function splitOutsideVbaStrings(value, separator) {
|
|
3060
|
-
const parts = [];
|
|
3061
|
-
let current = '';
|
|
3062
|
-
let inString = false;
|
|
3063
|
-
for (let i = 0; i < value.length; i++) {
|
|
3064
|
-
const ch = value[i];
|
|
3065
|
-
const next = value[i + 1];
|
|
3066
|
-
if (ch === '"') {
|
|
3067
|
-
current += ch;
|
|
3068
|
-
if (inString && next === '"') {
|
|
3069
|
-
current += next;
|
|
3070
|
-
i++;
|
|
3071
|
-
continue;
|
|
3072
|
-
}
|
|
3073
|
-
inString = !inString;
|
|
3074
|
-
continue;
|
|
3075
|
-
}
|
|
3076
|
-
if (!inString && ch === separator) {
|
|
3077
|
-
parts.push(current);
|
|
3078
|
-
current = '';
|
|
3079
|
-
continue;
|
|
3080
|
-
}
|
|
3081
|
-
current += ch;
|
|
3082
|
-
}
|
|
3083
|
-
parts.push(current);
|
|
3084
|
-
return parts;
|
|
3085
|
-
}
|
|
3086
|
-
/**
|
|
3087
|
-
* Issue #50 helper: return true iff `line.charAt(fromIndex..)` (after the
|
|
3088
|
-
* matched TempVars site, including any trailing whitespace) holds an `=`
|
|
3089
|
-
* (write-assignment) and not a `==` (VBA has no `==` operator, so a bare
|
|
3090
|
-
* check for `=` is safe). Used by `sweepTempVars` to classify a `TempVars`
|
|
3091
|
-
* access as read vs write from the local line context.
|
|
3092
|
-
*
|
|
3093
|
-
* We skip over trailing whitespace only — `.`, `(`, `<`, `>`, `*`, `+`
|
|
3094
|
-
* etc. all mean "this isn't an assignment target", and bare `=`
|
|
3095
|
-
* is the only access-suffix shape VBA syntax allows here. A line like
|
|
3096
|
-
* `TempVars!x = 1` (write) or `Debug.Print TempVars!x` (read) land in
|
|
3097
|
-
* the right bucket without further analysis.
|
|
3098
|
-
*
|
|
3099
|
-
* Strings should already be masked out of `line` (`maskStringContent`)
|
|
3100
|
-
* when this is called for the bang form (no string in scope anyway);
|
|
3101
|
-
* the paren form passes the ORIGINAL line — but for THAT form the
|
|
3102
|
-
* captured match ends at the closing `"` of the literal, so any `=`
|
|
3103
|
-
* that follows is unambiguously outside the string.
|
|
3104
|
-
*/
|
|
3105
|
-
function detectAssignmentSuffix(line, fromIndex) {
|
|
3106
|
-
let i = fromIndex;
|
|
3107
|
-
while (i < line.length) {
|
|
3108
|
-
const ch = line[i];
|
|
3109
|
-
if (ch === ' ' || ch === '\t') {
|
|
3110
|
-
i++;
|
|
3111
|
-
continue;
|
|
3112
|
-
}
|
|
3113
|
-
return ch === '=';
|
|
3114
|
-
}
|
|
3115
|
-
return false;
|
|
3116
|
-
}
|
|
3117
|
-
function unwrapVbaStringLiteral(raw) {
|
|
3118
|
-
const trimmed = raw.trim();
|
|
3119
|
-
if (!trimmed.startsWith('"'))
|
|
3120
|
-
return trimmed;
|
|
3121
|
-
let text = '';
|
|
3122
|
-
for (let i = 1; i < trimmed.length; i++) {
|
|
3123
|
-
const ch = trimmed[i];
|
|
3124
|
-
const next = trimmed[i + 1];
|
|
3125
|
-
if (ch === '"' && next === '"') {
|
|
3126
|
-
text += '"';
|
|
3127
|
-
i++;
|
|
3128
|
-
continue;
|
|
3129
|
-
}
|
|
3130
|
-
if (ch === '"')
|
|
3131
|
-
break;
|
|
3132
|
-
text += ch;
|
|
3133
|
-
}
|
|
3134
|
-
return text;
|
|
3135
|
-
}
|
|
3136
|
-
/**
|
|
3137
|
-
* Hueco 3 helper: parse an Access event-handler Sub name into its
|
|
3138
|
-
* `<ControlName>_<EventName>` components. Returns null when the name does
|
|
3139
|
-
* not match the convention, when the split yields an empty segment, or
|
|
3140
|
-
* when the control name is `Form` (form-level events are NOT control
|
|
3141
|
-
* handlers — `Form_Load` is the form's own lifecycle event, not a
|
|
3142
|
-
* command-button click).
|
|
3143
|
-
*
|
|
3144
|
-
* Splitting on the LAST underscore (rather than the first) lets
|
|
3145
|
-
* multi-word event names parse correctly: `ComandoAltaPM_BeforeDelConfirm`
|
|
3146
|
-
* yields control=`ComandoAltaPM`, event=`BeforeDelConfirm`, not
|
|
3147
|
-
* control=`ComandoAlta`, event=`PM_BeforeDelConfirm`.
|
|
3148
|
-
*/
|
|
3149
|
-
function parseEventHandlerName(name) {
|
|
3150
|
-
if (!name)
|
|
3151
|
-
return null;
|
|
3152
|
-
const lastUnderscore = name.lastIndexOf('_');
|
|
3153
|
-
if (lastUnderscore <= 0)
|
|
3154
|
-
return null; // no underscore OR starts with underscore
|
|
3155
|
-
const controlName = name.slice(0, lastUnderscore);
|
|
3156
|
-
const eventName = name.slice(lastUnderscore + 1);
|
|
3157
|
-
if (!controlName || !eventName)
|
|
3158
|
-
return null;
|
|
3159
|
-
// Form-level events live on the form, not on a control.
|
|
3160
|
-
if (controlName.toLowerCase() === 'form')
|
|
3161
|
-
return null;
|
|
3162
|
-
return { controlName, eventName };
|
|
3163
|
-
}
|
|
3164
276
|
//# sourceMappingURL=vba-extractor.js.map
|