code-auditor-mcp 3.4.12 → 3.4.13

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 (245) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/dist/analyzers/__verify_self_probe.d.ts +2 -0
  3. package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
  4. package/dist/analyzers/__verify_self_probe.js +5 -0
  5. package/dist/analyzers/__verify_self_probe.js.map +1 -0
  6. package/dist/analyzers/analyzerUtils.d.ts +34 -1
  7. package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
  8. package/dist/analyzers/analyzerUtils.js +61 -22
  9. package/dist/analyzers/analyzerUtils.js.map +1 -1
  10. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
  11. package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
  12. package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
  13. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  14. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
  15. package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
  16. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
  17. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  18. package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
  19. package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
  20. package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
  21. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  22. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
  23. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  24. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
  25. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  26. package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
  27. package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
  28. package/dist/analyzers/documentationAnalyzer.js +259 -198
  29. package/dist/analyzers/documentationAnalyzer.js.map +1 -1
  30. package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
  31. package/dist/analyzers/invariantsAnalyzer.js +57 -50
  32. package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
  33. package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
  34. package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
  35. package/dist/analyzers/orm/adapterRegistry.js +10 -0
  36. package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
  37. package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
  38. package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
  39. package/dist/analyzers/orm/drizzleAdapter.js +42 -44
  40. package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
  41. package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
  42. package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
  43. package/dist/analyzers/orm/prismaAdapter.js +30 -0
  44. package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
  45. package/dist/analyzers/provenance.d.ts +62 -2
  46. package/dist/analyzers/provenance.d.ts.map +1 -1
  47. package/dist/analyzers/provenance.js +443 -349
  48. package/dist/analyzers/provenance.js.map +1 -1
  49. package/dist/analyzers/reactAnalyzer.d.ts +11 -0
  50. package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
  51. package/dist/analyzers/reactAnalyzer.js +188 -125
  52. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  53. package/dist/analyzers/ruleRegistry.d.ts +110 -1
  54. package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
  55. package/dist/analyzers/ruleRegistry.js +1714 -124
  56. package/dist/analyzers/ruleRegistry.js.map +1 -1
  57. package/dist/analyzers/ruleTiming.d.ts +54 -0
  58. package/dist/analyzers/ruleTiming.d.ts.map +1 -0
  59. package/dist/analyzers/ruleTiming.js +71 -0
  60. package/dist/analyzers/ruleTiming.js.map +1 -0
  61. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
  62. package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
  63. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
  64. package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
  65. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
  66. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  67. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
  68. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  69. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
  70. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  71. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
  72. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  73. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
  74. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  75. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
  76. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  77. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
  78. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  79. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
  80. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  81. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
  82. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  83. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
  84. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  85. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
  86. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  87. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
  88. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  89. package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
  90. package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
  91. package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
  92. package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
  93. package/dist/analyzers/universal/schema/config.d.ts +31 -0
  94. package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
  95. package/dist/analyzers/universal/schema/config.js +57 -0
  96. package/dist/analyzers/universal/schema/config.js.map +1 -0
  97. package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
  98. package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
  99. package/dist/analyzers/universal/schema/discovery.js +616 -0
  100. package/dist/analyzers/universal/schema/discovery.js.map +1 -0
  101. package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
  102. package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
  103. package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
  104. package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
  105. package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
  106. package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
  107. package/dist/analyzers/universal/schema/migrations.js +238 -0
  108. package/dist/analyzers/universal/schema/migrations.js.map +1 -0
  109. package/dist/analyzers/universal/schema/types.d.ts +131 -0
  110. package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
  111. package/dist/analyzers/universal/schema/types.js +10 -0
  112. package/dist/analyzers/universal/schema/types.js.map +1 -0
  113. package/dist/analyzers/universal/schema/violations.d.ts +44 -0
  114. package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
  115. package/dist/analyzers/universal/schema/violations.js +54 -0
  116. package/dist/analyzers/universal/schema/violations.js.map +1 -0
  117. package/dist/auditRunner.d.ts.map +1 -1
  118. package/dist/auditRunner.js +89 -13
  119. package/dist/auditRunner.js.map +1 -1
  120. package/dist/cli.js +315 -40
  121. package/dist/cli.js.map +1 -1
  122. package/dist/config/configLoader.d.ts.map +1 -1
  123. package/dist/config/configLoader.js +9 -7
  124. package/dist/config/configLoader.js.map +1 -1
  125. package/dist/config/defaults.d.ts +12 -3
  126. package/dist/config/defaults.d.ts.map +1 -1
  127. package/dist/config/defaults.js +15 -5
  128. package/dist/config/defaults.js.map +1 -1
  129. package/dist/config/effectiveConfig.d.ts +75 -0
  130. package/dist/config/effectiveConfig.d.ts.map +1 -0
  131. package/dist/config/effectiveConfig.js +213 -0
  132. package/dist/config/effectiveConfig.js.map +1 -0
  133. package/dist/config/pathProfiles.d.ts +7 -4
  134. package/dist/config/pathProfiles.d.ts.map +1 -1
  135. package/dist/config/pathProfiles.js +8 -10
  136. package/dist/config/pathProfiles.js.map +1 -1
  137. package/dist/config/thresholdRationales.d.ts +45 -0
  138. package/dist/config/thresholdRationales.d.ts.map +1 -0
  139. package/dist/config/thresholdRationales.js +67 -0
  140. package/dist/config/thresholdRationales.js.map +1 -0
  141. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  142. package/dist/conventions/conventionMiner.js +6 -2
  143. package/dist/conventions/conventionMiner.js.map +1 -1
  144. package/dist/enforcement/diffGate.d.ts +55 -0
  145. package/dist/enforcement/diffGate.d.ts.map +1 -0
  146. package/dist/enforcement/diffGate.js +136 -0
  147. package/dist/enforcement/diffGate.js.map +1 -0
  148. package/dist/enforcement/gate.d.ts +36 -0
  149. package/dist/enforcement/gate.d.ts.map +1 -0
  150. package/dist/enforcement/gate.js +72 -0
  151. package/dist/enforcement/gate.js.map +1 -0
  152. package/dist/enforcement/suppressions.d.ts +41 -0
  153. package/dist/enforcement/suppressions.d.ts.map +1 -0
  154. package/dist/enforcement/suppressions.js +170 -0
  155. package/dist/enforcement/suppressions.js.map +1 -0
  156. package/dist/fingerprint.d.ts.map +1 -1
  157. package/dist/fingerprint.js +4 -1
  158. package/dist/fingerprint.js.map +1 -1
  159. package/dist/invariants/ruleEngine.d.ts +3 -4
  160. package/dist/invariants/ruleEngine.d.ts.map +1 -1
  161. package/dist/invariants/ruleEngine.js +2 -2
  162. package/dist/invariants/ruleEngine.js.map +1 -1
  163. package/dist/languages/LanguageOrchestrator.d.ts +14 -0
  164. package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
  165. package/dist/languages/LanguageOrchestrator.js +93 -55
  166. package/dist/languages/LanguageOrchestrator.js.map +1 -1
  167. package/dist/languages/LanguageRegistry.d.ts +8 -0
  168. package/dist/languages/LanguageRegistry.d.ts.map +1 -1
  169. package/dist/languages/LanguageRegistry.js +8 -0
  170. package/dist/languages/LanguageRegistry.js.map +1 -1
  171. package/dist/languages/RuntimeManager.d.ts +82 -60
  172. package/dist/languages/RuntimeManager.d.ts.map +1 -1
  173. package/dist/languages/RuntimeManager.js +404 -349
  174. package/dist/languages/RuntimeManager.js.map +1 -1
  175. package/dist/languages/UniversalAnalyzer.d.ts +51 -6
  176. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  177. package/dist/languages/UniversalAnalyzer.js +127 -88
  178. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  179. package/dist/languages/adapterBridge.d.ts +36 -0
  180. package/dist/languages/adapterBridge.d.ts.map +1 -1
  181. package/dist/languages/adapterBridge.js +89 -35
  182. package/dist/languages/adapterBridge.js.map +1 -1
  183. package/dist/languages/go/GoAdapter.d.ts +66 -23
  184. package/dist/languages/go/GoAdapter.d.ts.map +1 -1
  185. package/dist/languages/go/GoAdapter.js +553 -474
  186. package/dist/languages/go/GoAdapter.js.map +1 -1
  187. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
  188. package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
  189. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
  190. package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
  191. package/dist/languages/tree-sitter/converter.d.ts +18 -0
  192. package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
  193. package/dist/languages/tree-sitter/converter.js +18 -0
  194. package/dist/languages/tree-sitter/converter.js.map +1 -1
  195. package/dist/languages/tree-sitter/parser.d.ts +39 -2
  196. package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
  197. package/dist/languages/tree-sitter/parser.js +93 -12
  198. package/dist/languages/tree-sitter/parser.js.map +1 -1
  199. package/dist/languages/types.d.ts +67 -2
  200. package/dist/languages/types.d.ts.map +1 -1
  201. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
  202. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
  203. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
  204. package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
  205. package/dist/mcp-tools-shared.d.ts.map +1 -1
  206. package/dist/mcp-tools-shared.js.map +1 -1
  207. package/dist/pipeline.d.ts +13 -5
  208. package/dist/pipeline.d.ts.map +1 -1
  209. package/dist/pipeline.js +381 -200
  210. package/dist/pipeline.js.map +1 -1
  211. package/dist/pipelineAdapters.d.ts +3 -8
  212. package/dist/pipelineAdapters.d.ts.map +1 -1
  213. package/dist/pipelineAdapters.js +149 -94
  214. package/dist/pipelineAdapters.js.map +1 -1
  215. package/dist/presets/presets.d.ts +54 -0
  216. package/dist/presets/presets.d.ts.map +1 -0
  217. package/dist/presets/presets.js +181 -0
  218. package/dist/presets/presets.js.map +1 -0
  219. package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
  220. package/dist/reporting/sarifReportGenerator.js +0 -2
  221. package/dist/reporting/sarifReportGenerator.js.map +1 -1
  222. package/dist/ruleAliases.d.ts +67 -0
  223. package/dist/ruleAliases.d.ts.map +1 -0
  224. package/dist/ruleAliases.js +79 -0
  225. package/dist/ruleAliases.js.map +1 -0
  226. package/dist/styles/cssAstExtractor.d.ts.map +1 -1
  227. package/dist/styles/cssAstExtractor.js +78 -18
  228. package/dist/styles/cssAstExtractor.js.map +1 -1
  229. package/dist/styles/styleIndexer.js +10 -1
  230. package/dist/styles/styleIndexer.js.map +1 -1
  231. package/dist/styles/tailwindConfigLoader.d.ts +8 -6
  232. package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
  233. package/dist/styles/tailwindConfigLoader.js +61 -15
  234. package/dist/styles/tailwindConfigLoader.js.map +1 -1
  235. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  236. package/dist/styles/tailwindUtilityExpander.js +13 -1
  237. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  238. package/dist/types.d.ts +181 -1
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +8 -0
  241. package/dist/types.js.map +1 -1
  242. package/package.json +5 -3
  243. package/plugin/hooks/hooks.json +1 -1
  244. package/plugin/scripts/hook-audit.sh +25 -7
  245. package/plugin/skills/code-auditor/SKILL.md +5 -5
