@optave/codegraph 3.8.0 → 3.8.1

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 (296) hide show
  1. package/README.md +9 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +95 -87
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/watch.d.ts.map +1 -1
  28. package/dist/cli/commands/watch.js +16 -2
  29. package/dist/cli/commands/watch.js.map +1 -1
  30. package/dist/db/connection.d.ts.map +1 -1
  31. package/dist/db/connection.js +29 -26
  32. package/dist/db/connection.js.map +1 -1
  33. package/dist/db/query-builder.d.ts.map +1 -1
  34. package/dist/db/query-builder.js +16 -5
  35. package/dist/db/query-builder.js.map +1 -1
  36. package/dist/db/repository/base.d.ts +10 -0
  37. package/dist/db/repository/base.d.ts.map +1 -1
  38. package/dist/db/repository/base.js +17 -0
  39. package/dist/db/repository/base.js.map +1 -1
  40. package/dist/db/repository/native-repository.d.ts +6 -1
  41. package/dist/db/repository/native-repository.d.ts.map +1 -1
  42. package/dist/db/repository/native-repository.js +77 -1
  43. package/dist/db/repository/native-repository.js.map +1 -1
  44. package/dist/db/repository/nodes.d.ts.map +1 -1
  45. package/dist/db/repository/nodes.js +8 -4
  46. package/dist/db/repository/nodes.js.map +1 -1
  47. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  48. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  49. package/dist/db/repository/sqlite-repository.js +26 -0
  50. package/dist/db/repository/sqlite-repository.js.map +1 -1
  51. package/dist/domain/analysis/brief.d.ts.map +1 -1
  52. package/dist/domain/analysis/brief.js +13 -17
  53. package/dist/domain/analysis/brief.js.map +1 -1
  54. package/dist/domain/analysis/context.d.ts.map +1 -1
  55. package/dist/domain/analysis/context.js +14 -11
  56. package/dist/domain/analysis/context.js.map +1 -1
  57. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  58. package/dist/domain/analysis/dependencies.js +53 -52
  59. package/dist/domain/analysis/dependencies.js.map +1 -1
  60. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  61. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  62. package/dist/domain/analysis/fn-impact.js +33 -31
  63. package/dist/domain/analysis/fn-impact.js.map +1 -1
  64. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  65. package/dist/domain/analysis/implementations.js +11 -19
  66. package/dist/domain/analysis/implementations.js.map +1 -1
  67. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  68. package/dist/domain/analysis/module-map.js +55 -76
  69. package/dist/domain/analysis/module-map.js.map +1 -1
  70. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  71. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  72. package/dist/domain/analysis/query-helpers.js +15 -1
  73. package/dist/domain/analysis/query-helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/pipeline.js +255 -105
  76. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  77. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/stages/build-edges.js +22 -17
  79. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  81. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  83. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  84. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  85. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  86. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  88. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  89. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  90. package/dist/domain/graph/cycles.d.ts +6 -0
  91. package/dist/domain/graph/cycles.d.ts.map +1 -1
  92. package/dist/domain/graph/cycles.js +114 -22
  93. package/dist/domain/graph/cycles.js.map +1 -1
  94. package/dist/domain/graph/resolve.js +1 -1
  95. package/dist/domain/graph/resolve.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts +2 -0
  97. package/dist/domain/graph/watcher.d.ts.map +1 -1
  98. package/dist/domain/graph/watcher.js +170 -75
  99. package/dist/domain/graph/watcher.js.map +1 -1
  100. package/dist/domain/parser.d.ts +0 -5
  101. package/dist/domain/parser.d.ts.map +1 -1
  102. package/dist/domain/parser.js +13 -28
  103. package/dist/domain/parser.js.map +1 -1
  104. package/dist/domain/search/generator.js +1 -1
  105. package/dist/domain/search/generator.js.map +1 -1
  106. package/dist/domain/search/models.d.ts +4 -3
  107. package/dist/domain/search/models.d.ts.map +1 -1
  108. package/dist/domain/search/models.js +18 -5
  109. package/dist/domain/search/models.js.map +1 -1
  110. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  111. package/dist/domain/search/search/hybrid.js +29 -18
  112. package/dist/domain/search/search/hybrid.js.map +1 -1
  113. package/dist/extractors/go.js +36 -33
  114. package/dist/extractors/go.js.map +1 -1
  115. package/dist/extractors/helpers.d.ts.map +1 -1
  116. package/dist/extractors/helpers.js +40 -29
  117. package/dist/extractors/helpers.js.map +1 -1
  118. package/dist/extractors/java.js +58 -46
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.js +46 -45
  121. package/dist/extractors/javascript.js.map +1 -1
  122. package/dist/extractors/kotlin.js +84 -78
  123. package/dist/extractors/kotlin.js.map +1 -1
  124. package/dist/extractors/python.js +29 -24
  125. package/dist/extractors/python.js.map +1 -1
  126. package/dist/extractors/rust.js +41 -32
  127. package/dist/extractors/rust.js.map +1 -1
  128. package/dist/extractors/solidity.js +58 -67
  129. package/dist/extractors/solidity.js.map +1 -1
  130. package/dist/extractors/swift.js +83 -81
  131. package/dist/extractors/swift.js.map +1 -1
  132. package/dist/extractors/zig.js +58 -60
  133. package/dist/extractors/zig.js.map +1 -1
  134. package/dist/features/ast.d.ts +16 -14
  135. package/dist/features/ast.d.ts.map +1 -1
  136. package/dist/features/ast.js +83 -81
  137. package/dist/features/ast.js.map +1 -1
  138. package/dist/features/audit.d.ts.map +1 -1
  139. package/dist/features/audit.js +8 -6
  140. package/dist/features/audit.js.map +1 -1
  141. package/dist/features/branch-compare.d.ts.map +1 -1
  142. package/dist/features/branch-compare.js +69 -72
  143. package/dist/features/branch-compare.js.map +1 -1
  144. package/dist/features/communities.d.ts.map +1 -1
  145. package/dist/features/communities.js +19 -7
  146. package/dist/features/communities.js.map +1 -1
  147. package/dist/features/complexity.d.ts.map +1 -1
  148. package/dist/features/complexity.js +120 -125
  149. package/dist/features/complexity.js.map +1 -1
  150. package/dist/features/dataflow.d.ts.map +1 -1
  151. package/dist/features/dataflow.js +136 -137
  152. package/dist/features/dataflow.js.map +1 -1
  153. package/dist/features/flow.d.ts.map +1 -1
  154. package/dist/features/flow.js +84 -79
  155. package/dist/features/flow.js.map +1 -1
  156. package/dist/features/structure-query.d.ts.map +1 -1
  157. package/dist/features/structure-query.js +69 -65
  158. package/dist/features/structure-query.js.map +1 -1
  159. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  160. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  161. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  162. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  163. package/dist/graph/algorithms/leiden/partition.js +288 -266
  164. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  165. package/dist/graph/model.d.ts.map +1 -1
  166. package/dist/graph/model.js +5 -1
  167. package/dist/graph/model.js.map +1 -1
  168. package/dist/infrastructure/config.d.ts.map +1 -1
  169. package/dist/infrastructure/config.js +6 -4
  170. package/dist/infrastructure/config.js.map +1 -1
  171. package/dist/infrastructure/suppress.d.ts +25 -0
  172. package/dist/infrastructure/suppress.d.ts.map +1 -0
  173. package/dist/infrastructure/suppress.js +43 -0
  174. package/dist/infrastructure/suppress.js.map +1 -0
  175. package/dist/mcp/server.d.ts.map +1 -1
  176. package/dist/mcp/server.js +29 -24
  177. package/dist/mcp/server.js.map +1 -1
  178. package/dist/presentation/dataflow.d.ts.map +1 -1
  179. package/dist/presentation/dataflow.js +47 -38
  180. package/dist/presentation/dataflow.js.map +1 -1
  181. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  182. package/dist/presentation/diff-impact-mermaid.js +60 -51
  183. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  184. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  185. package/dist/presentation/queries-cli/exports.js +20 -14
  186. package/dist/presentation/queries-cli/exports.js.map +1 -1
  187. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  188. package/dist/presentation/queries-cli/impact.js +15 -13
  189. package/dist/presentation/queries-cli/impact.js.map +1 -1
  190. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  191. package/dist/presentation/queries-cli/inspect.js +101 -79
  192. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  193. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/overview.js +25 -16
  195. package/dist/presentation/queries-cli/overview.js.map +1 -1
  196. package/dist/presentation/queries-cli/path.js +26 -20
  197. package/dist/presentation/queries-cli/path.js.map +1 -1
  198. package/dist/presentation/result-formatter.d.ts +10 -0
  199. package/dist/presentation/result-formatter.d.ts.map +1 -1
  200. package/dist/presentation/result-formatter.js +16 -1
  201. package/dist/presentation/result-formatter.js.map +1 -1
  202. package/dist/presentation/viewer.d.ts.map +1 -1
  203. package/dist/presentation/viewer.js +18 -12
  204. package/dist/presentation/viewer.js.map +1 -1
  205. package/dist/shared/errors.d.ts +5 -0
  206. package/dist/shared/errors.d.ts.map +1 -1
  207. package/dist/shared/errors.js +5 -0
  208. package/dist/shared/errors.js.map +1 -1
  209. package/dist/shared/hierarchy.d.ts +8 -2
  210. package/dist/shared/hierarchy.d.ts.map +1 -1
  211. package/dist/shared/hierarchy.js +42 -1
  212. package/dist/shared/hierarchy.js.map +1 -1
  213. package/dist/shared/normalize.d.ts +6 -1
  214. package/dist/shared/normalize.d.ts.map +1 -1
  215. package/dist/shared/normalize.js +20 -12
  216. package/dist/shared/normalize.js.map +1 -1
  217. package/dist/shared/paginate.d.ts +0 -9
  218. package/dist/shared/paginate.d.ts.map +1 -1
  219. package/dist/shared/paginate.js +0 -15
  220. package/dist/shared/paginate.js.map +1 -1
  221. package/dist/types.d.ts +10 -4
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +7 -7
  224. package/src/ast-analysis/engine.ts +126 -105
  225. package/src/ast-analysis/metrics.ts +33 -11
  226. package/src/ast-analysis/shared.ts +33 -24
  227. package/src/ast-analysis/visitor-utils.ts +52 -32
  228. package/src/ast-analysis/visitor.ts +132 -71
  229. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  230. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  231. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  232. package/src/cli/commands/watch.ts +16 -2
  233. package/src/db/connection.ts +29 -28
  234. package/src/db/query-builder.ts +15 -3
  235. package/src/db/repository/base.ts +20 -0
  236. package/src/db/repository/native-repository.ts +79 -1
  237. package/src/db/repository/nodes.ts +13 -8
  238. package/src/db/repository/sqlite-repository.ts +29 -0
  239. package/src/domain/analysis/brief.ts +15 -25
  240. package/src/domain/analysis/context.ts +17 -10
  241. package/src/domain/analysis/dependencies.ts +67 -76
  242. package/src/domain/analysis/fn-impact.ts +36 -43
  243. package/src/domain/analysis/implementations.ts +11 -17
  244. package/src/domain/analysis/module-map.ts +58 -92
  245. package/src/domain/analysis/query-helpers.ts +18 -1
  246. package/src/domain/graph/builder/pipeline.ts +286 -97
  247. package/src/domain/graph/builder/stages/build-edges.ts +22 -18
  248. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  249. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  250. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  251. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  252. package/src/domain/graph/cycles.ts +110 -23
  253. package/src/domain/graph/resolve.ts +1 -1
  254. package/src/domain/graph/watcher.ts +202 -96
  255. package/src/domain/parser.ts +14 -26
  256. package/src/domain/search/generator.ts +1 -1
  257. package/src/domain/search/models.ts +17 -4
  258. package/src/domain/search/search/hybrid.ts +69 -51
  259. package/src/extractors/go.ts +43 -33
  260. package/src/extractors/helpers.ts +37 -23
  261. package/src/extractors/java.ts +66 -47
  262. package/src/extractors/javascript.ts +45 -46
  263. package/src/extractors/kotlin.ts +84 -77
  264. package/src/extractors/python.ts +31 -25
  265. package/src/extractors/rust.ts +37 -29
  266. package/src/extractors/solidity.ts +57 -61
  267. package/src/extractors/swift.ts +81 -80
  268. package/src/extractors/zig.ts +58 -61
  269. package/src/features/ast.ts +130 -110
  270. package/src/features/audit.ts +8 -6
  271. package/src/features/branch-compare.ts +105 -79
  272. package/src/features/communities.ts +25 -10
  273. package/src/features/complexity.ts +171 -134
  274. package/src/features/dataflow.ts +165 -175
  275. package/src/features/flow.ts +129 -92
  276. package/src/features/structure-query.ts +79 -64
  277. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  278. package/src/graph/algorithms/leiden/partition.ts +359 -294
  279. package/src/graph/model.ts +6 -1
  280. package/src/infrastructure/config.ts +6 -4
  281. package/src/infrastructure/suppress.ts +47 -0
  282. package/src/mcp/server.ts +53 -37
  283. package/src/presentation/dataflow.ts +50 -44
  284. package/src/presentation/diff-impact-mermaid.ts +104 -62
  285. package/src/presentation/queries-cli/exports.ts +21 -13
  286. package/src/presentation/queries-cli/impact.ts +15 -13
  287. package/src/presentation/queries-cli/inspect.ts +100 -81
  288. package/src/presentation/queries-cli/overview.ts +26 -16
  289. package/src/presentation/queries-cli/path.ts +33 -25
  290. package/src/presentation/result-formatter.ts +19 -1
  291. package/src/presentation/viewer.ts +42 -14
  292. package/src/shared/errors.ts +6 -0
  293. package/src/shared/hierarchy.ts +50 -2
  294. package/src/shared/normalize.ts +31 -12
  295. package/src/shared/paginate.ts +0 -17
  296. package/src/types.ts +24 -4
