@shanepadgett/tau-agent 0.26.0 → 0.28.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 (121) hide show
  1. package/docs/subagents.md +1 -1
  2. package/extensions/appshot/README.md +1 -1
  3. package/extensions/appshot/index.ts +5 -11
  4. package/extensions/context/sync.ts +1 -9
  5. package/extensions/context-pruning/README.md +2 -2
  6. package/extensions/context-pruning/index.ts +58 -57
  7. package/extensions/context-pruning/prune.ts +2 -2
  8. package/extensions/context-pruning/render.ts +51 -39
  9. package/extensions/context-pruning/settings.ts +4 -5
  10. package/extensions/explore/README.md +13 -14
  11. package/extensions/explore/ast/adapter.ts +97 -0
  12. package/extensions/explore/ast/engine.ts +470 -0
  13. package/extensions/explore/ast/format/ast-search.ts +97 -0
  14. package/extensions/explore/ast/format/composite.ts +26 -0
  15. package/extensions/explore/ast/format/context.ts +66 -0
  16. package/extensions/explore/ast/format/deps.ts +86 -0
  17. package/extensions/explore/ast/format/discover.ts +85 -0
  18. package/extensions/explore/ast/format/impact.ts +105 -0
  19. package/extensions/explore/ast/format/outline.ts +66 -0
  20. package/extensions/explore/ast/format/relationships.ts +81 -0
  21. package/extensions/explore/ast/format/show.ts +38 -0
  22. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  23. package/extensions/explore/ast/grammars/manifest.json +41 -0
  24. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  25. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  26. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  27. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  28. package/extensions/explore/ast/graph/relationships.ts +597 -0
  29. package/extensions/explore/ast/identity.ts +119 -0
  30. package/extensions/explore/ast/ir.ts +130 -0
  31. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  32. package/extensions/explore/ast/languages/csharp.ts +493 -0
  33. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  34. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  35. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  36. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  37. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  38. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  39. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  40. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  41. package/extensions/explore/ast/languages/go.ts +545 -0
  42. package/extensions/explore/ast/languages/java.ts +412 -0
  43. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  44. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  45. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  46. package/extensions/explore/ast/languages/odin.ts +407 -0
  47. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  48. package/extensions/explore/ast/languages/rust.ts +506 -0
  49. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  50. package/extensions/explore/ast/languages/swift.ts +466 -0
  51. package/extensions/explore/ast/languages/tree.ts +220 -0
  52. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  53. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  54. package/extensions/explore/ast/languages/typescript.ts +716 -0
  55. package/extensions/explore/ast/markdown.ts +145 -0
  56. package/extensions/explore/ast/package-surface.ts +42 -0
  57. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  58. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  59. package/extensions/explore/ast/queries/context.ts +406 -0
  60. package/extensions/explore/ast/queries/discover.ts +427 -0
  61. package/extensions/explore/ast/queries/impact.ts +250 -0
  62. package/extensions/explore/ast/queries/outline.ts +203 -0
  63. package/extensions/explore/ast/queries/show.ts +262 -0
  64. package/extensions/explore/ast/query.ts +48 -0
  65. package/extensions/explore/ast/read/hook.ts +125 -0
  66. package/extensions/explore/ast/read/policy.ts +55 -0
  67. package/extensions/explore/ast/registry.ts +120 -0
  68. package/extensions/explore/ast/scan.ts +76 -0
  69. package/extensions/explore/ast/slice.ts +31 -0
  70. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  71. package/extensions/explore/ast/tools/context.ts +126 -0
  72. package/extensions/explore/ast/tools/deps.ts +72 -0
  73. package/extensions/explore/ast/tools/discover.ts +208 -0
  74. package/extensions/explore/ast/tools/impact.ts +135 -0
  75. package/extensions/explore/ast/tools/outline.ts +200 -0
  76. package/extensions/explore/ast/tools/relationships.ts +160 -0
  77. package/extensions/explore/ast/tools/render.ts +121 -0
  78. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  79. package/extensions/explore/ast/tools/show.ts +116 -0
  80. package/extensions/explore/guidance.ts +52 -0
  81. package/extensions/explore/index.ts +91 -45
  82. package/extensions/explore/read/autoread.ts +118 -0
  83. package/extensions/explore/settings.ts +64 -0
  84. package/extensions/explore/traverse.ts +260 -86
  85. package/extensions/handoff/index.ts +1 -1
  86. package/extensions/patch/executor.ts +95 -1
  87. package/extensions/patch/index.ts +1 -0
  88. package/extensions/publish/index.ts +18 -85
  89. package/extensions/publish/publish-ui.ts +0 -4
  90. package/extensions/soul/README.md +7 -4
  91. package/extensions/soul/index.ts +11 -5
  92. package/extensions/soul/prompt.ts +30 -14
  93. package/extensions/soul/settings.ts +7 -2
  94. package/extensions/subagent/agents/context-sync.md +6 -10
  95. package/extensions/subagent/agents/review.md +44 -24
  96. package/extensions/subagent/agents/scout.md +53 -34
  97. package/extensions/subagent/run.ts +1 -1
  98. package/extensions/tau-help/help.md +2 -2
  99. package/package.json +6 -5
  100. package/schemas/tau.schema.json +51 -6
  101. package/{extensions/explore → shared}/autoread.ts +21 -7
  102. package/shared/bounded-text-result.ts +304 -0
  103. package/shared/events.ts +1 -0
  104. package/shared/full-file-knowledge.ts +49 -0
  105. package/shared/temporary-output-store.ts +275 -0
  106. package/extensions/explore/ast-tools.ts +0 -828
  107. package/extensions/explore/ast-worker.ts +0 -356
  108. package/extensions/explore/find.ts +0 -147
  109. package/extensions/explore/full-file-knowledge.ts +0 -234
  110. package/extensions/explore/grep.ts +0 -749
  111. package/extensions/explore/limits.ts +0 -22
  112. package/extensions/explore/ls.ts +0 -113
  113. package/extensions/explore/path-display.ts +0 -41
  114. package/extensions/explore/path-tree.ts +0 -150
  115. package/extensions/explore/read-cache.ts +0 -273
  116. package/extensions/explore/read-snapshots.ts +0 -57
  117. package/extensions/explore/read-stats-panel.ts +0 -154
  118. package/extensions/explore/read-stats.ts +0 -120
  119. package/extensions/explore/read.ts +0 -335
  120. package/extensions/explore/result.ts +0 -29
  121. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -0,0 +1,611 @@