@@ -65,7 +65,26 @@ export const DB_TYPES = new Set([
65
65
  'SqliteDatabase',
66
66
  'BetterSQLite3Database',
67
67
  ]);
68
- /** DB call methods — the fixed API surface (language-invariant) */
68
+ /**
69
+ * DB call methods — the fixed API surface (language-invariant).
70
+ *
71
+ * Spec 33 Item 11 FP category 5: the bare-identifier hybrid fallback in
72
+ * `isDBProvenanced` treated any `get(...)` / `each(...)` / `values(...)` call
73
+ * as a DB query, flagging lodash-style object accessors (e.g.
74
+ * `@directus/utils`'s `get(item, ...)`) as sql-injection. Those three names
75
+ * are also common non-DB methods (lodash `get`, jQuery/iterator
76
+ * `each`, Map/WebSocket `.values()`), so they are removed from the fallback —
77
+ * mirroring the `get`/`each` trim in CHANGELOG 3.4.9 (DB_CALL_METHOD_NAMES).
78
+ *
79
+ * `query` is deliberately RETAINED: it is a genuine query-execution method on
80
+ * mysql2, pg, node-postgres, D1 and Planetscale (`.query(...)`), and the
81
+ * spec-19 data-access fixtures exercise it as a canonical DB entry point.
82
+ * Removing it would turn real SQL-injection positives into false negatives.
83
+ *
84
+ * `raw` is deliberately retained: it is a genuine raw-execution method on
85
+ * D1 prepared statements, Knex, and Kysely, and the Item-6 taint-tracking
86
+ * fixtures exercise it as the canonical raw-SQL entry point.
87
+ */
69
88
  export const DB_CALL_METHODS = new Set([
70
89
  'exec',
71
90
  'prepare',
@@ -74,10 +93,7 @@ export const DB_CALL_METHODS = new Set([
74
93
  'all',
75
94
  'first',
76
95
  'query',
77
- 'get',
78
- 'each',
79
96
  'raw',
80
- 'values',
81
97
  ]);
82
98
  /** ORM method patterns — fixed API surface for ORM recognition (Spec 21 R1) */
83
99
  export const ORM_METHODS = new Set([
@@ -128,6 +144,9 @@ function matchesValidatorPackage(specifier) {
128
144
  *
129
145
  * An import like `import Database from 'better-sqlite3'` produces
130
146
  * `Database` as DB-provenanced with reason "package".
147
+ * @param adapter
148
+ * @param ast
149
+ * @returns
131
150
  */
132
151
  export function extractDBProvenancedImports(ast, adapter) {
133
152
  const seedMap = new Map();
@@ -159,6 +178,9 @@ export function extractDBProvenancedImports(ast, adapter) {
159
178
  /**
160
179
  * Extract all validator-provenanced identifiers from a file's imports.
161
180
  * Same pattern as extractDBProvenancedImports but for validator packages.
181
+ * @param adapter
182
+ * @param ast
183
+ * @returns
162
184
  */
163
185
  export function extractValidatorProvenancedImports(ast, adapter) {
164
186
  const seedMap = new Map();
@@ -189,6 +211,133 @@ export function extractValidatorProvenancedImports(ast, adapter) {
189
211
  // ═══════════════════════════════════════════════════════════════════════════
190
212
  // Provenance propagation (R1 core)
191
213
  // ═══════════════════════════════════════════════════════════════════════════
214
+ /**
215
+ * Propagate provenance through a variable declaration (rules 1-3, 8).
216
+ * Returns true if any new identifier was added.
217
+ */
218
+ function propagateVariableDeclaration(node, ctx) {
219
+ const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, ctx.adapter);
220
+ if (!nameNode)
221
+ return false;
222
+ let mutated = false;
223
+ // Rule 8: type annotation — let x: D1Database
224
+ if (typeAnnotationNode) {
225
+ mutated = propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx);
226
+ }
227
+ if (valueNode) {
228
+ mutated = propagateFromValue(nameNode, valueNode, ctx) || mutated;
229
+ }
230
+ return mutated;
231
+ }
232
+ /** Rule 8: type annotation — `let x: D1Database`. */
233
+ function propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx) {
234
+ if (nameNode.type !== 'identifier')
235
+ return false;
236
+ const typeText = ctx.adapter.getNodeText(typeAnnotationNode, ctx.sourceCode).trim();
237
+ if (!DB_TYPES.has(typeText))
238
+ return false;
239
+ const name = ctx.adapter.getNodeText(nameNode, ctx.sourceCode);
240
+ if (ctx.provenanceMap.has(name))
241
+ return false;
242
+ ctx.provenanceMap.set(name, {
243
+ identifier: name,
244
+ reason: 'type',
245
+ source: `type annotation ${typeText}`,
246
+ chain: [],
247
+ });
248
+ return true;
249
+ }
250
+ /** Propagate provenance from a declarator's value expression into its names. */
251
+ function propagateFromValue(nameNode, valueNode, ctx) {
252
+ const propagated = tryPropagateFromExpression(valueNode, ctx.adapter, ctx.sourceCode, ctx.provenanceMap);
253
+ if (!propagated)
254
+ return false;
255
+ let mutated = false;
256
+ for (const varName of extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode)) {
257
+ if (!ctx.provenanceMap.has(varName)) {
258
+ ctx.provenanceMap.set(varName, {
259
+ identifier: varName,
260
+ reason: 'propagation',
261
+ source: propagated.source,
262
+ chain: [...propagated.chain, propagated.identifier],
263
+ });
264
+ mutated = true;
265
+ }
266
+ }
267
+ return mutated;
268
+ }
269
+ /**
270
+ * Propagate provenance through a default parameter (rule 6).
271
+ * Returns true if a new identifier was added.
272
+ */
273
+ function propagateDefaultParameter(node, adapter, sourceCode, provenanceMap) {
274
+ const children = adapter.getChildren(node);
275
+ // assignment_pattern has [left, right]
276
+ if (children.length < 2)
277
+ return false;
278
+ const leftNode = children[0];
279
+ const rightNode = children[1];
280
+ if (leftNode.type !== 'identifier')
281
+ return false;
282
+ const paramName = adapter.getNodeText(leftNode, sourceCode);
283
+ const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
284
+ if (propagated && !provenanceMap.has(paramName)) {
285
+ provenanceMap.set(paramName, {
286
+ identifier: paramName,
287
+ reason: 'propagation',
288
+ source: `default parameter = ${propagated.source}`,
289
+ chain: [...propagated.chain, propagated.identifier],
290
+ });
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ /**
296
+ * Propagate provenance through a class field initialization (rule 7).
297
+ * Returns true if a new identifier was added.
298
+ */
299
+ function propagateClassField(node, adapter, sourceCode, provenanceMap) {
300
+ const children = adapter.getChildren(node);
301
+ // Typically [name, value] or [decorators..., name, value]
302
+ const nameChild = children.find((c) => c.type === 'property_identifier');
303
+ const valueChild = children.find((c) => c.type !== 'property_identifier' &&
304
+ c.type !== 'decorator' &&
305
+ c.type !== 'private' &&
306
+ c.type !== 'public' &&
307
+ c.type !== 'protected' &&
308
+ c.type !== 'static' &&
309
+ c.type !== 'readonly' &&
310
+ c.type !== 'abstract');
311
+ if (nameChild && valueChild) {
312
+ const fieldName = adapter.getNodeText(nameChild, sourceCode);
313
+ const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
314
+ if (propagated && !provenanceMap.has(fieldName)) {
315
+ provenanceMap.set(fieldName, {
316
+ identifier: fieldName,
317
+ reason: 'propagation',
318
+ source: `class field initialized from ${propagated.source}`,
319
+ chain: [...propagated.chain, propagated.identifier],
320
+ });
321
+ return true;
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+ function applyPropagationRule(node, parent, ctx) {
327
+ const { adapter, sourceCode, provenanceMap } = ctx;
328
+ if (node.type === 'variable_declarator') {
329
+ return propagateVariableDeclaration(node, ctx);
330
+ }
331
+ if (node.type === 'assignment_pattern' &&
332
+ parent?.type === 'formal_parameters') {
333
+ return propagateDefaultParameter(node, adapter, sourceCode, provenanceMap);
334
+ }
335
+ if (node.type === 'public_field_definition' ||
336
+ node.type === 'field_definition') {
337
+ return propagateClassField(node, adapter, sourceCode, provenanceMap);
338
+ }
339
+ return false;
340
+ }
192
341
  /**
193
342
  * Propagate provenance through assignments, destructuring, parameters,
194
343
  * class fields, and type annotations within a single file.
@@ -202,10 +351,16 @@ export function extractValidatorProvenancedImports(ast, adapter) {
202
351
  * 6. default parameter with DB value
203
352
  * 7. class field initialized with DB value
204
353
  * 8. type annotation with known DB type
354
+ * @param adapter
355
+ * @param ast
356
+ * @param seedMap
357
+ * @param sourceCode
358
+ * @returns
205
359
  */
206
360
  export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
207
361
  // Work on a copy so we can add newly-provenanced identifiers during the walk
208
362
  const provenanceMap = new Map(seedMap);
363
+ const ctx = { adapter, sourceCode, provenanceMap };
209
364
  // Keep iterating until no new identifiers are discovered (handles chains)
210
365
  let changed = true;
211
366
  let iterations = 0;
@@ -214,100 +369,8 @@ export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
214
369
  changed = false;
215
370
  iterations++;
216
371
  walkAST(ast.root, (node, parent) => {
217
- // ── Rule 1 & 2 & 3 & 8: variable declarations ──
218
- if (node.type === 'variable_declarator') {
219
- const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, adapter);
220
- if (!nameNode)
221
- return;
222
- // Rule 8: type annotation — let x: D1Database
223
- if (typeAnnotationNode && nameNode.type === 'identifier') {
224
- const typeText = adapter.getNodeText(typeAnnotationNode, sourceCode).trim();
225
- if (DB_TYPES.has(typeText)) {
226
- const name = adapter.getNodeText(nameNode, sourceCode);
227
- if (!provenanceMap.has(name)) {
228
- provenanceMap.set(name, {
229
- identifier: name,
230
- reason: 'type',
231
- source: `type annotation ${typeText}`,
232
- chain: [],
233
- });
234
- changed = true;
235
- }
236
- // Type-provenanced names count as DB-provenanced for further propagation
237
- }
238
- }
239
- if (valueNode) {
240
- const propagated = tryPropagateFromExpression(valueNode, adapter, sourceCode, provenanceMap);
241
- if (propagated) {
242
- // Extract the variable name(s) from the name node
243
- const varNames = extractPatternNames(nameNode, adapter, sourceCode);
244
- for (const varName of varNames) {
245
- if (!provenanceMap.has(varName)) {
246
- provenanceMap.set(varName, {
247
- identifier: varName,
248
- reason: 'propagation',
249
- source: propagated.source,
250
- chain: [...propagated.chain, propagated.identifier],
251
- });
252
- changed = true;
253
- }
254
- }
255
- }
256
- }
257
- return;
258
- }
259
- // ── Rule 6: default parameters ──
260
- if (node.type === 'assignment_pattern' &&
261
- parent?.type === 'formal_parameters') {
262
- const children = adapter.getChildren(node);
263
- // assignment_pattern has [left, right]
264
- if (children.length >= 2) {
265
- const leftNode = children[0];
266
- const rightNode = children[1];
267
- if (leftNode.type === 'identifier') {
268
- const paramName = adapter.getNodeText(leftNode, sourceCode);
269
- const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
270
- if (propagated && !provenanceMap.has(paramName)) {
271
- provenanceMap.set(paramName, {
272
- identifier: paramName,
273
- reason: 'propagation',
274
- source: `default parameter = ${propagated.source}`,
275
- chain: [...propagated.chain, propagated.identifier],
276
- });
277
- changed = true;
278
- }
279
- }
280
- }
281
- return;
282
- }
283
- // ── Rule 7: class field initialization ──
284
- if (node.type === 'public_field_definition' ||
285
- node.type === 'field_definition') {
286
- const children = adapter.getChildren(node);
287
- // Typically [name, value] or [decorators..., name, value]
288
- const nameChild = children.find((c) => c.type === 'property_identifier');
289
- const valueChild = children.find((c) => c.type !== 'property_identifier' &&
290
- c.type !== 'decorator' &&
291
- c.type !== 'private' &&
292
- c.type !== 'public' &&
293
- c.type !== 'protected' &&
294
- c.type !== 'static' &&
295
- c.type !== 'readonly' &&
296
- c.type !== 'abstract');
297
- if (nameChild && valueChild) {
298
- const fieldName = adapter.getNodeText(nameChild, sourceCode);
299
- const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
300
- if (propagated && !provenanceMap.has(fieldName)) {
301
- provenanceMap.set(fieldName, {
302
- identifier: fieldName,
303
- reason: 'propagation',
304
- source: `class field initialized from ${propagated.source}`,
305
- chain: [...propagated.chain, propagated.identifier],
306
- });
307
- changed = true;
308
- }
309
- }
310
- return;
372
+ if (applyPropagationRule(node, parent, ctx)) {
373
+ changed = true;
311
374
  }
312
375
  });
313
376
  }
@@ -337,24 +400,9 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
337
400
  // ── Rule 2: drizzle(env.DB) — call where callee is DB-provenanced ──
338
401
  // ── Rule 3: db.prepare(sql) — member expression call on DB receiver ──
339
402
  if (node.type === 'call_expression') {
340
- // Walk children before arguments to get the callee
341
- const calleeNode = getCallExpressionCallee(node, adapter);
342
- if (calleeNode) {
343
- // Case: simple identifier call — drizzle(...)
344
- if (calleeNode.type === 'identifier') {
345
- const name = adapter.getNodeText(calleeNode, sourceCode);
346
- if (name && provenanceMap.has(name)) {
347
- return provenanceMap.get(name);
348
- }
349
- }
350
- // Case: member expression — db.prepare(...)
351
- if (calleeNode.type === 'member_expression') {
352
- const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
353
- if (receiver && provenanceMap.has(receiver)) {
354
- return provenanceMap.get(receiver);
355
- }
356
- }
357
- }
403
+ const viaCall = tryCallProvenance(node, adapter, sourceCode, provenanceMap);
404
+ if (viaCall)
405
+ return viaCall;
358
406
  }
359
407
  // ── Simple identifier reference (for destructuring sources) ──
360
408
  if (node.type === 'identifier') {
@@ -372,6 +420,31 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
372
420
  }
373
421
  return null;
374
422
  }
423
+ /**
424
+ * Rule 2/3: check a call_expression whose callee is either a DB-provenanced
425
+ * identifier (e.g. `drizzle(...)`) or a member expression on a DB receiver
426
+ * (e.g. `db.prepare(sql)`).
427
+ */
428
+ function tryCallProvenance(node, adapter, sourceCode, provenanceMap) {
429
+ const calleeNode = getCallExpressionCallee(node, adapter);
430
+ if (!calleeNode)
431
+ return null;
432
+ // Case: simple identifier call — drizzle(...)
433
+ if (calleeNode.type === 'identifier') {
434
+ const name = adapter.getNodeText(calleeNode, sourceCode);
435
+ if (name && provenanceMap.has(name)) {
436
+ return provenanceMap.get(name);
437
+ }
438
+ }
439
+ // Case: member expression — db.prepare(...)
440
+ if (calleeNode.type === 'member_expression') {
441
+ const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
442
+ if (receiver && provenanceMap.has(receiver)) {
443
+ return provenanceMap.get(receiver);
444
+ }
445
+ }
446
+ return null;
447
+ }
375
448
  /**
376
449
  * Split a variable_declarator into its name, value, and type annotation
377
450
  * child nodes.
@@ -425,59 +498,57 @@ function splitVariableDeclarator(node, adapter) {
425
498
  */
426
499
  function extractPatternNames(node, adapter, sourceCode) {
427
500
  const names = [];
428
- function collect(node) {
429
- if (node.type === 'identifier') {
430
- const name = adapter.getNodeText(node, sourceCode);
431
- if (name)
432
- names.push(name);
433
- }
434
- else if (node.type === 'object_pattern') {
435
- // Each child is a shorthand_property_identifier, pair_pattern, or rest_pattern
436
- for (const child of adapter.getChildren(node)) {
437
- if (child.type === '{' ||
438
- child.type === '}' ||
439
- child.type === ',') {
440
- continue;
441
- }
442
- if (child.type === 'shorthand_property_identifier') {
443
- const name = adapter.getNodeText(child, sourceCode);
444
- if (name)
445
- names.push(name);
446
- }
447
- else if (child.type === 'pair_pattern') {
448
- // pair_pattern children: [property, value]
449
- // Extract from the value side (which might be an identifier or nested pattern)
450
- const pairChildren = adapter.getChildren(child);
451
- if (pairChildren.length >= 2) {
452
- collect(pairChildren[1]);
453
- }
454
- }
455
- else if (child.type === 'rest_pattern') {
456
- collectChildren(child, adapter, sourceCode, names);
457
- }
458
- else {
459
- collect(child);
460
- }
501
+ collectPatternNames(node, adapter, sourceCode, names);
502
+ return names;
503
+ }
504
+ /** Recursively collect identifier names from a destructuring pattern node. */
505
+ function collectPatternNames(node, adapter, sourceCode, names) {
506
+ if (node.type === 'identifier') {
507
+ const name = adapter.getNodeText(node, sourceCode);
508
+ if (name)
509
+ names.push(name);
510
+ return;
511
+ }
512
+ if (node.type === 'object_pattern') {
513
+ for (const child of adapter.getChildren(node)) {
514
+ if (child.type === '{' || child.type === '}' || child.type === ',')
515
+ continue;
516
+ if (child.type === 'shorthand_property_identifier') {
517
+ const name = adapter.getNodeText(child, sourceCode);
518
+ if (name)
519
+ names.push(name);
461
520
  }
462
- }
463
- else if (node.type === 'array_pattern') {
464
- for (const child of adapter.getChildren(node)) {
465
- if (child.type === '[' || child.type === ']' || child.type === ',') {
466
- continue;
521
+ else if (child.type === 'pair_pattern') {
522
+ // pair_pattern children: [property, value] — collect from value side
523
+ const pairChildren = adapter.getChildren(child);
524
+ if (pairChildren.length >= 2) {
525
+ collectPatternNames(pairChildren[1], adapter, sourceCode, names);
467
526
  }
468
- collect(child);
469
527
  }
470
- }
471
- else if (node.type === 'assignment_pattern') {
472
- // Default value in destructuring — collect from the left side
473
- const children = adapter.getChildren(node);
474
- if (children.length >= 1) {
475
- collect(children[0]);
528
+ else if (child.type === 'rest_pattern') {
529
+ collectChildren(child, adapter, sourceCode, names);
530
+ }
531
+ else {
532
+ collectPatternNames(child, adapter, sourceCode, names);
476
533
  }
477
534
  }
535
+ return;
536
+ }
537
+ if (node.type === 'array_pattern') {
538
+ for (const child of adapter.getChildren(node)) {
539
+ if (child.type === '[' || child.type === ']' || child.type === ',')
540
+ continue;
541
+ collectPatternNames(child, adapter, sourceCode, names);
542
+ }
543
+ return;
544
+ }
545
+ // assignment_pattern — default value in destructuring, collect from left side
546
+ if (node.type === 'assignment_pattern') {
547
+ const children = adapter.getChildren(node);
548
+ if (children.length >= 1) {
549
+ collectPatternNames(children[0], adapter, sourceCode, names);
550
+ }
478
551
  }
479
- collect(node);
480
- return names;
481
552
  }
482
553
  function collectChildren(node, adapter, sourceCode, names) {
483
554
  for (const child of adapter.getChildren(node)) {
@@ -594,6 +665,11 @@ function walkAST(root, visitor) {
594
665
  *
595
666
  * This is the main entry point — call once per file before analysis.
596
667
  * Combines import extraction, propagation, and mode-based fallback.
668
+ * @param adapter
669
+ * @param ast
670
+ * @param options
671
+ * @param sourceCode
672
+ * @returns
597
673
  */
598
674
  export function buildProvenanceContext(ast, adapter, sourceCode, options) {
599
675
  const mode = options.mode;
@@ -610,7 +686,11 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
610
686
  // 3. Fallback: in hybrid mode, add identifiers that match name lists
611
687
  // but weren't caught by provenance (R3)
612
688
  if (mode === 'hybrid') {
613
- dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode, options.dbReceiverNames ?? [], options.dbBindingNames ?? [], options.dbWrapperNames ?? []);
689
+ dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode, {
690
+ dbReceiverNames: options.dbReceiverNames ?? [],
691
+ dbBindingNames: options.dbBindingNames ?? [],
692
+ dbWrapperNames: options.dbWrapperNames ?? [],
693
+ });
614
694
  }
615
695
  // 4. In names mode, use ONLY name lists
616
696
  if (mode === 'names') {
@@ -622,74 +702,63 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
622
702
  mode,
623
703
  };
624
704
  }
625
- /**
626
- * Add fallback provenance entries for identifiers that match name lists
627
- * but weren't caught by the provenance chain (R3 hybrid mode).
628
- *
629
- * These entries carry `reason: 'fallback'` — visible in config detection
630
- * so users can audit and tighten their chains.
631
- */
632
- function addNameListFallbacks(provenanceMap, sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames) {
705
+ function addNameListFallbacks(provenanceMap, sourceCode, nameLists) {
706
+ const { dbReceiverNames, dbBindingNames, dbWrapperNames } = nameLists;
633
707
  const result = new Map(provenanceMap);
634
- // Scan for DB receiver names used as identifiers
635
- for (const name of dbReceiverNames) {
708
+ addIdentifierFallbacks(result, sourceCode, dbReceiverNames, 'dbReceiverNames');
709
+ addBindingFallbacks(result, sourceCode, dbBindingNames);
710
+ addIdentifierFallbacks(result, sourceCode, dbWrapperNames, 'dbWrapperNames');
711
+ return result;
712
+ }
713
+ /** Add fallback entries for identifiers that appear in source but lack provenance. */
714
+ function addIdentifierFallbacks(result, sourceCode, names, label) {
715
+ for (const name of names) {
636
716
  if (result.has(name))
637
717
  continue; // already provenanced — provenance wins
638
- // Check if this name appears as an identifier in the source
639
718
  if (identifierAppearsInSource(sourceCode, name)) {
640
719
  result.set(name, {
641
720
  identifier: name,
642
721
  reason: 'fallback',
643
- source: `name list match: dbReceiverNames contains "${name}"`,
722
+ source: `name list match: ${label} contains "${name}"`,
644
723
  chain: [],
645
724
  });
646
725
  }
647
726
  }
648
- // Scan for binding names like env.DB
649
- for (const binding of dbBindingNames) {
650
- if (sourceCode.includes(binding)) {
651
- // Extract the property after the dot, e.g. "DB" from "env.DB"
652
- const dotIdx = binding.lastIndexOf('.');
653
- const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
654
- // Also add the full binding path
655
- if (!result.has(binding)) {
656
- result.set(binding, {
657
- identifier: binding,
658
- reason: 'fallback',
659
- source: `name list match: dbBindingNames contains "${binding}"`,
660
- chain: [],
661
- });
662
- }
663
- if (shortName !== binding && !result.has(shortName)) {
664
- result.set(shortName, {
665
- identifier: shortName,
666
- reason: 'fallback',
667
- source: `from binding ${binding}`,
668
- chain: [],
669
- });
670
- }
671
- }
672
- }
673
- // Scan for DB wrapper function names (e.g. d1Query, d1Exec)
674
- for (const name of dbWrapperNames) {
675
- if (result.has(name))
727
+ }
728
+ /** Add fallback entries for binding names like `env.DB` (plus their short forms). */
729
+ function addBindingFallbacks(result, sourceCode, bindings) {
730
+ for (const binding of bindings) {
731
+ if (!sourceCode.includes(binding))
676
732
  continue;
677
- if (identifierAppearsInSource(sourceCode, name)) {
678
- result.set(name, {
679
- identifier: name,
733
+ const dotIdx = binding.lastIndexOf('.');
734
+ const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
735
+ if (!result.has(binding)) {
736
+ result.set(binding, {
737
+ identifier: binding,
738
+ reason: 'fallback',
739
+ source: `name list match: dbBindingNames contains "${binding}"`,
740
+ chain: [],
741
+ });
742
+ }
743
+ if (shortName !== binding && !result.has(shortName)) {
744
+ result.set(shortName, {
745
+ identifier: shortName,
680
746
  reason: 'fallback',
681
- source: `name list match: dbWrapperNames contains "${name}"`,
747
+ source: `from binding ${binding}`,
682
748
  chain: [],
683
749
  });
684
750
  }
685
751
  }
686
- return result;
687
752
  }
688
753
  /**
689
754
  * Build a provenance map using ONLY name lists (for names mode).
690
755
  */
691
756
  function buildNamesOnlyProvenance(sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames) {
692
- return addNameListFallbacks(new Map(), sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames);
757
+ return addNameListFallbacks(new Map(), sourceCode, {
758
+ dbReceiverNames,
759
+ dbBindingNames,
760
+ dbWrapperNames,
761
+ });
693
762
  }
694
763
  /** Check if an identifier name appears as a standalone identifier in source. */
695
764
  function identifierAppearsInSource(sourceCode, name) {
@@ -700,9 +769,6 @@ function identifierAppearsInSource(sourceCode, name) {
700
769
  function escapeRegex(s) {
701
770
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
702
771
  }
703
- // ═══════════════════════════════════════════════════════════════════════════
704
- // Core detection — is this call DB-provenanced?
705
- // ═══════════════════════════════════════════════════════════════════════════
706
772
  /**
707
773
  * Determine if a call-expression node's callee is DB-provenanced.
708
774
  *
@@ -714,11 +780,15 @@ function escapeRegex(s) {
714
780
  * 2. Member expression call → is the receiver DB-provenanced AND is the
715
781
  * method in the DB call method set?
716
782
  * 3. ORM patterns → receiver is DB-provenanced and method matches ORM API
783
+ *
784
+ * @param node the call-expression node whose callee is under test
785
+ * @param query bundled adapter, source, provenance context, and DB methods
786
+ * @returns true if the call's callee resolves to a DB-provenanced target
717
787
  */
718
- export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethods) {
788
+ export function isDBProvenanced(node, query) {
719
789
  if (node.type !== 'call_expression')
720
790
  return false;
721
- const methods = dbCallMethods ?? DB_CALL_METHODS;
791
+ const { adapter, sourceCode, context, methods } = query;
722
792
  const calleeNode = getCallExpressionCallee(node, adapter);
723
793
  if (!calleeNode)
724
794
  return false;
@@ -740,7 +810,7 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
740
810
  }
741
811
  // Case 2: Member expression — e.g. db.prepare(...)
742
812
  if (calleeNode.type === 'member_expression' || calleeNode.type === 'selector_expression') {
743
- return isMemberExpressionDBProvenanced(calleeNode, adapter, sourceCode, context, methods);
813
+ return isMemberExpressionDBProvenanced(calleeNode, query);
744
814
  }
745
815
  return false;
746
816
  }
@@ -751,38 +821,14 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
751
821
  * checks if it's in the provenance context, and verifies
752
822
  * the method matches the DB call/ORM API.
753
823
  */
754
- function isMemberExpressionDBProvenanced(node, adapter, sourceCode, context, methods) {
755
- // Walk the member expression chain to find the root
756
- let rootReceiver = null;
757
- let current = node;
758
- while (current.type === 'member_expression' || current.type === 'selector_expression') {
759
- const children = adapter.getChildren(current);
760
- const firstChild = children[0];
761
- if (!firstChild)
762
- break;
763
- if (firstChild.type === 'identifier') {
764
- rootReceiver = adapter.getNodeText(firstChild, sourceCode);
765
- break;
766
- }
767
- if (firstChild.type === 'member_expression' || firstChild.type === 'selector_expression') {
768
- // Compound receiver: env.DB.prepare → root is "env.DB", not "env".
769
- // Returning the full text of the inner member expression ensures
770
- // fallback entries match (e.g. dbBindingNames: ['env.DB']).
771
- rootReceiver = adapter.getNodeText(firstChild, sourceCode);
772
- break;
773
- }
774
- // this.db.prepare → root is a chain on `this`, check `this.xxx`
775
- if (firstChild.type === 'this' || firstChild.type === 'super') {
776
- rootReceiver = 'this';
777
- break;
778
- }
779
- break;
780
- }
824
+ function isMemberExpressionDBProvenanced(node, query) {
825
+ const { adapter, sourceCode, context } = query;
826
+ const rootReceiver = findRootReceiver(node, adapter, sourceCode);
781
827
  if (!rootReceiver)
782
828
  return false;
783
829
  // In names mode, check the method name directly
784
830
  if (context.mode === 'names') {
785
- return isDBMethodCall(node, adapter, sourceCode, context, methods);
831
+ return isDBMethodCall(node, query);
786
832
  }
787
833
  // Check if the root receiver is DB-provenanced.
788
834
  // Compound receivers (e.g. "db.users" or "env.DB") need to match both
@@ -801,16 +847,45 @@ function isMemberExpressionDBProvenanced(node, adapter, sourceCode, context, met
801
847
  if (rootReceiver !== 'this')
802
848
  return false;
803
849
  // For `this.xxx`, check if the method chain itself suggests DB usage
804
- return isDBMethodOnThis(node, adapter, sourceCode, context, methods);
850
+ return isDBMethodOnThis(node, query);
805
851
  }
806
852
  // Check that the method is in the DB call/ORM API
807
- return isDBMethodCall(node, adapter, sourceCode, context, methods);
853
+ return isDBMethodCall(node, query);
854
+ }
855
+ /**
856
+ * Walk a member/selector expression chain to its root receiver text.
857
+ * Returns `'this'` for `this.xxx` chains and `null` when no root is found.
858
+ */
859
+ function findRootReceiver(node, adapter, sourceCode) {
860
+ let current = node;
861
+ while (current.type === 'member_expression' || current.type === 'selector_expression') {
862
+ const children = adapter.getChildren(current);
863
+ const firstChild = children[0];
864
+ if (!firstChild)
865
+ break;
866
+ if (firstChild.type === 'identifier') {
867
+ return adapter.getNodeText(firstChild, sourceCode);
868
+ }
869
+ if (firstChild.type === 'member_expression' || firstChild.type === 'selector_expression') {
870
+ // Compound receiver: env.DB.prepare → root is "env.DB", not "env".
871
+ // Returning the full text of the inner member expression ensures
872
+ // fallback entries match (e.g. dbBindingNames: ['env.DB']).
873
+ return adapter.getNodeText(firstChild, sourceCode);
874
+ }
875
+ // this.db.prepare → root is a chain on `this`, check `this.xxx`
876
+ if (firstChild.type === 'this' || firstChild.type === 'super') {
877
+ return 'this';
878
+ }
879
+ break;
880
+ }
881
+ return null;
808
882
  }
809
883
  /**
810
884
  * Check if a call through a member expression uses a DB method
811
885
  * (exec, prepare, all, etc.) or an ORM method (find, insert, etc.).
812
886
  */
813
- function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
887
+ function isDBMethodCall(node, query) {
888
+ const { adapter, sourceCode, methods } = query;
814
889
  // Walk the member expression chain and check each property
815
890
  let current = node;
816
891
  while (current.type === 'member_expression' ||
@@ -820,7 +895,7 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
820
895
  for (const child of children) {
821
896
  if (child.type === 'property_identifier' ||
822
897
  child.type === 'field_identifier') {
823
- const propName = _adapter.getNodeText(child, _sourceCode);
898
+ const propName = adapter.getNodeText(child, sourceCode);
824
899
  const lower = propName.toLowerCase();
825
900
  if (methods.has(lower) || ORM_METHODS.has(lower)) {
826
901
  return true;
@@ -843,7 +918,8 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
843
918
  * For `this.xxx.method()` calls — check if the method chain suggests
844
919
  * DB access. Used when the receiver is `this` (not directly DB-provenanced).
845
920
  */
846
- function isDBMethodOnThis(node, adapter, sourceCode, _context, methods) {
921
+ function isDBMethodOnThis(node, query) {
922
+ const { adapter, sourceCode, methods } = query;
847
923
  // Walk the chain: this.db.prepare → check if any property matches DB methods
848
924
  let current = node;
849
925
  while (current.type === 'member_expression' ||
@@ -874,6 +950,9 @@ function isDBMethodOnThis(node, adapter, sourceCode, _context, methods) {
874
950
  * Check if an identifier is validator-provenanced (R4 infrastructure).
875
951
  *
876
952
  * This will be consumed by Spec 15's validator-bypass detection.
953
+ * @param context
954
+ * @param identifier
955
+ * @returns
877
956
  */
878
957
  export function isValidatorProvenanced(identifier, context) {
879
958
  return context.validatorProvenanced.has(identifier);
@@ -890,108 +969,124 @@ export function isValidatorProvenanced(identifier, context) {
890
969
  * (b) can be traced back to a provenanced source through assignments.
891
970
  *
892
971
  * Deferred: full implementation in R2 step.
972
+ * @param adapter
973
+ * @param fileAst
974
+ * @param provenancedSet
975
+ * @param sourceCode
976
+ * @returns
893
977
  */
894
978
  export function inferReceivers(provenancedSet, fileAst, adapter, sourceCode) {
895
979
  const inferred = { identifiers: [], evidence: [] };
896
980
  const seen = new Set();
897
981
  // Step 1: Build an assignment graph — name → source text of initializer
898
982
  const assignmentGraph = buildAssignmentGraph(fileAst, adapter, sourceCode);
983
+ const ctx = {
984
+ adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred,
985
+ };
899
986
  // Step 2: Walk all call expressions whose callee is a member expression
900
- walkAST(fileAst.root, (node) => {
901
- if (node.type !== 'call_expression')
902
- return;
903
- const callee = getCallExpressionCallee(node, adapter);
904
- if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
905
- return;
906
- // Extract method name (the property being called)
907
- const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
908
- if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
909
- return;
910
- // Get the receiver identifier
911
- const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
912
- if (!receiver)
913
- return;
914
- // Already in the provenanced set → skip (already first-class, not inferred)
915
- if (provenancedSet.has(receiver))
916
- return;
917
- // Already added to inferred
918
- if (seen.has(receiver))
919
- return;
920
- // Step 3: Trace receiver through assignment chain to see if it reaches
921
- // a provenanced source
922
- const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
923
- if (traced.found) {
924
- seen.add(receiver);
925
- inferred.identifiers.push(receiver);
926
- inferred.evidence.push({
927
- identifier: receiver,
928
- reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
929
- });
930
- }
931
- });
987
+ walkAST(fileAst.root, (node) => inferReceiverFromCall(node, ctx));
932
988
  return inferred;
933
989
  }
990
+ /** Infer a receiver from a single call_expression node, if it qualifies. */
991
+ function inferReceiverFromCall(node, ctx) {
992
+ const { adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred } = ctx;
993
+ if (node.type !== 'call_expression')
994
+ return;
995
+ const callee = getCallExpressionCallee(node, adapter);
996
+ if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
997
+ return;
998
+ const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
999
+ if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
1000
+ return;
1001
+ const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
1002
+ if (!receiver)
1003
+ return;
1004
+ // Already provenanced → first-class, not inferred; already added → skip
1005
+ if (provenancedSet.has(receiver) || seen.has(receiver))
1006
+ return;
1007
+ // Step 3: Trace receiver through assignment chain to a provenanced source
1008
+ const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
1009
+ if (traced.found) {
1010
+ seen.add(receiver);
1011
+ inferred.identifiers.push(receiver);
1012
+ inferred.evidence.push({
1013
+ identifier: receiver,
1014
+ reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
1015
+ });
1016
+ }
1017
+ }
934
1018
  /**
935
1019
  * Build a reverse assignment graph from the AST.
936
1020
  * Maps variable name → the text of its initializer expression.
937
1021
  * Handles: const/let/var declarations, parameter defaults, class fields.
938
1022
  */
939
1023
  function buildAssignmentGraph(ast, adapter, sourceCode) {
940
- const graph = new Map();
1024
+ const ctx = {
1025
+ graph: new Map(),
1026
+ adapter,
1027
+ sourceCode,
1028
+ };
941
1029
  walkAST(ast.root, (node, parent) => {
942
- // const/let/var x = <expr>
943
- if (node.type === 'variable_declarator') {
944
- const { nameNode, valueNode } = splitVariableDeclarator(node, adapter);
945
- if (nameNode && valueNode) {
946
- const names = extractPatternNames(nameNode, adapter, sourceCode);
947
- const valueText = adapter.getNodeText(valueNode, sourceCode);
948
- for (const name of names) {
949
- if (!graph.has(name))
950
- graph.set(name, valueText);
951
- }
952
- }
953
- return;
954
- }
955
- // Parameter defaults: function foo(x = <expr>)
956
- if (node.type === 'assignment_pattern' &&
957
- parent?.type === 'formal_parameters') {
958
- const children = adapter.getChildren(node);
959
- if (children.length >= 2) {
960
- const leftNode = children[0];
961
- const rightNode = children[1];
962
- if (leftNode.type === 'identifier') {
963
- const paramName = adapter.getNodeText(leftNode, sourceCode);
964
- const valueText = adapter.getNodeText(rightNode, sourceCode);
965
- if (!graph.has(paramName))
966
- graph.set(paramName, valueText);
967
- }
968
- }
969
- return;
970
- }
971
- // Class field: fieldName = <expr>
972
- if (node.type === 'public_field_definition' ||
973
- node.type === 'field_definition') {
974
- const children = adapter.getChildren(node);
975
- const nameChild = children.find((c) => c.type === 'property_identifier');
976
- const valueChild = children.find((c) => c.type !== 'property_identifier' &&
977
- c.type !== 'decorator' &&
978
- c.type !== 'private' &&
979
- c.type !== 'public' &&
980
- c.type !== 'protected' &&
981
- c.type !== 'static' &&
982
- c.type !== 'readonly' &&
983
- c.type !== 'abstract' &&
984
- c.type !== '=');
985
- if (nameChild && valueChild) {
986
- const fieldName = adapter.getNodeText(nameChild, sourceCode);
987
- const valueText = adapter.getNodeText(valueChild, sourceCode);
988
- if (!graph.has(fieldName))
989
- graph.set(fieldName, valueText);
990
- }
991
- return;
992
- }
1030
+ recordVariableDeclarator(node, ctx);
1031
+ recordParameterDefault(node, parent, ctx);
1032
+ recordClassField(node, ctx);
993
1033
  });
994
- return graph;
1034
+ return ctx.graph;
1035
+ }
1036
+ /** Record `const/let/var x = <expr>` into the assignment graph. */
1037
+ function recordVariableDeclarator(node, ctx) {
1038
+ if (node.type !== 'variable_declarator')
1039
+ return;
1040
+ const { nameNode, valueNode } = splitVariableDeclarator(node, ctx.adapter);
1041
+ if (!nameNode || !valueNode)
1042
+ return;
1043
+ const names = extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode);
1044
+ const valueText = ctx.adapter.getNodeText(valueNode, ctx.sourceCode);
1045
+ for (const name of names) {
1046
+ if (!ctx.graph.has(name))
1047
+ ctx.graph.set(name, valueText);
1048
+ }
1049
+ }
1050
+ /** Record parameter defaults (`function foo(x = <expr>)`) into the graph. */
1051
+ function recordParameterDefault(node, parent, ctx) {
1052
+ if (node.type !== 'assignment_pattern' || parent?.type !== 'formal_parameters') {
1053
+ return;
1054
+ }
1055
+ const children = ctx.adapter.getChildren(node);
1056
+ if (children.length < 2)
1057
+ return;
1058
+ const leftNode = children[0];
1059
+ const rightNode = children[1];
1060
+ if (leftNode.type !== 'identifier')
1061
+ return;
1062
+ const paramName = ctx.adapter.getNodeText(leftNode, ctx.sourceCode);
1063
+ const valueText = ctx.adapter.getNodeText(rightNode, ctx.sourceCode);
1064
+ if (!ctx.graph.has(paramName))
1065
+ ctx.graph.set(paramName, valueText);
1066
+ }
1067
+ /** Record class-field initializers (`fieldName = <expr>`) into the graph. */
1068
+ function recordClassField(node, ctx) {
1069
+ if (node.type !== 'public_field_definition' &&
1070
+ node.type !== 'field_definition') {
1071
+ return;
1072
+ }
1073
+ const children = ctx.adapter.getChildren(node);
1074
+ const nameChild = children.find((c) => c.type === 'property_identifier');
1075
+ const valueChild = children.find((c) => c.type !== 'property_identifier' &&
1076
+ c.type !== 'decorator' &&
1077
+ c.type !== 'private' &&
1078
+ c.type !== 'public' &&
1079
+ c.type !== 'protected' &&
1080
+ c.type !== 'static' &&
1081
+ c.type !== 'readonly' &&
1082
+ c.type !== 'abstract' &&
1083
+ c.type !== '=');
1084
+ if (!nameChild || !valueChild)
1085
+ return;
1086
+ const fieldName = ctx.adapter.getNodeText(nameChild, ctx.sourceCode);
1087
+ const valueText = ctx.adapter.getNodeText(valueChild, ctx.sourceCode);
1088
+ if (!ctx.graph.has(fieldName))
1089
+ ctx.graph.set(fieldName, valueText);
995
1090
  }
996
1091
  /**
997
1092
  * Trace a receiver identifier through the assignment graph to see if it
@@ -1012,10 +1107,8 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
1012
1107
  reason: provenancedSet.get(current).source,
1013
1108
  };
1014
1109
  }
1015
- // Also check if any identifier in the initializer text matches a provenanced source
1016
1110
  const initText = assignmentGraph.get(current);
1017
1111
  if (initText) {
1018
- // Extract identifiers from the initializer text and check against provenanced set
1019
1112
  const idents = extractTopLevelIdentifiers(initText);
1020
1113
  for (const ident of idents) {
1021
1114
  if (provenancedSet.has(ident)) {
@@ -1028,27 +1121,28 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
1028
1121
  }
1029
1122
  }
1030
1123
  }
1031
- // Move to the next link in the chain
1032
1124
  if (!initText || visited.has(initText))
1033
1125
  break;
1034
1126
  visited.add(initText);
1035
- // Try to find a next identifier to trace
1036
- const idents = extractTopLevelIdentifiers(initText);
1037
- let foundNext = false;
1038
- for (const ident of idents) {
1039
- if (assignmentGraph.has(ident) && !visited.has(ident)) {
1040
- chain.push(ident);
1041
- current = ident;
1042
- foundNext = true;
1043
- break;
1044
- }
1045
- }
1046
- if (!foundNext)
1127
+ const next = findNextTraceIdentifier(initText, assignmentGraph, visited);
1128
+ if (!next)
1047
1129
  break;
1130
+ chain.push(next);
1131
+ current = next;
1048
1132
  depth++;
1049
1133
  }
1050
1134
  return { found: false, chains: [], reason: '' };
1051
1135
  }
1136
+ /** Find the next assignment-graph identifier to trace, if any. */
1137
+ function findNextTraceIdentifier(initText, assignmentGraph, visited) {
1138
+ const idents = extractTopLevelIdentifiers(initText);
1139
+ for (const ident of idents) {
1140
+ if (assignmentGraph.has(ident) && !visited.has(ident)) {
1141
+ return ident;
1142
+ }
1143
+ }
1144
+ return null;
1145
+ }
1052
1146
  /**
1053
1147
  * Extract top-level identifier names from an expression text.
1054
1148
  * E.g., "db.prepare(sql)" → ["db"], "getConnection()" → ["getConnection"]