@optave/codegraph 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  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 +81 -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/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -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);
@@ -40,6 +40,32 @@ function classifyHalstead(node: TreeSitterNode, hRules: AnyRules, acc: Complexit
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Detect whether a branch node is an else-if that the DFS walk would NOT
45
+ * increment nesting for. Returns true for:
46
+ * - Pattern A (JS/C#/Rust): if_statement whose parent is else_clause
47
+ * - Pattern C (Go/Java): if_statement that is the alternative of parent if
48
+ *
49
+ * Pattern B (Python elif_clause) is not an issue because elif_clause is
50
+ * never in nestingNodes.
51
+ */
52
+ function isElseIfNonNesting(node: TreeSitterNode, type: string, cRules: AnyRules): boolean {
53
+ if (type !== cRules.ifNodeType) return false;
54
+
55
+ if (cRules.elseViaAlternative) {
56
+ // Pattern C
57
+ return (
58
+ node.parent?.type === cRules.ifNodeType &&
59
+ node.parent?.childForFieldName('alternative')?.id === node.id
60
+ );
61
+ }
62
+ if (cRules.elseNodeType) {
63
+ // Pattern A
64
+ return node.parent?.type === cRules.elseNodeType;
65
+ }
66
+ return false;
67
+ }
68
+
43
69
  function classifyBranchNode(
44
70
  node: TreeSitterNode,
45
71
  type: string,
@@ -156,6 +182,27 @@ function resetAccumulators(hRules: AnyRules | null | undefined): ComplexityAcc {
156
182
  };
157
183
  }
158
184
 
185
+ /** Classify a single node for all complexity metrics (Halstead, branching, logical ops, etc.). */
186
+ function classifyNode(
187
+ node: TreeSitterNode,
188
+ nestingLevel: number,
189
+ cRules: AnyRules,
190
+ hRules: AnyRules | null | undefined,
191
+ acc: ComplexityAcc,
192
+ ): void {
193
+ const type = node.type;
194
+
195
+ if (hRules) classifyHalstead(node, hRules, acc);
196
+ if (nestingLevel > acc.maxNesting) acc.maxNesting = nestingLevel;
197
+ if (type === cRules.logicalNodeType) classifyLogicalOp(node, cRules, acc);
198
+ if (type === cRules.optionalChainType) acc.cyclomatic++;
199
+ if (cRules.branchNodes.has(type) && node.childCount > 0) {
200
+ classifyBranchNode(node, type, nestingLevel, cRules, acc);
201
+ }
202
+ classifyPlainElse(node, type, cRules, acc);
203
+ if (cRules.caseNodes.has(type) && node.childCount > 0) acc.cyclomatic++;
204
+ }
205
+
159
206
  export function createComplexityVisitor(
160
207
  cRules: AnyRules,
161
208
  hRules?: AnyRules | null,
@@ -169,6 +216,13 @@ export function createComplexityVisitor(
169
216
  let funcDepth = 0;
170
217
  const results: PerFunctionResult[] = [];
171
218
 
219
+ // The walker increments context.nestingLevel for ALL nodes in nestingNodeTypes
220
+ // (including if_statement). But the DFS engine does NOT increment nesting for
221
+ // else-if if_statement nodes. Track a correction counter so children of else-if
222
+ // nodes see the correct (non-inflated) nesting level.
223
+ let nestingAdjust = 0;
224
+ const adjustNodeIds = new Set<number>();
225
+
172
226
  return {
173
227
  name: 'complexity',
174
228
  functionNodeTypes: cRules.functionNodes,
@@ -178,15 +232,13 @@ export function createComplexityVisitor(
178
232
  funcName: string | null,
179
233
  _context: VisitorContext,
180
234
  ): 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
- }
235
+ if (fileLevelWalk && !activeFuncNode) {
236
+ acc = resetAccumulators(hRules);
237
+ activeFuncNode = funcNode;
238
+ activeFuncName = funcName;
239
+ funcDepth = 0;
240
+ nestingAdjust = 0;
241
+ adjustNodeIds.clear();
190
242
  } else {
191
243
  funcDepth++;
192
244
  }
@@ -197,18 +249,14 @@ export function createComplexityVisitor(
197
249
  _funcName: string | null,
198
250
  _context: VisitorContext,
199
251
  ): 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
- }
252
+ if (fileLevelWalk && funcNode === activeFuncNode) {
253
+ results.push({
254
+ funcNode,
255
+ funcName: activeFuncName,
256
+ metrics: collectResult(funcNode, acc, hRules, langId),
257
+ });
258
+ activeFuncNode = null;
259
+ activeFuncName = null;
212
260
  } else {
213
261
  funcDepth--;
214
262
  }
@@ -217,29 +265,30 @@ export function createComplexityVisitor(
217
265
  enterNode(node: TreeSitterNode, context: VisitorContext): EnterNodeResult | undefined {
218
266
  if (fileLevelWalk && !activeFuncNode) return;
219
267
 
220
- const type = node.type;
221
- 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);
268
+ // In file-level mode, funcDepth starts at 0 for the active function.
269
+ // In function-level mode, funcDepth starts at 1 for the root function
270
+ // (since enterFunction always increments it). Nested functions add +1
271
+ // each level subtract 1 so the root function contributes 0 nesting
272
+ // and each nested level adds +1, matching the Rust engine's behavior.
273
+ const funcNesting = fileLevelWalk ? funcDepth : Math.max(0, funcDepth - 1);
274
+ const nestingLevel = context.nestingLevel + funcNesting - nestingAdjust;
275
+ classifyNode(node, nestingLevel, cRules, hRules, acc);
276
+
277
+ // If this is an else-if if_statement that the walker will treat as a
278
+ // nesting node (incrementing context.nestingLevel for children), but
279
+ // the DFS walk would NOT increment nesting for, compensate by bumping
280
+ // nestingAdjust so children see the correct level.
281
+ if (cRules.nestingNodes.has(node.type) && isElseIfNonNesting(node, node.type, cRules)) {
282
+ nestingAdjust++;
283
+ adjustNodeIds.add(node.id);
229
284
  }
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++;
240
285
  },
241
286
 
242
287
  exitNode(node: TreeSitterNode): void {
288
+ if (adjustNodeIds.has(node.id)) {
289
+ nestingAdjust--;
290
+ adjustNodeIds.delete(node.id);
291
+ }
243
292
  if (hRules?.skipTypes.has(node.type)) acc.halsteadSkipDepth--;
244
293
  },
245
294