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
@@ -0,0 +1,965 @@
1
+ /**
2
+ * SQL-context code analysis — table-reference extraction, SQL parsing, naming/
3
+ * query/injection checks, and AST/source helper functions.
4
+ *
5
+ * Spec 34 — extracted from UniversalSchemaAnalyzer.ts so the SQL-context
6
+ * analysis is importable and testable independently of the analyzer class.
7
+ * Every function here is a module-level free function with a signature
8
+ * identical to the original class method (minus `this`). Violations are built
9
+ * with `createSchemaViolation` instead of `this.createViolation`.
10
+ */
11
+ import { isDBProvenanced, DB_CALL_METHODS } from '../../provenance.js';
12
+ import { OrmAdapterRegistry } from '../../orm/index.js';
13
+ import { SQL_TAG_NAMES, DB_CALL_METHOD_NAMES, DB_RECEIVER_NAMES } from './config.js';
14
+ import { createSchemaViolation } from './violations.js';
15
+ /**
16
+ * Extract table references from a TypeScript/JavaScript AST.
17
+ *
18
+ * Four extraction strategies: (1) tagged-template SQL, (2) DB-call string
19
+ * arguments, (3) full-source scan for `.sql` files, (4) ORM adapter extraction.
20
+ *
21
+ * @param ast The parsed file AST.
22
+ * @param adapter The language adapter for the file's syntax.
23
+ * @param sourceCode The raw source text.
24
+ * @param ctx Bundled config / provenance / known-table catalog.
25
+ * @returns Table references extracted via all four strategies.
26
+ */
27
+ export function findTableReferences(ast, adapter, sourceCode, ctx) {
28
+ const references = [];
29
+ // (1) Tagged template SQL — e.g. sql`SELECT * FROM heroes`
30
+ references.push(...extractTaggedTemplateRefs(ast, adapter, sourceCode, ctx));
31
+ // (2) DB-call patterns — e.g. db.exec("SELECT * FROM heroes")
32
+ references.push(...extractDbCallRefs(ast, adapter, sourceCode, ctx));
33
+ // (3) .sql files — scan the entire source (the whole file IS SQL).
34
+ if (ast.filePath.endsWith('.sql')) {
35
+ const fileRefs = parseSqlTables(sourceCode, { line: 1, column: 1 }, sourceCode, ctx.allTables);
36
+ references.push(...fileRefs);
37
+ }
38
+ // (4) Spec 15 R2 — ORM-aware extraction (Drizzle + Prisma)
39
+ references.push(...extractOrmRefs(ast, adapter, sourceCode));
40
+ return references;
41
+ }
42
+ /**
43
+ * Strategy (1): tagged-template SQL — e.g. sql`SELECT * FROM heroes`.
44
+ * This is a syntax feature, not a naming convention — keep the sqlTagNames gate.
45
+ */
46
+ function extractTaggedTemplateRefs(ast, adapter, sourceCode, ctx) {
47
+ const { config, allTables } = ctx;
48
+ const references = [];
49
+ const sqlTags = config.sqlTagNames ?? [...SQL_TAG_NAMES];
50
+ const taggedTemplates = adapter.findNodes(ast, {
51
+ custom: (node) => {
52
+ if (node.type !== 'call_expression')
53
+ return false;
54
+ // Callee must be an identifier matching sqlTagNames
55
+ const callee = getCallee(node, adapter, sourceCode);
56
+ if (!callee || !sqlTags.includes(callee))
57
+ return false;
58
+ // Must have a template string argument
59
+ return hasTemplateArgument(node, adapter);
60
+ },
61
+ });
62
+ for (const callNode of taggedTemplates) {
63
+ const templateText = getTemplateText(callNode, adapter, sourceCode);
64
+ if (!templateText)
65
+ continue;
66
+ const location = getCallLocation(callNode);
67
+ references.push(...parseSqlTables(templateText, location, sourceCode, allTables));
68
+ }
69
+ return references;
70
+ }
71
+ /**
72
+ * Strategy (2): DB-call patterns — e.g. db.exec("SELECT * FROM heroes").
73
+ * Spec 21: provenance-based isDBProvenanced when available, falling back to
74
+ * the name-based isDbMemberCall for `names` mode / no context.
75
+ */
76
+ function extractDbCallRefs(ast, adapter, sourceCode, ctx) {
77
+ const { config, provenanceContext, allTables } = ctx;
78
+ const references = [];
79
+ const dbCalls = adapter.findNodes(ast, {
80
+ custom: (node) => {
81
+ if (node.type !== 'call_expression')
82
+ return false;
83
+ // Spec 21: Use provenance when available, fall back to name-based check
84
+ if (provenanceContext && provenanceContext.mode !== 'names') {
85
+ return isDBProvenanced(node, { adapter, sourceCode, context: provenanceContext, methods: DB_CALL_METHODS });
86
+ }
87
+ // Legacy name-based check for names mode / no context
88
+ const callee = getCallee(node, adapter, sourceCode);
89
+ if (!callee)
90
+ return false;
91
+ const dbMethods = config.dbCallMethods ?? [...DB_CALL_METHOD_NAMES];
92
+ const dbReceivers = config.dbReceiverNames ?? [...DB_RECEIVER_NAMES];
93
+ return isDbMemberCall(callee, dbMethods, dbReceivers);
94
+ },
95
+ });
96
+ for (const callNode of dbCalls) {
97
+ const firstArg = getFirstStringArgument(callNode, adapter, sourceCode);
98
+ if (!firstArg)
99
+ continue;
100
+ const location = getCallLocation(callNode);
101
+ references.push(...parseSqlTables(firstArg, location, sourceCode, allTables));
102
+ }
103
+ return references;
104
+ }
105
+ /**
106
+ * Strategy (4): ORM-aware extraction (Drizzle + Prisma) via the registered
107
+ * adapter. Complements raw-SQL extraction by picking up ORM-specific patterns
108
+ * like db.select().from(users) and prisma.user.findMany().
109
+ */
110
+ function extractOrmRefs(ast, adapter, sourceCode) {
111
+ const references = [];
112
+ const ormRegistry = OrmAdapterRegistry.getInstance();
113
+ const ormAdapter = ormRegistry.getAdapterForFile(ast.filePath);
114
+ if (!ormAdapter)
115
+ return references;
116
+ try {
117
+ const ormRefs = ormAdapter.extractTableReferences(ast, adapter, sourceCode);
118
+ for (const ormRef of ormRefs) {
119
+ references.push({
120
+ table: ormRef.table,
121
+ type: ormRef.type,
122
+ location: ormRef.location,
123
+ context: ormRef.context,
124
+ });
125
+ }
126
+ }
127
+ catch {
128
+ // ORM extraction is best-effort — failures don't block raw-SQL extraction.
129
+ }
130
+ return references;
131
+ }
132
+ /**
133
+ * SQL patterns anchored to SQL keywords (not arbitrary substrings).
134
+ *
135
+ * Uses Unicode-aware \p{L} so non-Latin table names (日, 注文, пользователи)
136
+ * are correctly matched — \w is ASCII-only. Spec 21 R5. No trailing \b:
137
+ * greedy [\p{L}\p{N}_]* consumes the full identifier and \b after a closing
138
+ * quote (non-word char) fails, blocking quoted-table extraction.
139
+ */
140
+ function sqlTablePatterns() {
141
+ return [
142
+ { regex: /\bFROM\s+([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'select' },
143
+ { regex: /\bJOIN\s+([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'select' },
144
+ { regex: /\bINSERT\s+INTO\s+([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'insert' },
145
+ { regex: /\bUPDATE\s+([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'update' },
146
+ { regex: /\bDELETE\s+FROM\s+([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'delete' },
147
+ { regex: /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"']?)([\p{L}_][\p{L}\p{N}_]*)\1/giu, type: 'create' },
148
+ ];
149
+ }
150
+ /**
151
+ * Run the keyword-anchored SQL patterns over `cleaned`, skipping system
152
+ * tables, table-valued functions, module-specifier FROMs, short CTE/alias
153
+ * identifiers, and SQL keywords.
154
+ */
155
+ function matchSqlPatterns(ctx) {
156
+ const { sqlText, cleaned, baseLocation, sourceCode, allTables } = ctx;
157
+ const references = [];
158
+ for (const { regex, type } of sqlTablePatterns()) {
159
+ // Create fresh regex since we might consume with exec
160
+ const re = new RegExp(regex.source, regex.flags);
161
+ let match;
162
+ while ((match = re.exec(cleaned)) !== null) {
163
+ const table = match[2]; // The table name (capture group 2)
164
+ if (!table || isSystemTable(table) || isTableValuedFunction(table))
165
+ continue;
166
+ // Skip JS/TS module specifiers (`import x from 'mod'`) that step (3)'s
167
+ // full-source scan of migration `.ts` files otherwise captures as tables.
168
+ if (isModuleImportFrom(cleaned, match.index))
169
+ continue;
170
+ // Skip very short identifiers (likely CTE names / bare aliases like 'x',
171
+ // 't', 'o', 'c') unless they are known table names — the guard catches
172
+ // false positives from single-char CTE/alias identifiers.
173
+ if (!isSqlKeyword(table) && table.length < 3 && !allTables?.has(table.toLowerCase()))
174
+ continue;
175
+ // Skip common false positives: common variable names, keywords
176
+ if (isSqlKeyword(table))
177
+ continue;
178
+ // Calculate position in original source
179
+ const offset = sqlText.indexOf(match[0]);
180
+ const location = offset >= 0
181
+ ? offsetToLocation(sourceCode, sourceCode.indexOf(cleaned) + offset, baseLocation)
182
+ : baseLocation;
183
+ references.push({
184
+ table,
185
+ type,
186
+ location,
187
+ context: match[0].trim(),
188
+ });
189
+ }
190
+ }
191
+ return references;
192
+ }
193
+ /**
194
+ * Parse a SQL string (possibly with template expressions) into table references.
195
+ *
196
+ * @param sqlText The SQL text to parse.
197
+ * @param baseLocation Location of the SQL text within its source file.
198
+ * @param sourceCode The full source file contents.
199
+ * @param allTables Optional known table set for match filtering.
200
+ * @returns Table references discovered in the SQL text.
201
+ */
202
+ export function parseSqlTables(sqlText, baseLocation, sourceCode, allTables) {
203
+ // R2.3: Strip template expressions — `${prefix}_builds` → `_builds`
204
+ // (the prefix is replaced with empty, the suffix remains for matching)
205
+ const cleaned = resolveTemplateExpressions(sqlText);
206
+ const ctx = { sqlText, cleaned, baseLocation, sourceCode, allTables };
207
+ let references = matchSqlPatterns(ctx);
208
+ // Template sentinel filter: resolveTemplateExpressions() replaces
209
+ // ${...} with __TMPL__. Strip these before alias extraction and
210
+ // before returning — __TMPL__ is never a real table name.
211
+ references = references.filter(ref => !ref.table.startsWith('__TMPL__'));
212
+ // Spec 22 R4.3: Filter out alias identifiers.
213
+ // "FROM x AS t" defines t as an alias; later references like "JOIN t.posts"
214
+ // would capture t via the JOIN regex. Scan for explicit AS aliases.
215
+ const aliasIds = extractAliasIdentifiers(cleaned);
216
+ if (aliasIds.size > 0) {
217
+ return references.filter(ref => !aliasIds.has(ref.table.toLowerCase()));
218
+ }
219
+ return references;
220
+ }
221
+ /**
222
+ * Spec 22 R4.3: Extract alias identifiers from SQL text.
223
+ *
224
+ * Detects both explicit (`FROM x AS t`) and bare (`FROM x t`) aliases
225
+ * so they can be filtered from table-references in parseSqlTables().
226
+ * Without this, "JOIN t.posts" captures t via the JOIN regex when t is
227
+ * an alias for the real table x.
228
+ *
229
+ * @param sqlText The SQL text to scan for alias identifiers.
230
+ * @returns Lowercased alias identifiers to filter from table references.
231
+ */
232
+ export function extractAliasIdentifiers(sqlText) {
233
+ const aliases = new Set();
234
+ // CTE: WITH <name> AS ( — the CTE name is an alias, not a real table.
235
+ // Without this, "WITH fresh AS (SELECT ...)" causes 'fresh' to be
236
+ // captured by FROM/JOIN/subquery patterns and flagged as unknown-table.
237
+ const cteRe = /\bWITH\s+([\p{L}_][\p{L}\p{N}_]*)\s+AS\s*\(/giu;
238
+ let m;
239
+ while ((m = cteRe.exec(sqlText)) !== null) {
240
+ aliases.add(m[1].toLowerCase());
241
+ }
242
+ // Explicit: FROM/JOIN <table> AS <alias>
243
+ const explicitRe = /\b(?:FROM|JOIN)\s+[\p{L}_][\p{L}\p{N}_]*\s+AS\s+([\p{L}_][\p{L}\p{N}_]*)\b/giu;
244
+ while ((m = explicitRe.exec(sqlText)) !== null) {
245
+ aliases.add(m[1].toLowerCase());
246
+ }
247
+ // Subquery bare alias: FROM (SELECT ...) <alias>
248
+ // The '(' stops the bare FROM/JOIN regex below because \w+ can't match it.
249
+ // Pattern: FROM/JOIN \s* \( ... \) \s* <alias>
250
+ const subqueryRe = /\b(?:FROM|JOIN)\s*\([^)]*\)\s+([\p{L}_][\p{L}\p{N}_]*)\b/giu;
251
+ while ((m = subqueryRe.exec(sqlText)) !== null) {
252
+ const alias = m[1];
253
+ if (!isSqlKeyword(alias)) {
254
+ aliases.add(alias.toLowerCase());
255
+ }
256
+ }
257
+ // Bare: FROM/JOIN <table> <alias> (alias is a bare identifier, not a keyword)
258
+ // Pattern: keyword + table + word — the third word is the alias if it's
259
+ // not a SQL keyword and not followed by '.' (table.column reference).
260
+ const bareRe = /\b(?:FROM|JOIN)\s+([\p{L}_][\p{L}\p{N}_]*)\s+([\p{L}_][\p{L}\p{N}_]*)\b/giu;
261
+ while ((m = bareRe.exec(sqlText)) !== null) {
262
+ const alias = m[2];
263
+ // Don't add if it looks like a keyword or is followed by '.' (table ref)
264
+ if (!isSqlKeyword(alias)) {
265
+ const afterMatch = sqlText.substring(m.index + m[0].length);
266
+ if (!/^\s*\./.test(afterMatch)) {
267
+ aliases.add(alias.toLowerCase());
268
+ }
269
+ }
270
+ }
271
+ return aliases;
272
+ }
273
+ /**
274
+ * R2.3: Resolve template expressions in SQL text.
275
+ *
276
+ * Uses the sentinel `__TMPL__` instead of an empty string. An empty
277
+ * replacement produces whitespace artifacts (e.g. `FROM t WHERE`
278
+ * when `${tableName}` is stripped), which causes the bare-alias regex
279
+ * in extractAliasIdentifiers() to misalign: `t` lands in the table-name
280
+ * capture group instead of the alias group, and is never denylisted.
281
+ *
282
+ * `__TMPL__` keeps the token boundaries intact so alias extraction
283
+ * correctly identifies `t` as the alias. `__TMPL__` table references
284
+ * are filtered in parseSqlTables().
285
+ */
286
+ export function resolveTemplateExpressions(text) {
287
+ return text.replace(/\$\{[^}]+\}/g, '__TMPL__');
288
+ }
289
+ /**
290
+ * Return known table names within edit distance ≤ maxDist.
291
+ *
292
+ * @param name The candidate table name to match.
293
+ * @param knownTables The known-table catalog.
294
+ * @param maxDist Maximum Levenshtein edit distance to include.
295
+ * @returns Up to three known tables within the distance, quoted, nearest first.
296
+ */
297
+ export function getNearestTableSuggestions(name, knownTables, maxDist) {
298
+ const results = [];
299
+ for (const known of knownTables) {
300
+ const dist = levenshteinDistance(name.toLowerCase(), known.toLowerCase());
301
+ if (dist <= maxDist) {
302
+ results.push({ table: known, distance: dist });
303
+ }
304
+ }
305
+ // Sort by distance ascending
306
+ results.sort((a, b) => a.distance - b.distance);
307
+ return results.slice(0, 3).map(r => `'${r.table}'`);
308
+ }
309
+ /**
310
+ * Levenshtein edit distance between two strings.
311
+ *
312
+ * @param a First string.
313
+ * @param b Second string.
314
+ * @returns The edit distance, or Infinity when length difference exceeds 3.
315
+ */
316
+ export function levenshteinDistance(a, b) {
317
+ const m = a.length;
318
+ const n = b.length;
319
+ // Optimize: early exit if length difference exceeds threshold
320
+ if (Math.abs(m - n) > 3)
321
+ return Infinity;
322
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
323
+ for (let i = 0; i <= m; i++)
324
+ dp[i][0] = i;
325
+ for (let j = 0; j <= n; j++)
326
+ dp[0][j] = j;
327
+ for (let i = 1; i <= m; i++) {
328
+ for (let j = 1; j <= n; j++) {
329
+ if (a[i - 1] === b[j - 1]) {
330
+ dp[i][j] = dp[i - 1][j - 1];
331
+ }
332
+ else {
333
+ dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
334
+ }
335
+ }
336
+ }
337
+ return dp[m][n];
338
+ }
339
+ /**
340
+ * Check table naming conventions against references.
341
+ *
342
+ * @param references Table references extracted from the file.
343
+ * @param filePath The file under analysis.
344
+ * @returns Naming-convention and reserved-word violations.
345
+ */
346
+ export function checkNamingConventions(references, filePath) {
347
+ const violations = [];
348
+ for (const ref of references) {
349
+ if (/[A-Z]/.test(ref.table) && !ref.table.endsWith('Table')) {
350
+ violations.push(createSchemaViolation(filePath, ref.location, `Table name '${ref.table}' should use snake_case convention`, { severity: 'suggestion', rule: 'table-naming-convention', symbol: ref.table }));
351
+ }
352
+ const reserved = ['user', 'order', 'group', 'table', 'column', 'index'];
353
+ if (reserved.includes(ref.table.toLowerCase())) {
354
+ violations.push(createSchemaViolation(filePath, ref.location, `Table name '${ref.table}' is a reserved word. Consider using a different name.`, { severity: 'warning', rule: 'reserved-word', symbol: ref.table }));
355
+ }
356
+ }
357
+ return violations;
358
+ }
359
+ /**
360
+ * Check query patterns (per-function query-count ceiling).
361
+ *
362
+ * @param ast The parsed file AST.
363
+ * @param adapter The language adapter for the file's syntax.
364
+ * @param sourceCode The raw source text.
365
+ * @param config Schema analyzer configuration (maxQueriesPerFunction ceiling).
366
+ * @returns Too-many-queries violations.
367
+ */
368
+ export function checkQueryPatterns(ast, adapter, sourceCode, config) {
369
+ const violations = [];
370
+ const functions = adapter.extractFunctions(ast);
371
+ for (const func of functions) {
372
+ const funcNode = findNodeByLocation(ast.root, func.location.start);
373
+ if (!funcNode)
374
+ continue;
375
+ const funcText = adapter.getNodeText(funcNode, sourceCode);
376
+ const queryCount = countQueries(funcText);
377
+ if (queryCount > (config.maxQueriesPerFunction || 5)) {
378
+ violations.push(createSchemaViolation(ast.filePath, func.location.start, `Function '${func.name}' has ${queryCount} queries, exceeding the maximum of ${config.maxQueriesPerFunction}`, { severity: 'warning', rule: 'too-many-queries', symbol: func.name }));
379
+ }
380
+ }
381
+ // N+1 query detection is handled by the data-access analyzer (loop-query rule).
382
+ // The two were consolidated in Spec-19 Corrective Batch Item 3 — see CHANGELOG.
383
+ return violations;
384
+ }
385
+ /** Regexes for dangerous query/execute call sites (global flag for iteration). */
386
+ const DANGEROUS_SQL_PATTERNS = [
387
+ /query\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`/g,
388
+ /query\s*\(\s*['"][^'"]*['"]?\s*\+/g,
389
+ /execute\s*\(\s*['"][^'"]*['"]?\s*\+/g,
390
+ ];
391
+ /**
392
+ * Evaluate a single dangerous-pattern match: skip parameterized queries and
393
+ * taint-safe dynamic strings, else emit an sql-injection violation.
394
+ */
395
+ function checkInjectionMatch(ctx, match) {
396
+ const { ast, adapter, sourceCode, symbolOrdinals, violations } = ctx;
397
+ // Parameterized queries pass a bound-params argument (`query(sql, params)`).
398
+ // When the matched literal is followed by `, params` the interpolated
399
+ // `${...}` segments are compile-time clauses whose `?` placeholders are
400
+ // bound by that argument — not an injection vector. The data-access
401
+ // analyzer's checkQuerySecurity applies the same signal.
402
+ const afterMatch = sourceCode.slice(match.index + match[0].length);
403
+ if (/^\s*,/.test(afterMatch))
404
+ return;
405
+ const location = offsetToLocation(sourceCode, match.index, { line: 1, column: 1 });
406
+ // Find enclosing function from the AST at this position
407
+ const node = findClosestNodeAt(ast.root, location, adapter);
408
+ // Taint-aware safety check (Spec 33 Item 11a): clear the finding when the
409
+ // query argument's dynamic parts are all provably safe. The naive regex
410
+ // cannot tell trusted-DDL interpolation (`query(\`CREATE TABLE ${name}...\`)`
411
+ // with a constant/sanitized name) from raw-input interpolation, so the
412
+ // distinction is delegated to the adapter's dynamic-string safety analysis
413
+ // (isSafeInterpolation / resolveLocalConstant) — the same signal the
414
+ // data-access analyzer already trusts for sql-injection-risk.
415
+ const callNode = findEnclosingCallExpression(node, adapter);
416
+ if (callNode && isAllDynamicPartsSafe(callNode, ast, adapter, sourceCode)) {
417
+ return;
418
+ }
419
+ const enclosingFn = node ? findEnclosingFunctionName(node, adapter) : 'top-level';
420
+ const baseSymbol = `${enclosingFn}:sql-injection`;
421
+ const ordinal = (symbolOrdinals.get(baseSymbol) ?? 0) + 1;
422
+ symbolOrdinals.set(baseSymbol, ordinal);
423
+ const symbol = ordinal > 1 ? `${baseSymbol}:${ordinal}` : baseSymbol;
424
+ violations.push(createSchemaViolation(ast.filePath, location, 'Potential SQL injection vulnerability. Use parameterized queries.',
425
+ // Spec 11 R4 blanket demotion: all survivors → suggestion
426
+ { severity: 'suggestion', rule: 'sql-injection', symbol }));
427
+ }
428
+ /**
429
+ * Detect potential SQL injection in query/execute calls.
430
+ *
431
+ * @param ast The parsed file AST.
432
+ * @param adapter The language adapter for the file's syntax.
433
+ * @param sourceCode The raw source text.
434
+ * @returns SQL-injection violations.
435
+ */
436
+ export function checkSQLInjection(ast, adapter, sourceCode) {
437
+ const violations = [];
438
+ const ctx = {
439
+ ast,
440
+ adapter,
441
+ sourceCode,
442
+ symbolOrdinals: new Map(),
443
+ violations,
444
+ };
445
+ for (const pattern of DANGEROUS_SQL_PATTERNS) {
446
+ // Clone regex to reset state (global regexes track lastIndex)
447
+ const re = new RegExp(pattern.source, pattern.flags);
448
+ let match;
449
+ while ((match = re.exec(sourceCode)) !== null) {
450
+ checkInjectionMatch(ctx, match);
451
+ }
452
+ }
453
+ return violations;
454
+ }
455
+ /**
456
+ * Walk up from a node to the enclosing call_expression, if any. Used by
457
+ * checkSQLInjection to map a regex match location back to the query(...)/
458
+ * execute(...) call whose string argument is under test.
459
+ */
460
+ function findEnclosingCallExpression(node, adapter) {
461
+ let current = node;
462
+ while (current) {
463
+ if (adapter.getNodeType(current) === 'call_expression')
464
+ return current;
465
+ current = adapter.getParent(current);
466
+ }
467
+ return null;
468
+ }
469
+ /**
470
+ * True when a dynamic query/execute string argument is provably safe to embed
471
+ * in SQL — every interpolated sub-part is a compile-time constant, quote-escaped
472
+ * sanitizer, safe ternary/array-join, or guard-validated parameter. Mirrors
473
+ * UniversalDataAccessAnalyzer.isSafeDynamicPart (minus its config-driven
474
+ * sanitizer allowlist, which checkSQLInjection has no config for); used to
475
+ * clear trusted-DDL false positives (Spec 33 Item 11a).
476
+ */
477
+ function isAllDynamicPartsSafe(callNode, ast, adapter, sourceCode) {
478
+ // No dynamic-string capability → cannot prove safety → keep the legacy hit.
479
+ if (!adapter.isDynamicStringConstruction || !adapter.getDynamicParts) {
480
+ return false;
481
+ }
482
+ // A non-dynamic argument (plain string literal) has no interpolation.
483
+ if (!adapter.isDynamicStringConstruction(callNode))
484
+ return true;
485
+ const parts = adapter.getDynamicParts(callNode, sourceCode);
486
+ if (parts.length === 0)
487
+ return true;
488
+ for (const part of parts) {
489
+ // Prefer the adapter's cross-function safety analysis — it clears quote-escape
490
+ // sanitizers, safe ternaries/array-joins, safe local helper calls, and
491
+ // guard-validated parameters, and subsumes the static-constant check.
492
+ if (part.node && adapter.isSafeInterpolation) {
493
+ if (!adapter.isSafeInterpolation(part.node, ast, sourceCode))
494
+ return false;
495
+ continue;
496
+ }
497
+ // Fallback for adapters without isSafeInterpolation: resolve identifiers
498
+ // to compile-time constants only.
499
+ if (part.isIdentifier) {
500
+ const resolved = part.node && adapter.resolveLocalConstant
501
+ ? adapter.resolveLocalConstant(part.node, ast, sourceCode)
502
+ : null;
503
+ if (!(resolved && resolved.isStatic))
504
+ return false;
505
+ continue;
506
+ }
507
+ // Non-identifier expression with no safety analysis → cannot prove safe.
508
+ return false;
509
+ }
510
+ return true;
511
+ }
512
+ /**
513
+ * Check for references to tables absent from the known catalog — R2.4, with
514
+ * Levenshtein suggestions. Spec 24 Item 4 Part B applies a 10:1 fail-open
515
+ * ratio: when the unknown:known ratio exceeds 10 (or no known tables exist),
516
+ * the catalog is not trustworthy enough to flag individual references, so the
517
+ * check silently skips instead of emitting a wall of unknown-table noise.
518
+ *
519
+ * @param references Table references extracted from the file.
520
+ * @param allTables The known-table catalog.
521
+ * @param filePath The file under analysis.
522
+ * @returns Unknown-table violations (empty when the fail-open ratio triggers).
523
+ */
524
+ export function checkMissingReferences(references, allTables, filePath) {
525
+ const violations = [];
526
+ const unknownRefs = references.filter(ref => !allTables.has(ref.table) && !isSystemTable(ref.table) && !isTableValuedFunction(ref.table));
527
+ const knownCount = allTables.size;
528
+ const unknownCount = unknownRefs.length;
529
+ if (knownCount === 0 || unknownCount / Math.max(knownCount, 1) > 10) {
530
+ // Fail-open: silently skip (the catalog is unreliable at this ratio).
531
+ return violations;
532
+ }
533
+ for (const ref of unknownRefs) {
534
+ const suggestions = getNearestTableSuggestions(ref.table, allTables, 2);
535
+ const msg = suggestions.length > 0
536
+ ? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
537
+ : `Reference to unknown table '${ref.table}' (${ref.type})`;
538
+ violations.push(createSchemaViolation(filePath, ref.location, msg, { severity: 'suggestion', rule: 'unknown-table', symbol: ref.table }));
539
+ }
540
+ return violations;
541
+ }
542
+ /**
543
+ * Extract the callee text from a call_expression node.
544
+ *
545
+ * @param node The call_expression node.
546
+ * @param adapter The language adapter for the file's syntax.
547
+ * @param sourceCode The raw source text.
548
+ * @returns The callee text (e.g. "db.exec"), or null when absent.
549
+ */
550
+ export function getCallee(node, adapter, sourceCode) {
551
+ // For db.exec() → callee is "db.exec"
552
+ if (!node.children)
553
+ return null;
554
+ for (const child of node.children) {
555
+ const type = adapter.getNodeType(child);
556
+ if (type === 'identifier' ||
557
+ type === 'member_expression' ||
558
+ type === 'call_expression') {
559
+ return adapter.getNodeText(child, sourceCode).trim();
560
+ }
561
+ }
562
+ return null;
563
+ }
564
+ /**
565
+ * Check whether a call_expression has a template string argument.
566
+ *
567
+ * @param node The call_expression node.
568
+ * @param adapter The language adapter for the file's syntax.
569
+ * @returns True when a template-string argument is present.
570
+ */
571
+ export function hasTemplateArgument(node, adapter) {
572
+ if (!node.children)
573
+ return false;
574
+ for (const child of node.children) {
575
+ const type = adapter.getNodeType(child);
576
+ if (type === 'template_string' || type === 'template_literal') {
577
+ return true;
578
+ }
579
+ }
580
+ return false;
581
+ }
582
+ /**
583
+ * Get the text of the first template string argument.
584
+ *
585
+ * @param node The call_expression node.
586
+ * @param adapter The language adapter for the file's syntax.
587
+ * @param sourceCode The raw source text.
588
+ * @returns The trimmed template text, or null when absent.
589
+ */
590
+ export function getTemplateText(node, adapter, sourceCode) {
591
+ if (!node.children)
592
+ return null;
593
+ for (const child of node.children) {
594
+ const type = adapter.getNodeType(child);
595
+ if (type === 'template_string' || type === 'template_literal') {
596
+ return adapter.getNodeText(child, sourceCode).trim();
597
+ }
598
+ }
599
+ return null;
600
+ }
601
+ /**
602
+ * Get the first string/template argument from a call expression.
603
+ *
604
+ * @param node The call_expression node.
605
+ * @param adapter The language adapter for the file's syntax.
606
+ * @param sourceCode The raw source text.
607
+ * @returns The unquoted string/template argument, or null when absent.
608
+ */
609
+ export function getFirstStringArgument(node, adapter, sourceCode) {
610
+ if (!node.children)
611
+ return null;
612
+ // Look for 'arguments' child first
613
+ for (const child of node.children) {
614
+ const type = adapter.getNodeType(child);
615
+ if (type === 'arguments' && child.children) {
616
+ for (const arg of child.children) {
617
+ const argType = adapter.getNodeType(arg);
618
+ if (argType === 'string' ||
619
+ argType === 'template_string' ||
620
+ argType === 'template_literal') {
621
+ const text = adapter.getNodeText(arg, sourceCode).trim();
622
+ // Strip surrounding quotes from string literals
623
+ if ((text.startsWith("'") && text.endsWith("'")) ||
624
+ (text.startsWith('"') && text.endsWith('"')) ||
625
+ (text.startsWith('`') && text.endsWith('`'))) {
626
+ return text.slice(1, -1);
627
+ }
628
+ return text;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ return null;
634
+ }
635
+ /**
636
+ * Check if a callee is a DB member call like db.exec, database.query, etc.
637
+ *
638
+ * @param calleeText The callee text (e.g. "db.exec").
639
+ * @param methods The allowed DB call method names.
640
+ * @param receivers The allowed DB receiver names.
641
+ * @returns True when the callee is a permitted receiver.method DB call.
642
+ */
643
+ export function isDbMemberCall(calleeText, methods, receivers) {
644
+ // calleeText might be like "db.exec"
645
+ const dotIdx = calleeText.indexOf('.');
646
+ if (dotIdx === -1)
647
+ return false;
648
+ const receiver = calleeText.substring(0, dotIdx);
649
+ const method = calleeText.substring(dotIdx + 1);
650
+ return receivers.includes(receiver) && methods.includes(method);
651
+ }
652
+ /**
653
+ * Get the line/column location of the call expression.
654
+ *
655
+ * @param node The call_expression node.
656
+ * @returns The node's start line/column.
657
+ */
658
+ export function getCallLocation(node) {
659
+ return node.location.start;
660
+ }
661
+ /**
662
+ * Convert a character offset to a line/column location.
663
+ *
664
+ * @param sourceCode The source text the offset is relative to.
665
+ * @param offset The character offset.
666
+ * @param base Fallback location returned when offset is out of range.
667
+ * @returns The 1-based line/column for the offset.
668
+ */
669
+ export function offsetToLocation(sourceCode, offset, base) {
670
+ if (offset < 0 || offset >= sourceCode.length)
671
+ return base;
672
+ const before = sourceCode.substring(0, offset);
673
+ const lineOffset = before.split('\n').length - 1;
674
+ const lastNewline = before.lastIndexOf('\n');
675
+ const column = lastNewline >= 0 ? offset - lastNewline : offset + 1;
676
+ // offset is absolute in sourceCode — lineOffset is 0-based, so +1 gives
677
+ // the correct 1-based line. base.line is the fallback guard only.
678
+ return { line: lineOffset + 1, column };
679
+ }
680
+ /**
681
+ * True when `table` is a well-known system table/schema name.
682
+ *
683
+ * @param table The candidate table name.
684
+ * @returns True for system schemas/tables (information_schema, pg_catalog, …).
685
+ */
686
+ export function isSystemTable(table) {
687
+ const systemTables = [
688
+ 'information_schema',
689
+ 'pg_catalog',
690
+ 'mysql',
691
+ 'performance_schema',
692
+ 'sys',
693
+ 'sqlite_master',
694
+ 'sqlite_sequence',
695
+ ];
696
+ return systemTables.some(st => table.toLowerCase() === st || table.toLowerCase().startsWith(st + '.'));
697
+ }
698
+ /**
699
+ * True when `name` is a SQL table-valued function rather than a real table.
700
+ * These appear after FROM/JOIN (`FROM json_each(...)`, `FROM generate_series(...)`)
701
+ * so the keyword-anchored patterns capture them, but they are functions, not
702
+ * tables — flagging them as unknown-table is a false positive.
703
+ *
704
+ * Covers the well-known SQLite (json_each/json_tree) and PostgreSQL
705
+ * (unnest/generate_series/json_array_elements/...) set, plus DuckDB's
706
+ * read_csv/read_parquet/parquet_scan families. Not exhaustive by design:
707
+ * these are the function names that appear in real SELECT ... FROM fn(...).
708
+ *
709
+ * @param name The candidate identifier.
710
+ * @returns True when `name` is a known table-valued function.
711
+ */
712
+ export function isTableValuedFunction(name) {
713
+ const tvfs = new Set([
714
+ // SQLite
715
+ 'json_each', 'json_tree',
716
+ // PostgreSQL
717
+ 'unnest', 'generate_series', 'generate_subscripts',
718
+ 'json_array_elements', 'jsonb_array_elements',
719
+ 'json_array_elements_text', 'jsonb_array_elements_text',
720
+ 'json_each_text', 'jsonb_each_text', 'jsonb_each',
721
+ 'json_object_keys', 'jsonb_object_keys',
722
+ 'regexp_split_to_table', 'string_to_table',
723
+ // DuckDB / ClickHouse-ish readers
724
+ 'read_csv', 'read_csv_auto', 'read_parquet', 'read_json', 'read_json_auto',
725
+ 'parquet_scan', 'csv_scan', 'glob', 'range',
726
+ ]);
727
+ return tvfs.has(name.toLowerCase());
728
+ }
729
+ /**
730
+ * Common SQL keywords and identifiers that are not real table names.
731
+ *
732
+ * @param word The candidate identifier.
733
+ * @returns True when `word` is a SQL keyword/reserved identifier.
734
+ */
735
+ export function isSqlKeyword(word) {
736
+ const keywords = new Set([
737
+ 'select', 'from', 'where', 'join', 'inner', 'outer', 'left', 'right',
738
+ 'full', 'cross', 'on', 'and', 'or', 'not', 'in', 'as', 'is', 'null',
739
+ 'like', 'between', 'order', 'group', 'by', 'having', 'limit', 'offset',
740
+ 'union', 'all', 'distinct', 'case', 'when', 'then', 'else', 'end',
741
+ 'insert', 'into', 'values', 'update', 'set', 'delete', 'create',
742
+ 'table', 'alter', 'drop', 'index', 'view', 'if', 'exists', 'primary',
743
+ 'key', 'foreign', 'references', 'constraint', 'default', 'unique',
744
+ 'check', 'asc', 'desc', 'count', 'sum', 'avg', 'min', 'max',
745
+ 'integer', 'text', 'varchar', 'text', 'boolean', 'float', 'blob',
746
+ 'real', 'timestamp', 'date', 'time', 'datetime', 'serial', 'bigint',
747
+ 'the', 'a', 'an',
748
+ ]);
749
+ return keywords.has(word.toLowerCase());
750
+ }
751
+ /**
752
+ * True when the `from` keyword at `fromIndex` is a JavaScript/TypeScript
753
+ * module specifier (`import x from 'mod'`, `import { a } from 'mod'`,
754
+ * `export { a } from 'mod'`, `export * from 'mod'`) rather than a SQL
755
+ * FROM clause.
756
+ *
757
+ * Step (3) of findTableReferences() full-source scans `.ts` files under
758
+ * `migrations/`, where a case-insensitive `\bFROM\s+([`"']?)...` pattern
759
+ * matches `from 'typeorm'` / `from 'path'` / `from 'dotenv'` and records the
760
+ * module name as an unknown table. SQL never introduces FROM with
761
+ * `import`/`export`, so requiring that introducer at a statement boundary is
762
+ * unambiguous — and only checking the introducer at a line/`;` boundary
763
+ * avoids misreading a SQL comment (`-- import data`) or a table named
764
+ * `import_log` as a module statement.
765
+ *
766
+ * @param sqlText The source text being scanned for a FROM introducer.
767
+ * @param fromIndex The character index of the `from` keyword within `sqlText`.
768
+ * @returns True when the introducer preceding `fromIndex` is an import/export.
769
+ */
770
+ export function isModuleImportFrom(sqlText, fromIndex) {
771
+ let start = fromIndex;
772
+ while (start > 0) {
773
+ const prev = sqlText[start - 1];
774
+ // Stop at a statement terminator or a blank line — a module statement's
775
+ // `import`/`export` introducer never crosses these.
776
+ if (prev === ';')
777
+ break;
778
+ if (prev === '\n' && start >= 2 && sqlText[start - 2] === '\n')
779
+ break;
780
+ start--;
781
+ }
782
+ const prefix = sqlText.slice(start, fromIndex);
783
+ return /(?:^|[\n;])\s*(?:import|export)\b/m.test(prefix);
784
+ }
785
+ /**
786
+ * Count the number of DB queries a function body issues.
787
+ *
788
+ * A `query`/`execute` method call is one query; each standalone SQL keyword
789
+ * (SELECT, INSERT INTO, UPDATE, DELETE FROM) outside such a call is also one
790
+ * query. SQL keywords inside a call's argument are not counted separately —
791
+ * otherwise a single `query('SELECT ...')` call is counted twice (once for the
792
+ * call, once for the SQL it carries).
793
+ *
794
+ * @param text The function body text.
795
+ * @returns The number of DB queries the function issues.
796
+ */
797
+ export function countQueries(text) {
798
+ const callCount = (text.match(/\.(?:query|execute)\s*\(/g) || []).length;
799
+ const bodyless = stripQueryCallBodies(text);
800
+ const sqlPatterns = [
801
+ /SELECT\s+/gi,
802
+ /INSERT\s+INTO/gi,
803
+ /UPDATE\s+/gi,
804
+ /DELETE\s+FROM/gi,
805
+ ];
806
+ let sqlCount = 0;
807
+ for (const pattern of sqlPatterns) {
808
+ const matches = bodyless.match(pattern);
809
+ if (matches)
810
+ sqlCount += matches.length;
811
+ }
812
+ return callCount + sqlCount;
813
+ }
814
+ /**
815
+ * Blank out the bodies of `query(...)`/`execute(...)` calls (balanced-paren
816
+ * aware) so SQL keywords inside their arguments are not double-counted.
817
+ *
818
+ * @param text The function body text.
819
+ * @returns The text with `query`/`execute` call bodies replaced by spaces.
820
+ */
821
+ function stripQueryCallBodies(text) {
822
+ const re = /\.(?:query|execute)\s*\(/g;
823
+ let result = '';
824
+ let last = 0;
825
+ let m;
826
+ while ((m = re.exec(text)) !== null) {
827
+ const openParen = m.index + m[0].length;
828
+ let depth = 1;
829
+ let i = openParen;
830
+ while (i < text.length && depth > 0) {
831
+ if (text[i] === '(')
832
+ depth++;
833
+ else if (text[i] === ')')
834
+ depth--;
835
+ i++;
836
+ }
837
+ result += text.slice(last, openParen);
838
+ result += ' '.repeat(Math.max(0, i - openParen));
839
+ last = i;
840
+ re.lastIndex = i;
841
+ }
842
+ result += text.slice(last);
843
+ return result;
844
+ }
845
+ /**
846
+ * Breadth-first search for the AST node whose start location exactly matches
847
+ * the given line/column.
848
+ *
849
+ * @param root The AST root node.
850
+ * @param location The target line/column.
851
+ * @returns The matching node, or null when absent.
852
+ */
853
+ export function findNodeByLocation(root, location) {
854
+ const queue = [root];
855
+ while (queue.length > 0) {
856
+ const node = queue.shift();
857
+ if (node.location.start.line === location.line &&
858
+ node.location.start.column === location.column) {
859
+ return node;
860
+ }
861
+ if (node.children) {
862
+ queue.push(...node.children);
863
+ }
864
+ }
865
+ return null;
866
+ }
867
+ /**
868
+ * Find the nearest AST node at a source location — walks the tree looking
869
+ * for the deepest node that contains the given line/column.
870
+ *
871
+ * @param root The AST root node.
872
+ * @param location The target line/column.
873
+ * @param adapter The language adapter for the file's syntax.
874
+ * @returns The deepest node containing the location, or null.
875
+ */
876
+ export function findClosestNodeAt(root, location, adapter) {
877
+ let best = null;
878
+ let bestDepth = -1;
879
+ const walk = (node, depth) => {
880
+ const start = node.location.start;
881
+ const end = node.location.end;
882
+ // Check if node contains the location
883
+ if ((start.line < location.line ||
884
+ (start.line === location.line && start.column <= location.column)) &&
885
+ (end.line > location.line ||
886
+ (end.line === location.line && end.column >= location.column))) {
887
+ if (depth > bestDepth) {
888
+ best = node;
889
+ bestDepth = depth;
890
+ }
891
+ if (node.children) {
892
+ for (const child of node.children) {
893
+ walk(child, depth + 1);
894
+ }
895
+ }
896
+ }
897
+ };
898
+ walk(root, 0);
899
+ return best;
900
+ }
901
+ /**
902
+ * Walk up the AST from a node to find the enclosing function or method name.
903
+ * Matches the same scheme as UniversalDataAccessAnalyzer.findEnclosingFunctionName.
904
+ *
905
+ * @param node The node to start the walk from.
906
+ * @param adapter The language adapter for the file's syntax.
907
+ * @returns The enclosing function/method name, or "top-level".
908
+ */
909
+ export function findEnclosingFunctionName(node, adapter) {
910
+ let current = node;
911
+ while (current) {
912
+ const type = adapter.getNodeType(current);
913
+ if (type === 'arrow_function' ||
914
+ type === 'function_declaration' ||
915
+ type === 'function_expression' ||
916
+ type === 'generator_function_declaration' ||
917
+ type === 'generator_function_expression' ||
918
+ type === 'method_definition') {
919
+ const name = getNodeName(current, adapter);
920
+ if (name)
921
+ return name;
922
+ }
923
+ if (adapter.isMethod(current)) {
924
+ const name = getNodeName(current, adapter);
925
+ if (name)
926
+ return name;
927
+ }
928
+ current = adapter.getParent(current);
929
+ }
930
+ return 'top-level';
931
+ }
932
+ /**
933
+ * Extract a human-readable name from an AST node.
934
+ * Matches the same scheme as UniversalDataAccessAnalyzer.getNodeName.
935
+ *
936
+ * @param node The AST node.
937
+ * @param adapter The language adapter for the file's syntax.
938
+ * @returns The node's name, or "" when none is found.
939
+ */
940
+ export function getNodeName(node, adapter) {
941
+ // Try explicit name/text on the converted ASTNode (some adapters set it)
942
+ if (node.name && typeof node.name === 'string') {
943
+ return node.name;
944
+ }
945
+ if (node.text && typeof node.text === 'string') {
946
+ return node.text;
947
+ }
948
+ // Fall back to the raw tree-sitter node's text content (leaf identifiers etc.)
949
+ const rawText = node.raw?.text;
950
+ if (typeof rawText === 'string' && rawText.length > 0) {
951
+ return rawText;
952
+ }
953
+ if (node.children) {
954
+ for (const child of node.children) {
955
+ const childType = adapter.getNodeType(child);
956
+ if (childType === 'identifier' || childType === 'property_identifier') {
957
+ const name = getNodeName(child, adapter);
958
+ if (name)
959
+ return name;
960
+ }
961
+ }
962
+ }
963
+ return '';
964
+ }
965
+ //# sourceMappingURL=codeAnalysis.js.map