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
@@ -3,8 +3,17 @@
3
3
  *
4
4
  * Uses web-tree-sitter WASM parsers behind the adapter seam — no TypeScript compiler API.
5
5
  * Supports .ts, .tsx, .js, and .jsx files.
6
+ *
7
+ * The adapter is split across a leaf-first inheritance chain (Spec-33
8
+ * interface-segregation): each link contributes a cohesive slice of the
9
+ * adapter so no single class exceeds the SOLID class-size threshold.
10
+ * Ancestors hold the tree-traversal, extraction, and scope-resolution
11
+ * primitives; descendants compose them into the public LanguageAdapter
12
+ * surface. The mutually-recursive cross-function safety cluster is
13
+ * co-located in TsSafetyAnalysis (TypeScript forbids a base class calling a
14
+ * subclass method, so those must live in one class).
6
15
  */
7
- import { getParser } from '../tree-sitter/parser.js';
16
+ import { getParser, parseWithRecovery } from '../tree-sitter/parser.js';
8
17
  import { toASTNode, toSourceLocation } from '../tree-sitter/converter.js';
9
18
  // ---------------------------------------------------------------------------
10
19
  // Source code storage
@@ -14,316 +23,28 @@ import { toASTNode, toSourceLocation } from '../tree-sitter/converter.js';
14
23
  * can recover the original source text.
15
24
  */
16
25
  const sourceCodeMap = new WeakMap();
26
+ /**
27
+ * A sentinel ASTNode standing in for a provably-constant string. Used when
28
+ * binding `.map()` callback element parameters (which iterate a static array)
29
+ * during cross-function safety analysis: the element is unknown but its value
30
+ * is constrained to the array's compile-time elements, so it is safe to embed
31
+ * in a SQL fragment the same way a literal is.
32
+ */
33
+ const SAFE_STRING_NODE = {
34
+ type: 'string',
35
+ range: [-1, -1],
36
+ location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
37
+ raw: {
38
+ type: 'string',
39
+ text: "''",
40
+ startIndex: -1,
41
+ endIndex: -1,
42
+ },
43
+ };
17
44
  // ---------------------------------------------------------------------------
18
- // Adapter implementation
45
+ // Traversal + primitive helpers
19
46
  // ---------------------------------------------------------------------------
