@aroman22/codegraph-vba-darwin-arm64 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/lib/dist/bin/codegraph.js +13 -7
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/bin/daemon-release.d.ts +7 -0
  4. package/lib/dist/bin/daemon-release.d.ts.map +1 -0
  5. package/lib/dist/bin/daemon-release.js +18 -0
  6. package/lib/dist/bin/daemon-release.js.map +1 -0
  7. package/lib/dist/db/queries.d.ts +30 -0
  8. package/lib/dist/db/queries.d.ts.map +1 -1
  9. package/lib/dist/db/queries.js +51 -1
  10. package/lib/dist/db/queries.js.map +1 -1
  11. package/lib/dist/extraction/access-erd-extractor.d.ts +57 -0
  12. package/lib/dist/extraction/access-erd-extractor.d.ts.map +1 -0
  13. package/lib/dist/extraction/access-erd-extractor.js +389 -0
  14. package/lib/dist/extraction/access-erd-extractor.js.map +1 -0
  15. package/lib/dist/extraction/extraction-version.d.ts +1 -1
  16. package/lib/dist/extraction/extraction-version.js +1 -1
  17. package/lib/dist/extraction/grammars.d.ts +20 -0
  18. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  19. package/lib/dist/extraction/grammars.js +34 -0
  20. package/lib/dist/extraction/grammars.js.map +1 -1
  21. package/lib/dist/extraction/index.d.ts +1 -0
  22. package/lib/dist/extraction/index.d.ts.map +1 -1
  23. package/lib/dist/extraction/index.js +371 -351
  24. package/lib/dist/extraction/index.js.map +1 -1
  25. package/lib/dist/extraction/parse-pool.d.ts +8 -3
  26. package/lib/dist/extraction/parse-pool.d.ts.map +1 -1
  27. package/lib/dist/extraction/parse-pool.js +1 -2
  28. package/lib/dist/extraction/parse-pool.js.map +1 -1
  29. package/lib/dist/extraction/parse-worker.js +7 -6
  30. package/lib/dist/extraction/parse-worker.js.map +1 -1
  31. package/lib/dist/extraction/sql-query-extractor.d.ts +16 -14
  32. package/lib/dist/extraction/sql-query-extractor.d.ts.map +1 -1
  33. package/lib/dist/extraction/sql-query-extractor.js +43 -31
  34. package/lib/dist/extraction/sql-query-extractor.js.map +1 -1
  35. package/lib/dist/extraction/sql-table-scan.d.ts +184 -0
  36. package/lib/dist/extraction/sql-table-scan.d.ts.map +1 -0
  37. package/lib/dist/extraction/sql-table-scan.js +288 -0
  38. package/lib/dist/extraction/sql-table-scan.js.map +1 -0
  39. package/lib/dist/extraction/tree-sitter.d.ts +10 -1
  40. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  41. package/lib/dist/extraction/tree-sitter.js +34 -3
  42. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  43. package/lib/dist/extraction/vba/call-sweep.d.ts +2 -7
  44. package/lib/dist/extraction/vba/call-sweep.d.ts.map +1 -1
  45. package/lib/dist/extraction/vba/call-sweep.js +144 -90
  46. package/lib/dist/extraction/vba/call-sweep.js.map +1 -1
  47. package/lib/dist/extraction/vba/calls.d.ts +24 -4
  48. package/lib/dist/extraction/vba/calls.d.ts.map +1 -1
  49. package/lib/dist/extraction/vba/calls.js +42 -12
  50. package/lib/dist/extraction/vba/calls.js.map +1 -1
  51. package/lib/dist/extraction/vba/constants.d.ts +7 -14
  52. package/lib/dist/extraction/vba/constants.d.ts.map +1 -1
  53. package/lib/dist/extraction/vba/constants.js +8 -36
  54. package/lib/dist/extraction/vba/constants.js.map +1 -1
  55. package/lib/dist/extraction/vba/context.d.ts +443 -7
  56. package/lib/dist/extraction/vba/context.d.ts.map +1 -1
  57. package/lib/dist/extraction/vba/context.js +325 -29
  58. package/lib/dist/extraction/vba/context.js.map +1 -1
  59. package/lib/dist/extraction/vba/controls.d.ts +18 -1
  60. package/lib/dist/extraction/vba/controls.d.ts.map +1 -1
  61. package/lib/dist/extraction/vba/controls.js +99 -27
  62. package/lib/dist/extraction/vba/controls.js.map +1 -1
  63. package/lib/dist/extraction/vba/declarations.d.ts +1 -6
  64. package/lib/dist/extraction/vba/declarations.d.ts.map +1 -1
  65. package/lib/dist/extraction/vba/declarations.js +5 -39
  66. package/lib/dist/extraction/vba/declarations.js.map +1 -1
  67. package/lib/dist/extraction/vba/dims.d.ts +11 -7
  68. package/lib/dist/extraction/vba/dims.d.ts.map +1 -1
  69. package/lib/dist/extraction/vba/dims.js +265 -29
  70. package/lib/dist/extraction/vba/dims.js.map +1 -1
  71. package/lib/dist/extraction/vba/docmd.d.ts +29 -2
  72. package/lib/dist/extraction/vba/docmd.d.ts.map +1 -1
  73. package/lib/dist/extraction/vba/docmd.js +297 -18
  74. package/lib/dist/extraction/vba/docmd.js.map +1 -1
  75. package/lib/dist/extraction/vba/enums-consts.d.ts +7 -14
  76. package/lib/dist/extraction/vba/enums-consts.d.ts.map +1 -1
  77. package/lib/dist/extraction/vba/enums-consts.js +11 -45
  78. package/lib/dist/extraction/vba/enums-consts.js.map +1 -1
  79. package/lib/dist/extraction/vba/error-channel.d.ts +57 -0
  80. package/lib/dist/extraction/vba/error-channel.d.ts.map +1 -0
  81. package/lib/dist/extraction/vba/error-channel.js +128 -0
  82. package/lib/dist/extraction/vba/error-channel.js.map +1 -0
  83. package/lib/dist/extraction/vba/errors.d.ts +64 -0
  84. package/lib/dist/extraction/vba/errors.d.ts.map +1 -0
  85. package/lib/dist/extraction/vba/errors.js +775 -0
  86. package/lib/dist/extraction/vba/errors.js.map +1 -0
  87. package/lib/dist/extraction/vba/filesystem-statements.d.ts +23 -0
  88. package/lib/dist/extraction/vba/filesystem-statements.d.ts.map +1 -0
  89. package/lib/dist/extraction/vba/filesystem-statements.js +70 -0
  90. package/lib/dist/extraction/vba/filesystem-statements.js.map +1 -0
  91. package/lib/dist/extraction/vba/implements.d.ts +1 -6
  92. package/lib/dist/extraction/vba/implements.d.ts.map +1 -1
  93. package/lib/dist/extraction/vba/implements.js +1 -22
  94. package/lib/dist/extraction/vba/implements.js.map +1 -1
  95. package/lib/dist/extraction/vba/labels.d.ts +26 -0
  96. package/lib/dist/extraction/vba/labels.d.ts.map +1 -0
  97. package/lib/dist/extraction/vba/labels.js +180 -0
  98. package/lib/dist/extraction/vba/labels.js.map +1 -0
  99. package/lib/dist/extraction/vba/module-vars.d.ts +36 -0
  100. package/lib/dist/extraction/vba/module-vars.d.ts.map +1 -0
  101. package/lib/dist/extraction/vba/module-vars.js +145 -0
  102. package/lib/dist/extraction/vba/module-vars.js.map +1 -0
  103. package/lib/dist/extraction/vba/options.d.ts +88 -0
  104. package/lib/dist/extraction/vba/options.d.ts.map +1 -0
  105. package/lib/dist/extraction/vba/options.js +39 -0
  106. package/lib/dist/extraction/vba/options.js.map +1 -0
  107. package/lib/dist/extraction/vba/parameters.d.ts +35 -0
  108. package/lib/dist/extraction/vba/parameters.d.ts.map +1 -0
  109. package/lib/dist/extraction/vba/parameters.js +110 -0
  110. package/lib/dist/extraction/vba/parameters.js.map +1 -0
  111. package/lib/dist/extraction/vba/procedures.d.ts +1 -9
  112. package/lib/dist/extraction/vba/procedures.d.ts.map +1 -1
  113. package/lib/dist/extraction/vba/procedures.js +174 -112
  114. package/lib/dist/extraction/vba/procedures.js.map +1 -1
  115. package/lib/dist/extraction/vba/rules.d.ts +10 -5
  116. package/lib/dist/extraction/vba/rules.d.ts.map +1 -1
  117. package/lib/dist/extraction/vba/rules.js +19 -0
  118. package/lib/dist/extraction/vba/rules.js.map +1 -1
  119. package/lib/dist/extraction/vba/runtime-objects.d.ts +59 -0
  120. package/lib/dist/extraction/vba/runtime-objects.d.ts.map +1 -0
  121. package/lib/dist/extraction/vba/runtime-objects.js +106 -0
  122. package/lib/dist/extraction/vba/runtime-objects.js.map +1 -0
  123. package/lib/dist/extraction/vba/signature.d.ts +57 -0
  124. package/lib/dist/extraction/vba/signature.d.ts.map +1 -0
  125. package/lib/dist/extraction/vba/signature.js +164 -0
  126. package/lib/dist/extraction/vba/signature.js.map +1 -0
  127. package/lib/dist/extraction/vba/sql-wrapper.d.ts +76 -3
  128. package/lib/dist/extraction/vba/sql-wrapper.d.ts.map +1 -1
  129. package/lib/dist/extraction/vba/sql-wrapper.js +725 -151
  130. package/lib/dist/extraction/vba/sql-wrapper.js.map +1 -1
  131. package/lib/dist/extraction/vba/tempvars.d.ts.map +1 -1
  132. package/lib/dist/extraction/vba/tempvars.js +4 -0
  133. package/lib/dist/extraction/vba/tempvars.js.map +1 -1
  134. package/lib/dist/extraction/vba/text-utils.d.ts +62 -2
  135. package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -1
  136. package/lib/dist/extraction/vba/text-utils.js +84 -6
  137. package/lib/dist/extraction/vba/text-utils.js.map +1 -1
  138. package/lib/dist/extraction/vba-extractor.d.ts +18 -1
  139. package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
  140. package/lib/dist/extraction/vba-extractor.js +111 -16
  141. package/lib/dist/extraction/vba-extractor.js.map +1 -1
  142. package/lib/dist/extraction/vba-form-extractor.d.ts +28 -14
  143. package/lib/dist/extraction/vba-form-extractor.d.ts.map +1 -1
  144. package/lib/dist/extraction/vba-form-extractor.js +56 -21
  145. package/lib/dist/extraction/vba-form-extractor.js.map +1 -1
  146. package/lib/dist/extraction/vba-preprocess.d.ts +89 -3
  147. package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
  148. package/lib/dist/extraction/vba-preprocess.js +221 -57
  149. package/lib/dist/extraction/vba-preprocess.js.map +1 -1
  150. package/lib/dist/extraction/vba-source.d.ts +0 -10
  151. package/lib/dist/extraction/vba-source.d.ts.map +1 -1
  152. package/lib/dist/extraction/vba-source.js +0 -13
  153. package/lib/dist/extraction/vba-source.js.map +1 -1
  154. package/lib/dist/extraction/vba-test-manifest-extractor.d.ts +0 -6
  155. package/lib/dist/extraction/vba-test-manifest-extractor.d.ts.map +1 -1
  156. package/lib/dist/extraction/vba-test-manifest-extractor.js +0 -1
  157. package/lib/dist/extraction/vba-test-manifest-extractor.js.map +1 -1
  158. package/lib/dist/index.d.ts.map +1 -1
  159. package/lib/dist/index.js +9 -0
  160. package/lib/dist/index.js.map +1 -1
  161. package/lib/dist/mcp/daemon-manager.d.ts.map +1 -1
  162. package/lib/dist/mcp/daemon-manager.js +7 -3
  163. package/lib/dist/mcp/daemon-manager.js.map +1 -1
  164. package/lib/dist/mcp/daemon-paths.d.ts +6 -0
  165. package/lib/dist/mcp/daemon-paths.d.ts.map +1 -1
  166. package/lib/dist/mcp/daemon-paths.js +15 -0
  167. package/lib/dist/mcp/daemon-paths.js.map +1 -1
  168. package/lib/dist/mcp/daemon-registry.d.ts +82 -7
  169. package/lib/dist/mcp/daemon-registry.d.ts.map +1 -1
  170. package/lib/dist/mcp/daemon-registry.js +557 -45
  171. package/lib/dist/mcp/daemon-registry.js.map +1 -1
  172. package/lib/dist/mcp/daemon-watchdog.d.ts +12 -0
  173. package/lib/dist/mcp/daemon-watchdog.d.ts.map +1 -1
  174. package/lib/dist/mcp/daemon-watchdog.js +37 -7
  175. package/lib/dist/mcp/daemon-watchdog.js.map +1 -1
  176. package/lib/dist/mcp/daemon.d.ts +20 -1
  177. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  178. package/lib/dist/mcp/daemon.js +123 -30
  179. package/lib/dist/mcp/daemon.js.map +1 -1
  180. package/lib/dist/mcp/index.d.ts.map +1 -1
  181. package/lib/dist/mcp/index.js +13 -2
  182. package/lib/dist/mcp/index.js.map +1 -1
  183. package/lib/dist/mcp/proxy.d.ts +32 -0
  184. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  185. package/lib/dist/mcp/proxy.js +163 -31
  186. package/lib/dist/mcp/proxy.js.map +1 -1
  187. package/lib/dist/mcp/tools.d.ts.map +1 -1
  188. package/lib/dist/mcp/tools.js +29 -1
  189. package/lib/dist/mcp/tools.js.map +1 -1
  190. package/lib/dist/project-config.d.ts +43 -2
  191. package/lib/dist/project-config.d.ts.map +1 -1
  192. package/lib/dist/project-config.js +86 -2
  193. package/lib/dist/project-config.js.map +1 -1
  194. package/lib/dist/resolution/index.d.ts +47 -2
  195. package/lib/dist/resolution/index.d.ts.map +1 -1
  196. package/lib/dist/resolution/index.js +160 -5
  197. package/lib/dist/resolution/index.js.map +1 -1
  198. package/lib/dist/resolution/name-matcher.d.ts +25 -0
  199. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  200. package/lib/dist/resolution/name-matcher.js +59 -0
  201. package/lib/dist/resolution/name-matcher.js.map +1 -1
  202. package/lib/dist/resolution/vba-runtime-objects.d.ts +11 -11
  203. package/lib/dist/resolution/vba-runtime-objects.d.ts.map +1 -1
  204. package/lib/dist/resolution/vba-runtime-objects.js +18 -38
  205. package/lib/dist/resolution/vba-runtime-objects.js.map +1 -1
  206. package/lib/dist/types.d.ts +13 -5
  207. package/lib/dist/types.d.ts.map +1 -1
  208. package/lib/dist/types.js +17 -0
  209. package/lib/dist/types.js.map +1 -1
  210. package/lib/node_modules/.modules.yaml +1 -1
  211. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  212. package/lib/package.json +2 -1
  213. package/package.json +1 -1