1
+ import type { Node, Tree } from "web-tree-sitter";
2
+ import type { ExtractResult, GrammarAdapter, LanguageCapabilities } from "../adapter.ts";
3
+ import { resolveKotlinFileDep } from "./jvm-file-deps.ts";
4
+ import type { CallSite, Decl, ImportRef, Visibility } from "../ir.ts";
5
+ import {
6
+ applyDoc,
7
+ callSite,
8
+ docSpanBefore,
9
+ docSpanTrailingChild,
10
+ endLine,
11
+ finishDecl,
12
+ nameText,
13
+ qualify,
14
+ startLine,
15
+ walkBodyNodes,
16
+ type DocSpan,
17
+ } from "./tree.ts";
18
+
19
+ const CAPABILITIES: LanguageCapabilities = {
20
+ shape: true,
21
+ search: true,
22
+ fileDeps: true,
23
+ callEdges: true,
24
+ packageSurface: false,
25
+ };
26
+
27
+ const IMPORT_NOISE = new Set([
28
+ "as",
29
+ "break",
30
+ "class",
31
+ "continue",
32
+ "do",
33
+ "else",
34
+ "false",
35
+ "for",
36
+ "fun",
37
+ "if",
38
+ "in",
39
+ "interface",
40
+ "is",
41
+ "null",
42
+ "object",
43
+ "package",
44
+ "return",
45
+ "super",
46
+ "this",
47
+ "throw",
48
+ "true",
49
+ "try",
50
+ "typealias",
51
+ "typeof",
52
+ "val",
53
+ "var",
54
+ "when",
55
+ "while",
56
+ "by",
57
+ "catch",
58
+ "constructor",
59
+ "delegate",
60
+ "dynamic",
61
+ "field",
62
+ "file",
63
+ "finally",
64
+ "get",
65
+ "import",
66
+ "init",
67
+ "param",
68
+ "property",
69
+ "receiver",
70
+ "set",
71
+ "setparam",
72
+ "where",
73
+ "actual",
74
+ "abstract",
75
+ "annotation",
76
+ "companion",
77
+ "const",
78
+ "crossinline",
79
+ "data",
80
+ "enum",
81
+ "expect",
82
+ "external",
83
+ "final",
84
+ "infix",
85
+ "inline",
86
+ "inner",
87
+ "internal",
88
+ "lateinit",
89
+ "noinline",
90
+ "open",
91
+ "operator",
92
+ "out",
93
+ "override",
94
+ "private",
95
+ "protected",
96
+ "public",
97
+ "reified",
98
+ "sealed",
99
+ "suspend",
100
+ "tailrec",
101
+ "vararg",
102
+ ]);
103
+
104
+ const EXTENSIONS = [".kt", ".ktm", ".kts"] as const;
105
+
106
+ // Node names from fwcd/tree-sitter-kotlin — do not assume Java grammar names.
107
+ const CLASS_DECLARATION = "class_declaration";
108
+ const OBJECT_DECLARATION = "object_declaration";
109
+ const FUNCTION_DECLARATION = "function_declaration";
110
+ const PROPERTY_DECLARATION = "property_declaration";
111
+ const TYPE_ALIAS = "type_alias";
112
+ const COMPANION_OBJECT = "companion_object";
113
+ const SECONDARY_CONSTRUCTOR = "secondary_constructor";
114
+ const CLASS_BODY = "class_body";
115
+ const ENUM_CLASS_BODY = "enum_class_body";
116
+ const ENUM_ENTRY = "enum_entry";
117
+ const PRIMARY_CONSTRUCTOR = "primary_constructor";
118
+ const CLASS_PARAMETER = "class_parameter";
119
+ const MODIFIERS = "modifiers";
120
+ const VISIBILITY_MODIFIER = "visibility_modifier";
121
+ const PROPERTY_MODIFIER = "property_modifier";
122
+ const TYPE_IDENTIFIER = "type_identifier";
123
+ const SIMPLE_IDENTIFIER = "simple_identifier";
124
+ const VARIABLE_DECLARATION = "variable_declaration";
125
+ const FUNCTION_BODY = "function_body";
126
+ const PACKAGE_HEADER = "package_header";
127
+ const IMPORT_LIST = "import_list";
128
+ const IMPORT_HEADER = "import_header";
129
+ const LINE_COMMENT = "line_comment";
130
+ const MULTILINE_COMMENT = "multiline_comment";
131
+
132
+ const COMMENT_TYPES = [LINE_COMMENT, MULTILINE_COMMENT] as const;
133
+
134
+ const NESTED_SCOPE = new Set(["function_declaration", "class_declaration", "object_declaration", "companion_object"]);
135
+
136
+ function extractCalls(body: Node | null): CallSite[] {
137
+ if (body === null) return [];
138
+ const out: CallSite[] = [];
139
+ walkBodyNodes(
140
+ body,
141
+ (n) => NESTED_SCOPE.has(n.type),
142
+ (node) => {
143
+ if (node.type === "constructor_invocation") {
144
+ const user = node.namedChildren.find((c) => c.type === "user_type") ?? node.namedChildren[0];
145
+ if (user === undefined) return;
146
+ const ids = [...user.descendantsOfType("type_identifier"), ...user.descendantsOfType("simple_identifier")];
147
+ const last = ids[ids.length - 1] ?? user;
148
+ const site = callSite(last.text, startLine(last), last.startIndex, last.endIndex, "construct");
149
+ if (site !== undefined) out.push(site);
150
+ return;
151
+ }
152
+ if (node.type !== "call_expression") return;
153
+ const callee = node.namedChildren[0];
154
+ if (callee === undefined) return;
155
+ let leafNode = callee;
156
+ let receiver = "";
157
+ if (callee.type === "navigation_expression") {
158
+ const ids = callee.descendantsOfType("simple_identifier");
159
+ const last = ids[ids.length - 1];
160
+ if (last !== undefined) leafNode = last;
161
+ receiver = callee.text.slice(0, Math.max(0, callee.text.length - leafNode.text.length - 1));
162
+ }
163
+ // Bare TypeName(...) with no receiver → construct when capitalized.
164
+ const ch = leafNode.text[0];
165
+ const kind =
166
+ receiver.length === 0 && ch !== undefined && ch === ch.toUpperCase() && ch !== ch.toLowerCase()
167
+ ? "construct"
168
+ : "call";
169
+ const site = callSite(
170
+ leafNode.text,
171
+ startLine(leafNode),
172
+ leafNode.startIndex,
173
+ leafNode.endIndex,
174
+ kind,
175
+ receiver,
176
+ );
177
+ if (site !== undefined) out.push(site);
178
+ },
179
+ );
180
+ return out;
181
+ }
182
+
183
+ function heritageBases(node: Node): string[] {
184
+ const bases: string[] = [];
185
+ const pushUserType = (typeNode: Node): void => {
186
+ // user_type: package.path.Name — take last simple_identifier / type_identifier.
187
+ const ids = [
188
+ ...typeNode.descendantsOfType("type_identifier"),
189
+ ...typeNode.descendantsOfType("simple_identifier"),
190
+ ];
191
+ const last = ids[ids.length - 1];
192
+ if (last !== undefined && last.text.length > 0) bases.push(last.text);
193
+ };
194
+ const specs =
195
+ node.namedChildren.find((c) => c.type === "delegation_specifiers") ??
196
+ node.namedChildren.find((c) => c.type === "delegation_specifier");
197
+ if (specs === undefined) return [];
198
+ const list = specs.type === "delegation_specifier" ? [specs] : specs.namedChildren;
199
+ for (const spec of list) {
200
+ if (spec.type !== "delegation_specifier" && specs.type !== "delegation_specifier") {
201
+ if (spec.type === "user_type") pushUserType(spec);
202
+ continue;
203
+ }
204
+ const user = spec.descendantsOfType("user_type")[0] ?? spec.namedChildren.find((c) => c.type === "user_type");
205
+ if (user !== undefined) pushUserType(user);
206
+ }
207
+ return [...new Set(bases)];
208
+ }
209
+
210
+ function previousNamed(node: Node): Node | null {
211
+ let cursor = node.previousSibling;
212
+ while (cursor !== null && !cursor.isNamed) cursor = cursor.previousSibling;
213
+ return cursor;
214
+ }
215
+
216
+ /**
217
+ * KDoc often attaches as a trailing child of package_header / import_header instead of a sibling.
218
+ */
219
+ function kotlinDoc(node: Node, source: string): DocSpan | undefined {
220
+ const direct = docSpanBefore(node, source, [], COMMENT_TYPES);
221
+ if (direct !== undefined) return direct;
222
+
223
+ const prev = previousNamed(node);
224
+ if (prev === null) return undefined;
225
+ if (prev.type === PACKAGE_HEADER) {
226
+ return docSpanTrailingChild(prev, node, source, COMMENT_TYPES);
227
+ }
228
+ if (prev.type === IMPORT_LIST) {
229
+ const onList = docSpanTrailingChild(prev, node, source, COMMENT_TYPES);
230
+ if (onList !== undefined) return onList;
231
+ const headers = prev.namedChildren.filter((child) => child.type === IMPORT_HEADER);
232
+ const last = headers[headers.length - 1];
233
+ if (last !== undefined) return docSpanTrailingChild(last, node, source, COMMENT_TYPES);
234
+ }
235
+ return undefined;
236
+ }
237
+
238
+ function visibilityOf(node: Node): Visibility {
239
+ const mods = node.namedChildren.find((child) => child.type === MODIFIERS);
240
+ if (mods === undefined) return "public";
241
+ for (const child of mods.namedChildren) {
242
+ if (child.type !== VISIBILITY_MODIFIER) continue;
243
+ const text = child.text;
244
+ if (text === "public" || text === "private" || text === "protected" || text === "internal") return text;
245
+ }
246
+ return "public";
247
+ }
248
+
249
+ function isConstProperty(node: Node): boolean {
250
+ const mods = node.namedChildren.find((child) => child.type === MODIFIERS);
251
+ if (mods === undefined) return false;
252
+ return mods.namedChildren.some((child) => child.type === PROPERTY_MODIFIER && child.text === "const");
253
+ }
254
+
255
+ function typeIdentifierName(node: Node): string {
256
+ const direct = node.namedChildren.find((child) => child.type === TYPE_IDENTIFIER);
257
+ return direct === undefined ? "" : direct.text;
258
+ }
259
+
260
+ function simpleName(node: Node | null): string {
261
+ if (node === null) return "";
262
+ if (node.type === SIMPLE_IDENTIFIER) return node.text;
263
+ const inner = node.namedChildren.find((child) => child.type === SIMPLE_IDENTIFIER);
264
+ return inner === undefined ? nameText(node) : inner.text;
265
+ }
266
+
267
+ function classKind(node: Node): Decl["kind"] {
268
+ // Keywords are anonymous children: interface / enum / class.
269
+ let hasEnum = false;
270
+ let hasInterface = false;
271
+ for (const child of node.children) {
272
+ if (child.type === "enum") hasEnum = true;
273
+ if (child.type === "interface") hasInterface = true;
274
+ }
275
+ if (hasEnum) return "enum";
276
+ if (hasInterface) return "interface";
277
+ return "class";
278
+ }
279
+
280
+ function functionBody(node: Node): Node | null {
281
+ const body = node.namedChildren.find((child) => child.type === FUNCTION_BODY);
282
+ return body ?? null;
283
+ }
284
+
285
+ function leaf(
286
+ node: Node,
287
+ owner: string,
288
+ kind: Decl["kind"],
289
+ name: string,
290
+ visibility: Visibility,
291
+ body: Node | null,
292
+ children: Decl[],
293
+ doc: DocSpan | undefined,
294
+ ): Decl | undefined {
295
+ if (name.length === 0) return undefined;
296
+ const callable = kind === "function" || kind === "method" || kind === "constructor";
297
+ const typeLike =
298
+ kind === "class" || kind === "interface" || kind === "enum" || kind === "struct" || kind === "object";
299
+ const decl = finishDecl(
300
+ {
301
+ kind,
302
+ name,
303
+ qualifiedName: qualify(owner, name),
304
+ startLine: startLine(node),
305
+ endLine: endLine(node),
306
+ startOffset: node.startIndex,
307
+ endOffset: node.endIndex,
308
+ visibility,
309
+ exported: visibility === "public" || visibility === "internal",
310
+ children,
311
+ calls: callable ? extractCalls(body) : [],
312
+ bases: typeLike ? heritageBases(node) : [],
313
+ },
314
+ body,
315
+ );
316
+ if (body === null) decl.signatureEndOffset = node.endIndex;
317
+ return applyDoc(decl, doc);
318
+ }
319
+
320
+ function primaryConstructorProperties(ctor: Node, owner: string): Decl[] {
321
+ const out: Decl[] = [];
322
+ for (const node of ctor.namedChildren) {
323
+ if (node.type !== CLASS_PARAMETER) continue;
324
+ // Only val/var parameters become properties.
325
+ const hasBinding = node.children.some((child) => child.type === "binding_pattern_kind");
326
+ if (!hasBinding) continue;
327
+ const name = simpleName(node.namedChildren.find((child) => child.type === SIMPLE_IDENTIFIER) ?? null);
328
+ const decl = leaf(node, owner, "property", name, visibilityOf(node), null, [], undefined);
329
+ if (decl !== undefined) out.push(decl);
330
+ }
331
+ return out;
332
+ }
333
+
334
+ function propertyName(node: Node): string {
335
+ const variable = node.namedChildren.find((child) => child.type === VARIABLE_DECLARATION);
336
+ if (variable === undefined) return "";
337
+ return simpleName(variable.namedChildren.find((child) => child.type === SIMPLE_IDENTIFIER) ?? null);
338
+ }
339
+
340
+ function walkClassBody(body: Node, owner: string, source: string): Decl[] {
341
+ const out: Decl[] = [];
342
+ for (const node of body.namedChildren) {
343
+ if (node.type === LINE_COMMENT || node.type === MULTILINE_COMMENT) continue;
344
+ if (node.type === ENUM_ENTRY) {
345
+ const name = simpleName(node.namedChildren.find((child) => child.type === SIMPLE_IDENTIFIER) ?? null);
346
+ const decl = leaf(node, owner, "enumMember", name, "public", null, [], undefined);
347
+ if (decl !== undefined) out.push(decl);
348
+ continue;
349
+ }
350
+ out.push(...declsFromNode(node, owner, source));
351
+ }
352
+ return out;
353
+ }
354
+
355
+ function declsFromNode(node: Node, owner: string, source: string): Decl[] {
356
+ if (node.isError || node.type === "ERROR") {
357
+ const out: Decl[] = [];
358
+ for (const child of node.children) {
359
+ if (child.isNamed || child.isError) out.push(...declsFromNode(child, owner, source));
360
+ }
361
+ return out;
362
+ }
363
+
364
+ const doc = kotlinDoc(node, source);
365
+ const visibility = visibilityOf(node);
366
+
367
+ switch (node.type) {
368
+ case CLASS_DECLARATION: {
369
+ const name = typeIdentifierName(node);
370
+ const kind = classKind(node);
371
+ const qn = qualify(owner, name);
372
+ const children: Decl[] = [];
373
+ const primary = node.namedChildren.find((child) => child.type === PRIMARY_CONSTRUCTOR);
374
+ if (primary !== undefined) children.push(...primaryConstructorProperties(primary, qn));
375
+ const body =
376
+ node.namedChildren.find((child) => child.type === CLASS_BODY || child.type === ENUM_CLASS_BODY) ?? null;
377
+ if (body !== null) children.push(...walkClassBody(body, qn, source));
378
+ const decl = leaf(node, owner, kind, name, visibility, body, children, doc);
379
+ return decl === undefined ? [] : [decl];
380
+ }
381
+ case OBJECT_DECLARATION: {
382
+ const name = typeIdentifierName(node);
383
+ const body = node.namedChildren.find((child) => child.type === CLASS_BODY) ?? null;
384
+ const qn = qualify(owner, name);
385
+ const children = body === null ? [] : walkClassBody(body, qn, source);
386
+ const decl = leaf(node, owner, "object", name, visibility, body, children, doc);
387
+ return decl === undefined ? [] : [decl];
388
+ }
389
+ case COMPANION_OBJECT: {
390
+ // Nest under owner; use explicit name or "Companion".
391
+ const named = typeIdentifierName(node);
392
+ const name = named.length > 0 ? named : "Companion";
393
+ const body = node.namedChildren.find((child) => child.type === CLASS_BODY) ?? null;
394
+ const qn = qualify(owner, name);
395
+ const children = body === null ? [] : walkClassBody(body, qn, source);
396
+ const decl = leaf(node, owner, "object", name, visibility, body, children, doc);
397
+ return decl === undefined ? [] : [decl];
398
+ }
399
+ case FUNCTION_DECLARATION: {
400
+ const name = simpleName(node.namedChildren.find((child) => child.type === SIMPLE_IDENTIFIER) ?? null);
401
+ const body = functionBody(node);
402
+ const kind = owner.length > 0 ? "method" : "function";
403
+ const decl = leaf(node, owner, kind, name, visibility, body, [], doc);
404
+ return decl === undefined ? [] : [decl];
405
+ }
406
+ case SECONDARY_CONSTRUCTOR: {
407
+ const decl = leaf(node, owner, "constructor", "constructor", visibility, null, [], doc);
408
+ return decl === undefined ? [] : [decl];
409
+ }
410
+ case PROPERTY_DECLARATION: {
411
+ const name = propertyName(node);
412
+ const kind = isConstProperty(node) ? "constant" : "property";
413
+ const getter = node.namedChildren.find((child) => child.type === "getter");
414
+ const body = getter === undefined ? null : (functionBody(getter) ?? getter);
415
+ const decl = leaf(node, owner, kind, name, visibility, body, [], doc);
416
+ return decl === undefined ? [] : [decl];
417
+ }
418
+ case TYPE_ALIAS: {
419
+ const name = typeIdentifierName(node);
420
+ const decl = leaf(node, owner, "typeAlias", name, visibility, null, [], doc);
421
+ return decl === undefined ? [] : [decl];
422
+ }
423
+ default:
424
+ // fwcd/tree-sitter-kotlin#75: newline / actual primary ctor splits class from body.
425
+ // Body lands as call_expression / comparison_expression + lambda; walk those only.
426
+ if (node.type === "infix_expression") {
427
+ const companion = companionFromMisparsedInfix(node, owner, source);
428
+ if (companion !== undefined) return [companion];
429
+ }
430
+ if (RECOVERY_CONTAINERS.has(node.type)) {
431
+ const out: Decl[] = [];
432
+ for (const child of node.namedChildren) {
433
+ out.push(...declsFromNode(child, owner, source));
434
+ }
435
+ return out;
436
+ }
437
+ return [];
438
+ }
439
+ }
440
+
441
+ /**
442
+ * When class body fails to attach (fwcd/tree-sitter-kotlin#75 ASI split), companion forms as:
443
+ * A) companion object { } → infix(simple_identifier companion, simple_identifier object, lambda_literal)
444
+ * B) actual companion object { } → infix(..., companion, object_literal) // "object" inside object_literal
445
+ * Observed on okio ByteString expect/actual variants.
446
+ */
447
+ function companionFromMisparsedInfix(node: Node, owner: string, source: string): Decl | undefined {
448
+ const named = node.namedChildren;
449
+ let companionIdx = -1;
450
+ for (let i = 0; i < named.length; i += 1) {
451
+ const child = named[i];
452
+ if (child !== undefined && child.type === SIMPLE_IDENTIFIER && child.text === "companion") {
453
+ companionIdx = i;
454
+ break;
455
+ }
456
+ }
457
+ if (companionIdx < 0) return undefined;
458
+ const after = named[companionIdx + 1];
459
+ if (after === undefined) return undefined;
460
+
461
+ let body: Node | null = null;
462
+ let memberRoot: Node | null = null;
463
+
464
+ if (after.type === SIMPLE_IDENTIFIER && after.text === "object") {
465
+ const bodyNode = named[companionIdx + 2];
466
+ if (bodyNode === undefined) return undefined;
467
+ if (bodyNode.type === "lambda_literal") {
468
+ body = bodyNode;
469
+ memberRoot = bodyNode.namedChildren.find((child) => child.type === "statements") ?? null;
470
+ } else if (bodyNode.type === "annotated_lambda") {
471
+ const lambda = bodyNode.namedChildren.find((child) => child.type === "lambda_literal");
472
+ if (lambda === undefined) return undefined;
473
+ body = lambda;
474
+ memberRoot = lambda.namedChildren.find((child) => child.type === "statements") ?? null;
475
+ } else {
476
+ return undefined;
477
+ }
478
+ } else if (after.type === "object_literal") {
479
+ body = after;
480
+ memberRoot = after.namedChildren.find((child) => child.type === CLASS_BODY) ?? null;
481
+ } else {
482
+ return undefined;
483
+ }
484
+
485
+ const name = "Companion";
486
+ const qn = qualify(owner, name);
487
+ const children =
488
+ memberRoot === null
489
+ ? []
490
+ : memberRoot.type === CLASS_BODY
491
+ ? walkClassBody(memberRoot, qn, source)
492
+ : memberRoot.namedChildren.flatMap((child) => declsFromNode(child, qn, source));
493
+ return leaf(node, owner, "object", name, visibilityOf(node), body, children, kotlinDoc(node, source));
494
+ }
495
+
496
+ /** Recovery containers only — not a general expression walk. */
497
+ const RECOVERY_CONTAINERS = new Set([
498
+ "comparison_expression",
499
+ "lambda_literal",
500
+ "annotated_lambda",
501
+ "statements",
502
+ "call_expression",
503
+ "call_suffix",
504
+ "annotated_expression",
505
+ "prefix_expression",
506
+ // infix_expression handled above for companion; still walk other infixes for nested decls.
507
+ "infix_expression",
508
+ ]);
509
+
510
+ const TYPE_KINDS = new Set<Decl["kind"]>(["class", "interface", "object", "enum", "struct"]);
511
+
512
+ function requalify(decl: Decl, owner: string): Decl {
513
+ const qualifiedName = qualify(owner, decl.name);
514
+ const kind =
515
+ decl.kind === "function" && owner.length > 0
516
+ ? "method"
517
+ : decl.kind === "method" && owner.length === 0
518
+ ? "function"
519
+ : decl.kind;
520
+ return {
521
+ ...decl,
522
+ kind,
523
+ qualifiedName,
524
+ children: decl.children.map((child) => requalify(child, qualifiedName)),
525
+ };
526
+ }
527
+
528
+ function isRehomeBarrier(decl: Decl): boolean {
529
+ if (decl.kind === "typeAlias") return true;
530
+ // Misparsed companion becomes a top-level object; still absorb into the empty class.
531
+ if (decl.kind === "object" && decl.name === "Companion") return false;
532
+ return TYPE_KINDS.has(decl.kind);
533
+ }
534
+
535
+ /**
536
+ * When expect/actual class body fails to attach (fwcd#75), members land as following top-level decls.
537
+ * Fold free functions/properties/Companion into the preceding empty type until the next real type.
538
+ */
539
+ function rehomeOrphanMembers(decls: Decl[]): Decl[] {
540
+ const out: Decl[] = [];
541
+ for (let i = 0; i < decls.length; i += 1) {
542
+ const decl = decls[i];
543
+ if (decl === undefined) continue;
544
+ if (!TYPE_KINDS.has(decl.kind) || decl.children.length > 0) {
545
+ out.push(decl);
546
+ continue;
547
+ }
548
+ const members: Decl[] = [];
549
+ let j = i + 1;
550
+ while (j < decls.length) {
551
+ const next = decls[j];
552
+ if (next === undefined || isRehomeBarrier(next)) break;
553
+ members.push(requalify(next, decl.qualifiedName));
554
+ j += 1;
555
+ }
556
+ if (members.length === 0) {
557
+ out.push(decl);
558
+ continue;
559
+ }
560
+ const last = members[members.length - 1];
561
+ out.push({
562
+ ...decl,
563
+ children: members,
564
+ endLine: last === undefined ? decl.endLine : last.endLine,
565
+ endOffset: last === undefined ? decl.endOffset : last.endOffset,
566
+ });
567
+ i = j - 1;
568
+ }
569
+ return out;
570
+ }
571
+
572
+ function collectImports(root: Node): ImportRef[] {
573
+ const imports: ImportRef[] = [];
574
+ for (const node of root.namedChildren) {
575
+ if (node.type !== IMPORT_LIST) continue;
576
+ for (const header of node.namedChildren) {
577
+ if (header.type !== IMPORT_HEADER) continue;
578
+ const id = header.namedChildren.find((child) => child.type === "identifier");
579
+ const specifier = id === undefined ? header.text.replace(/^import\s+/, "").trim() : id.text;
580
+ imports.push({
581
+ specifier,
582
+ startLine: startLine(header),
583
+ startOffset: header.startIndex,
584
+ endOffset: header.endIndex,
585
+ bindings: [],
586
+ });
587
+ }
588
+ }
589
+ return imports;
590
+ }
591
+
592
+ function extractKotlin(tree: Tree, source: string): ExtractResult {
593
+ const root = tree.rootNode;
594
+ const decls: Decl[] = [];
595
+ for (const node of root.children) {
596
+ if (node.isNamed || node.isError) decls.push(...declsFromNode(node, "", source));
597
+ }
598
+ return { decls: rehomeOrphanMembers(decls), imports: collectImports(root) };
599
+ }
600
+
601
+ export function kotlinAdapter(): GrammarAdapter {
602
+ return {
603
+ mode: "grammar",
604
+ id: "kotlin",
605
+ extensions: EXTENSIONS,
606
+ capabilities: CAPABILITIES,
607
+ importNoiseIdentifiers: IMPORT_NOISE,
608
+ extract: extractKotlin,
609
+ resolveFileDep: resolveKotlinFileDep,
610
+ };
611
+ }