@@ -20,6 +20,127 @@ import type {
20
20
  WalkResults,
21
21
  } from '../types.js';
22
22
 
23
+ /** Merge each visitor's custom functionNodeTypes into the master set. */
24
+ function mergeFunctionNodeTypes(visitors: Visitor[], base: Set<string>): Set<string> {
25
+ const merged = new Set(base);
26
+ for (const v of visitors) {
27
+ if (v.functionNodeTypes) {
28
+ for (const t of v.functionNodeTypes) merged.add(t);
29
+ }
30
+ }
31
+ return merged;
32
+ }
33
+
34
+ /** Initialize all visitors for a given language. */
35
+ function initVisitors(visitors: Visitor[], langId: string): void {
36
+ for (const v of visitors) {
37
+ if (v.init) v.init(langId);
38
+ }
39
+ }
40
+
41
+ /** Check whether a visitor should be skipped at the current depth. */
42
+ function isSkipped(
43
+ skipDepths: Map<number, number>,
44
+ visitorIndex: number,
45
+ currentDepth: number,
46
+ ): boolean {
47
+ const skipAt = skipDepths.get(visitorIndex);
48
+ // Skipped if skip was requested at a shallower (or equal) depth
49
+ // We skip descendants, not the node itself, so skip when currentDepth > skipAt
50
+ return skipAt !== undefined && currentDepth > skipAt;
51
+ }
52
+
53
+ /** Dispatch enterFunction hooks to all non-skipped visitors. */
54
+ function dispatchEnterFunction(
55
+ visitors: Visitor[],
56
+ skipDepths: Map<number, number>,
57
+ node: TreeSitterNode,
58
+ funcName: string | null,
59
+ context: VisitorContext,
60
+ depth: number,
61
+ ): void {
62
+ for (let i = 0; i < visitors.length; i++) {
63
+ const v = visitors[i]!;
64
+ if (v.enterFunction && !isSkipped(skipDepths, i, depth)) {
65
+ v.enterFunction(node, funcName, context);
66
+ }
67
+ }
68
+ }
69
+
70
+ /** Dispatch enterNode hooks and track skipChildren requests. */
71
+ function dispatchEnterNode(
72
+ visitors: Visitor[],
73
+ skipDepths: Map<number, number>,
74
+ node: TreeSitterNode,
75
+ context: VisitorContext,
76
+ depth: number,
77
+ ): void {
78
+ for (let i = 0; i < visitors.length; i++) {
79
+ const v = visitors[i]!;
80
+ if (v.enterNode && !isSkipped(skipDepths, i, depth)) {
81
+ const result = v.enterNode(node, context);
82
+ if (result?.skipChildren) {
83
+ skipDepths.set(i, depth);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ /** Dispatch exitNode hooks to all non-skipped visitors. */
90
+ function dispatchExitNode(
91
+ visitors: Visitor[],
92
+ skipDepths: Map<number, number>,
93
+ node: TreeSitterNode,
94
+ context: VisitorContext,
95
+ depth: number,
96
+ ): void {
97
+ for (let i = 0; i < visitors.length; i++) {
98
+ const v = visitors[i]!;
99
+ if (v.exitNode && !isSkipped(skipDepths, i, depth)) {
100
+ v.exitNode(node, context);
101
+ }
102
+ }
103
+ }
104
+
105
+ /** Clear skip flags for visitors that started skipping at this depth. */
106
+ function clearSkipFlags(
107
+ skipDepths: Map<number, number>,
108
+ visitorCount: number,
109
+ depth: number,
110
+ ): void {
111
+ for (let i = 0; i < visitorCount; i++) {
112
+ if (skipDepths.get(i) === depth) {
113
+ skipDepths.delete(i);
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Dispatch exitFunction hooks to all non-skipped visitors. */
119
+ function dispatchExitFunction(
120
+ visitors: Visitor[],
121
+ skipDepths: Map<number, number>,
122
+ node: TreeSitterNode,
123
+ funcName: string | null,
124
+ context: VisitorContext,
125
+ depth: number,
126
+ ): void {
127
+ for (let i = 0; i < visitors.length; i++) {
128
+ const v = visitors[i]!;
129
+ if (v.exitFunction && !isSkipped(skipDepths, i, depth)) {
130
+ v.exitFunction(node, funcName, context);
131
+ }
132
+ }
133
+ }
134
+
135
+ /** Collect finish() results from all visitors into a name-keyed map. */
136
+ function collectResults(visitors: Visitor[]): WalkResults {
137
+ const results: WalkResults = {};
138
+ for (const v of visitors) {
139
+ results[v.name] = v.finish ? v.finish() : undefined;
140
+ }
141
+ return results;
142
+ }
143
+
23
144
  /**
24
145
  * Walk an AST root with multiple visitors in a single DFS pass.
25
146
  *
@@ -44,18 +165,8 @@ export function walkWithVisitors(
44
165
  getFunctionName = () => null,
45
166
  } = options;
46
167
 
47
- // Merge all visitors' functionNodeTypes into the master set
48
- const allFuncTypes = new Set(functionNodeTypes);
49
- for (const v of visitors) {
50
- if (v.functionNodeTypes) {
51
- for (const t of v.functionNodeTypes) allFuncTypes.add(t);
52
- }
53
- }
54
-
55
- // Initialize visitors
56
- for (const v of visitors) {
57
- if (v.init) v.init(langId);
58
- }
168
+ const allFuncTypes = mergeFunctionNodeTypes(visitors, functionNodeTypes);
169
+ initVisitors(visitors, langId);
59
170
 
60
171
  // Shared context object (mutated during walk)
61
172
  const scopeStack: ScopeEntry[] = [];
@@ -66,95 +177,45 @@ export function walkWithVisitors(
66
177
  scopeStack,
67
178
  };
68
179
 
69
- // Track which visitors have requested skipChildren at each depth
70
- // Key: visitor index, Value: depth at which skip was requested
71
180
  const skipDepths = new Map<number, number>();
72
181
 
73
182
  function walk(node: TreeSitterNode | null, depth: number): void {
74
183
  if (!node) return;
75
184
 
76
185
  const type = node.type;
77
- const isFunction = allFuncTypes.has(type);
186
+ const isFuncBoundary = allFuncTypes.has(type);
78
187
  let funcName: string | null = null;
79
188
 
80
- // Function boundary: enter
81
- if (isFunction) {
189
+ if (isFuncBoundary) {
82
190
  funcName = getFunctionName(node);
83
191
  context.currentFunction = node;
84
192
  scopeStack.push({ funcName, funcNode: node, params: new Map(), locals: new Map() });
85
- for (let i = 0; i < visitors.length; i++) {
86
- const v = visitors[i]!;
87
- if (v.enterFunction && !isSkipped(i, depth)) {
88
- v.enterFunction(node, funcName, context);
89
- }
90
- }
193
+ dispatchEnterFunction(visitors, skipDepths, node, funcName, context, depth);
91
194
  }
92
195
 
93
- // enterNode hooks
94
- for (let i = 0; i < visitors.length; i++) {
95
- const v = visitors[i]!;
96
- if (v.enterNode && !isSkipped(i, depth)) {
97
- const result = v.enterNode(node, context);
98
- if (result?.skipChildren) {
99
- skipDepths.set(i, depth);
100
- }
101
- }
102
- }
196
+ dispatchEnterNode(visitors, skipDepths, node, context, depth);
103
197
 
104
- // Nesting tracking
105
198
  const addsNesting = nestingNodeTypes.has(type);
106
199
  if (addsNesting) context.nestingLevel++;
107
200
 
108
- // Recurse children using node.child(i) (all children, not just named)
109
201
  for (let i = 0; i < node.childCount; i++) {
110
202
  walk(node.child(i), depth + 1);
111
203
  }
112
204
 
113
- // Undo nesting
114
205
  if (addsNesting) context.nestingLevel--;
115
206
 
116
- // exitNode hooks
117
- for (let i = 0; i < visitors.length; i++) {
118
- const v = visitors[i]!;
119
- if (v.exitNode && !isSkipped(i, depth)) {
120
- v.exitNode(node, context);
121
- }
122
- }
123
-
124
- // Clear skip for any visitor that started skipping at this depth
125
- for (let i = 0; i < visitors.length; i++) {
126
- if (skipDepths.get(i) === depth) {
127
- skipDepths.delete(i);
128
- }
129
- }
207
+ dispatchExitNode(visitors, skipDepths, node, context, depth);
208
+ clearSkipFlags(skipDepths, visitors.length, depth);
130
209
 
131
- // Function boundary: exit
132
- if (isFunction) {
133
- for (let i = 0; i < visitors.length; i++) {
134
- const v = visitors[i]!;
135
- if (v.exitFunction && !isSkipped(i, depth)) {
136
- v.exitFunction(node, funcName, context);
137
- }
138
- }
210
+ if (isFuncBoundary) {
211
+ dispatchExitFunction(visitors, skipDepths, node, funcName, context, depth);
139
212
  scopeStack.pop();
140
213
  context.currentFunction =
141
214
  scopeStack.length > 0 ? scopeStack[scopeStack.length - 1]!.funcNode : null;
142
215
  }
143
216
  }
144
217
 
145
- function isSkipped(visitorIndex: number, currentDepth: number): boolean {
146
- const skipAt = skipDepths.get(visitorIndex);
147
- // Skipped if skip was requested at a shallower (or equal) depth
148
- // We skip descendants, not the node itself, so skip when currentDepth > skipAt
149
- return skipAt !== undefined && currentDepth > skipAt;
150
- }
151
-
152
218
  walk(rootNode, 0);
153
219
 
154
- // Collect results
155
- const results: WalkResults = {};
156
- for (const v of visitors) {
157
- results[v.name] = v.finish ? v.finish() : undefined;
158
- }
159
- return results;
220
+ return collectResults(visitors);
160
221
  }
@@ -44,33 +44,33 @@ function extractExpressionText(node: TreeSitterNode): string | null {
44
44
  return truncate(node.text);
45
45
  }
46
46
 
47
- function extractName(kind: string, node: TreeSitterNode): string | null {
48
- if (kind === 'throw') {
49
- for (let i = 0; i < node.childCount; i++) {
50
- const child = node.child(i);
51
- if (!child) continue;
52
- if (child.type === 'new_expression') return extractNewName(child);
53
- if (child.type === 'call_expression') {
54
- const fn = child.childForFieldName('function');
55
- return fn ? fn.text : child.text?.split('(')[0] || '?';
56
- }
57
- if (child.type === 'identifier') return child.text;
47
+ /** Extract the name from a throw statement's child nodes. */
48
+ function extractThrowName(node: TreeSitterNode): string | null {
49
+ for (let i = 0; i < node.childCount; i++) {
50
+ const child = node.child(i);
51
+ if (!child) continue;
52
+ if (child.type === 'new_expression') return extractNewName(child);
53
+ if (child.type === 'call_expression') {
54
+ const fn = child.childForFieldName('function');
55
+ return fn ? fn.text : child.text?.split('(')[0] || '?';
58
56
  }
59
- return truncate(node.text);
57
+ if (child.type === 'identifier') return child.text;
60
58
  }
61
- if (kind === 'await') {
62
- for (let i = 0; i < node.childCount; i++) {
63
- const child = node.child(i);
64
- if (!child) continue;
65
- if (child.type === 'call_expression') {
66
- const fn = child.childForFieldName('function');
67
- return fn ? fn.text : child.text?.split('(')[0] || '?';
68
- }
69
- if (child.type === 'identifier' || child.type === 'member_expression') {
70
- return child.text;
71
- }
59
+ return truncate(node.text);
60
+ }
61
+
62
+ /** Extract the name from an await expression's child nodes. */
63
+ function extractAwaitName(node: TreeSitterNode): string | null {
64
+ for (let i = 0; i < node.childCount; i++) {
65
+ const child = node.child(i);
66
+ if (!child) continue;
67
+ if (child.type === 'call_expression') {
68
+ const fn = child.childForFieldName('function');
69
+ return fn ? fn.text : child.text?.split('(')[0] || '?';
70
+ }
71
+ if (child.type === 'identifier' || child.type === 'member_expression') {
72
+ return child.text;
72
73
  }
73
- return truncate(node.text);
74
74
  }
75
75
  return truncate(node.text);
76
76
  }
@@ -102,40 +102,43 @@ export function createAstStoreVisitor(
102
102
  return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
103
103
  }
104
104
 
105
+ function resolveNameAndText(
106
+ node: TreeSitterNode,
107
+ kind: string,
108
+ ): { name: string | null | undefined; text: string | null; skip?: boolean } {
109
+ switch (kind) {
110
+ case 'new':
111
+ return { name: extractNewName(node), text: truncate(node.text) };
112
+ case 'throw':
113
+ return { name: extractThrowName(node), text: extractExpressionText(node) };
114
+ case 'await':
115
+ return { name: extractAwaitName(node), text: extractExpressionText(node) };
116
+ case 'string': {
117
+ const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
118
+ if (content.length < 2) return { name: null, text: null, skip: true };
119
+ return { name: truncate(content, 100), text: truncate(node.text) };
120
+ }
121
+ case 'regex':
122
+ return { name: node.text || '?', text: truncate(node.text) };
123
+ default:
124
+ return { name: undefined, text: null };
125
+ }
126
+ }
127
+
105
128
  function collectNode(node: TreeSitterNode, kind: string): void {
106
129
  if (matched.has(node.id)) return;
107
130
 
108
- const line = node.startPosition.row + 1;
109
- let name: string | null | undefined;
110
- let text: string | null = null;
111
-
112
- if (kind === 'new') {
113
- name = extractNewName(node);
114
- text = truncate(node.text);
115
- } else if (kind === 'throw') {
116
- name = extractName('throw', node);
117
- text = extractExpressionText(node);
118
- } else if (kind === 'await') {
119
- name = extractName('await', node);
120
- text = extractExpressionText(node);
121
- } else if (kind === 'string') {
122
- const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
123
- if (content.length < 2) return;
124
- name = truncate(content, 100);
125
- text = truncate(node.text);
126
- } else if (kind === 'regex') {
127
- name = node.text || '?';
128
- text = truncate(node.text);
129
- }
131
+ const resolved = resolveNameAndText(node, kind);
132
+ if (resolved.skip) return;
130
133
 
131
134
  rows.push({
132
135
  file: relPath,
133
- line,
136
+ line: node.startPosition.row + 1,
134
137
  kind,
135
- name,
136
- text,
138
+ name: resolved.name,
139
+ text: resolved.text,
137
140
  receiver: null,
138
- parentNodeId: resolveParentNodeId(line),
141
+ parentNodeId: resolveParentNodeId(node.startPosition.row + 1),
139
142
  });
140
143
 
141
144
  matched.add(node.id);
@@ -156,6 +156,27 @@ function resetAccumulators(hRules: AnyRules | null | undefined): ComplexityAcc {
156
156
  };
157
157
  }
158
158
 
159
+ /** Classify a single node for all complexity metrics (Halstead, branching, logical ops, etc.). */
160
+ function classifyNode(
161
+ node: TreeSitterNode,
162
+ nestingLevel: number,
163
+ cRules: AnyRules,
164
+ hRules: AnyRules | null | undefined,
165
+ acc: ComplexityAcc,
166
+ ): void {
167
+ const type = node.type;
168
+
169
+ if (hRules) classifyHalstead(node, hRules, acc);
170
+ if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
171
+ if (type === cRules.logicalNodeType) classifyLogicalOp(node, cRules, acc);
172
+ if (type === cRules.optionalChainType) acc.cyclomatic++;
173
+ if (cRules.branchNodes.has(type) && node.childCount > 0) {
174
+ classifyBranchNode(node, type, nestingLevel, cRules, acc);
175
+ }
176
+ classifyPlainElse(node, type, cRules, acc);
177
+ if (cRules.caseNodes.has(type) && node.childCount > 0) acc.cyclomatic++;
178
+ }
179
+
159
180
  export function createComplexityVisitor(
160
181
  cRules: AnyRules,
161
182
  hRules?: AnyRules | null,
@@ -178,15 +199,11 @@ export function createComplexityVisitor(
178
199
  funcName: string | null,
179
200
  _context: VisitorContext,
180
201
  ): void {
181
- if (fileLevelWalk) {
182
- if (!activeFuncNode) {
183
- acc = resetAccumulators(hRules);
184
- activeFuncNode = funcNode;
185
- activeFuncName = funcName;
186
- funcDepth = 0;
187
- } else {
188
- funcDepth++;
189
- }
202
+ if (fileLevelWalk && !activeFuncNode) {
203
+ acc = resetAccumulators(hRules);
204
+ activeFuncNode = funcNode;
205
+ activeFuncName = funcName;
206
+ funcDepth = 0;
190
207
  } else {
191
208
  funcDepth++;
192
209
  }
@@ -197,18 +214,14 @@ export function createComplexityVisitor(
197
214
  _funcName: string | null,
198
215
  _context: VisitorContext,
199
216
  ): void {
200
- if (fileLevelWalk) {
201
- if (funcNode === activeFuncNode) {
202
- results.push({
203
- funcNode,
204
- funcName: activeFuncName,
205
- metrics: collectResult(funcNode, acc, hRules, langId),
206
- });
207
- activeFuncNode = null;
208
- activeFuncName = null;
209
- } else {
210
- funcDepth--;
211
- }
217
+ if (fileLevelWalk && funcNode === activeFuncNode) {
218
+ results.push({
219
+ funcNode,
220
+ funcName: activeFuncName,
221
+ metrics: collectResult(funcNode, acc, hRules, langId),
222
+ });
223
+ activeFuncNode = null;
224
+ activeFuncName = null;
212
225
  } else {
213
226
  funcDepth--;
214
227
  }
@@ -217,26 +230,8 @@ export function createComplexityVisitor(
217
230
  enterNode(node: TreeSitterNode, context: VisitorContext): EnterNodeResult | undefined {
218
231
  if (fileLevelWalk && !activeFuncNode) return;
219
232
 
220
- const type = node.type;
221
233
  const nestingLevel = fileLevelWalk ? context.nestingLevel + funcDepth : context.nestingLevel;
222
-
223
- if (hRules) classifyHalstead(node, hRules, acc);
224
-
225
- if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
226
-
227
- if (type === cRules.logicalNodeType) {
228
- classifyLogicalOp(node, cRules, acc);
229
- }
230
-
231
- if (type === cRules.optionalChainType) acc.cyclomatic++;
232
-
233
- if (cRules.branchNodes.has(type) && node.childCount > 0) {
234
- classifyBranchNode(node, type, nestingLevel, cRules, acc);
235
- }
236
-
237
- classifyPlainElse(node, type, cRules, acc);
238
-
239
- if (cRules.caseNodes.has(type) && node.childCount > 0) acc.cyclomatic++;
234
+ classifyNode(node, nestingLevel, cRules, hRules, acc);
240
235
  },
241
236
 
242
237
  exitNode(node: TreeSitterNode): void {
@@ -116,14 +116,13 @@ function isCall(node: TreeSitterNode | null, isCallNode: (t: string) => boolean)
116
116
  return node != null && isCallNode(node.type);
117
117
  }
118
118
 
119
- function handleVarDeclarator(
119
+ /** Resolve the value node from a variable declarator, trying multiple strategies. */
120
+ function resolveValueNode(
120
121
  node: TreeSitterNode,
122
+ nameNode: TreeSitterNode | null,
121
123
  rules: AnyRules,
122
- scopeStack: ScopeEntry[],
123
- assignments: DataflowAssignment[],
124
124
  isCallNode: (t: string) => boolean,
125
- ): void {
126
- let nameNode = node.childForFieldName(rules.varNameField);
125
+ ): TreeSitterNode | null {
127
126
  let valueNode: TreeSitterNode | null = rules.varValueField
128
127
  ? node.childForFieldName(rules.varValueField)
129
128
  : null;
@@ -146,52 +145,97 @@ function handleVarDeclarator(
146
145
  }
147
146
  }
148
147
 
149
- if (rules.expressionListType) {
150
- if (nameNode && nameNode.type === rules.expressionListType)
151
- nameNode = nameNode.namedChildren[0] ?? null;
152
- if (valueNode && valueNode.type === rules.expressionListType)
153
- valueNode = valueNode.namedChildren[0] ?? null;
148
+ return valueNode;
149
+ }
150
+
151
+ /** Unwrap expression-list wrappers from name/value nodes. */
152
+ function unwrapExpressionList(
153
+ nameNode: TreeSitterNode | null,
154
+ valueNode: TreeSitterNode | null,
155
+ rules: AnyRules,
156
+ ): { name: TreeSitterNode | null; value: TreeSitterNode | null } {
157
+ if (!rules.expressionListType) return { name: nameNode, value: valueNode };
158
+ const name =
159
+ nameNode && nameNode.type === rules.expressionListType
160
+ ? (nameNode.namedChildren[0] ?? null)
161
+ : nameNode;
162
+ const value =
163
+ valueNode && valueNode.type === rules.expressionListType
164
+ ? (valueNode.namedChildren[0] ?? null)
165
+ : valueNode;
166
+ return { name, value };
167
+ }
168
+
169
+ /** Record a destructured call assignment (object or array destructuring). */
170
+ function recordDestructuredAssignment(
171
+ nameNode: TreeSitterNode,
172
+ node: TreeSitterNode,
173
+ callee: string,
174
+ scope: ScopeEntry,
175
+ assignments: DataflowAssignment[],
176
+ rules: AnyRules,
177
+ ): void {
178
+ const names = extractParamNames(nameNode, rules);
179
+ for (const n of names) {
180
+ assignments.push({
181
+ varName: n,
182
+ callerFunc: scope.funcName!,
183
+ sourceCallName: callee,
184
+ expression: truncate(node.text),
185
+ line: node.startPosition.row + 1,
186
+ });
187
+ scope.locals.set(n, { type: 'destructured', callee });
154
188
  }
189
+ }
190
+
191
+ /** Record a simple (non-destructured) call assignment. */
192
+ function recordSimpleAssignment(
193
+ nameNode: TreeSitterNode,
194
+ node: TreeSitterNode,
195
+ callee: string,
196
+ scope: ScopeEntry,
197
+ assignments: DataflowAssignment[],
198
+ ): void {
199
+ const varName = nameNode.text;
200
+ assignments.push({
201
+ varName,
202
+ callerFunc: scope.funcName!,
203
+ sourceCallName: callee,
204
+ expression: truncate(node.text),
205
+ line: node.startPosition.row + 1,
206
+ });
207
+ scope.locals.set(varName, { type: 'call_return', callee });
208
+ }
209
+
210
+ function handleVarDeclarator(
211
+ node: TreeSitterNode,
212
+ rules: AnyRules,
213
+ scopeStack: ScopeEntry[],
214
+ assignments: DataflowAssignment[],
215
+ isCallNode: (t: string) => boolean,
216
+ ): void {
217
+ const rawName = node.childForFieldName(rules.varNameField);
218
+ const rawValue = resolveValueNode(node, rawName, rules, isCallNode);
219
+ const { name: nameNode, value: valueNode } = unwrapExpressionList(rawName, rawValue, rules);
155
220
 
156
221
  const scope = currentScope(scopeStack);
157
222
  if (!nameNode || !valueNode || !scope) return;
158
223
 
159
224
  const unwrapped = unwrapAwait(valueNode, rules);
160
225
  const callExpr = isCall(unwrapped, isCallNode) ? unwrapped : null;
226
+ if (!callExpr) return;
161
227
 
162
- if (callExpr) {
163
- const callee = resolveCalleeName(callExpr, rules);
164
- if (callee && scope.funcName) {
165
- if (
166
- (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
167
- (rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
168
- ) {
169
- const names = extractParamNames(nameNode, rules);
170
- for (const n of names) {
171
- assignments.push({
172
- varName: n,
173
- callerFunc: scope.funcName,
174
- sourceCallName: callee,
175
- expression: truncate(node.text),
176
- line: node.startPosition.row + 1,
177
- });
178
- scope.locals.set(n, { type: 'destructured', callee });
179
- }
180
- } else {
181
- const varName =
182
- nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
183
- ? nameNode.text
184
- : nameNode.text;
185
- assignments.push({
186
- varName,
187
- callerFunc: scope.funcName,
188
- sourceCallName: callee,
189
- expression: truncate(node.text),
190
- line: node.startPosition.row + 1,
191
- });
192
- scope.locals.set(varName, { type: 'call_return', callee });
193
- }
194
- }
228
+ const callee = resolveCalleeName(callExpr, rules);
229
+ if (!callee || !scope.funcName) return;
230
+
231
+ const isDestructured =
232
+ (rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
233
+ (rules.arrayDestructType && nameNode.type === rules.arrayDestructType);
234
+
235
+ if (isDestructured) {
236
+ recordDestructuredAssignment(nameNode, node, callee, scope, assignments, rules);
237
+ } else {
238
+ recordSimpleAssignment(nameNode, node, callee, scope, assignments);
195
239
  }
196
240
  }
197
241
 
@@ -5,9 +5,23 @@ import type { CommandDefinition } from '../types.js';
5
5
  export const command: CommandDefinition = {
6
6
  name: 'watch [dir]',
7
7
  description: 'Watch project for file changes and incrementally update the graph',
8
- async execute([dir], _opts, ctx) {
8
+ options: [
9
+ ['--poll', 'Use stat-based polling (default on Windows to avoid ReFS/Dev Drive crashes)'],
10
+ ['--native', 'Force native OS file watchers instead of polling'],
11
+ ['--poll-interval <ms>', 'Polling interval in milliseconds (default: 2000)'],
12
+ ],
13
+ async execute([dir], opts, ctx) {
9
14
  const root = path.resolve(dir || '.');
10
15
  const engine = ctx.program.opts().engine;
11
- await watchProject(root, { engine });
16
+ if (opts.poll && opts.native) {
17
+ ctx.program.error('--poll and --native are mutually exclusive');
18
+ }
19
+ // Explicit --poll or --native wins; otherwise let watcher auto-detect by platform
20
+ const poll = opts.poll ? true : opts.native ? false : undefined;
21
+ await watchProject(root, {
22
+ engine,
23
+ poll,
24
+ pollInterval: opts.pollInterval ? Number(opts.pollInterval) : undefined,
25
+ });
12
26
  },
13
27
  };