@@ -1,93 +1,273 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.scanSqlInLine = scanSqlInLine;
3
+ exports.DEFAULT_SQL_WRAPPERS = exports.RUNTIME_BINDING_SYNTHESIZER = void 0;
4
+ exports.compileSqlWrappers = compileSqlWrappers;
4
5
  exports.trackSqlVariableAssignment = trackSqlVariableAssignment;
6
+ exports.scanDomainFunctionsInLine = scanDomainFunctionsInLine;
7
+ exports.scanSqlInLine = scanSqlInLine;
5
8
  /**
6
9
  * SQL-in-strings sweep (REQ-CODE-8). Scans SQL wrapper calls
7
10
  * (`DoCmd.RunSQL "…"`, `*db.OpenRecordset "…"`, `getdb().Execute "…"`) and
8
11
  * variable-form executions, tracks `sql = sql & "…"` accumulation, and emits
9
12
  * `references` edges (via `ctx.emitReference`) to the table names found in the
10
13
  * FROM/JOIN/INTO/UPDATE clauses.
14
+ *
15
+ * Issue #203: the `FROM/JOIN/INTO/UPDATE <table>` regex is the canonical
16
+ * source from `src/extraction/sql-table-scan.ts` — every table-name
17
+ * capture path in the project imports from there so a reserved word
18
+ * (`WHERE`, `ORDER`, `SET`, …) can never be emitted as a table
19
+ * reference, and a non-literal operand dropped by `&`-concatenation
20
+ * (`"DELETE FROM " & tabla & " WHERE x"`) is replaced with a `?`
21
+ * sentinel that the regex can never match. The shared module also
22
+ * emits the read/write access direction so this file no longer
23
+ * re-implements `classifySqlAccess`.
24
+ *
25
+ * Issue #244: which receivers count as a database handle is no longer baked
26
+ * into the scanning regexes. `DEFAULT_SQL_WRAPPERS` lists the built-in ones
27
+ * and `codegraph.json` → `vba.sqlWrappers` extends that list with a project's
28
+ * own accessors. Config entries are identifier fragments or `receiver.method`
29
+ * pairs — never raw regex, which in this per-line hot path would be a
30
+ * catastrophic-backtracking foot-gun.
31
+ *
32
+ * Issue #255: the Access domain aggregate functions (`DLookup`, `DCount`, …)
33
+ * name a table or a saved query in their SECOND argument. They live here
34
+ * rather than in a file of their own because the only thing they add is a call
35
+ * shape — the "is this a saved query?" gate and the emit path are the ones
36
+ * issue #253 already built, reused verbatim.
11
37
  */