20
- export class TreeSitterTypeScriptAdapter {
21
- name = 'typescript';
22
- fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
23
- // -- File detection -------------------------------------------------------
24
- supportsFile(filePath) {
25
- return this.fileExtensions.some((ext) => filePath.endsWith(ext));
26
- }
27
- // -- Parsing --------------------------------------------------------------
28
- async parse(filePath, content) {
29
- const isTsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
30
- const lang = isTsx ? 'tsx' : filePath.endsWith('.go') ? 'go' : 'typescript';
31
- const parser = getParser(lang, isTsx);
32
- const tree = parser.parse(content);
33
- if (!tree)
34
- throw new Error(`Failed to parse file: ${filePath}`);
35
- const errors = [];
36
- // Collect ERROR nodes as parse errors
37
- this.collectErrors(tree.rootNode, errors);
38
- const root = toASTNode(tree.rootNode, undefined, lang);
39
- const ast = {
40
- root,
41
- language: lang,
42
- filePath,
43
- errors,
44
- };
45
- sourceCodeMap.set(ast, content);
46
- return ast;
47
- }
48
- // -- AST Navigation -------------------------------------------------------
49
- findNodes(ast, pattern) {
50
- const results = [];
51
- this.walk(ast.root, (node) => {
52
- if (this.matchesPattern(node, pattern)) {
53
- results.push(node);
54
- }
55
- });
56
- return results;
57
- }
58
- getParent(node) {
59
- return node.parent ?? null;
60
- }
61
- getChildren(node) {
62
- return node.children ?? [];
63
- }
64
- getSiblings(node) {
65
- if (!node.parent?.children)
66
- return [];
67
- return node.parent.children.filter((c) => c !== node);
68
- }
69
- // -- Node Information -----------------------------------------------------
70
- getNodeType(node) {
71
- return node.type;
72
- }
73
- getNodeText(node, sourceCode) {
74
- return sourceCode.slice(node.range[0], node.range[1]);
75
- }
76
- getNodeName(node) {
77
- const syntaxNode = node.raw;
78
- return this.extractName(syntaxNode);
79
- }
80
- getNodeLocation(node) {
81
- return node.location;
82
- }
83
- // -- Language-Specific Extraction -----------------------------------------
84
- extractFunctions(ast) {
85
- const sourceCode = sourceCodeMap.get(ast) ?? '';
86
- const functions = [];
87
- this.walk(ast.root, (node) => {
88
- const syntaxNode = node.raw;
89
- const type = syntaxNode.type;
90
- if (type === 'function_declaration' ||
91
- type === 'generator_function_declaration' ||
92
- type === 'function_expression' ||
93
- type === 'arrow_function' ||
94
- type === 'method_definition') {
95
- const fn = this.buildFunctionInfo(syntaxNode, sourceCode);
96
- if (fn)
97
- functions.push(fn);
98
- }
99
- });
100
- return functions;
101
- }
102
- extractClasses(ast) {
103
- const sourceCode = sourceCodeMap.get(ast) ?? '';
104
- const classes = [];
105
- this.walk(ast.root, (node) => {
106
- const syntaxNode = node.raw;
107
- if (syntaxNode.type === 'class_declaration') {
108
- const cls = this.buildClassInfo(syntaxNode, sourceCode);
109
- if (cls)
110
- classes.push(cls);
111
- }
112
- });
113
- return classes;
114
- }
115
- extractImports(ast) {
116
- const sourceCode = sourceCodeMap.get(ast) ?? '';
117
- const imports = [];
118
- this.walk(ast.root, (node) => {
119
- const syntaxNode = node.raw;
120
- if (syntaxNode.type === 'import_statement') {
121
- const imp = this.buildImportInfo(syntaxNode, sourceCode);
122
- if (imp)
123
- imports.push(imp);
124
- }
125
- });
126
- return imports;
127
- }
128
- extractExports(ast) {
129
- const sourceCode = sourceCodeMap.get(ast) ?? '';
130
- const exports = [];
131
- this.walk(ast.root, (node) => {
132
- const syntaxNode = node.raw;
133
- if (syntaxNode.type === 'export_statement') {
134
- const ex = this.buildExportInfo(syntaxNode, sourceCode);
135
- if (ex)
136
- exports.push(ex);
137
- }
138
- });
139
- return exports;
140
- }
141
- // -- Pattern Matching Helpers ---------------------------------------------
142
- isClass(node) {
143
- const type = node.raw.type;
144
- return type === 'class_declaration' || type === 'class_expression';
145
- }
146
- isFunction(node) {
147
- const type = node.raw.type;
148
- return (type === 'function_declaration' ||
149
- type === 'function_expression' ||
150
- type === 'arrow_function' ||
151
- type === 'generator_function_declaration' ||
152
- type === 'generator_function_expression');
153
- }
154
- isMethod(node) {
155
- return node.raw.type === 'method_definition';
156
- }
157
- isInterface(node) {
158
- return node.raw.type === 'interface_declaration';
159
- }
160
- isImport(node) {
161
- return node.raw.type === 'import_statement';
162
- }
163
- isExport(node) {
164
- return node.raw.type === 'export_statement';
165
- }
166
- isLoop(node) {
167
- const type = node.raw.type;
168
- return (type === 'for_statement' ||
169
- type === 'for_in_statement' ||
170
- type === 'while_statement' ||
171
- type === 'do_statement');
172
- }
173
- isConditional(node) {
174
- const type = node.raw.type;
175
- return (type === 'if_statement' ||
176
- type === 'switch_statement' ||
177
- type === 'ternary_expression' ||
178
- type === 'switch_case');
179
- }
180
- isVariableDeclaration(node) {
181
- const type = node.raw.type;
182
- return (type === 'variable_declaration' ||
183
- type === 'lexical_declaration' ||
184
- type === 'variable_declarator');
185
- }
186
- // -- Advanced Features ----------------------------------------------------
187
- getTypeInfo(node) {
188
- const syntaxNode = node.raw;
189
- return this.extractTypeAnnotation(syntaxNode);
190
- }
191
- getDocumentation(node) {
192
- const syntaxNode = node.raw;
193
- return this.extractDocumentation(syntaxNode);
194
- }
195
- getComplexity(node) {
196
- const syntaxNode = node.raw;
197
- return this.calculateCyclomaticComplexity(syntaxNode);
198
- }
199
- // -- Optional: Interfaces -------------------------------------------------
200
- extractInterfaces(ast) {
201
- const sourceCode = sourceCodeMap.get(ast) ?? '';
202
- const interfaces = [];
203
- this.walk(ast.root, (node) => {
204
- const syntaxNode = node.raw;
205
- if (syntaxNode.type === 'interface_declaration') {
206
- const iface = this.buildInterfaceInfo(syntaxNode, sourceCode);
207
- if (iface)
208
- interfaces.push(iface);
209
- }
210
- });
211
- return interfaces;
212
- }
213
- // -- Optional: Raw imports (static, dynamic, require) ----------------------
214
- extractRawImports(_filePath, content) {
215
- const results = [];
216
- const parser = getParser('typescript');
217
- const tree = parser.parse(content);
218
- if (!tree)
219
- return results;
220
- // Static imports
221
- this.walkRaw(tree.rootNode, (node) => {
222
- if (node.type === 'import_statement') {
223
- const source = this.getChildByType(node, 'string');
224
- if (source) {
225
- const specifier = source.text.slice(1, -1); // strip quotes
226
- results.push({
227
- moduleSpecifier: specifier,
228
- isStatic: true,
229
- isDynamic: false,
230
- isRequire: false,
231
- line: source.startPosition.row,
232
- });
233
- }
234
- }
235
- // Dynamic import(): import('...')
236
- if (node.type === 'call_expression') {
237
- const fn = node.firstChild;
238
- if (fn?.type === 'import') {
239
- const args = this.getChildByType(node, 'arguments');
240
- if (args) {
241
- const strNode = this.findFirstNamedChild(args, 'string');
242
- if (strNode) {
243
- const specifier = strNode.text.slice(1, -1);
244
- results.push({
245
- moduleSpecifier: specifier,
246
- isStatic: false,
247
- isDynamic: true,
248
- isRequire: false,
249
- line: strNode.startPosition.row,
250
- });
251
- }
252
- }
253
- }
254
- // require('...')
255
- if (fn?.type === 'identifier' && fn.text === 'require') {
256
- const args = this.getChildByType(node, 'arguments');
257
- if (args) {
258
- const strNode = this.findFirstNamedChild(args, 'string');
259
- if (strNode) {
260
- const specifier = strNode.text.slice(1, -1);
261
- results.push({
262
- moduleSpecifier: specifier,
263
- isStatic: false,
264
- isDynamic: false,
265
- isRequire: true,
266
- line: strNode.startPosition.row,
267
- });
268
- }
269
- }
270
- }
271
- }
272
- });
273
- return results;
274
- }
275
- // -- Optional: Exported symbol names --------------------------------------
276
- extractExportedSymbols(ast) {
277
- const symbols = [];
278
- this.walk(ast.root, (astNode) => {
279
- const node = astNode.raw;
280
- if (node.type === 'export_statement') {
281
- // export function foo / export class Foo / export const x
282
- const declaration = this.findFirstNamedChild(node, [
283
- 'function_declaration',
284
- 'class_declaration',
285
- 'lexical_declaration',
286
- 'variable_declaration',
287
- 'interface_declaration',
288
- 'type_alias_declaration',
289
- 'enum_declaration',
290
- ]);
291
- if (declaration) {
292
- const name = this.extractName(declaration);
293
- if (name) {
294
- symbols.push({ name, line: declaration.startPosition.row });
295
- return;
296
- }
297
- }
298
- // export { foo, bar } or export { default }
299
- const clause = this.getChildByType(node, 'export_clause');
300
- if (clause) {
301
- for (const child of clause.namedChildren) {
302
- if (child.type === 'export_specifier') {
303
- const nameNode = this.getChildByType(child, 'identifier');
304
- if (nameNode) {
305
- symbols.push({
306
- name: nameNode.text,
307
- line: nameNode.startPosition.row,
308
- });
309
- }
310
- }
311
- }
312
- }
313
- // export default <expression>
314
- if (node.childForFieldName?.('value')) {
315
- symbols.push({
316
- name: 'default',
317
- line: node.startPosition.row,
318
- });
319
- }
320
- }
321
- });
322
- return symbols;
323
- }
324
- // ---------------------------------------------------------------------------
325
- // Internal helpers
326
- // ---------------------------------------------------------------------------
47
+ class TsTraversalHelpers {
327
48
  /** Walk all ASTNodes in a tree (depth-first). */
328
49
  walk(node, visitor) {
329
50
  visitor(node);
@@ -353,93 +74,164 @@ export class TreeSitterTypeScriptAdapter {
353
74
  this.collectErrors(child, errors);
354
75
  }
355
76
  }
356
- /** Match an ASTNode against a NodePattern. */
357
- matchesPattern(node, pattern) {
358
- const syntaxNode = node.raw;
359
- // type matching
360
- if (pattern.type !== undefined) {
361
- const types = Array.isArray(pattern.type) ? pattern.type : [pattern.type];
362
- if (!types.includes(syntaxNode.type))
363
- return false;
77
+ /** Find first child of node matching one of the given types. */
78
+ getChildByType(node, type) {
79
+ for (const child of node.children) {
80
+ if (child.type === type)
81
+ return child;
364
82
  }
365
- // name matching
366
- if (pattern.name !== undefined) {
367
- const nodeName = this.extractName(syntaxNode);
368
- if (nodeName === null)
369
- return false;
370
- if (typeof pattern.name === 'string') {
371
- if (nodeName !== pattern.name)
372
- return false;
373
- }
374
- else if (pattern.name instanceof RegExp) {
375
- if (!pattern.name.test(nodeName))
376
- return false;
377
- }
83
+ return null;
84
+ }
85
+ /** Find the first named child matching one of the given types. */
86
+ findFirstNamedChild(node, types) {
87
+ const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
88
+ for (const child of node.namedChildren) {
89
+ if (typeSet.has(child.type))
90
+ return child;
378
91
  }
379
- // hasChild matching
380
- if (pattern.hasChild !== undefined) {
381
- const childNodes = node.children ?? [];
382
- const hasMatch = childNodes.some((c) => this.matchesPattern(c, pattern.hasChild));
383
- if (!hasMatch)
384
- return false;
92
+ return null;
93
+ }
94
+ /** Check if node has a direct child (named or anonymous) with the given text. */
95
+ hasChild(node, text) {
96
+ for (const child of node.children) {
97
+ if (child.text === text || child.type === text)
98
+ return true;
385
99
  }
386
- // hasParent matching
387
- if (pattern.hasParent !== undefined) {
388
- if (!node.parent)
389
- return false;
390
- if (!this.matchesPattern(node.parent, pattern.hasParent))
391
- return false;
100
+ return false;
101
+ }
102
+ /** Get the operator text from a binary expression node. */
103
+ getOperator(node) {
104
+ for (const child of node.children) {
105
+ if (!child.isNamed &&
106
+ ['&&', '||', '+', '-', '*', '/', '%', '===', '!==', '==', '!='].includes(child.type)) {
107
+ return child.type;
108
+ }
392
109
  }
393
- // custom predicate
394
- if (pattern.custom !== undefined) {
395
- if (!pattern.custom(node))
396
- return false;
110
+ return null;
111
+ }
112
+ /** Check if a node has an `async` modifier keyword. */
113
+ hasModifier(node, modifier) {
114
+ for (const child of node.children) {
115
+ if (!child.isNamed && child.type === modifier)
116
+ return true;
117
+ // tree-sitter may represent modifiers as named decorator/modifier nodes
118
+ if (child.isNamed &&
119
+ (child.type === modifier ||
120
+ child.type === 'accessibility_modifier' ||
121
+ child.type === 'override_modifier' ||
122
+ child.type === 'readonly' ||
123
+ child.type === 'static' ||
124
+ child.type === 'abstract')) {
125
+ if (child.text === modifier)
126
+ return true;
127
+ }
397
128
  }
398
- return true;
129
+ return false;
130
+ }
131
+ /** Check if a node is under an export_statement. */
132
+ isNodeExported(node) {
133
+ const parent = node.parent;
134
+ if (parent?.type === 'export_statement')
135
+ return true;
136
+ // Also check for `export` keyword on the node itself (TS module syntax)
137
+ for (const child of node.children) {
138
+ if (!child.isNamed && child.type === 'export')
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ extractParameters(node, sourceCode) {
144
+ const params = [];
145
+ const formalParams = node.childForFieldName?.('parameters');
146
+ if (!formalParams)
147
+ return params;
148
+ for (const child of formalParams.namedChildren) {
149
+ if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
150
+ const paramNode = child.childForFieldName?.('pattern');
151
+ const name = paramNode?.text ?? child.firstNamedChild?.text;
152
+ const isOptional = child.type === 'optional_parameter';
153
+ const typeNode = child.childForFieldName?.('type');
154
+ let defaultValue;
155
+ // Check for a default value
156
+ const valueNode = child.childForFieldName?.('value');
157
+ if (valueNode) {
158
+ defaultValue = valueNode.text;
159
+ }
160
+ if (name) {
161
+ params.push({
162
+ name,
163
+ type: typeNode?.text.replace(/^:\s*/, ''),
164
+ optional: isOptional,
165
+ defaultValue,
166
+ });
167
+ }
168
+ }
169
+ }
170
+ return params;
171
+ }
172
+ /** Extract ordered parameter names from a function node. */
173
+ getParamNames(fnNode) {
174
+ const raw = fnNode.raw;
175
+ const paramsNode = raw.childForFieldName?.('parameters');
176
+ if (!paramsNode)
177
+ return [];
178
+ const names = [];
179
+ for (const child of paramsNode.namedChildren) {
180
+ if (child.type === 'identifier') {
181
+ names.push(child.text);
182
+ continue;
183
+ }
184
+ const pattern = child.childForFieldName?.('pattern');
185
+ if (pattern && pattern.type === 'identifier') {
186
+ names.push(pattern.text);
187
+ continue;
188
+ }
189
+ for (const c of child.namedChildren) {
190
+ if (c.type === 'identifier') {
191
+ names.push(c.text);
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ return names;
197
+ }
198
+ /** Wrap a raw tree-sitter node into a detached ASTNode for local analysis. */
199
+ wrapRaw(raw) {
200
+ if (!raw)
201
+ return null;
202
+ return toASTNode(raw, undefined, 'typescript');
399
203
  }
204
+ /** Return the ASTNodes for a call expression's arguments. */
205
+ getCallArgASTNodes(callNode) {
206
+ const raw = callNode.raw;
207
+ const argsNode = raw.childForFieldName?.('arguments');
208
+ if (!argsNode)
209
+ return [];
210
+ return argsNode.namedChildren.map((c) => this.wrapRaw(c));
211
+ }
212
+ }
213
+ // ---------------------------------------------------------------------------
214
+ // Name / type / documentation extraction
215
+ // ---------------------------------------------------------------------------
216
+ class TsNameDocumentation extends TsTraversalHelpers {
400
217
  /** Extract the name/identifier from a tree-sitter TreeSitterNode. */
401
218
  extractName(node) {
402
219
  switch (node.type) {
403
220
  case 'function_declaration':
404
221
  case 'generator_function_declaration':
405
222
  case 'class_declaration':
223
+ case 'abstract_class_declaration':
406
224
  case 'interface_declaration':
407
225
  case 'enum_declaration':
408
- case 'type_alias_declaration': {
409
- const nameNode = node.childForFieldName?.('name');
410
- if (nameNode)
411
- return nameNode.text;
412
- // Fallback: find first identifier child
413
- for (const child of node.namedChildren) {
414
- if (child.type === 'identifier' || child.type === 'type_identifier') {
415
- return child.text;
416
- }
417
- }
418
- return null;
419
- }
226
+ case 'type_alias_declaration':
227
+ return this.resolveNameField(node, ['identifier', 'type_identifier']);
420
228
  case 'method_definition':
421
- case 'public_field_definition': {
422
- const nameNode = node.childForFieldName?.('name');
423
- if (nameNode)
424
- return nameNode.text;
425
- // property_identifier or string
426
- for (const child of node.namedChildren) {
427
- if (child.type === 'property_identifier' ||
428
- child.type === 'string') {
429
- return child.text.replace(/^["']|["']$/g, '');
430
- }
431
- }
432
- return null;
433
- }
434
- case 'variable_declarator': {
435
- const nameNode = node.childForFieldName?.('name');
436
- if (nameNode)
437
- return nameNode.text;
438
- return null;
439
- }
229
+ case 'public_field_definition':
230
+ return this.resolveNameField(node, ['property_identifier', 'string']);
231
+ case 'variable_declarator':
232
+ return this.resolveNameField(node, []);
440
233
  case 'lexical_declaration':
441
234
  case 'variable_declaration': {
442
- // Find the declarator child and extract its name
443
235
  for (const child of node.namedChildren) {
444
236
  if (child.type === 'variable_declarator') {
445
237
  return this.extractName(child);
@@ -448,24 +240,15 @@ export class TreeSitterTypeScriptAdapter {
448
240
  return null;
449
241
  }
450
242
  case 'arrow_function': {
451
- // Check if assigned to a variable: const foo = () => {}
452
243
  const parent = node.parent;
453
- if (parent?.type === 'variable_declarator') {
454
- return this.extractName(parent);
455
- }
456
- return null;
244
+ return parent?.type === 'variable_declarator' ? this.extractName(parent) : null;
457
245
  }
458
246
  case 'function_expression': {
459
- // Might have a name: const foo = function bar() {}
460
247
  const nameNode = node.childForFieldName?.('name');
461
248
  if (nameNode)
462
249
  return nameNode.text;
463
- // Check if assigned to a variable
464
250
  const parent = node.parent;
465
- if (parent?.type === 'variable_declarator') {
466
- return this.extractName(parent);
467
- }
468
- return null;
251
+ return parent?.type === 'variable_declarator' ? this.extractName(parent) : null;
469
252
  }
470
253
  case 'property_identifier':
471
254
  return node.text;
@@ -475,6 +258,21 @@ export class TreeSitterTypeScriptAdapter {
475
258
  return null;
476
259
  }
477
260
  }
261
+ /** Return `node`'s `name` field text, else the first named child whose type
262
+ * is in `fallbackTypes` (stripping quotes from `string` children). */
263
+ resolveNameField(node, fallbackTypes) {
264
+ const nameNode = node.childForFieldName?.('name');
265
+ if (nameNode)
266
+ return nameNode.text;
267
+ for (const child of node.namedChildren) {
268
+ if (fallbackTypes.includes(child.type)) {
269
+ return child.type === 'string'
270
+ ? child.text.replace(/^["']|["']$/g, '')
271
+ : child.text;
272
+ }
273
+ }
274
+ return null;
275
+ }
478
276
  /** Extract a type annotation string from a node. */
479
277
  extractTypeAnnotation(node) {
480
278
  // Function return type
@@ -538,37 +336,36 @@ export class TreeSitterTypeScriptAdapter {
538
336
  const text = comments.join('\n').trim();
539
337
  return this.cleanCommentText(text);
540
338
  }
541
- // When the immediate parent is a wrapper like `export_statement`, it won't
542
- // contain comment nodes the JSDoc lives at the grandparent (program) level
543
- // as a sibling of the wrapper. Ascend and check for an adjacent comment.
544
- // Adjacency guard prevents misattributing file-level comments: only a
545
- // comment whose end row is immediately before the wrapper's start row
546
- // (no blank line gap) is treated as JSDoc for the function inside.
547
- if (parent.type === 'export_statement' && parent.parent) {
548
- const gpSiblings = parent.parent.children;
549
- let parentIndex = -1;
550
- for (let i = 0; i < gpSiblings.length; i++) {
551
- if (gpSiblings[i].equals(parent)) {
552
- parentIndex = i;
553
- break;
554
- }
339
+ // When the parent is `export_statement`, the JSDoc lives at the grandparent
340
+ // (program) level as a sibling of the wrapper ascend and look for it.
341
+ return this.findExportStatementDoc(parent);
342
+ }
343
+ /** When `parent` is an `export_statement`, find an adjacent /** JSDoc comment
344
+ * at the grandparent level. Adjacency guard (comment ends on line L, wrapper
345
+ * starts on L+1) prevents misattributing file-level comments. */
346
+ findExportStatementDoc(parent) {
347
+ if (parent.type !== 'export_statement' || !parent.parent)
348
+ return null;
349
+ const gpSiblings = parent.parent.children;
350
+ let parentIndex = -1;
351
+ for (let i = 0; i < gpSiblings.length; i++) {
352
+ if (gpSiblings[i].equals(parent)) {
353
+ parentIndex = i;
354
+ break;
555
355
  }
556
- if (parentIndex > 0) {
557
- // Look backwards from the export_statement in the grandparent's children
558
- for (let i = parentIndex - 1; i >= 0; i--) {
559
- const sibling = gpSiblings[i];
560
- if (sibling.type === 'comment' && sibling.text.trimStart().startsWith('/**')) {
561
- // Only attribute if comment is adjacent — no blank line gap.
562
- // Adjacent: comment ends on line L, export_statement starts on L+1.
563
- if (sibling.endPosition.row + 1 === parent.startPosition.row) {
564
- return this.cleanCommentText(sibling.text);
565
- }
566
- break; // Found a comment but not adjacent — stop looking
567
- }
568
- else if (sibling.isNamed) {
569
- break; // Stop at named siblings
570
- }
356
+ }
357
+ if (parentIndex <= 0)
358
+ return null;
359
+ for (let i = parentIndex - 1; i >= 0; i--) {
360
+ const sibling = gpSiblings[i];
361
+ if (sibling.type === 'comment' && sibling.text.trimStart().startsWith('/**')) {
362
+ if (sibling.endPosition.row + 1 === parent.startPosition.row) {
363
+ return this.cleanCommentText(sibling.text);
571
364
  }
365
+ break; // Found a comment but not adjacent — stop looking
366
+ }
367
+ else if (sibling.isNamed) {
368
+ break; // Stop at named siblings
572
369
  }
573
370
  }
574
371
  return null;
@@ -623,17 +420,55 @@ export class TreeSitterTypeScriptAdapter {
623
420
  });
624
421
  return complexity;
625
422
  }
626
- /** Get the operator text from a binary expression node. */
627
- getOperator(node) {
628
- for (const child of node.children) {
629
- if (!child.isNamed &&
630
- ['&&', '||', '+', '-', '*', '/', '%', '===', '!==', '==', '!='].includes(child.type)) {
631
- return child.type;
423
+ /** Match an ASTNode against a NodePattern. */
424
+ matchesPattern(node, pattern) {
425
+ const syntaxNode = node.raw;
426
+ // type matching
427
+ if (pattern.type !== undefined) {
428
+ const types = Array.isArray(pattern.type) ? pattern.type : [pattern.type];
429
+ if (!types.includes(syntaxNode.type))
430
+ return false;
431
+ }
432
+ // name matching
433
+ if (pattern.name !== undefined) {
434
+ const nodeName = this.extractName(syntaxNode);
435
+ if (nodeName === null)
436
+ return false;
437
+ if (typeof pattern.name === 'string') {
438
+ if (nodeName !== pattern.name)
439
+ return false;
440
+ }
441
+ else if (pattern.name instanceof RegExp) {
442
+ if (!pattern.name.test(nodeName))
443
+ return false;
632
444
  }
633
445
  }
634
- return null;
446
+ // hasChild matching
447
+ if (pattern.hasChild !== undefined) {
448
+ const childNodes = node.children ?? [];
449
+ const hasMatch = childNodes.some((c) => this.matchesPattern(c, pattern.hasChild));
450
+ if (!hasMatch)
451
+ return false;
452
+ }
453
+ // hasParent matching
454
+ if (pattern.hasParent !== undefined) {
455
+ if (!node.parent)
456
+ return false;
457
+ if (!this.matchesPattern(node.parent, pattern.hasParent))
458
+ return false;
459
+ }
460
+ // custom predicate
461
+ if (pattern.custom !== undefined) {
462
+ if (!pattern.custom(node))
463
+ return false;
464
+ }
465
+ return true;
635
466
  }
636
- // -- Extraction helpers ----------------------------------------------------
467
+ }
468
+ // ---------------------------------------------------------------------------
469
+ // Extraction builders (functions, classes, imports, exports, interfaces)
470
+ // ---------------------------------------------------------------------------
471
+ class TsExtraction extends TsNameDocumentation {
637
472
  buildFunctionInfo(node, sourceCode) {
638
473
  const name = this.extractName(node);
639
474
  if (name === null && node.type !== 'arrow_function')
@@ -679,28 +514,10 @@ export class TreeSitterTypeScriptAdapter {
679
514
  if (implementsClause) {
680
515
  implementsList = implementsClause.namedChildren.map((c) => c.text);
681
516
  }
682
- // Extract methods
683
517
  const classBody = node.childForFieldName?.('body');
684
- const methods = [];
685
- const properties = [];
686
- if (classBody) {
687
- for (const member of classBody.namedChildren) {
688
- if (member.type === 'method_definition') {
689
- const fnInfo = this.buildFunctionInfo(member, sourceCode);
690
- if (fnInfo) {
691
- fnInfo.className = name;
692
- fnInfo.isMethod = true;
693
- methods.push(fnInfo);
694
- }
695
- }
696
- else if (member.type === 'public_field_definition' ||
697
- member.type === 'field_definition') {
698
- const propInfo = this.buildPropertyInfo(member);
699
- if (propInfo)
700
- properties.push(propInfo);
701
- }
702
- }
703
- }
518
+ const { methods, properties } = classBody
519
+ ? this.buildClassMembers(classBody, sourceCode, name)
520
+ : { methods: [], properties: [] };
704
521
  return {
705
522
  name,
706
523
  location: toSourceLocation(node),
@@ -713,6 +530,28 @@ export class TreeSitterTypeScriptAdapter {
713
530
  jsDoc: jsDoc ?? undefined,
714
531
  };
715
532
  }
533
+ /** Build method/property lists from a class body's members. */
534
+ buildClassMembers(classBody, sourceCode, className) {
535
+ const methods = [];
536
+ const properties = [];
537
+ for (const member of classBody.namedChildren) {
538
+ if (member.type === 'method_definition') {
539
+ const fnInfo = this.buildFunctionInfo(member, sourceCode);
540
+ if (fnInfo) {
541
+ fnInfo.className = className;
542
+ fnInfo.isMethod = true;
543
+ methods.push(fnInfo);
544
+ }
545
+ }
546
+ else if (member.type === 'public_field_definition' ||
547
+ member.type === 'field_definition') {
548
+ const propInfo = this.buildPropertyInfo(member);
549
+ if (propInfo)
550
+ properties.push(propInfo);
551
+ }
552
+ }
553
+ return { methods, properties };
554
+ }
716
555
  buildPropertyInfo(node) {
717
556
  const name = this.extractName(node);
718
557
  if (!name)
@@ -742,90 +581,77 @@ export class TreeSitterTypeScriptAdapter {
742
581
  return null;
743
582
  const source = sourceNode.text.slice(1, -1); // strip quotes
744
583
  const specifiers = [];
745
- // import defaultExport from 'module'
746
- // import * as namespace from 'module'
747
- // import { named } from 'module'
748
- //
749
- // Tree-sitter nests named imports inside import_clause -> named_imports,
750
- // so we walk into import_clause children recursively.
751
- const collectSpecifiers = (child) => {
752
- if (child.type === 'import_specifier') {
753
- const nameNode = child.childForFieldName?.('name');
754
- const aliasNode = child.childForFieldName?.('alias');
755
- if (nameNode) {
756
- specifiers.push({
757
- name: nameNode.text,
758
- alias: aliasNode?.text,
759
- isDefault: false,
760
- isNamespace: false,
761
- });
762
- }
584
+ for (const child of node.namedChildren) {
585
+ this.collectImportSpecifiers(child, specifiers);
586
+ }
587
+ return {
588
+ source,
589
+ specifiers,
590
+ location: toSourceLocation(node),
591
+ };
592
+ }
593
+ /** Recursively collect import specifiers (default / named / namespace).
594
+ * Tree-sitter nests named imports inside import_clause -> named_imports. */
595
+ collectImportSpecifiers(child, specifiers) {
596
+ if (child.type === 'import_specifier') {
597
+ const nameNode = child.childForFieldName?.('name');
598
+ const aliasNode = child.childForFieldName?.('alias');
599
+ if (nameNode) {
600
+ specifiers.push({
601
+ name: nameNode.text,
602
+ alias: aliasNode?.text,
603
+ isDefault: false,
604
+ isNamespace: false,
605
+ });
763
606
  }
764
- else if (child.type === 'namespace_import') {
765
- const nameNode = child.childForFieldName?.('name');
766
- if (nameNode) {
767
- specifiers.push({
768
- name: nameNode.text,
769
- isDefault: false,
770
- isNamespace: true,
771
- });
772
- }
607
+ }
608
+ else if (child.type === 'namespace_import') {
609
+ const nameNode = child.childForFieldName?.('name');
610
+ if (nameNode) {
611
+ specifiers.push({
612
+ name: nameNode.text,
613
+ isDefault: false,
614
+ isNamespace: true,
615
+ });
773
616
  }
774
- else if (child.type === 'import') {
775
- // default import: `import foo from '...'`
776
- const nameNode = node.childForFieldName?.('name');
777
- if (nameNode) {
617
+ }
618
+ else if (child.type === 'import_clause') {
619
+ // Default import: `import foo from '...'` — import_clause children include
620
+ // an identifier for the default binding and optionally named_imports for
621
+ // mixed imports (`import foo, { bar }`).
622
+ for (const grandchild of child.namedChildren) {
623
+ if (grandchild.type === 'identifier') {
778
624
  specifiers.push({
779
- name: nameNode.text,
625
+ name: grandchild.text,
780
626
  isDefault: true,
781
627
  isNamespace: false,
782
628
  });
783
629
  }
784
- }
785
- else {
786
- // Recurse into containers: import_clause, named_imports
787
- for (const grandchild of child.namedChildren) {
788
- collectSpecifiers(grandchild);
630
+ else {
631
+ this.collectImportSpecifiers(grandchild, specifiers);
789
632
  }
790
633
  }
791
- };
792
- for (const child of node.namedChildren) {
793
- collectSpecifiers(child);
794
634
  }
795
- return {
796
- source,
797
- specifiers,
798
- location: toSourceLocation(node),
799
- };
635
+ else {
636
+ // Recurse into containers: import_clause, named_imports
637
+ for (const grandchild of child.namedChildren) {
638
+ this.collectImportSpecifiers(grandchild, specifiers);
639
+ }
640
+ }
800
641
  }
801
642
  buildExportInfo(node, _sourceCode) {
802
643
  // export default <expression>
803
644
  const isDefault = this.hasChild(node, 'default');
804
- // export { foo, bar } [from '...']
805
- const clause = node.childForFieldName?.('clause');
806
- if (clause?.type === 'export_clause') {
807
- const sourceNode = node.childForFieldName?.('source');
808
- const exports = [];
809
- for (const spec of clause.namedChildren) {
810
- if (spec.type === 'export_specifier') {
811
- const nameNode = spec.childForFieldName?.('name');
812
- if (nameNode) {
813
- exports.push({
814
- name: nameNode.text,
815
- location: toSourceLocation(nameNode),
816
- isDefault: false,
817
- source: sourceNode?.text.slice(1, -1),
818
- });
819
- }
820
- }
821
- }
822
- // Return first (handled by caller iterating export_statement nodes)
823
- return exports[0] ?? null;
824
- }
645
+ // export { foo, bar } [from '...'] — return the first named export
646
+ // (caller iterates export_statement nodes).
647
+ const named = this.buildNamedExport(node);
648
+ if (named)
649
+ return named;
825
650
  // export function/class/const/let/var name
826
651
  const declaration = this.findFirstNamedChild(node, [
827
652
  'function_declaration',
828
653
  'class_declaration',
654
+ 'abstract_class_declaration',
829
655
  'lexical_declaration',
830
656
  'variable_declaration',
831
657
  ]);
@@ -853,6 +679,27 @@ export class TreeSitterTypeScriptAdapter {
853
679
  }
854
680
  return null;
855
681
  }
682
+ /** Build the ExportInfo for an `export { a, b } [from '...']` statement. */
683
+ buildNamedExport(node) {
684
+ const clause = node.childForFieldName?.('clause');
685
+ if (clause?.type !== 'export_clause')
686
+ return null;
687
+ const sourceNode = node.childForFieldName?.('source');
688
+ for (const spec of clause.namedChildren) {
689
+ if (spec.type === 'export_specifier') {
690
+ const nameNode = spec.childForFieldName?.('name');
691
+ if (nameNode) {
692
+ return {
693
+ name: nameNode.text,
694
+ location: toSourceLocation(nameNode),
695
+ isDefault: false,
696
+ source: sourceNode?.text.slice(1, -1),
697
+ };
698
+ }
699
+ }
700
+ }
701
+ return null;
702
+ }
856
703
  buildInterfaceInfo(node, _sourceCode) {
857
704
  const name = this.extractName(node);
858
705
  if (!name)
@@ -866,31 +713,9 @@ export class TreeSitterTypeScriptAdapter {
866
713
  }
867
714
  // Members
868
715
  const body = node.childForFieldName?.('body');
869
- const members = [];
870
- if (body) {
871
- for (const member of body.namedChildren) {
872
- if (member.type === 'method_signature') {
873
- const memberName = member.childForFieldName?.('name')?.text;
874
- if (memberName) {
875
- members.push({
876
- name: memberName,
877
- type: 'method',
878
- location: toSourceLocation(member),
879
- });
880
- }
881
- }
882
- else if (member.type === 'property_signature') {
883
- const memberName = member.childForFieldName?.('name')?.text;
884
- if (memberName) {
885
- members.push({
886
- name: memberName,
887
- type: 'property',
888
- location: toSourceLocation(member),
889
- });
890
- }
891
- }
892
- }
893
- }
716
+ const members = body
717
+ ? this.buildInterfaceMembers(body)
718
+ : [];
894
719
  return {
895
720
  name,
896
721
  location: toSourceLocation(node),
@@ -899,414 +724,350 @@ export class TreeSitterTypeScriptAdapter {
899
724
  isExported,
900
725
  };
901
726
  }
902
- // -- Parameter extraction --------------------------------------------------
903
- extractParameters(node, sourceCode) {
904
- const params = [];
905
- const formalParams = node.childForFieldName?.('parameters');
906
- if (!formalParams)
907
- return params;
908
- for (const child of formalParams.namedChildren) {
909
- if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
910
- const paramNode = child.childForFieldName?.('pattern');
911
- const name = paramNode?.text ?? child.firstNamedChild?.text;
912
- const isOptional = child.type === 'optional_parameter';
913
- const typeNode = child.childForFieldName?.('type');
914
- let defaultValue;
915
- // Check for a default value
916
- const valueNode = child.childForFieldName?.('value');
917
- if (valueNode) {
918
- defaultValue = valueNode.text;
919
- }
920
- if (name) {
921
- params.push({
922
- name,
923
- type: typeNode?.text.replace(/^:\s*/, ''),
924
- optional: isOptional,
925
- defaultValue,
727
+ /** Build method/property member descriptors from an interface body. */
728
+ buildInterfaceMembers(body) {
729
+ const members = [];
730
+ for (const member of body.namedChildren) {
731
+ if (member.type === 'method_signature' || member.type === 'property_signature') {
732
+ const memberName = member.childForFieldName?.('name')?.text;
733
+ if (memberName) {
734
+ members.push({
735
+ name: memberName,
736
+ type: member.type === 'method_signature' ? 'method' : 'property',
737
+ location: toSourceLocation(member),
926
738
  });
927
739
  }
928
740
  }
929
741
  }
930
- return params;
931
- }
932
- // -- Context helpers -------------------------------------------------------
933
- /** Check if a node has an `async` modifier keyword. */
934
- hasModifier(node, modifier) {
935
- for (const child of node.children) {
936
- if (!child.isNamed && child.type === modifier)
937
- return true;
938
- // tree-sitter may represent modifiers as named decorator/modifier nodes
939
- if (child.isNamed &&
940
- (child.type === modifier ||
941
- child.type === 'accessibility_modifier' ||
942
- child.type === 'override_modifier' ||
943
- child.type === 'readonly' ||
944
- child.type === 'static' ||
945
- child.type === 'abstract')) {
946
- if (child.text === modifier)
947
- return true;
948
- }
949
- }
950
- return false;
951
- }
952
- /** Check if a node is under an export_statement. */
953
- isNodeExported(node) {
954
- const parent = node.parent;
955
- if (parent?.type === 'export_statement')
956
- return true;
957
- // Also check for `export` keyword on the node itself (TS module syntax)
958
- for (const child of node.children) {
959
- if (!child.isNamed && child.type === 'export')
960
- return true;
961
- }
962
- return false;
742
+ return members;
963
743
  }
964
744
  /** Get the enclosing class name for a method. */
965
745
  getEnclosingClassName(node) {
966
746
  let current = node.parent;
967
747
  while (current) {
968
- if (current.type === 'class_declaration') {
748
+ if (current.type === 'class_declaration' || current.type === 'abstract_class_declaration') {
969
749
  return this.extractName(current) ?? undefined;
970
750
  }
971
751
  current = current.parent ?? null;
972
752
  }
973
753
  return undefined;
974
754
  }
975
- // -- Tree traversal helpers ------------------------------------------------
976
- /** Find first child of node matching one of the given types. */
977
- getChildByType(node, type) {
978
- for (const child of node.children) {
979
- if (child.type === type)
980
- return child;
981
- }
982
- return null;
983
- }
984
- /** Find the first named child matching one of the given types. */
985
- findFirstNamedChild(node, types) {
986
- const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
987
- for (const child of node.namedChildren) {
988
- if (typeSet.has(child.type))
989
- return child;
990
- }
991
- return null;
992
- }
993
- /** Check if node has a direct child (named or anonymous) with the given text. */
994
- hasChild(node, text) {
995
- for (const child of node.children) {
996
- if (child.text === text || child.type === text)
997
- return true;
998
- }
999
- return false;
1000
- }
1001
- // -- String Construction Capabilities (v3.4.7) ----------------------------
1002
- /**
1003
- * Returns true when the node is a dynamically-constructed string in TS/JS:
1004
- * - template_string with template_substitution children
1005
- * - binary_expression with + operator (string concatenation)
1006
- * - call_expression with .concat() method
1007
- * String-like identifiers and plain string literals are never dynamic.
1008
- */
1009
- isDynamicStringConstruction(node) {
1010
- const type = node.raw.type;
1011
- if (type === 'template_string') {
1012
- // template_string is dynamic only if it has template_substitution children
1013
- for (const child of node.children ?? []) {
1014
- if (child.raw.type === 'template_substitution') {
1015
- return true;
1016
- }
1017
- }
1018
- return false;
1019
- }
1020
- if (type === 'binary_expression') {
1021
- // Check for string concatenation: operands include a string literal
1022
- const children = node.children ?? [];
1023
- const hasStringLiteral = children.some(c => c.raw.type === 'string');
1024
- return hasStringLiteral;
1025
- }
1026
- if (type === 'call_expression') {
1027
- // Check for .concat() calls
1028
- const text = node.raw.text;
1029
- if (text.includes('.concat(') || text.includes('?.concat('))
1030
- return true;
1031
- // Recurse into arguments: query(binaryExpression) where the argument
1032
- // itself is a dynamic string construction.
1033
- for (const child of node.children ?? []) {
1034
- if (child.raw.type === 'arguments') {
1035
- for (const arg of child.children ?? []) {
1036
- const argType = arg.raw.type;
1037
- if (argType === '(' || argType === ')' || argType === ',')
1038
- continue;
1039
- if (this.isDynamicStringConstruction(arg))
1040
- return true;
1041
- }
1042
- }
1043
- }
1044
- return false;
1045
- }
1046
- return false;
1047
- }
1048
- /**
1049
- * Extracts dynamic sub-parts from a string construction node.
1050
- * Returns template_substitution text for template literals,
1051
- * non-string-literal operands for binary expressions,
1052
- * and non-literal arguments for concat calls.
1053
- */
1054
- getDynamicParts(node, sourceCode) {
1055
- const type = node.raw.type;
1056
- const parts = [];
1057
- // For call_expressions (other than .concat), walk into the first
1058
- // argument that is itself a dynamic string construction.
1059
- if (type === 'call_expression') {
1060
- const text = node.raw.text;
1061
- // .concat() handled below
1062
- if (!text.includes('.concat(') && !text.includes('?.concat(')) {
1063
- for (const child of node.children ?? []) {
1064
- if (child.raw.type === 'arguments') {
1065
- for (const arg of child.children ?? []) {
1066
- const argType = arg.raw.type;
1067
- if (argType === '(' || argType === ')' || argType === ',')
1068
- continue;
1069
- if (this.isDynamicStringConstruction(arg)) {
1070
- return this.getDynamicParts(arg, sourceCode);
1071
- }
1072
- }
1073
- }
1074
- }
755
+ /** Collect static imports, dynamic `import()`, and `require()` calls. */
756
+ collectRawImport(node, results) {
757
+ if (node.type === 'import_statement') {
758
+ const source = this.getChildByType(node, 'string');
759
+ if (source) {
760
+ results.push({
761
+ moduleSpecifier: source.text.slice(1, -1), // strip quotes
762
+ isStatic: true,
763
+ isDynamic: false,
764
+ isRequire: false,
765
+ line: source.startPosition.row,
766
+ });
1075
767
  }
1076
768
  }
1077
- if (type === 'template_string') {
1078
- for (const child of node.children ?? []) {
1079
- if (child.raw.type === 'template_substitution') {
1080
- const text = sourceCode.slice(child.range[0], child.range[1]);
1081
- // Strip the ${ } wrapper to get the inner identifier/expression
1082
- // tree-sitter: template_substitution text includes ${ and }
1083
- const inner = text.startsWith('${') ? text.slice(2, -1).trim() : text;
1084
- const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(inner);
1085
- // Extract the actual identifier node from inside the template_substitution
1086
- // (tree-sitter nests it as: ${ <identifier> }), so resolveLocalConstant
1087
- // can use its range to extract the raw name.
1088
- let idNode;
1089
- if (isId) {
1090
- for (const subChild of child.children ?? []) {
1091
- const subType = subChild.raw.type;
1092
- if (subType === 'identifier') {
1093
- idNode = subChild;
1094
- break;
1095
- }
1096
- }
1097
- }
1098
- parts.push({ text: inner, isIdentifier: isId, node: idNode ?? (isId ? child : undefined) });
1099
- }
1100
- }
1101
- return parts;
1102
- }
1103
- if (type === 'binary_expression') {
1104
- for (const child of node.children ?? []) {
1105
- const childType = child.raw.type;
1106
- if (childType !== 'string' && childType !== '+' && childType !== 'template_string') {
1107
- const text = sourceCode.slice(child.range[0], child.range[1]);
1108
- const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text.trim());
1109
- parts.push({ text: text.trim(), isIdentifier: isId, node: isId ? child : undefined });
1110
- }
769
+ if (node.type === 'call_expression') {
770
+ const fn = node.firstChild;
771
+ if (fn?.type === 'import') {
772
+ this.pushCallImport(node, results, /* isRequire */ false);
1111
773
  }
1112
- return parts;
1113
- }
1114
- if (type === 'call_expression') {
1115
- // .concat() — arguments after the first are dynamic
1116
- for (const child of node.children ?? []) {
1117
- if (child.raw.type === 'arguments') {
1118
- for (const arg of child.children ?? []) {
1119
- const argType = arg.raw.type;
1120
- if (argType !== '(' && argType !== ')' && argType !== ',') {
1121
- const text = arg.raw.text.trim();
1122
- const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text);
1123
- parts.push({ text, isIdentifier: isId, node: isId ? arg : undefined });
1124
- }
1125
- }
1126
- }
774
+ else if (fn?.type === 'identifier' && fn.text === 'require') {
775
+ this.pushCallImport(node, results, /* isRequire */ true);
1127
776
  }
1128
- return parts;
1129
777
  }
1130
- return parts;
1131
778
  }
1132
- /**
1133
- * Resolves a local const/let/var declaration for an identifier node.
1134
- * Searches within the enclosing function scope for the declaration site.
1135
- * Returns null when the identifier is a parameter, complex expression,
1136
- * or cannot be statically resolved.
1137
- */
1138
- resolveLocalConstant(identifierNode, ast, sourceCode) {
1139
- const idName = sourceCode.slice(identifierNode.range[0], identifierNode.range[1]).trim();
1140
- if (!idName)
1141
- return null;
1142
- // Find the enclosing function or file scope. First search within
1143
- // the enclosing function; if not found, fall back to the program-level
1144
- // (module) scope — constants declared at module level are accessible
1145
- // inside any function in that module.
1146
- const enclosing = this.findEnclosingScope(identifierNode, ast);
1147
- const scopeRoot = enclosing ?? ast.root;
1148
- let declNode = this.findDeclarationInScope(scopeRoot, idName);
1149
- // If the enclosing scope is a function (not the program) and we didn't
1150
- // find the declaration there, also search the program-level scope.
1151
- if (!declNode && enclosing && enclosing !== ast.root) {
1152
- declNode = this.findDeclarationInScope(ast.root, idName);
1153
- }
1154
- // If still not found in local declarations, check if the identifier
1155
- // is imported (import { X } from … or import X from …). Imported
1156
- // symbols are compile-time constants — they're resolved at link time,
1157
- // not at runtime, so user input cannot reach them via import bindings.
1158
- if (!declNode) {
1159
- const importResult = this.resolveImportConstant(idName, ast);
1160
- if (importResult)
1161
- return importResult;
1162
- // tree-sitter-typescript (v0.x) parses both "for…in" and "for…of"
1163
- // as `for_in_statement`. The loop variable is a bare `identifier`
1164
- // child — NOT wrapped in `variable_declarator` / `lexical_declaration`.
1165
- // Walk up from the identifier to a `for_in_statement` and trace
1166
- // through the iterable to determine static-ness.
1167
- // const TABLES = ["a", "b"];
1168
- // for (const table of TABLES) { … `${table}` … }
1169
- const idRaw = identifierNode.raw;
1170
- let tsCurrent = idRaw.parent;
1171
- while (tsCurrent) {
1172
- if (tsCurrent.type === 'for_in_statement') {
1173
- const left = tsCurrent.childForFieldName?.('left');
1174
- if (left && left.text === idName) {
1175
- const right = tsCurrent.childForFieldName?.('right');
1176
- if (right && right.type === 'identifier') {
1177
- const iterName = right.text;
1178
- let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
1179
- if (!iterDecl && enclosing && enclosing !== ast.root) {
1180
- iterDecl = this.findDeclarationInScope(ast.root, iterName);
1181
- }
1182
- if (iterDecl) {
1183
- const iterRaw = iterDecl.raw;
1184
- const iterValue = iterRaw.childForFieldName?.('value');
1185
- const iterLine = iterDecl.location.start.line;
1186
- const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
1187
- || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
1188
- if (!iterReassigned && this.isStaticValueNode(iterValue)) {
1189
- const initText = iterValue ? sourceCode.slice(iterValue.startIndex, iterValue.endIndex).trim() : '';
1190
- return { initText, isStatic: true, declLine: iterDecl.location.start.line };
1191
- }
1192
- }
1193
- }
1194
- }
1195
- break;
1196
- }
1197
- tsCurrent = tsCurrent.parent;
1198
- }
1199
- return null;
1200
- }
1201
- // Extract value from AST (handles multiline declarations that the old
1202
- // regex missed — `.` doesn't match `\n` so `.+?` truncated at newlines).
1203
- const raw = declNode.raw;
1204
- const valueNode = raw.childForFieldName?.('value');
1205
- const declLine = declNode.location.start.line;
1206
- // Check for reassignment after declaration
1207
- const reassigned = this.hasReassignment(scopeRoot, idName, declLine);
1208
- // Determine if static by inspecting the value node's AST type. String
1209
- // literals and substitution-free template strings are compile-time constants
1210
- // regardless of whether they contain `?` placeholders — SQL fragment
1211
- // constants (WHERE clauses, column lists) are just as static as
1212
- // parameterised query strings.
1213
- let isStatic = !reassigned && this.isStaticValueNode(valueNode);
1214
- // For-of / for-in loop variables: the declarator has no `value` field
1215
- // (the iterable is on the for-statement's `right` child). Trace the
1216
- // iterable to see if all possible loop values are known constants.
1217
- // const TABLES = ["a", "b"];
1218
- // for (const table of TABLES) { … `${table}` … }
1219
- if (!isStatic && !valueNode) {
1220
- const forParent = this.findEnclosingForStatement(declNode);
1221
- if (forParent) {
1222
- const forRaw = forParent.raw;
1223
- const iterable = forRaw.childForFieldName?.('right');
1224
- if (iterable && iterable.type === 'identifier') {
1225
- const iterName = iterable.text;
1226
- let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
1227
- if (!iterDecl && enclosing && enclosing !== ast.root) {
1228
- iterDecl = this.findDeclarationInScope(ast.root, iterName);
1229
- }
1230
- if (iterDecl) {
1231
- const iterRaw = iterDecl.raw;
1232
- const iterValue = iterRaw.childForFieldName?.('value');
1233
- const iterLine = iterDecl.location.start.line;
1234
- const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
1235
- || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
1236
- if (!iterReassigned && this.isStaticValueNode(iterValue)) {
1237
- isStatic = true;
1238
- }
1239
- }
1240
- }
779
+ /** Push a `import('...')` or `require('...')` record for a call expression. */
780
+ pushCallImport(node, results, isRequire) {
781
+ const args = this.getChildByType(node, 'arguments');
782
+ const strNode = args ? this.findFirstNamedChild(args, 'string') : null;
783
+ if (!strNode)
784
+ return;
785
+ results.push({
786
+ moduleSpecifier: strNode.text.slice(1, -1),
787
+ isStatic: false,
788
+ isDynamic: !isRequire,
789
+ isRequire,
790
+ line: strNode.startPosition.row,
791
+ });
792
+ }
793
+ /** Collect the exported symbol(s) declared by a single `export_statement`. */
794
+ collectExportedSymbol(node, symbols) {
795
+ // export function foo / export class Foo / export const x
796
+ const declaration = this.findFirstNamedChild(node, [
797
+ 'function_declaration',
798
+ 'class_declaration',
799
+ 'abstract_class_declaration',
800
+ 'lexical_declaration',
801
+ 'variable_declaration',
802
+ 'interface_declaration',
803
+ 'type_alias_declaration',
804
+ 'enum_declaration',
805
+ ]);
806
+ if (declaration) {
807
+ const name = this.extractName(declaration);
808
+ if (name) {
809
+ symbols.push({ name, line: declaration.startPosition.row });
810
+ return;
1241
811
  }
1242
812
  }
1243
- // When the value is a simple identifier (e.g. `table` ← `tables`),
1244
- // trace through to the linked declaration. This handles patterns like:
1245
- // const TABLES = ["a", "b"];
1246
- // for (const table of TABLES) { … `${table}` … }
1247
- // where the variable's value is known at compile time because the
1248
- // iterable is a constant array.
1249
- if (!isStatic && valueNode && valueNode.type === 'identifier') {
1250
- const linkedName = valueNode.text;
1251
- if (linkedName && linkedName !== idName) {
1252
- let linkedDecl = this.findDeclarationInScope(scopeRoot, linkedName);
1253
- if (!linkedDecl && enclosing && enclosing !== ast.root) {
1254
- linkedDecl = this.findDeclarationInScope(ast.root, linkedName);
1255
- }
1256
- if (linkedDecl) {
1257
- const linkedRaw = linkedDecl.raw;
1258
- const linkedValue = linkedRaw.childForFieldName?.('value');
1259
- const linkedLine = linkedDecl.location.start.line;
1260
- const linkedReassigned = this.hasReassignment(scopeRoot, linkedName, linkedLine)
1261
- || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, linkedName, linkedLine) : false);
1262
- if (!linkedReassigned && this.isStaticValueNode(linkedValue)) {
1263
- isStatic = true;
813
+ // export { foo, bar } or export { default }
814
+ const clause = this.getChildByType(node, 'export_clause');
815
+ if (clause) {
816
+ for (const child of clause.namedChildren) {
817
+ if (child.type === 'export_specifier') {
818
+ const nameNode = this.getChildByType(child, 'identifier');
819
+ if (nameNode) {
820
+ symbols.push({ name: nameNode.text, line: nameNode.startPosition.row });
1264
821
  }
1265
822
  }
1266
823
  }
1267
824
  }
1268
- // Extract init text from the value node; fall back to the full declaration.
1269
- let initText;
1270
- if (valueNode) {
1271
- initText = sourceCode.slice(valueNode.startIndex, valueNode.endIndex).trim();
1272
- }
1273
- else {
1274
- // No initializer node — try regex as a last resort.
1275
- const declText = sourceCode.slice(declNode.range[0], declNode.range[1]);
1276
- const declMatch = declText.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$|^(\w+)\s*=\s*(.+?);?\s*$/s);
1277
- initText = declMatch
1278
- ? (declMatch[2] ?? declMatch[4]).replace(/;\s*$/, '').trim()
1279
- : '';
825
+ // export default <expression>
826
+ if (node.childForFieldName?.('value')) {
827
+ symbols.push({ name: 'default', line: node.startPosition.row });
1280
828
  }
1281
- return { initText, isStatic, declLine };
1282
829
  }
1283
- /** Determine whether a tree-sitter value node represents a static (compile-time
1284
- * constant) expression — no variables, function calls, or runtime evaluation. */
1285
- isStaticValueNode(node) {
1286
- if (!node)
1287
- return false;
1288
- const type = node.type;
1289
- // Literals that are trivially compile-time constants.
1290
- if (type === 'string')
1291
- return true;
1292
- if (type === 'number')
1293
- return true;
1294
- if (type === 'true' || type === 'false' || type === 'null' || type === 'undefined')
1295
- return true;
1296
- // Template strings: static only when they contain no ${…} substitutions.
1297
- if (type === 'template_string') {
1298
- for (let i = 0; i < node.childCount; i++) {
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // Public LanguageAdapter surface: parsing, navigation, extraction
833
+ // ---------------------------------------------------------------------------
834
+ class TsPublicApi extends TsExtraction {
835
+ name = 'typescript';
836
+ fileExtensions = ['.ts', '.tsx', '.js', '.jsx'];
837
+ supportsFile(filePath) {
838
+ return this.fileExtensions.some((ext) => filePath.endsWith(ext));
839
+ }
840
+ async parse(filePath, content) {
841
+ const isTsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
842
+ const lang = isTsx ? 'tsx' : filePath.endsWith('.go') ? 'go' : 'typescript';
843
+ const tree = await parseWithRecovery(lang, isTsx, content);
844
+ if (!tree)
845
+ throw new Error(`Failed to parse file: ${filePath}`);
846
+ const errors = [];
847
+ // Collect ERROR nodes as parse errors
848
+ this.collectErrors(tree.rootNode, errors);
849
+ const root = toASTNode(tree.rootNode, undefined, lang);
850
+ const ast = {
851
+ root,
852
+ language: lang,
853
+ filePath,
854
+ errors,
855
+ dispose: () => tree.delete(),
856
+ };
857
+ sourceCodeMap.set(ast, content);
858
+ return ast;
859
+ }
860
+ findNodes(ast, pattern) {
861
+ const results = [];
862
+ this.walk(ast.root, (node) => {
863
+ if (this.matchesPattern(node, pattern)) {
864
+ results.push(node);
865
+ }
866
+ });
867
+ return results;
868
+ }
869
+ getParent(node) {
870
+ return node.parent ?? null;
871
+ }
872
+ getChildren(node) {
873
+ return node.children ?? [];
874
+ }
875
+ getSiblings(node) {
876
+ if (!node.parent?.children)
877
+ return [];
878
+ return node.parent.children.filter((c) => c !== node);
879
+ }
880
+ getNodeType(node) {
881
+ return node.type;
882
+ }
883
+ getNodeText(node, sourceCode) {
884
+ return sourceCode.slice(node.range[0], node.range[1]);
885
+ }
886
+ getNodeName(node) {
887
+ const syntaxNode = node.raw;
888
+ return this.extractName(syntaxNode);
889
+ }
890
+ getNodeLocation(node) {
891
+ return node.location;
892
+ }
893
+ extractFunctions(ast) {
894
+ const sourceCode = sourceCodeMap.get(ast) ?? '';
895
+ const functions = [];
896
+ this.walk(ast.root, (node) => {
897
+ const syntaxNode = node.raw;
898
+ const type = syntaxNode.type;
899
+ if (type === 'function_declaration' ||
900
+ type === 'generator_function_declaration' ||
901
+ type === 'function_expression' ||
902
+ type === 'arrow_function' ||
903
+ type === 'method_definition') {
904
+ const fn = this.buildFunctionInfo(syntaxNode, sourceCode);
905
+ if (fn)
906
+ functions.push(fn);
907
+ }
908
+ });
909
+ return functions;
910
+ }
911
+ extractClasses(ast) {
912
+ const sourceCode = sourceCodeMap.get(ast) ?? '';
913
+ const classes = [];
914
+ this.walk(ast.root, (node) => {
915
+ const syntaxNode = node.raw;
916
+ if (syntaxNode.type === 'class_declaration' || syntaxNode.type === 'abstract_class_declaration') {
917
+ const cls = this.buildClassInfo(syntaxNode, sourceCode);
918
+ if (cls)
919
+ classes.push(cls);
920
+ }
921
+ });
922
+ return classes;
923
+ }
924
+ extractImports(ast) {
925
+ const sourceCode = sourceCodeMap.get(ast) ?? '';
926
+ const imports = [];
927
+ this.walk(ast.root, (node) => {
928
+ const syntaxNode = node.raw;
929
+ if (syntaxNode.type === 'import_statement') {
930
+ const imp = this.buildImportInfo(syntaxNode, sourceCode);
931
+ if (imp)
932
+ imports.push(imp);
933
+ }
934
+ });
935
+ return imports;
936
+ }
937
+ extractExports(ast) {
938
+ const sourceCode = sourceCodeMap.get(ast) ?? '';
939
+ const exports = [];
940
+ this.walk(ast.root, (node) => {
941
+ const syntaxNode = node.raw;
942
+ if (syntaxNode.type === 'export_statement') {
943
+ const ex = this.buildExportInfo(syntaxNode, sourceCode);
944
+ if (ex)
945
+ exports.push(ex);
946
+ }
947
+ });
948
+ return exports;
949
+ }
950
+ }
951
+ // ---------------------------------------------------------------------------
952
+ // Node predicates + advanced + optional capabilities
953
+ // ---------------------------------------------------------------------------
954
+ class TsPredicatesOptional extends TsPublicApi {
955
+ isClass(node) {
956
+ const type = node.raw.type;
957
+ return type === 'class_declaration' || type === 'abstract_class_declaration' || type === 'class_expression';
958
+ }
959
+ isFunction(node) {
960
+ const type = node.raw.type;
961
+ return (type === 'function_declaration' ||
962
+ type === 'function_expression' ||
963
+ type === 'arrow_function' ||
964
+ type === 'generator_function_declaration' ||
965
+ type === 'generator_function_expression');
966
+ }
967
+ isMethod(node) {
968
+ return node.raw.type === 'method_definition';
969
+ }
970
+ isInterface(node) {
971
+ return node.raw.type === 'interface_declaration';
972
+ }
973
+ isImport(node) {
974
+ return node.raw.type === 'import_statement';
975
+ }
976
+ isExport(node) {
977
+ return node.raw.type === 'export_statement';
978
+ }
979
+ isLoop(node) {
980
+ const type = node.raw.type;
981
+ return (type === 'for_statement' ||
982
+ type === 'for_in_statement' ||
983
+ type === 'while_statement' ||
984
+ type === 'do_statement');
985
+ }
986
+ isConditional(node) {
987
+ const type = node.raw.type;
988
+ return (type === 'if_statement' ||
989
+ type === 'switch_statement' ||
990
+ type === 'ternary_expression' ||
991
+ type === 'switch_case');
992
+ }
993
+ isVariableDeclaration(node) {
994
+ const type = node.raw.type;
995
+ return (type === 'variable_declaration' ||
996
+ type === 'lexical_declaration' ||
997
+ type === 'variable_declarator');
998
+ }
999
+ getTypeInfo(node) {
1000
+ const syntaxNode = node.raw;
1001
+ return this.extractTypeAnnotation(syntaxNode);
1002
+ }
1003
+ getDocumentation(node) {
1004
+ const syntaxNode = node.raw;
1005
+ return this.extractDocumentation(syntaxNode);
1006
+ }
1007
+ getComplexity(node) {
1008
+ const syntaxNode = node.raw;
1009
+ return this.calculateCyclomaticComplexity(syntaxNode);
1010
+ }
1011
+ extractInterfaces(ast) {
1012
+ const sourceCode = sourceCodeMap.get(ast) ?? '';
1013
+ const interfaces = [];
1014
+ this.walk(ast.root, (node) => {
1015
+ const syntaxNode = node.raw;
1016
+ if (syntaxNode.type === 'interface_declaration') {
1017
+ const iface = this.buildInterfaceInfo(syntaxNode, sourceCode);
1018
+ if (iface)
1019
+ interfaces.push(iface);
1020
+ }
1021
+ });
1022
+ return interfaces;
1023
+ }
1024
+ extractRawImports(_filePath, content) {
1025
+ const results = [];
1026
+ const parser = getParser('typescript');
1027
+ const tree = parser.parse(content);
1028
+ if (!tree)
1029
+ return results;
1030
+ this.walkRaw(tree.rootNode, (node) => this.collectRawImport(node, results));
1031
+ return results;
1032
+ }
1033
+ extractExportedSymbols(ast) {
1034
+ const symbols = [];
1035
+ this.walk(ast.root, (astNode) => {
1036
+ const node = astNode.raw;
1037
+ if (node.type === 'export_statement')
1038
+ this.collectExportedSymbol(node, symbols);
1039
+ });
1040
+ return symbols;
1041
+ }
1042
+ }
1043
+ // ---------------------------------------------------------------------------
1044
+ // Scope + static-value resolution
1045
+ // ---------------------------------------------------------------------------
1046
+ class TsScopeStatic extends TsPredicatesOptional {
1047
+ /** Determine whether a tree-sitter value node represents a static (compile-time
1048
+ * constant) expression — no variables, function calls, or runtime evaluation. */
1049
+ isStaticValueNode(node) {
1050
+ if (!node)
1051
+ return false;
1052
+ const type = node.type;
1053
+ // Literals that are trivially compile-time constants.
1054
+ if (type === 'string')
1055
+ return true;
1056
+ if (type === 'number')
1057
+ return true;
1058
+ if (type === 'true' || type === 'false' || type === 'null' || type === 'undefined')
1059
+ return true;
1060
+ // Template strings: static only when they contain no ${…} substitutions.
1061
+ if (type === 'template_string') {
1062
+ for (let i = 0; i < node.childCount; i++) {
1299
1063
  const child = node.child(i);
1300
1064
  if (child && child.type === 'template_substitution')
1301
1065
  return false;
1302
1066
  }
1303
1067
  return true;
1304
1068
  }
1305
- // Array literals: static when all elements are compile-time constants.
1306
- // This enables tracing through patterns like:
1307
- // const TABLES = ["a", "b"];
1308
- // for (const t of TABLES) { … `${t}` … }
1309
- // where every possible value of `t` is known at compile time.
1069
+ // Array literals are static when every element is a compile-time constant
1070
+ // (enables tracing `for (const t of TABLES)` over a static column list).
1310
1071
  if (type === 'array') {
1311
1072
  for (let i = 0; i < node.childCount; i++) {
1312
1073
  const child = node.child(i);
@@ -1318,6 +1079,14 @@ export class TreeSitterTypeScriptAdapter {
1318
1079
  }
1319
1080
  return true;
1320
1081
  }
1082
+ // Type assertions (`[...] as const`, `x as T`, `x satisfies T`) wrap an inner
1083
+ // expression without changing its runtime value — unwrap and judge that.
1084
+ if (type === 'as_expression' ||
1085
+ type === 'type_assertion' ||
1086
+ type === 'satisfies_expression' ||
1087
+ type === 'non_null_expression') {
1088
+ return this.isStaticValueNode(node.namedChild(0));
1089
+ }
1321
1090
  // Everything else (identifiers, call expressions, binary expressions,
1322
1091
  // object literals, regexes, etc.) is conservatively treated as non-static.
1323
1092
  return false;
@@ -1421,14 +1190,18 @@ export class TreeSitterTypeScriptAdapter {
1421
1190
  declLine: r.declLine,
1422
1191
  };
1423
1192
  }
1424
- /** Check if identifier targetName is reassigned in scope after declLine. */
1193
+ /** Check if identifier targetName is reassigned in scope after declLine.
1194
+ * Handles both plain assignment (`x = …`) and augmented assignment
1195
+ * (`x += …`, `x -= …`, etc.) — the latter accumulates into a string the
1196
+ * same way `x = x + …` does, so a variable built up with `+=` is just as
1197
+ * runtime-dependent as one reassigned with `=`. */
1425
1198
  hasReassignment(scopeRoot, targetName, declLine) {
1426
1199
  let found = false;
1427
1200
  this.walk(scopeRoot, (astNode) => {
1428
1201
  if (found)
1429
1202
  return;
1430
1203
  const t = astNode.raw.type;
1431
- if (t === 'assignment_expression') {
1204
+ if (t === 'assignment_expression' || t === 'augmented_assignment_expression') {
1432
1205
  if (astNode.location.start.line < declLine)
1433
1206
  return;
1434
1207
  const raw = astNode.raw;
@@ -1440,20 +1213,743 @@ export class TreeSitterTypeScriptAdapter {
1440
1213
  });
1441
1214
  return found;
1442
1215
  }
1443
- /** Helper: extract text of arguments from a call_expression node. */
1444
- getCallArguments(node) {
1445
- const args = [];
1216
+ /** Find a function declaration (named or arrow-assigned) by name in the file. */
1217
+ findFunctionDeclaration(name, ast) {
1218
+ let result = null;
1219
+ this.walk(ast.root, (node) => {
1220
+ if (result)
1221
+ return;
1222
+ const raw = node.raw;
1223
+ const t = raw.type;
1224
+ if (t === 'function_declaration' || t === 'generator_function_declaration') {
1225
+ const nameNode = raw.childForFieldName?.('name');
1226
+ if (nameNode && nameNode.text === name)
1227
+ result = node;
1228
+ }
1229
+ else if (t === 'variable_declarator') {
1230
+ const nameNode = raw.childForFieldName?.('name');
1231
+ const valueNode = raw.childForFieldName?.('value');
1232
+ if (nameNode && nameNode.type === 'identifier' && nameNode.text === name
1233
+ && valueNode && ['arrow_function', 'function_expression', 'function'].includes(valueNode.type)) {
1234
+ result = this.wrapRaw(valueNode);
1235
+ }
1236
+ }
1237
+ });
1238
+ return result;
1239
+ }
1240
+ /** Extract a function's name (named functions and arrow-function assignments). */
1241
+ getFunctionName(node) {
1242
+ const raw = node.raw;
1243
+ const t = raw.type;
1244
+ if (t === 'function_declaration' || t === 'generator_function_declaration'
1245
+ || t === 'function_expression' || t === 'method_definition') {
1246
+ const nameNode = raw.childForFieldName?.('name');
1247
+ if (nameNode && nameNode.text)
1248
+ return nameNode.text;
1249
+ }
1250
+ if (t === 'arrow_function') {
1251
+ const parent = node.parent;
1252
+ if (parent) {
1253
+ const pRaw = parent.raw;
1254
+ if (pRaw.type === 'variable_declarator') {
1255
+ const nameNode = pRaw.childForFieldName?.('name');
1256
+ if (nameNode && nameNode.type === 'identifier')
1257
+ return nameNode.text;
1258
+ }
1259
+ }
1260
+ }
1261
+ return null;
1262
+ }
1263
+ /** Find all call sites of a named function in the file. */
1264
+ findCallSites(fnName, ast) {
1265
+ const sites = [];
1266
+ this.walk(ast.root, (node) => {
1267
+ const raw = node.raw;
1268
+ if (raw.type !== 'call_expression')
1269
+ return;
1270
+ const fn = raw.childForFieldName?.('function');
1271
+ if (fn && fn.type === 'identifier' && fn.text === fnName)
1272
+ sites.push(node);
1273
+ });
1274
+ return sites;
1275
+ }
1276
+ }
1277
+ // ---------------------------------------------------------------------------
1278
+ // Dynamic string construction + local-constant resolution
1279
+ // ---------------------------------------------------------------------------
1280
+ class TsDynamicStringConstruction extends TsScopeStatic {
1281
+ /**
1282
+ * Returns true when the node is a dynamically-constructed string in TS/JS:
1283
+ * - template_string with template_substitution children
1284
+ * - binary_expression with + operator (string concatenation)
1285
+ * - call_expression with .concat() method
1286
+ * String-like identifiers and plain string literals are never dynamic.
1287
+ */
1288
+ isDynamicStringConstruction(node) {
1289
+ const type = node.raw.type;
1290
+ if (type === 'template_string') {
1291
+ // template_string is dynamic only if it has template_substitution children
1292
+ for (const child of node.children ?? []) {
1293
+ if (child.raw.type === 'template_substitution') {
1294
+ return true;
1295
+ }
1296
+ }
1297
+ return false;
1298
+ }
1299
+ if (type === 'binary_expression') {
1300
+ // Check for string concatenation: operands include a string literal
1301
+ const children = node.children ?? [];
1302
+ const hasStringLiteral = children.some(c => c.raw.type === 'string');
1303
+ return hasStringLiteral;
1304
+ }
1305
+ if (type === 'call_expression') {
1306
+ // Check for .concat() calls
1307
+ const text = node.raw.text;
1308
+ if (text.includes('.concat(') || text.includes('?.concat('))
1309
+ return true;
1310
+ // Recurse into arguments: query(binaryExpression) where the argument
1311
+ // itself is a dynamic string construction.
1312
+ for (const child of node.children ?? []) {
1313
+ if (child.raw.type === 'arguments') {
1314
+ for (const arg of child.children ?? []) {
1315
+ const argType = arg.raw.type;
1316
+ if (argType === '(' || argType === ')' || argType === ',')
1317
+ continue;
1318
+ if (this.isDynamicStringConstruction(arg))
1319
+ return true;
1320
+ }
1321
+ }
1322
+ }
1323
+ return false;
1324
+ }
1325
+ return false;
1326
+ }
1327
+ /**
1328
+ * Extracts dynamic sub-parts from a string construction node.
1329
+ * Returns template_substitution text for template literals,
1330
+ * non-string-literal operands for binary expressions,
1331
+ * and non-literal arguments for concat calls.
1332
+ */
1333
+ getDynamicParts(node, sourceCode) {
1334
+ const type = node.raw.type;
1335
+ if (type === 'call_expression') {
1336
+ const nested = this.getNestedDynamicCallParts(node, sourceCode);
1337
+ if (nested)
1338
+ return nested;
1339
+ return this.getCallArgParts(node);
1340
+ }
1341
+ if (type === 'template_string')
1342
+ return this.getTemplateStringParts(node, sourceCode);
1343
+ if (type === 'binary_expression')
1344
+ return this.getBinaryExpressionParts(node, sourceCode);
1345
+ return [];
1346
+ }
1347
+ /** For a non-`.concat()` call, recurse into the first argument that is itself
1348
+ * a dynamic string construction; null when there is none (or it's .concat). */
1349
+ getNestedDynamicCallParts(node, sourceCode) {
1350
+ const text = node.raw.text;
1351
+ if (text.includes('.concat(') || text.includes('?.concat('))
1352
+ return null;
1446
1353
  for (const child of node.children ?? []) {
1447
- if (child.raw.type === 'arguments') {
1448
- for (const arg of child.children ?? []) {
1449
- const argType = arg.raw.type;
1450
- if (argType !== '(' && argType !== ')' && argType !== ',') {
1451
- args.push(arg.raw.text);
1354
+ if (child.raw.type !== 'arguments')
1355
+ continue;
1356
+ for (const arg of child.children ?? []) {
1357
+ const argType = arg.raw.type;
1358
+ if (argType === '(' || argType === ')' || argType === ',')
1359
+ continue;
1360
+ if (this.isDynamicStringConstruction(arg)) {
1361
+ return this.getDynamicParts(arg, sourceCode);
1362
+ }
1363
+ }
1364
+ }
1365
+ return null;
1366
+ }
1367
+ /** `.concat()` (or a call with no nested dynamic arg): every argument is dynamic. */
1368
+ getCallArgParts(node) {
1369
+ const parts = [];
1370
+ for (const child of node.children ?? []) {
1371
+ if (child.raw.type !== 'arguments')
1372
+ continue;
1373
+ for (const arg of child.children ?? []) {
1374
+ const argType = arg.raw.type;
1375
+ if (argType === '(' || argType === ')' || argType === ',')
1376
+ continue;
1377
+ const text = arg.raw.text.trim();
1378
+ const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text);
1379
+ parts.push({ text, isIdentifier: isId, node: arg });
1380
+ }
1381
+ }
1382
+ return parts;
1383
+ }
1384
+ /** Dynamic parts from a template string's `${…}` substitutions. */
1385
+ getTemplateStringParts(node, sourceCode) {
1386
+ const parts = [];
1387
+ for (const child of node.children ?? []) {
1388
+ if (child.raw.type !== 'template_substitution')
1389
+ continue;
1390
+ const text = sourceCode.slice(child.range[0], child.range[1]);
1391
+ // Strip the ${ } wrapper to get the inner identifier/expression.
1392
+ const inner = text.startsWith('${') ? text.slice(2, -1).trim() : text;
1393
+ const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(inner);
1394
+ // The expression inside ${…} is the single named child; attach it for
1395
+ // isSafeInterpolation() cross-function safety checks.
1396
+ let exprNode;
1397
+ for (const subChild of child.children ?? []) {
1398
+ if (subChild.raw.type !== 'template_substitution') {
1399
+ exprNode = subChild;
1400
+ break;
1401
+ }
1402
+ }
1403
+ parts.push({ text: inner, isIdentifier: isId, node: exprNode ?? child });
1404
+ }
1405
+ return parts;
1406
+ }
1407
+ /** Dynamic parts from a `+`-concatenation of strings and expressions. */
1408
+ getBinaryExpressionParts(node, sourceCode) {
1409
+ const parts = [];
1410
+ for (const child of node.children ?? []) {
1411
+ const childType = child.raw.type;
1412
+ if (childType === 'string' || childType === '+' || childType === 'template_string')
1413
+ continue;
1414
+ const text = sourceCode.slice(child.range[0], child.range[1]);
1415
+ const isId = /^[$\p{L}_][\p{L}\p{N}_$]*$/u.test(text.trim());
1416
+ parts.push({ text: text.trim(), isIdentifier: isId, node: child });
1417
+ }
1418
+ return parts;
1419
+ }
1420
+ }
1421
+ class TsConstantResolution extends TsDynamicStringConstruction {
1422
+ /**
1423
+ * Resolves a local const/let/var declaration for an identifier node.
1424
+ * Searches within the enclosing function scope for the declaration site.
1425
+ * Returns null when the identifier is a parameter, complex expression,
1426
+ * or cannot be statically resolved.
1427
+ */
1428
+ resolveLocalConstant(identifierNode, ast, sourceCode) {
1429
+ const idName = sourceCode.slice(identifierNode.range[0], identifierNode.range[1]).trim();
1430
+ if (!idName)
1431
+ return null;
1432
+ // Enclosing function/file scope, falling back to the module scope for
1433
+ // module-level constants.
1434
+ const enclosing = this.findEnclosingScope(identifierNode, ast);
1435
+ const scopeRoot = enclosing ?? ast.root;
1436
+ const scope = { scopeRoot, enclosing, ast };
1437
+ const resolved = this.resolveDeclarationOrConstant(identifierNode, idName, sourceCode, scope);
1438
+ if (resolved.constant)
1439
+ return resolved.constant;
1440
+ const declNode = resolved.declNode;
1441
+ if (!declNode)
1442
+ return null;
1443
+ return this.resolveFromDeclaration(declNode, idName, sourceCode, scope);
1444
+ }
1445
+ /** Determine static-ness and init text from a resolved declaration node.
1446
+ *
1447
+ * "Static" means the value is a compile-time constant that is never
1448
+ * reassigned. String literals and substitution-free template strings are
1449
+ * constants regardless of `?` placeholders — SQL fragment constants (WHERE
1450
+ * clauses, column lists) are as static as parameterised query strings.
1451
+ *
1452
+ * The value is extracted from the AST rather than regex so that multiline
1453
+ * declarations survive (`.` doesn't match `\n`). */
1454
+ resolveFromDeclaration(declNode, idName, sourceCode, scope) {
1455
+ const { scopeRoot, enclosing, ast } = scope;
1456
+ const raw = declNode.raw;
1457
+ const valueNode = raw.childForFieldName?.('value');
1458
+ const declLine = declNode.location.start.line;
1459
+ const reassigned = this.hasReassignment(scopeRoot, idName, declLine);
1460
+ let isStatic = !reassigned && this.isStaticValueNode(valueNode);
1461
+ // For-of / for-in loop variables: the declarator has no `value` field
1462
+ // (the iterable is on the for-statement's `right` child). Trace the
1463
+ // iterable to see if all possible loop values are known constants.
1464
+ if (!isStatic && !valueNode && this.traceForOfLoopVariable(declNode, scopeRoot, enclosing, ast)) {
1465
+ isStatic = true;
1466
+ }
1467
+ // Value is a simple identifier (e.g. `table` ← `tables`): trace through to
1468
+ // the linked declaration (handles `for (const table of TABLES)` where the
1469
+ // iterable is a compile-time constant array).
1470
+ if (!isStatic && valueNode?.type === 'identifier' && this.traceLinkedIdentifier(valueNode, idName, scope)) {
1471
+ isStatic = true;
1472
+ }
1473
+ const initText = this.extractInitText(valueNode, declNode, sourceCode);
1474
+ return { initText, isStatic, declLine };
1475
+ }
1476
+ /** Resolve `idName` to a declaration in the enclosing scope (falling back to
1477
+ * the program-level scope), or — when no local declaration exists — to an
1478
+ * imported constant or a `for…in`/`for…of` loop variable. Returns either a
1479
+ * declaration node or a fully resolved constant. */
1480
+ resolveDeclarationOrConstant(identifierNode, idName, sourceCode, scope) {
1481
+ const { scopeRoot, enclosing, ast } = scope;
1482
+ let declNode = this.findDeclarationInScope(scopeRoot, idName);
1483
+ // If the enclosing scope is a function (not the program) and we didn't
1484
+ // find the declaration there, also search the program-level scope.
1485
+ if (!declNode && enclosing && enclosing !== ast.root) {
1486
+ declNode = this.findDeclarationInScope(ast.root, idName);
1487
+ }
1488
+ if (declNode)
1489
+ return { declNode, constant: null };
1490
+ // Not in local declarations — imported symbols are compile-time constants
1491
+ // resolved at link time, not runtime, so user input cannot reach them via
1492
+ // import bindings.
1493
+ const importResult = this.resolveImportConstant(idName, ast);
1494
+ if (importResult)
1495
+ return { declNode: null, constant: importResult };
1496
+ // tree-sitter-typescript (v0.x) parses both "for…in" and "for…of" as
1497
+ // `for_in_statement`; the loop variable is a bare identifier child.
1498
+ const forInResult = this.traceForInLoopVariable(identifierNode, idName, sourceCode, scope);
1499
+ if (forInResult)
1500
+ return { declNode: null, constant: forInResult };
1501
+ return { declNode: null, constant: null };
1502
+ }
1503
+ /** Trace a `for…in`/`for…of` loop variable whose identifier appears directly
1504
+ * under a `for_in_statement` `left` child. tree-sitter-typescript (v0.x)
1505
+ * parses both loop forms as `for_in_statement`; the loop variable is a bare
1506
+ * `identifier` child — NOT wrapped in `variable_declarator` /
1507
+ * `lexical_declaration`. Walk up from the identifier to a `for_in_statement`
1508
+ * and trace through the iterable to determine static-ness.
1509
+ * const TABLES = ["a", "b"];
1510
+ * for (const table of TABLES) { … `${table}` … }
1511
+ */
1512
+ traceForInLoopVariable(identifierNode, idName, sourceCode, scope) {
1513
+ const { scopeRoot, enclosing, ast } = scope;
1514
+ const idRaw = identifierNode.raw;
1515
+ let tsCurrent = idRaw.parent;
1516
+ while (tsCurrent) {
1517
+ if (tsCurrent.type === 'for_in_statement') {
1518
+ const left = tsCurrent.childForFieldName?.('left');
1519
+ if (left && left.text === idName) {
1520
+ const right = tsCurrent.childForFieldName?.('right');
1521
+ if (right && right.type === 'identifier') {
1522
+ const iterName = right.text;
1523
+ let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
1524
+ if (!iterDecl && enclosing && enclosing !== ast.root) {
1525
+ iterDecl = this.findDeclarationInScope(ast.root, iterName);
1526
+ }
1527
+ if (iterDecl) {
1528
+ const iterRaw = iterDecl.raw;
1529
+ const iterValue = iterRaw.childForFieldName?.('value');
1530
+ const iterLine = iterDecl.location.start.line;
1531
+ const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
1532
+ || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
1533
+ if (!iterReassigned && this.isStaticValueNode(iterValue)) {
1534
+ const initText = iterValue ? sourceCode.slice(iterValue.startIndex, iterValue.endIndex).trim() : '';
1535
+ return { initText, isStatic: true, declLine: iterDecl.location.start.line };
1536
+ }
1537
+ }
1452
1538
  }
1453
1539
  }
1540
+ break;
1541
+ }
1542
+ tsCurrent = tsCurrent.parent;
1543
+ }
1544
+ return null;
1545
+ }
1546
+ /** Trace a for-of / for-in loop variable whose declarator has no `value` field
1547
+ * (the iterable is on the for-statement's `right` child). Returns true when
1548
+ * all possible loop values are known compile-time constants. */
1549
+ traceForOfLoopVariable(declNode, scopeRoot, enclosing, ast) {
1550
+ const forParent = this.findEnclosingForStatement(declNode);
1551
+ if (!forParent)
1552
+ return false;
1553
+ const forRaw = forParent.raw;
1554
+ const iterable = forRaw.childForFieldName?.('right');
1555
+ if (!iterable || iterable.type !== 'identifier')
1556
+ return false;
1557
+ const iterName = iterable.text;
1558
+ let iterDecl = this.findDeclarationInScope(scopeRoot, iterName);
1559
+ if (!iterDecl && enclosing && enclosing !== ast.root) {
1560
+ iterDecl = this.findDeclarationInScope(ast.root, iterName);
1561
+ }
1562
+ if (!iterDecl)
1563
+ return false;
1564
+ const iterRaw = iterDecl.raw;
1565
+ const iterValue = iterRaw.childForFieldName?.('value');
1566
+ const iterLine = iterDecl.location.start.line;
1567
+ const iterReassigned = this.hasReassignment(scopeRoot, iterName, iterLine)
1568
+ || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, iterName, iterLine) : false);
1569
+ return !iterReassigned && this.isStaticValueNode(iterValue);
1570
+ }
1571
+ /** Trace an identifier value (e.g. `table` ← `tables`) to its linked
1572
+ * declaration. Returns true when the linked initializer is a static
1573
+ * constant and is never reassigned. */
1574
+ traceLinkedIdentifier(valueNode, idName, scope) {
1575
+ const { scopeRoot, enclosing, ast } = scope;
1576
+ const linkedName = valueNode.text;
1577
+ if (!linkedName || linkedName === idName)
1578
+ return false;
1579
+ let linkedDecl = this.findDeclarationInScope(scopeRoot, linkedName);
1580
+ if (!linkedDecl && enclosing && enclosing !== ast.root) {
1581
+ linkedDecl = this.findDeclarationInScope(ast.root, linkedName);
1582
+ }
1583
+ if (!linkedDecl)
1584
+ return false;
1585
+ const linkedRaw = linkedDecl.raw;
1586
+ const linkedValue = linkedRaw.childForFieldName?.('value');
1587
+ const linkedLine = linkedDecl.location.start.line;
1588
+ const linkedReassigned = this.hasReassignment(scopeRoot, linkedName, linkedLine)
1589
+ || (enclosing && enclosing !== ast.root ? this.hasReassignment(ast.root, linkedName, linkedLine) : false);
1590
+ return !linkedReassigned && this.isStaticValueNode(linkedValue);
1591
+ }
1592
+ /** Extract the initializer text from a declaration: the value node when
1593
+ * present, otherwise a regex fallback over the full declaration text. */
1594
+ extractInitText(valueNode, declNode, sourceCode) {
1595
+ if (valueNode) {
1596
+ return sourceCode.slice(valueNode.startIndex, valueNode.endIndex).trim();
1597
+ }
1598
+ // No initializer node — try regex as a last resort.
1599
+ const declText = sourceCode.slice(declNode.range[0], declNode.range[1]);
1600
+ const declMatch = declText.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$|^(\w+)\s*=\s*(.+?);?\s*$/s);
1601
+ return declMatch
1602
+ ? (declMatch[2] ?? declMatch[4]).replace(/;\s*$/, '').trim()
1603
+ : '';
1604
+ }
1605
+ /** True when the identifier names the enclosing function's parameter AND a
1606
+ * guard call (assert/validate/check/ensure/guard-prefixed) is invoked on it
1607
+ * before its use. Guards throw on invalid input, so a parameter that has
1608
+ * passed one cannot carry unvalidated attacker data past the check. */
1609
+ isGuardValidatedParameter(identifierNode, ast) {
1610
+ const idRaw = identifierNode.raw;
1611
+ const paramName = idRaw.text;
1612
+ const enclosing = this.findEnclosingScope(identifierNode, ast);
1613
+ if (!enclosing || enclosing === ast.root)
1614
+ return false;
1615
+ if (!this.getParamNames(enclosing).includes(paramName))
1616
+ return false;
1617
+ let guarded = false;
1618
+ this.walk(enclosing, (node) => {
1619
+ if (guarded)
1620
+ return;
1621
+ const raw = node.raw;
1622
+ if (raw.type !== 'call_expression')
1623
+ return;
1624
+ const fn = raw.childForFieldName?.('function');
1625
+ if (!fn)
1626
+ return;
1627
+ let calleeName = null;
1628
+ if (fn.type === 'identifier')
1629
+ calleeName = fn.text;
1630
+ else if (fn.type === 'member_expression') {
1631
+ calleeName = fn.childForFieldName?.('property')?.text ?? null;
1632
+ }
1633
+ if (!calleeName)
1634
+ return;
1635
+ if (!/^(assert|validate|check|ensure|guard)([A-Z_]|$)/i.test(calleeName))
1636
+ return;
1637
+ const argsNode = raw.childForFieldName?.('arguments');
1638
+ const first = argsNode?.namedChildren[0] ?? null;
1639
+ if (first && first.type === 'identifier' && first.text === paramName
1640
+ && raw.startIndex < idRaw.startIndex) {
1641
+ guarded = true;
1642
+ }
1643
+ });
1644
+ return guarded;
1645
+ }
1646
+ }
1647
+ class TsSafetyAnalysis extends TsConstantResolution {
1648
+ /** Public entry point (LanguageAdapter.isSafeInterpolation). */
1649
+ isSafeInterpolation(node, ast, sourceCode) {
1650
+ return this.isSafeExpression(node, {
1651
+ ast,
1652
+ sourceCode,
1653
+ seen: new Set(),
1654
+ seenFns: new Set(),
1655
+ paramMap: new Map(),
1656
+ });
1657
+ }
1658
+ /** Recursive safety check against the traversal context in `ctx`. */
1659
+ isSafeExpression(node, ctx) {
1660
+ if (!node)
1661
+ return false;
1662
+ if (node === SAFE_STRING_NODE)
1663
+ return true;
1664
+ const raw = node.raw;
1665
+ // `seen` is path-based (added on entry, removed on exit) and keyed by the
1666
+ // node's unique `id` — NOT `startIndex`, which collides between an
1667
+ // expression and its leading operand (a `binary_expression` and its left
1668
+ // string share a start offset). Path-based semantics let a shared value
1669
+ // (one literal bound to two parameters) be re-checked via sibling branches
1670
+ // while still breaking true cycles (a node in its own ancestor chain).
1671
+ const key = raw.id;
1672
+ if (ctx.seen.has(key))
1673
+ return false;
1674
+ ctx.seen.add(key);
1675
+ const result = this.evaluateSafeExpression(node, raw, ctx);
1676
+ ctx.seen.delete(key);
1677
+ return result;
1678
+ }
1679
+ /** Evaluate a single expression node's safety, dispatching on its AST type. */
1680
+ evaluateSafeExpression(node, raw, ctx) {
1681
+ // Structural nodes (wrappers, binary, ternary) recurse into children.
1682
+ const structural = this.evaluateStructuralExpression(node, raw, ctx);
1683
+ if (structural !== null)
1684
+ return structural;
1685
+ switch (raw.type) {
1686
+ case 'string':
1687
+ case 'number':
1688
+ case 'true':
1689
+ case 'false':
1690
+ case 'null':
1691
+ case 'undefined':
1692
+ case 'regex':
1693
+ return true;
1694
+ case 'identifier':
1695
+ return this.isSafeIdentifier(node, ctx);
1696
+ case 'template_string':
1697
+ return this.isSafeTemplateString(node, ctx);
1698
+ case 'array':
1699
+ return this.isSafeArray(node, ctx);
1700
+ case 'call_expression':
1701
+ return this.isSafeCallExpression(node, ctx);
1702
+ // member_expression, object/class literals, await/async, etc. — a value we
1703
+ // cannot prove safe. Conservative: stay flagged.
1704
+ default:
1705
+ return false;
1706
+ }
1707
+ }
1708
+ /** Handle structural nodes that recurse into child expressions (parenthesized
1709
+ * wrappers, type casts/assertions, ternaries, binary ops). Returns `null`
1710
+ * when the node is not structural, so the caller can fall through to the
1711
+ * leaf switch. */
1712
+ evaluateStructuralExpression(node, raw, ctx) {
1713
+ switch (raw.type) {
1714
+ case 'parenthesized_expression': {
1715
+ const inner = (node.children ?? []).find((c) => !['(', ')'].includes(c.raw.type));
1716
+ return this.isSafeExpression(inner ?? null, ctx);
1717
+ }
1718
+ case 'as_expression':
1719
+ case 'type_assertion':
1720
+ case 'satisfies_expression':
1721
+ case 'non_null_expression': {
1722
+ const inner = raw.namedChild?.(0);
1723
+ return this.isSafeExpression(this.wrapRaw(inner), ctx);
1724
+ }
1725
+ case 'ternary_expression': {
1726
+ const consequence = raw.childForFieldName?.('consequence');
1727
+ const alternative = raw.childForFieldName?.('alternative');
1728
+ return this.isSafeExpression(this.wrapRaw(consequence), ctx)
1729
+ && this.isSafeExpression(this.wrapRaw(alternative), ctx);
1730
+ }
1731
+ case 'binary_expression': {
1732
+ const left = raw.childForFieldName?.('left');
1733
+ const right = raw.childForFieldName?.('right');
1734
+ return this.isSafeExpression(this.wrapRaw(left), ctx)
1735
+ && this.isSafeExpression(this.wrapRaw(right), ctx);
1736
+ }
1737
+ default:
1738
+ return null;
1739
+ }
1740
+ }
1741
+ /** Identifier safety: a bound parameter, compile-time constant, or validated
1742
+ * parameter at all call sites. */
1743
+ isSafeIdentifier(node, ctx) {
1744
+ const name = node.raw.text;
1745
+ // A parameter bound at the current call site — recurse into its value.
1746
+ if (ctx.paramMap.has(name)) {
1747
+ return this.isSafeExpression(ctx.paramMap.get(name) ?? null, ctx);
1748
+ }
1749
+ // Compile-time constant (string/number/static array/imported symbol).
1750
+ const resolved = this.resolveLocalConstant(node, ctx.ast, ctx.sourceCode);
1751
+ if (resolved && resolved.isStatic) {
1752
+ return true;
1753
+ }
1754
+ if (this.isDeclarationValueSafe(node, ctx)) {
1755
+ return true;
1756
+ }
1757
+ if (this.isGuardValidatedParameter(node, ctx.ast)) {
1758
+ return true;
1759
+ }
1760
+ if (this.isParamSafeAtAllCallSites(node, ctx)) {
1761
+ return true;
1762
+ }
1763
+ return false;
1764
+ }
1765
+ /** A template string is safe only if every `${…}` substitution is safe. */
1766
+ isSafeTemplateString(node, ctx) {
1767
+ for (const child of node.children ?? []) {
1768
+ const ct = child.raw.type;
1769
+ if (ct !== 'template_substitution')
1770
+ continue;
1771
+ const inner = (child.children ?? []).find((c) => c.raw.type !== 'template_substitution') ?? child;
1772
+ if (!this.isSafeExpression(inner, ctx)) {
1773
+ return false;
1774
+ }
1775
+ }
1776
+ return true;
1777
+ }
1778
+ /** An array literal is safe only if every element is safe. */
1779
+ isSafeArray(node, ctx) {
1780
+ for (const child of node.children ?? []) {
1781
+ const ct = child.raw.type;
1782
+ if (ct === ',' || ct === '[' || ct === ']')
1783
+ continue;
1784
+ if (!this.isSafeExpression(child, ctx)) {
1785
+ return false;
1786
+ }
1787
+ }
1788
+ return true;
1789
+ }
1790
+ /** Decide whether a call expression is provably safe to embed in SQL. */
1791
+ isSafeCallExpression(node, ctx) {
1792
+ const raw = node.raw;
1793
+ const fnNode = raw.childForFieldName?.('function');
1794
+ if (!fnNode)
1795
+ return false;
1796
+ // NOTE: manual quote-escaping (`x.replace(/'/g, "''")`) is deliberately NOT
1797
+ // treated as a sanitizer here — see the module-level comment above. It
1798
+ // covers only the single-quote case and would otherwise bless a real
1799
+ // vulnerability.
1800
+ // 1. Static-array `.map().join()` chain (e.g. `FLAGS.map((c) => \`a.${c}\`).join(", ")`).
1801
+ if (this.isSafeMapJoin(node, ctx))
1802
+ return true;
1803
+ // 2. Local function call with a safe body and safe call sites.
1804
+ if (this.isLocalFunctionCallSafe(node, ctx))
1805
+ return true;
1806
+ return false;
1807
+ }
1808
+ /** True when the node is `.join(...)` over `.map(...)` of a static array whose
1809
+ * callback body is safe for every element. */
1810
+ isSafeMapJoin(node, ctx) {
1811
+ const raw = node.raw;
1812
+ const fnNode = raw.childForFieldName?.('function');
1813
+ if (!fnNode || fnNode.type !== 'member_expression')
1814
+ return false;
1815
+ const joinProp = fnNode.childForFieldName?.('property');
1816
+ const joinObj = fnNode.childForFieldName?.('object');
1817
+ if (!joinProp || !joinObj || joinProp.text !== 'join')
1818
+ return false;
1819
+ if (joinObj.type !== 'call_expression')
1820
+ return false;
1821
+ const mapFn = joinObj.childForFieldName?.('function');
1822
+ const mapArgs = joinObj.childForFieldName?.('arguments');
1823
+ if (!mapFn || mapFn.type !== 'member_expression')
1824
+ return false;
1825
+ const mapProp = mapFn.childForFieldName?.('property');
1826
+ const mapObj = mapFn.childForFieldName?.('object');
1827
+ if (!mapProp || !mapObj || mapProp.text !== 'map')
1828
+ return false;
1829
+ // The array being mapped must itself be provably safe (static array/const).
1830
+ if (!this.isSafeExpression(this.wrapRaw(mapObj), ctx))
1831
+ return false;
1832
+ // The callback must be safe for any element — bind its first parameter to a
1833
+ // compile-time string sentinel and check the callback body.
1834
+ const callback = mapArgs?.namedChildren[0] ?? null;
1835
+ if (!callback || !['arrow_function', 'function_expression', 'function'].includes(callback.type))
1836
+ return false;
1837
+ const cbParams = this.getParamNames(this.wrapRaw(callback));
1838
+ if (cbParams.length === 0)
1839
+ return false;
1840
+ const bound = new Map(ctx.paramMap);
1841
+ bound.set(cbParams[0], SAFE_STRING_NODE);
1842
+ return this.isBodySafeUnderParams(this.wrapRaw(callback), { ...ctx, paramMap: bound });
1843
+ }
1844
+ /** True when the node is a call to a local (in-file) function whose body is
1845
+ * safe under the values passed at this call site. */
1846
+ isLocalFunctionCallSafe(node, ctx) {
1847
+ const raw = node.raw;
1848
+ const fnNode = raw.childForFieldName?.('function');
1849
+ if (!fnNode || fnNode.type !== 'identifier')
1850
+ return false;
1851
+ const calleeName = fnNode.text;
1852
+ if (!calleeName || ctx.seenFns.has(calleeName))
1853
+ return false;
1854
+ ctx.seenFns.add(calleeName);
1855
+ const decl = this.findFunctionDeclaration(calleeName, ctx.ast);
1856
+ if (!decl)
1857
+ return false;
1858
+ const paramNames = this.getParamNames(decl);
1859
+ const argNodes = this.getCallArgASTNodes(node);
1860
+ const bound = new Map(ctx.paramMap);
1861
+ paramNames.forEach((p, i) => bound.set(p, argNodes[i] ?? null));
1862
+ return this.isBodySafeUnderParams(decl, { ...ctx, paramMap: bound });
1863
+ }
1864
+ /** Check a function's body returns only safe values, under the `paramMap`
1865
+ * already bound in `ctx` to the values passed at the call site. */
1866
+ isBodySafeUnderParams(fnNode, ctx) {
1867
+ const raw = fnNode.raw;
1868
+ const body = raw.childForFieldName?.('body');
1869
+ if (!body)
1870
+ return false;
1871
+ if (body.type === 'statement_block') {
1872
+ const returns = [];
1873
+ const collect = (n) => {
1874
+ if (n.type === 'return_statement')
1875
+ returns.push(n);
1876
+ for (const c of n.namedChildren)
1877
+ collect(c);
1878
+ };
1879
+ collect(body);
1880
+ if (returns.length === 0)
1881
+ return false;
1882
+ for (const r of returns) {
1883
+ const expr = r.namedChildren.find((c) => c.type !== 'return_statement') ?? null;
1884
+ if (!expr)
1885
+ return false;
1886
+ if (!this.isSafeExpression(this.wrapRaw(expr), ctx))
1887
+ return false;
1454
1888
  }
1889
+ return true;
1890
+ }
1891
+ // Arrow-function expression body (no braces).
1892
+ return this.isSafeExpression(this.wrapRaw(body), ctx);
1893
+ }
1894
+ /** True when the identifier names the enclosing function's parameter AND every
1895
+ * in-file call site of that function passes a provably-safe value for that
1896
+ * parameter position. */
1897
+ isParamSafeAtAllCallSites(identifierNode, ctx) {
1898
+ const idRaw = identifierNode.raw;
1899
+ const paramName = idRaw.text;
1900
+ const enclosing = this.findEnclosingScope(identifierNode, ctx.ast);
1901
+ if (!enclosing || enclosing === ctx.ast.root)
1902
+ return false;
1903
+ const paramNames = this.getParamNames(enclosing);
1904
+ const paramIndex = paramNames.indexOf(paramName);
1905
+ if (paramIndex < 0)
1906
+ return false;
1907
+ const fnName = this.getFunctionName(enclosing);
1908
+ if (!fnName)
1909
+ return false;
1910
+ const callSites = this.findCallSites(fnName, ctx.ast);
1911
+ if (callSites.length === 0)
1912
+ return false;
1913
+ for (const site of callSites) {
1914
+ const arg = this.getCallArgASTNodes(site)[paramIndex];
1915
+ if (!arg)
1916
+ return false;
1917
+ if (!this.isSafeExpression(arg, { ...ctx, paramMap: new Map() }))
1918
+ return false;
1455
1919
  }
1456
- return args;
1920
+ return true;
1457
1921
  }
1922
+ /** True when the identifier names a local variable whose initializer is a
1923
+ * provably-safe expression and which is never reassigned. */
1924
+ isDeclarationValueSafe(identifierNode, ctx) {
1925
+ const idRaw = identifierNode.raw;
1926
+ const name = idRaw.text;
1927
+ const enclosing = this.findEnclosingScope(identifierNode, ctx.ast);
1928
+ const scopeRoot = enclosing ?? ctx.ast.root;
1929
+ let declNode = this.findDeclarationInScope(scopeRoot, name);
1930
+ if (!declNode && enclosing && enclosing !== ctx.ast.root) {
1931
+ declNode = this.findDeclarationInScope(ctx.ast.root, name);
1932
+ }
1933
+ if (!declNode)
1934
+ return false;
1935
+ const declLine = declNode.location.start.line;
1936
+ const reassigned = this.hasReassignment(scopeRoot, name, declLine)
1937
+ || (enclosing && enclosing !== ctx.ast.root ? this.hasReassignment(ctx.ast.root, name, declLine) : false);
1938
+ if (reassigned)
1939
+ return false;
1940
+ const declRaw = declNode.raw;
1941
+ const valueRaw = declRaw.childForFieldName?.('value');
1942
+ if (!valueRaw)
1943
+ return false;
1944
+ return this.isSafeExpression(this.wrapRaw(valueRaw), ctx);
1945
+ }
1946
+ }
1947
+ // ---------------------------------------------------------------------------
1948
+ // Adapter (composes the slices above)
1949
+ // ---------------------------------------------------------------------------
1950
+ /**
1951
+ * Tree sitter type script adapter.
1952
+ */
1953
+ export class TreeSitterTypeScriptAdapter extends TsSafetyAnalysis {
1458
1954
  }
1459
1955
  //# sourceMappingURL=TreeSitterTypeScriptAdapter.js.map