12
- const vba_preprocess_1 = require("../vba-preprocess");
13
38
  const text_utils_1 = require("./text-utils");
14
- /** SQL wrapper helpers — order matters because `db.Execute` is a suffix of others. */
15
- const SQL_WRAPPERS = [
16
- { name: 'DoCmd.RunSQL', re: /\bDoCmd\.RunSQL\s+"((?:[^"]|"")*)"/g },
17
- { name: '*db.OpenRecordset', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.OpenRecordset\s+"((?:[^"]|"")*)"/giu },
18
- { name: '*db.Execute', re: /\b(?:\p{L}[\p{L}\p{N}_]*)?db\b(?:\(\))?\.Execute\s+"((?:[^"]|"")*)"/giu },
19
- // Fix 4 (Issue #4): inline-literal forms `getdb().Execute "..."` and
20
- // `getdb().OpenRecordset "..."` — the variable form is covered by
21
- // SQL_VAR_EXEC_RE but the direct-literal form was missing.
22
- { name: 'getdb().Execute', re: /\bgetdb\(\)\.Execute\s+"((?:[^"]|"")*)"/g },
23
- { name: 'getdb().OpenRecordset', re: /\bgetdb\(\)\.OpenRecordset\s+"((?:[^"]|"")*)"/g },
24
- ];
39
+ const sql_table_scan_1 = require("../sql-table-scan");
25
40
  /** SQL assigned to a local variable, e.g. `m_SQL = "SELECT ..." & ...`. */
26
41
  const SQL_VAR_ASSIGN_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*=\s*(.*)$/iu;
27
- /** SQL wrapper called with a variable, e.g. `getdb().Execute m_SQL`. */
28
- const 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;
29
- /**
30
- * Issue #42: `DoCmd.RunSQL <identifier>` (variable form) — the dominant
31
- * Access idiom for executing a dynamically-built SQL string. Today only
32
- * the literal form `DoCmd.RunSQL "DELETE FROM X"` is tracked via the
33
- * `SQL_WRAPPERS` regex; the variable form silently dropped table impact for
34
- * every procedure that builds SQL in a string and runs it through
35
- * `DoCmd.RunSQL`.
36
- *
37
- * This regex is the DoCmd.RunSQL analogue of `SQL_VAR_EXEC_RE` above and
38
- * is iterated by `scanSqlInLine`. When a match is found, the captured
39
- * identifier is resolved against `sqlVariables` (populated by
40
- * `trackSqlVariableAssignment` with `&`-accumulate semantics — Issue #13)
41
- * and the resulting SQL string drives `emitSqlTableReferences`.
42
- *
43
- * The optional `(?:\(\))?` + `\s*\(?` shape lets the regex match both
44
- * the parenthesised form `DoCmd.RunSQL(strSQL)` and the no-paren form
45
- * `DoCmd.RunSQL strSQL` that the existing SQL_WRAPPERS literal regex
46
- * does not cover. The captured identifier is the only thing we need —
47
- * we DO NOT try to parse what the variable points at; that's the
48
- * existing `sqlVariables` map's job.
49
- */
50
- const SQL_VAR_DOCMD_RUNSQL_RE = /\bDoCmd\.RunSQL\s*\(?\s*(\p{L}[\p{L}\p{N}_]*)\s*\)?/giu;
51
- /**
52
- * SQL table-name regex scoped to the clauses that introduce a table
53
- * reference: `FROM <t>`, `JOIN <t>`, `INTO <t>`, `UPDATE <t>`. Adding
54
- * `JOIN` lets the scanner pick up tables from joined fragments that
55
- * arrive via `&`-concatenated wrapper literals (e.g.
56
- * `db.Execute "FROM A" & " JOIN B"`); without it the second literal's
57
- * table was silently dropped even though the wrapper regex now matches
58
- * the chain.
59
- *
60
- * The captured table name is an optional bracketed/unbracketed schema
61
- * prefix followed by a `.`, then a bracketed-or-bare identifier — so
62
- * `FROM dbo.tblCustomers` and `FROM [My Schema].[My Table]` come
63
- * through as one composite reference. Without the prefix the regex
64
- * still matches a single identifier byte-identical to the old shape.
65
- * Brackets in the captured composite are stripped by
66
- * `emitSqlTableReferences` (`replace(/[\[\]]/g, '')`), so the public
67
- * node name is the unwrapped form `dbo.tblCustomers` /
68
- * `My Schema.My Table` — matching how plain `[Order Details]` is also
69
- * unwrapped to `Order Details`. The identifier class
70
- * `\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*` (same as the saved-queries
71
- * `TABLE_RE` in `sql-query-extractor.ts`) ensures bracketed names
72
- * with spaces`[Order Details]`, `[My Schema]`, `[My Table]` —
73
- * are captured whole.
74
- */
75
- const SQL_TABLE_RE = /\b(FROM|JOIN|INTO|UPDATE)\s+((?:(?:\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*)\.)?(?:\[[^\]]+\]|\p{L}[\p{L}\p{N}_]*))/giu;
76
- /**
77
- * Classify a table reference as a data `read` or `write` from the SQL verb,
78
- * so `metadata.access` lets consumers answer "who WRITES table X" vs "who
79
- * READS table X". The mutating targets are writes: `INSERT INTO <t>`,
80
- * `UPDATE <t>`, and the `FROM <t>` of a `DELETE` (Access's `DELETE FROM x`
81
- * makes that FROM the delete target). Every other `FROM`/`JOIN` source table
82
- * including the source of an `INSERT ... SELECT` — is a read.
83
- */
84
- function classifySqlAccess(sqlString, clause) {
85
- const kw = clause.toUpperCase();
86
- if (kw === 'INTO' || kw === 'UPDATE')
87
- return 'write';
88
- if (kw === 'FROM' && /^\s*DELETE\b/i.test(sqlString))
89
- return 'write';
90
- return 'read';
42
+ /**
43
+ * `metadata.synthesizedBy` for a table reached through a SQL wrapper call
44
+ * (`DoCmd.RunSQL`, `db.Execute`, `db.OpenRecordset`, …).
45
+ */
46
+ const SQL_TABLE_SYNTHESIZER = 'vba-sql-table';
47
+ /**
48
+ * Issue #252 `metadata.synthesizedBy` for a table reached through SQL
49
+ * assigned to a binding property at RUNTIME. Deliberately distinct from the
50
+ * `.form.txt` sweep's static `vba-row-source` so an audit can tell the two
51
+ * provenances apart: one is what the designer stored, the other is what the
52
+ * code actually binds.
53
+ */
54
+ exports.RUNTIME_BINDING_SYNTHESIZER = 'vba-row-source-dynamic';
55
+ /**
56
+ * Issue #252 — one receiver-chain segment on the left of a binding
57
+ * assignment: a plain identifier or a `[bracketed name]`, optionally
58
+ * followed by a single call/index parenthesis (`Controls("cbo")`,
59
+ * `Item(0)`). Parentheses are matched non-nested on purpose — a
60
+ * bounded, backtracking-safe shape for a per-line hot path.
61
+ */
62
+ const BINDING_SEGMENT = String.raw `(?:\[[^\]\r\n]+\]|[\p{L}_][\p{L}\p{N}_]*)(?:\s*\([^()\r\n]*\))?`;
63
+ /**
64
+ * Issue #252 — `<receiver>.(RowSource|RecordSource|ControlSource|Filter|OrderBy) = <value>`.
65
+ *
66
+ * Group 1 is the property, group 2 the raw right-hand side.
67
+ *
68
+ * Shape notes, because each piece earns its place:
69
+ *
70
+ * - Anchored at `^\s*`, and the receiver is built out of identifier
71
+ * segments joined by `.` or `!` never a permissive `.*`. That is what
72
+ * rejects a COMPARISON (`If Me.Filter = "…" Then`): `If` is an
73
+ * identifier segment but no `.` follows it, and the pattern cannot
74
+ * restart mid-line. A `'` comment line is rejected by the same anchor.
75
+ * - The receiver chain is OPTIONAL so the bare `With` form
76
+ * (`With Me.cbo` `.RowSource = "…"`) resolves; that spelling is
77
+ * pervasive in real Access code.
78
+ * - `(?![\p{L}\p{N}_])` after the property name keeps `Me.FilterOn = True`
79
+ * and `Me.OrderByOn = True` out they are booleans, not SQL.
80
+ * - Every quantifier is bounded by a mandatory separator (`.`/`!`/`(`), so
81
+ * the pattern cannot backtrack catastrophically on a long line.
82
+ */
83
+ const BINDING_ASSIGN_RE = new RegExp(String.raw `^\s*(?:${BINDING_SEGMENT}(?:\s*[.!]\s*${BINDING_SEGMENT})*)?\s*[.!]\s*(RowSource|RecordSource|ControlSource|Filter|OrderBy)(?![\p{L}\p{N}_])\s*=\s*(.+)$`, 'iu');
84
+ /** Issue #252 a right-hand side that is exactly one bare identifier. */
85
+ const BINDING_VAR_RE = /^\p{L}[\p{L}\p{N}_]*$/u;
86
+ /**
87
+ * Issue #244the two execution-site regexes.
88
+ *
89
+ * Both capture the RECEIVER (group 1) and the METHOD (group 2) generically
90
+ * and leave the "is this a database handle?" decision to
91
+ * {@link receiverMatches}, driven by the wrapper list. The pre-#244 shape
92
+ * hard-coded the receiver as "any identifier ENDING in `db`", which silently
93
+ * dropped every per-backend accessor a real multi-database Access project
94
+ * uses (`getdbHPS()`, `getdbExpedientes()`, `dbToUse`, `m_dbLanzadera`, and
95
+ * every DAO `QueryDef` receiver).
96
+ *
97
+ * Shape notes, in the order they appear:
98
+ *
99
+ * - `(?:\(\))?` — the accessor-call form `getdb().Execute`.
100
+ * - `(?=[\s("])` / `(?=[\s(])` — the method name must END at whitespace, an
101
+ * opening paren, or (literal form) the opening quote. Without it the
102
+ * greedy method group can give characters back and read `db.Executed` as
103
+ * method `Execute` + argument `d`.
104
+ * - `\s*\(?\s*` — the parenthesised call form. Its absence in the literal
105
+ * regex was the second defect the issue reports:
106
+ * `getdb().OpenRecordset("SELECT * FROM TbX")` fell through BOTH the
107
+ * literal path (which demanded whitespace before the quote) and the
108
+ * variable path (a literal is not an identifier), so the statement
109
+ * produced no table reference at all.
110
+ *
111
+ * Every quantifier is bounded and the alternatives inside the literal body
112
+ * (`[^"]` vs `""`) are disjoint, so neither regex can backtrack
113
+ * catastrophically — which is also why config entries are identifier
114
+ * fragments and never raw regex.
115
+ */
116
+ const SQL_WRAPPER_LITERAL_RE = /\b(\p{L}[\p{L}\p{N}_]*)(?:\(\))?\.(\p{L}[\p{L}\p{N}_]*)(?=[\s("])\s*\(?\s*"((?:[^"]|"")*)"/giu;
117
+ /**
118
+ * Variable form — `getdb().Execute strSQL`, `DoCmd.RunSQL(strSQL)`,
119
+ * `qdf.Execute sql`. Group 3 is the identifier, resolved against the
120
+ * `sqlVariables` map that `trackSqlVariableAssignment` fills with
121
+ * `&`-accumulate semantics (Issue #13). We deliberately do NOT try to parse
122
+ * what the variable points at here; unresolved identifiers are skipped.
123
+ */
124
+ const SQL_WRAPPER_VAR_RE = /\b(\p{L}[\p{L}\p{N}_]*)(?:\(\))?\.(\p{L}[\p{L}\p{N}_]*)(?=[\s(])\s*\(?\s*(\p{L}[\p{L}\p{N}_]*)\s*\)?/giu;
125
+ /**
126
+ * The methods a BARE identifier entry (`"getdb"`) implies. An explicit
127
+ * `receiver.method` entry (`"cnn.Execute"`) names its own method and ignores
128
+ * this set.
129
+ */
130
+ const DEFAULT_WRAPPER_METHODS = new Set([
131
+ 'openrecordset',
132
+ 'execute',
133
+ ]);
134
+ /**
135
+ * Issue #244 — the wrapper list used when `codegraph.json` →
136
+ * `vba.sqlWrappers` is absent. A STRICT SUPERSET of the pre-#244 behaviour:
137
+ *
138
+ * - `db` covers every receiver the old `…db\b` regex reached (see
139
+ * {@link receiverMatches} for the suffix arm that preserves it) plus the
140
+ * `db`-prefixed locals it missed (`dbUse`, `dbToUse`, `m_dbLanzadera`).
141
+ * - `getdb` covers the per-backend accessor family (`getdbHPS()`,
142
+ * `getdbExpedientes()`, `getdbLanzadera()`) the issue measured at 26-36%
143
+ * of all execution sites.
144
+ * - `CurrentDb` / `DBEngine` are the Access built-ins.
145
+ * - `qd` / `qdf` are the conventional DAO `QueryDef` receivers.
146
+ * - `DoCmd.RunSQL` is the Access statement form (both the literal and the
147
+ * variable spelling, previously two dedicated regexes).
148
+ * - `Connection.Execute` / `Recordset.Open` are the ADO pair.
149
+ *
150
+ * Project-specific accessors that do not fit these names are added through
151
+ * `vba.sqlWrappers`, which EXTENDS this list rather than replacing it.
152
+ */
153
+ exports.DEFAULT_SQL_WRAPPERS = [
154
+ 'db',
155
+ 'getdb',
156
+ 'CurrentDb',
157
+ 'DBEngine',
158
+ 'qd',
159
+ 'qdf',
160
+ 'DoCmd.RunSQL',
161
+ 'Connection.Execute',
162
+ 'Recordset.Open',
163
+ ];
164
+ /**
165
+ * Parse the plain-string wrapper entries into matchers and pair them with a
166
+ * fresh pair of scanning RegExps.
167
+ *
168
+ * `configured` entries are APPENDED to {@link DEFAULT_SQL_WRAPPERS}, never
169
+ * substituted for them: a project that names its own accessor must not lose
170
+ * `CurrentDb` in the trade. Entries are accepted in exactly two forms — a
171
+ * bare identifier fragment (`"getdb"`) or a `receiver.method` pair
172
+ * (`"cnn.Execute"`). Anything else is dropped here; `project-config.ts`
173
+ * already warned about it at load time.
174
+ */
175
+ function compileSqlWrappers(configured) {
176
+ const matchers = [];
177
+ const seen = new Set();
178
+ for (const raw of [...exports.DEFAULT_SQL_WRAPPERS, ...(configured ?? [])]) {
179
+ const entry = (raw ?? '').trim().toLowerCase();
180
+ if (!entry || seen.has(entry))
181
+ continue;
182
+ const dot = entry.indexOf('.');
183
+ if (dot < 0) {
184
+ if (!/^\p{L}[\p{L}\p{N}_]*$/u.test(entry))
185
+ continue;
186
+ seen.add(entry);
187
+ matchers.push({ receiver: entry, method: null });
188
+ continue;
189
+ }
190
+ const receiver = entry.slice(0, dot);
191
+ const method = entry.slice(dot + 1);
192
+ if (!/^\p{L}[\p{L}\p{N}_]*$/u.test(receiver) ||
193
+ !/^\p{L}[\p{L}\p{N}_]*$/u.test(method)) {
194
+ continue;
195
+ }
196
+ seen.add(entry);
197
+ matchers.push({ receiver, method });
198
+ }
199
+ return {
200
+ literalRe: new RegExp(SQL_WRAPPER_LITERAL_RE.source, SQL_WRAPPER_LITERAL_RE.flags),
201
+ varRe: new RegExp(SQL_WRAPPER_VAR_RE.source, SQL_WRAPPER_VAR_RE.flags),
202
+ matchers,
203
+ };
204
+ }
205
+ /**
206
+ * The default wrappers, compiled once for the whole module. Used by any
207
+ * `VbaExtractorContext` built without options (tests, out-of-repo callers) so
208
+ * "no config" behaves exactly like "config absent" and still never compiles a
209
+ * RegExp per line. Safe to share: `scanSqlInLine` resets `lastIndex` before
210
+ * every scan and never re-enters itself.
211
+ */
212
+ let defaultCompiled = null;
213
+ function defaultSqlWrappers() {
214
+ if (defaultCompiled === null)
215
+ defaultCompiled = compileSqlWrappers();
216
+ return defaultCompiled;
217
+ }
218
+ /**
219
+ * Does `receiver` name a database handle under `pattern`?
220
+ *
221
+ * A leading `m_` / `p_` scope prefix is stripped first — `m_dbLanzadera` is
222
+ * the same handle as `dbLanzadera`, and VBA codebases use both spellings for
223
+ * the same variable.
224
+ *
225
+ * Then TWO arms, both comparing case-insensitively:
226
+ *
227
+ * - PREFIX — the arm this issue adds, and the one every per-backend
228
+ * accessor needs: `getdbHPS`, `getdbExpedientes`, `dbUse`, `dbToUse`.
229
+ * The pattern must END AT A SEGMENT BOUNDARY: what follows it is either
230
+ * nothing, or a character that is neither a lowercase letter nor `_`
231
+ * (a capital or a digit).
232
+ * Without that, `db` would also claim `dbg`, `dbase` and `db_test` —
233
+ * names that merely START with the same two letters. Every execution
234
+ * receiver in the three measured corpora (`getdbNC`, `getdbAGEDO`,
235
+ * `dbUse`, `p_db`, `qdf`, …) clears the boundary.
236
+ * - SUFFIX — `MiBaseDatosdb` for pattern `db`. This is what the pre-#244
237
+ * regex `\b(?:\p{L}[\p{L}\p{N}_]*)?db\b` matched, kept so the defaults
238
+ * stay a strict superset of the old behaviour instead of trading one
239
+ * blind spot for another.
240
+ *
241
+ * The method check in {@link matchesWrapper} is the second gate: `dbg.Print`
242
+ * fails both the boundary rule AND the method rule.
243
+ */
244
+ function receiverMatches(receiver, pattern) {
245
+ const lower = receiver.toLowerCase();
246
+ const bare = lower.startsWith('m_') || lower.startsWith('p_') ? lower.slice(2) : lower;
247
+ if (bare.endsWith(pattern))
248
+ return true;
249
+ if (!bare.startsWith(pattern))
250
+ return false;
251
+ if (bare.length === pattern.length)
252
+ return true;
253
+ // Segment boundary: read the ORIGINAL (un-lowercased) character so
254
+ // `dbUse` splits at the capital and `dbase` does not split at all.
255
+ const next = receiver.slice(receiver.length - bare.length).charAt(pattern.length);
256
+ return next !== '_' && next === next.toUpperCase();
257
+ }
258
+ /** Is `<receiver>.<method>` an execution site under any configured wrapper? */
259
+ function matchesWrapper(wrappers, receiver, method) {
260
+ const methodLower = method.toLowerCase();
261
+ for (const matcher of wrappers.matchers) {
262
+ const methodOk = matcher.method === null
263
+ ? DEFAULT_WRAPPER_METHODS.has(methodLower)
264
+ : matcher.method === methodLower;
265
+ if (!methodOk)
266
+ continue;
267
+ if (receiverMatches(receiver, matcher.receiver))
268
+ return true;
269
+ }
270
+ return false;
91
271
  }
92
272
  /**
93
273
  * Regex matching the chained `& "..."` literals that may follow a
@@ -101,70 +281,25 @@ function classifySqlAccess(sqlString, clause) {
101
281
  * Cross-physical-line concat via `_` continuation is OUT OF SCOPE for
102
282
  * v1 (deferred; see commit message).
103
283
  */
104
- const SQL_WRAPPER_CHAIN_RE = /&\s*"((?:[^"]|"")*)"/g;
105
284
  /**
106
285
  * Given the text that follows a SQL wrapper's first literal on the same
107
- * physical line, return the contents of every `& "..."` chained literal
108
- * in source order. Operates per-physical-line only VBA `_` line
109
- * continuation across physical lines is handled separately by
110
- * `collectStringLiteralText` for the variable-assignment path.
286
+ * physical line, return one fragment per `&`-concatenated operand in
287
+ * source order. Each fragment is either the CONTENT of a `"..."` literal
288
+ * or a `?` sentinel for any non-literal operand (a variable, function
289
+ * call, expression). The `?` sentinel can never match the
290
+ * `scanSqlTables` identifier class, so `"DELETE FROM " & tabla & " WHERE x"`
291
+ * becomes `"DELETE FROM ? WHERE x"` — the reserved-word reject list
292
+ * drops the `WHERE` capture and no `vba-sql-table` edge is emitted.
293
+ *
294
+ * Operates per-physical-line only — VBA `_` line continuation across
295
+ * physical lines is handled separately by `collectConcatFragments` for
296
+ * the variable-assignment path (see below).
111
297
  */
112
298
  function collectSqlWrapperChain(rest) {
113
- const out = [];
114
- const re = new RegExp(SQL_WRAPPER_CHAIN_RE.source, SQL_WRAPPER_CHAIN_RE.flags);
115
- let m;
116
- while ((m = re.exec(rest)) !== null) {
117
- out.push(m[1] ?? '');
118
- }
119
- return out;
120
- }
121
- function scanSqlInLine(ctx, line, lineNum, dedupe, sqlVariables) {
122
- for (const { re } of SQL_WRAPPERS) {
123
- // Each wrapper regex is stateful (has /g); reset before use.
124
- const localRe = new RegExp(re.source, re.flags);
125
- let m;
126
- while ((m = localRe.exec(line)) !== null) {
127
- const firstLiteral = m[1] ?? '';
128
- // After the wrapper regex consumes up to and including the closing
129
- // `"` of the first literal, walk the rest of the line for any
130
- // `& "..."` chains and concatenate every literal's content. Joining
131
- // with a space (mirrors `collectStringLiteralText`) keeps adjacent
132
- // `FROM tblA` & `FROM tblB` separated so `SQL_TABLE_RE` finds both.
133
- const rest = line.slice(m.index + m[0].length);
134
- const chain = collectSqlWrapperChain(rest);
135
- const joined = [firstLiteral, ...chain].join(' ');
136
- emitSqlTableReferences(ctx, joined, lineNum, dedupe);
137
- }
138
- }
139
- const localRe = new RegExp(SQL_VAR_EXEC_RE.source, SQL_VAR_EXEC_RE.flags);
140
- let vm;
141
- while ((vm = localRe.exec(line)) !== null) {
142
- const varName = (vm[1] ?? '').toLowerCase();
143
- const sqlString = sqlVariables.get(varName);
144
- if (!sqlString)
145
- continue;
146
- emitSqlTableReferences(ctx, sqlString, lineNum, dedupe);
147
- }
148
- // Issue #42: `DoCmd.RunSQL <identifier>` (variable form). Mirrors the
149
- // SQL_VAR_EXEC_RE path above but for the Access-style `DoCmd.RunSQL`
150
- // idiom — the dominant pattern in real-world VBA modules. Resolve the
151
- // captured identifier against `sqlVariables` (populated by
152
- // `trackSqlVariableAssignment` with `&`-accumulate semantics, Issue
153
- // #13) and feed the resolved SQL string into `emitSqlTableReferences`.
154
- // Unresolved identifiers (no row in the map) are silently skipped —
155
- // same graceful-no-op contract as SQL_VAR_EXEC_RE.
156
- const docmdLocalRe = new RegExp(SQL_VAR_DOCMD_RUNSQL_RE.source, SQL_VAR_DOCMD_RUNSQL_RE.flags);
157
- let dm;
158
- while ((dm = docmdLocalRe.exec(line)) !== null) {
159
- const varName = (dm[1] ?? '').toLowerCase();
160
- const sqlString = sqlVariables.get(varName);
161
- if (!sqlString)
162
- continue;
163
- emitSqlTableReferences(ctx, sqlString, lineNum, dedupe);
164
- }
299
+ return collectConcatFragments(rest);
165
300
  }
166
301
  /**
167
- * #13 fix: `sql = sql & "..."` (self-referential concatenation) must
302
+ * Issue #13: `sql = sql & "..."` (self-referential concatenation) must
168
303
  * ACCUMULATE the new fragment onto whatever was already tracked for
169
304
  * `varName`, not overwrite it. Overwriting silently dropped earlier
170
305
  * fragments' tables — typically the initial `FROM <table>` in
@@ -196,34 +331,473 @@ function trackSqlVariableAssignment(lines, lineIndex, sqlVariables) {
196
331
  sqlVariables.set(varName, newFragment);
197
332
  }
198
333
  }
334
+ /**
335
+ * Walk the lines starting at `startIndex` and collect every
336
+ * `&`-concatenated fragment — `"..."` literal CONTENT or a `?`
337
+ * sentinel for non-literal operands. Multi-line concat (via `_`
338
+ * continuation) is handled by carrying on while the current physical
339
+ * line ends with `&`.
340
+ *
341
+ * Replaces the legacy implementation that used `extractStringLiterals`
342
+ * to extract every `"..."` and silently dropped every non-literal
343
+ * operand between them — the source of Issue #203's silently-wrong
344
+ * `vba-sql-table` captures.
345
+ */
199
346
  function collectStringLiteralText(lines, startIndex) {
200
347
  const fragments = [];
201
348
  for (let i = startIndex; i < lines.length; i++) {
202
349
  const line = lines[i] ?? '';
203
- for (const lit of (0, vba_preprocess_1.extractStringLiterals)(line)) {
204
- fragments.push(lit.text);
205
- }
350
+ fragments.push(...collectConcatFragments(line));
206
351
  if (!line.trimEnd().endsWith('&'))
207
352
  break;
208
353
  }
209
354
  return fragments.join(' ');
210
355
  }
211
- function emitSqlTableReferences(ctx, sqlString, lineNum, dedupe) {
212
- // Scan the SQL string for FROM/INTO/UPDATE <table>.
213
- // Preserve the source regex's `/u` flag (Unicode property classes)
214
- // — hardcoding `'gi'` here would silently break non-ASCII identifiers.
215
- const tableRe = new RegExp(SQL_TABLE_RE.source, SQL_TABLE_RE.flags);
216
- let tm;
217
- while ((tm = tableRe.exec(sqlString)) !== null) {
218
- const clause = tm[1] ?? '';
219
- const table = (tm[2] ?? '').replace(/[\[\]]/g, '');
220
- if (!table)
221
- continue;
222
- const key = `${lineNum}:${table}`;
356
+ /**
357
+ * Walk a single source string and emit one fragment per
358
+ * `&`-concatenated operand.
359
+ *
360
+ * - A `"..."` literal → its content (with `""` doubled-quote escapes
361
+ * collapsed to a single `"`).
362
+ * - A non-literal operand (any non-whitespace token — variable,
363
+ * function call, expression, the `m_SQL = ` prefix on an
364
+ * assignment) that sits BETWEEN two operands in the chain → emit
365
+ * a `?` sentinel so the gap can never match an identifier in
366
+ * `scanSqlTables`.
367
+ * - Leading / trailing / inter-fragment whitespace is skipped (the
368
+ * `[...].join(' ')` in the callers handles the gap).
369
+ *
370
+ * Implementation: a small state machine that walks the string,
371
+ * alternating between two modes:
372
+ *
373
+ * 1. IN-LITERAL — collect until matching `"`, honour `""` escapes.
374
+ * 2. IN-GAP — scan until next `&` (concat operator) or next `"` (start
375
+ * of next literal). If the gap contained any non-whitespace
376
+ * characters AND we previously emitted a literal, emit a `?`
377
+ * sentinel — that's the "we don't know this table" signal the
378
+ * regex needs.
379
+ *
380
+ * Why mode 2 stops at `"`: a `"` inside a non-literal operand is the
381
+ * start of the NEXT literal in the chain; treating it as part of the
382
+ * gap would silently consume a real literal and re-introduce the
383
+ * Issue #203 bug.
384
+ */
385
+ function collectConcatFragments(src) {
386
+ const out = [];
387
+ let i = 0;
388
+ let lastEmittedLiteral = false;
389
+ while (i < src.length) {
390
+ const ch = src[i] ?? '';
391
+ if (ch === '"') {
392
+ // Mode 1 — literal.
393
+ let text = '';
394
+ i++;
395
+ while (i < src.length) {
396
+ const c = src[i] ?? '';
397
+ if (c === '"' && src[i + 1] === '"') {
398
+ text += '"';
399
+ i += 2;
400
+ continue;
401
+ }
402
+ if (c === '"' || c === '\n') {
403
+ i++;
404
+ break;
405
+ }
406
+ text += c;
407
+ i++;
408
+ }
409
+ out.push(text);
410
+ lastEmittedLiteral = true;
411
+ continue;
412
+ }
413
+ if (/\s/.test(ch)) {
414
+ i++;
415
+ continue;
416
+ }
417
+ // Mode 2 — gap (non-literal operand). Walk until the next `"` or
418
+ // `&`. If we saw non-whitespace content AND a literal came before
419
+ // us in this chain, emit the `?` sentinel so the gap can never
420
+ // bridge two SQL keywords.
421
+ let sawContent = false;
422
+ while (i < src.length) {
423
+ const c = src[i] ?? '';
424
+ if (c === '"' || c === '&')
425
+ break;
426
+ if (!/\s/.test(c))
427
+ sawContent = true;
428
+ i++;
429
+ }
430
+ if (src[i] === '&') {
431
+ i++;
432
+ }
433
+ if (sawContent && lastEmittedLiteral) {
434
+ out.push('?');
435
+ }
436
+ lastEmittedLiteral = false;
437
+ }
438
+ return out;
439
+ }
440
+ /**
441
+ * Issue #253: `QueryDefs("nombreConsulta")` names a saved query directly,
442
+ * with no wrapper method involved, so it gets its own pattern rather than a
443
+ * row in the wrapper table. String-literal argument only — a variable holds a
444
+ * name nothing static can know.
445
+ */
446
+ const QUERY_DEFS_RE = /\bQueryDefs\s*\(\s*"([^"]*)"\s*\)/giu;
447
+ /**
448
+ * Issue #253: does this literal read as a SQL statement rather than the name
449
+ * of a saved query?
450
+ *
451
+ * The gate is deliberately WIDER than the four DML verbs the issue names. A
452
+ * literal is rejected as SQL if it contains any of these keywords anywhere,
453
+ * because the cost of the two mistakes is not symmetric: treating SQL as a
454
+ * query name emits a reference to a name no `queries/*.sql` will ever carry
455
+ * (a permanent `failed` reference and a confusing one), while treating an
456
+ * oddly-named query as SQL emits nothing at all. Silent beats wrong.
457
+ */
458
+ const SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|INTO|VALUES|SET|CREATE|ALTER|DROP|UNION|GROUP|ORDER|HAVING|TRANSFORM|PARAMETERS|EXEC|EXECUTE|DISTINCT|TABLE)\b/iu;
459
+ /**
460
+ * Issue #253: an Access object name — letters, digits, underscores and
461
+ * interior spaces, nothing else. No punctuation, no operators, no wildcards.
462
+ *
463
+ * The length cap is not cosmetic: it is the last line of defence against a
464
+ * long keyword-free string (a message, a `Value List` payload) being read as
465
+ * a query name. Access itself caps object names at 64 characters.
466
+ */
467
+ const ACCESS_OBJECT_NAME_RE = /^[\p{L}_][\p{L}\p{N}_ ]{0,63}$/u;
468
+ /** True when a wrapper's literal argument names a saved query, not a statement. */
469
+ function looksLikeSavedQueryName(text) {
470
+ const trimmed = text.trim();
471
+ if (!trimmed)
472
+ return false;
473
+ if (SQL_KEYWORD_RE.test(trimmed))
474
+ return false;
475
+ return ACCESS_OBJECT_NAME_RE.test(trimmed);
476
+ }
477
+ /**
478
+ * Issue #253: emit ONE `dao-query` unresolved reference for a saved-query
479
+ * name, from the CALLING PROCEDURE (not the module).
480
+ *
481
+ * The procedure is the source on purpose: `SqlQueryExtractor` already links
482
+ * `query -> table`, so a `procedure -> query` reference is the hop that makes
483
+ * `procedure -> query -> table` traversable end to end. An edge hung off the
484
+ * module would leave that flow broken at its first hop.
485
+ *
486
+ * No node is created. The resolver binds this to the REAL `query` node built
487
+ * from `queries/<Name>.sql`, and declines when there is none — a name that
488
+ * matches neither a query nor a table stays an actionable `failed` reference
489
+ * rather than becoming a fabricated table placeholder.
490
+ */
491
+ function emitSavedQueryReference(ctx, queryName, lineNum, column, caller, dedupe) {
492
+ const trimmed = queryName.trim();
493
+ if (!trimmed)
494
+ return;
495
+ // The `query-name:` prefix keeps this de-dup bucket disjoint from the table
496
+ // and external-backend buckets that share the same set.
497
+ const key = `${lineNum}:query-name:${trimmed.toLocaleLowerCase('en-US')}`;
498
+ if (dedupe.has(key))
499
+ return;
500
+ dedupe.add(key);
501
+ ctx.unresolvedReferences.push({
502
+ fromNodeId: ctx.findOrCreateFunctionNodeId(caller),
503
+ referenceName: trimmed,
504
+ referenceKind: 'dao-query',
505
+ line: lineNum,
506
+ column,
507
+ filePath: ctx.filePath,
508
+ language: 'vba',
509
+ metadata: { synthesizedBy: 'vba-query-name' },
510
+ });
511
+ }
512
+ /**
513
+ * Issue #255 (task T13): the Access domain aggregate functions. Every one of
514
+ * them takes the DOMAIN — the name of a table or of a saved query — as its
515
+ * SECOND argument.
516
+ *
517
+ * The regex recognises the call shape only. The name gate and the emit path
518
+ * are T11's (`looksLikeSavedQueryName` / `emitSavedQueryReference`), so a
519
+ * domain name is answered by exactly the same "is this a saved query?" rule
520
+ * that already answers `OpenRecordset("qryX")` and `QueryDefs("qryX")` — one
521
+ * resolver, not two.
522
+ *
523
+ * The trailing `\s*\(` is load-bearing twice over: it anchors the name so a
524
+ * project helper called `DLookupSeguro(` can never be read as `DLookup`, and
525
+ * it hands {@link secondArgumentText} the paren to start walking from.
526
+ */
527
+ const DOMAIN_FUNCTION_RE = /\b(DLookup|DCount|DSum|DMax|DMin|DAvg|DFirst|DLast)\s*\(/giu;
528
+ /**
529
+ * A domain argument we are willing to read: EXACTLY one string literal and
530
+ * nothing else. A variable (`DLookup("N", tabla)`) or an expression
531
+ * (`DLookup("N", "Tb" & sufijo)`) names something only the runtime knows, and
532
+ * is skipped in silence rather than guessed at.
533
+ */
534
+ const LONE_STRING_LITERAL_RE = /^"((?:[^"]|"")*)"$/u;
535
+ /**
536
+ * Return the raw source text of the SECOND argument of the call whose opening
537
+ * paren sits at `openParen`, or `null` when the call has fewer than two
538
+ * arguments.
539
+ *
540
+ * Walks characters instead of splitting on `,` because two ordinary spellings
541
+ * break a naive split: a comma inside the criteria literal
542
+ * (`DLookup("N", "T", "Id In (1,2)")`) and a comma inside a nested call
543
+ * (`DLookup("N", "T", "Id=" & Nz(x, 0))`). Only a comma at depth 1 and outside
544
+ * a string literal separates arguments.
545
+ */
546
+ function secondArgumentText(src, openParen) {
547
+ const args = [];
548
+ let current = '';
549
+ let depth = 1;
550
+ let i = openParen + 1;
551
+ while (i < src.length && args.length < 2) {
552
+ const ch = src[i] ?? '';
553
+ if (ch === '"') {
554
+ // Copy the literal through verbatim, honouring `""` escapes, so a comma
555
+ // or a paren inside it can never be read as structure.
556
+ current += ch;
557
+ i++;
558
+ while (i < src.length) {
559
+ const c = src[i] ?? '';
560
+ if (c === '"' && src[i + 1] === '"') {
561
+ current += '""';
562
+ i += 2;
563
+ continue;
564
+ }
565
+ current += c;
566
+ i++;
567
+ if (c === '"')
568
+ break;
569
+ }
570
+ continue;
571
+ }
572
+ if (ch === '(') {
573
+ depth++;
574
+ current += ch;
575
+ i++;
576
+ continue;
577
+ }
578
+ if (ch === ')') {
579
+ depth--;
580
+ if (depth === 0) {
581
+ args.push(current);
582
+ break;
583
+ }
584
+ current += ch;
585
+ i++;
586
+ continue;
587
+ }
588
+ if (ch === ',' && depth === 1) {
589
+ args.push(current);
590
+ current = '';
591
+ i++;
592
+ continue;
593
+ }
594
+ current += ch;
595
+ i++;
596
+ }
597
+ return args.length >= 2 ? args[1] ?? null : null;
598
+ }
599
+ /**
600
+ * Does the file being scanned declare its own procedure by this name?
601
+ *
602
+ * A project that ships its own `DLookup` wrapper means the call site is user
603
+ * code, not the Access built-in, and its second argument carries no promise of
604
+ * naming a domain. The call itself still resolves to that user code through
605
+ * the ordinary call-site scan — this gate only stops the extra domain
606
+ * reference from being invented on top of it.
607
+ *
608
+ * `localProcs` is keyed by the DECLARED spelling and VBA identifiers are
609
+ * case-insensitive, so the keys are compared folded. The procedures sweep is a
610
+ * PRE-walk (see `vba-extractor.ts`), so a helper declared further down the
611
+ * file is already visible by the time this line is scanned.
612
+ */
613
+ function declaresOwnProcedure(ctx, name) {
614
+ const wanted = name.toLocaleLowerCase('en-US');
615
+ for (const declared of ctx.localProcs.keys()) {
616
+ if (declared.toLocaleLowerCase('en-US') === wanted)
617
+ return true;
618
+ }
619
+ return false;
620
+ }
621
+ /**
622
+ * Issue #255: scan one line for domain-aggregate calls and emit the saved-query
623
+ * reference their second argument names.
624
+ *
625
+ * Deliberately NOT handled: a domain spelled as a full SQL statement
626
+ * (`DLookup("N", "SELECT Id FROM T")`), which Access also accepts. It is
627
+ * rejected by `looksLikeSavedQueryName`'s verb gate and produces nothing —
628
+ * the same silence T11 chose for every literal it cannot classify.
629
+ */
630
+ function scanDomainFunctionsInLine(ctx, line, lineNum, dedupe, caller) {
631
+ DOMAIN_FUNCTION_RE.lastIndex = 0;
632
+ let m;
633
+ while ((m = DOMAIN_FUNCTION_RE.exec(line)) !== null) {
634
+ if (declaresOwnProcedure(ctx, m[1] ?? ''))
635
+ continue;
636
+ // `m[0]` ends with the opening paren, so its last index is where the
637
+ // argument list starts.
638
+ const raw = secondArgumentText(line, m.index + m[0].length - 1);
639
+ if (raw === null)
640
+ continue;
641
+ const literal = LONE_STRING_LITERAL_RE.exec(raw.trim());
642
+ if (!literal)
643
+ continue;
644
+ const domain = (literal[1] ?? '').replace(/""/g, '"');
645
+ if (!looksLikeSavedQueryName(domain))
646
+ continue;
647
+ emitSavedQueryReference(ctx, domain, lineNum, m.index, caller, dedupe);
648
+ }
649
+ }
650
+ function scanSqlInLine(ctx, line, lineNum, dedupe, sqlVariables, caller) {
651
+ // Issue #244: one compiled wrapper set per extractor, cached on the
652
+ // context. A context built without options (tests, out-of-repo callers)
653
+ // falls back to the module-level defaults — never to a per-line compile.
654
+ const wrappers = ctx.sqlWrappers ?? defaultSqlWrappers();
655
+ // Literal form — `getdb().Execute "DELETE FROM T"`,
656
+ // `CurrentDb.OpenRecordset("SELECT * FROM T")`, `DoCmd.RunSQL "…"`.
657
+ const literalRe = wrappers.literalRe;
658
+ literalRe.lastIndex = 0;
659
+ let m;
660
+ while ((m = literalRe.exec(line)) !== null) {
661
+ if (!matchesWrapper(wrappers, m[1] ?? '', m[2] ?? ''))
662
+ continue;
663
+ const firstLiteral = m[3] ?? '';
664
+ // After the wrapper regex consumes up to and including the closing
665
+ // `"` of the first literal, walk the rest of the line for any
666
+ // `& "..."` chains and concatenate every literal's content. Joining
667
+ // with a space (mirrors `collectStringLiteralText`) keeps adjacent
668
+ // `FROM tblA` & `FROM tblB` separated so `scanSqlTables` finds both.
669
+ const rest = line.slice(m.index + m[0].length);
670
+ const chain = collectSqlWrapperChain(rest);
671
+ const joined = [firstLiteral, ...chain].join(' ');
672
+ // Issue #253: a wrapper handed a literal that is NOT a statement but IS a
673
+ // bare Access object name is naming a saved query. Verb detection wins:
674
+ // anything that reads as SQL takes the table path below, unchanged.
675
+ if (caller && chain.length === 0 && looksLikeSavedQueryName(joined)) {
676
+ emitSavedQueryReference(ctx, joined, lineNum, m.index, caller, dedupe);
677
+ continue;
678
+ }
679
+ emitSqlTableReferences(ctx, joined, lineNum, dedupe);
680
+ }
681
+ // Variable form — `getdb().Execute strSQL`, `qdf.Execute sql`,
682
+ // `DoCmd.RunSQL(strSQL)` (Issue #42). The captured identifier is resolved
683
+ // against `sqlVariables` (populated by `trackSqlVariableAssignment` with
684
+ // `&`-accumulate semantics, Issue #13); an identifier with no row in the
685
+ // map is silently skipped — we never guess what a variable holds.
686
+ const varRe = wrappers.varRe;
687
+ varRe.lastIndex = 0;
688
+ let vm;
689
+ while ((vm = varRe.exec(line)) !== null) {
690
+ if (!matchesWrapper(wrappers, vm[1] ?? '', vm[2] ?? ''))
691
+ continue;
692
+ const varName = (vm[3] ?? '').toLowerCase();
693
+ const sqlString = sqlVariables.get(varName);
694
+ if (!sqlString)
695
+ continue;
696
+ // A variable that accumulated a bare name rather than a statement names a
697
+ // saved query, exactly as the literal form does.
698
+ if (caller && looksLikeSavedQueryName(sqlString)) {
699
+ emitSavedQueryReference(ctx, sqlString, lineNum, vm.index, caller, dedupe);
700
+ continue;
701
+ }
702
+ emitSqlTableReferences(ctx, sqlString, lineNum, dedupe);
703
+ }
704
+ // Issue #253: `db.QueryDefs("nombreConsulta")` names a saved query with no
705
+ // wrapper method in sight, so it is scanned independently of the wrapper
706
+ // table. Literal argument only.
707
+ if (caller) {
708
+ QUERY_DEFS_RE.lastIndex = 0;
709
+ let qm;
710
+ while ((qm = QUERY_DEFS_RE.exec(line)) !== null) {
711
+ const name = qm[1] ?? '';
712
+ if (!looksLikeSavedQueryName(name))
713
+ continue;
714
+ emitSavedQueryReference(ctx, name, lineNum, qm.index, caller, dedupe);
715
+ }
716
+ // Issue #255: `DLookup("Nombre", "TbUsuarios", "Id=1")` and its seven
717
+ // siblings name their domain — a table or a saved query — in argument 2.
718
+ // Same gate, same emit path, same resolver as the saved-query names above.
719
+ scanDomainFunctionsInLine(ctx, line, lineNum, dedupe, caller);
720
+ }
721
+ // Issue #252 — SQL bound to a form/control property at runtime.
722
+ scanRuntimeBindingAssignment(ctx, line, lineNum, dedupe, sqlVariables);
723
+ }
724
+ /**
725
+ * Issue #252 — SQL assigned to a binding property at runtime.
726
+ *
727
+ * This is the half of the data-binding problem the `.form.txt` sweep cannot
728
+ * see. The measured corpus binds in code, not in the designer: 161
729
+ * `.RowSource =` assignments across `.cls`/`.bas`, against ZERO
730
+ * `RecordSource` and ZERO `ControlSource` occurrences in `00_EXPEDIENTES`'s
731
+ * `.form.txt` files. Everything those statements touch was invisible.
732
+ *
733
+ * The right-hand side goes through the SAME pipeline the wrapper path uses —
734
+ * {@link collectConcatFragments} then `scanSqlTables` — deliberately, and not
735
+ * a parallel one. That is what makes the `?` sentinel for non-literal
736
+ * operands and the shared reserved-word reject list apply here for free:
737
+ * `.RowSource = "SELECT " & campo & " FROM " & tabla` becomes
738
+ * `SELECT ? FROM ?` and yields no table at all, rather than the confident
739
+ * wrong `FROM` / `WHERE` captures Issue #203 documents.
740
+ *
741
+ * A value that is NOT SQL — a bare saved-query name, a `Value List` payload,
742
+ * a plain field name on `ControlSource` — produces nothing here on its own,
743
+ * because `scanSqlTables` finds no clause keyword in it. Connecting a bare
744
+ * query name to its saved query is T11's job, not this one; guessing would
745
+ * be exactly the "confident wrong edge" the project forbids.
746
+ */
747
+ function scanRuntimeBindingAssignment(ctx, line, lineNum, dedupe, sqlVariables) {
748
+ const m = BINDING_ASSIGN_RE.exec(line);
749
+ if (!m)
750
+ return;
751
+ const rhs = (m[2] ?? '').trim();
752
+ if (!rhs)
753
+ return;
754
+ // Literal form — one or more `"..."` operands, possibly `&`-chained.
755
+ const fragments = collectConcatFragments(rhs);
756
+ if (fragments.length > 0) {
757
+ emitSqlTableReferences(ctx, fragments.join(' '), lineNum, dedupe, exports.RUNTIME_BINDING_SYNTHESIZER);
758
+ return;
759
+ }
760
+ // Variable form — `.RowSource = strSQL`. Resolved against the SAME
761
+ // `sqlVariables` map the wrapper path uses, so the `&`-accumulate
762
+ // semantics of Issue #13 and the procedure scoping of Issue #204 apply
763
+ // unchanged. An identifier with no row in the map is skipped: we never
764
+ // guess what a variable holds.
765
+ if (!BINDING_VAR_RE.test(rhs))
766
+ return;
767
+ const sqlString = sqlVariables.get(rhs.toLowerCase());
768
+ if (!sqlString)
769
+ return;
770
+ emitSqlTableReferences(ctx, sqlString, lineNum, dedupe, exports.RUNTIME_BINDING_SYNTHESIZER);
771
+ }
772
+ function emitSqlTableReferences(ctx, sqlString, lineNum, dedupe, synthesizedBy = SQL_TABLE_SYNTHESIZER) {
773
+ // Issue #203: delegate to the shared scanner. It owns the
774
+ // `FROM/JOIN/INTO/UPDATE <table>` regex, the reserved-word reject
775
+ // list, the `?`-sentinel fallback (see `vba/sql-wrapper.ts:144-178`)
776
+ // and the read/write access direction.
777
+ //
778
+ // Issue #252: the de-dup key is namespaced by `synthesizedBy` so the
779
+ // runtime-binding path and the wrapper path keep disjoint buckets. A
780
+ // single line can legitimately be both (`Set rs = db.OpenRecordset(sql)`
781
+ // has no binding, but a `With` body can interleave the two shapes), and
782
+ // one path must never silence the other's edge.
783
+ for (const row of (0, sql_table_scan_1.scanSqlTables)(sqlString)) {
784
+ const key = `${synthesizedBy}:${lineNum}:${row.table}`;
785
+ if (dedupe.has(key))
786
+ continue;
787
+ dedupe.add(key);
788
+ ctx.emitReference(row.table, lineNum, 0, synthesizedBy, row.access);
789
+ }
790
+ // Issue #256: the Access `IN "<path>"` clause points the statement at
791
+ // another database file. It is a cross-backend edge, so it gets its
792
+ // own `file`-kind target rather than being mistaken for a table.
793
+ // The `external-db:` prefix keeps this de-dup bucket disjoint from the
794
+ // table bucket above (a table name can never contain a colon).
795
+ for (const backendPath of (0, sql_table_scan_1.scanSqlExternalBackends)(sqlString)) {
796
+ const key = `${lineNum}:external-db:${backendPath}`;
223
797
  if (dedupe.has(key))
224
798
  continue;
225
799
  dedupe.add(key);
226
- ctx.emitReference(table, lineNum, 0, 'vba-sql-table', classifySqlAccess(sqlString, clause));
800
+ ctx.emitExternalBackendReference(backendPath, lineNum, 0);
227
801
  }
228
802
  }
229
803
  //# sourceMappingURL=sql-wrapper.js.map