clast-ai 0.1.2 → 0.2.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 (294) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +468 -398
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +266 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/migrate.d.ts +35 -0
  59. package/dist/migrate.js +194 -0
  60. package/dist/migrate.js.map +1 -0
  61. package/dist/parser/extractor.d.ts +21 -16
  62. package/dist/parser/extractor.js +700 -228
  63. package/dist/parser/extractor.js.map +1 -1
  64. package/dist/parser/index.d.ts +17 -21
  65. package/dist/parser/index.js +115 -171
  66. package/dist/parser/index.js.map +1 -1
  67. package/dist/parser/langs/c.d.ts +3 -0
  68. package/dist/parser/langs/c.js +879 -0
  69. package/dist/parser/langs/c.js.map +1 -0
  70. package/dist/parser/langs/csharp.d.ts +2 -0
  71. package/dist/parser/langs/csharp.js +548 -0
  72. package/dist/parser/langs/csharp.js.map +1 -0
  73. package/dist/parser/langs/dart.d.ts +2 -0
  74. package/dist/parser/langs/dart.js +893 -0
  75. package/dist/parser/langs/dart.js.map +1 -0
  76. package/dist/parser/langs/go.d.ts +2 -0
  77. package/dist/parser/langs/go.js +455 -0
  78. package/dist/parser/langs/go.js.map +1 -0
  79. package/dist/parser/langs/java.d.ts +2 -0
  80. package/dist/parser/langs/java.js +390 -0
  81. package/dist/parser/langs/java.js.map +1 -0
  82. package/dist/parser/langs/php.d.ts +2 -0
  83. package/dist/parser/langs/php.js +486 -0
  84. package/dist/parser/langs/php.js.map +1 -0
  85. package/dist/parser/langs/python.d.ts +2 -0
  86. package/dist/parser/langs/python.js +216 -0
  87. package/dist/parser/langs/python.js.map +1 -0
  88. package/dist/parser/langs/ruby.d.ts +2 -0
  89. package/dist/parser/langs/ruby.js +602 -0
  90. package/dist/parser/langs/ruby.js.map +1 -0
  91. package/dist/parser/langs/rust.d.ts +2 -0
  92. package/dist/parser/langs/rust.js +931 -0
  93. package/dist/parser/langs/rust.js.map +1 -0
  94. package/dist/parser/langs/swift.d.ts +2 -0
  95. package/dist/parser/langs/swift.js +616 -0
  96. package/dist/parser/langs/swift.js.map +1 -0
  97. package/dist/parser/langs/typescript.d.ts +4 -0
  98. package/dist/parser/langs/typescript.js +535 -0
  99. package/dist/parser/langs/typescript.js.map +1 -0
  100. package/dist/parser/registry.d.ts +16 -0
  101. package/dist/parser/registry.js +87 -0
  102. package/dist/parser/registry.js.map +1 -0
  103. package/dist/parser/returns.d.ts +9 -0
  104. package/dist/parser/returns.js +152 -0
  105. package/dist/parser/returns.js.map +1 -0
  106. package/dist/parser/spec.d.ts +165 -0
  107. package/dist/parser/spec.js +188 -0
  108. package/dist/parser/spec.js.map +1 -0
  109. package/dist/query/arity.d.ts +27 -0
  110. package/dist/query/arity.js +187 -0
  111. package/dist/query/arity.js.map +1 -0
  112. package/dist/query/callgraph.d.ts +113 -0
  113. package/dist/query/callgraph.js +332 -0
  114. package/dist/query/callgraph.js.map +1 -0
  115. package/dist/query/format.d.ts +36 -0
  116. package/dist/query/format.js +136 -0
  117. package/dist/query/format.js.map +1 -0
  118. package/dist/query/hierarchy.d.ts +44 -0
  119. package/dist/query/hierarchy.js +441 -0
  120. package/dist/query/hierarchy.js.map +1 -0
  121. package/dist/query/resolve.d.ts +265 -0
  122. package/dist/query/resolve.js +1957 -0
  123. package/dist/query/resolve.js.map +1 -0
  124. package/dist/query/search.d.ts +35 -0
  125. package/dist/query/search.js +379 -0
  126. package/dist/query/search.js.map +1 -0
  127. package/dist/query/words.d.ts +30 -0
  128. package/dist/query/words.js +201 -0
  129. package/dist/query/words.js.map +1 -0
  130. package/dist/server/index.d.ts +1 -3
  131. package/dist/server/index.js +313 -73
  132. package/dist/server/index.js.map +1 -1
  133. package/dist/tools/context.d.ts +35 -0
  134. package/dist/tools/context.js +79 -0
  135. package/dist/tools/context.js.map +1 -0
  136. package/dist/tools/explore.d.ts +51 -0
  137. package/dist/tools/explore.js +779 -0
  138. package/dist/tools/explore.js.map +1 -0
  139. package/dist/tools/impact.d.ts +21 -0
  140. package/dist/tools/impact.js +743 -0
  141. package/dist/tools/impact.js.map +1 -0
  142. package/dist/tools/search.d.ts +59 -0
  143. package/dist/tools/search.js +843 -0
  144. package/dist/tools/search.js.map +1 -0
  145. package/dist/tools/stats.d.ts +26 -0
  146. package/dist/tools/stats.js +604 -0
  147. package/dist/tools/stats.js.map +1 -0
  148. package/dist/types.d.ts +95 -40
  149. package/dist/types.js +9 -1
  150. package/dist/types.js.map +1 -1
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/watcher/index.d.ts +18 -16
  155. package/dist/watcher/index.js +94 -77
  156. package/dist/watcher/index.js.map +1 -1
  157. package/package.json +21 -24
  158. package/plugin/server.mjs +72 -0
  159. package/skills/audit/SKILL.md +37 -0
  160. package/skills/deadcode/SKILL.md +33 -0
  161. package/skills/explore/SKILL.md +54 -0
  162. package/skills/impact/SKILL.md +24 -0
  163. package/skills/status/SKILL.md +11 -0
  164. package/src/cli.ts +265 -0
  165. package/src/config/defaults.ts +50 -32
  166. package/src/config/index.ts +23 -14
  167. package/src/db/index.ts +540 -43
  168. package/src/engine.ts +30 -0
  169. package/src/indexer/imports.ts +241 -0
  170. package/src/indexer/index.ts +369 -0
  171. package/src/indexer/process.ts +63 -0
  172. package/src/indexer/scan.ts +226 -0
  173. package/src/indexer/worker.ts +24 -0
  174. package/src/llm/comment-generator.ts +52 -103
  175. package/src/llm/index.ts +80 -80
  176. package/src/llm/prompts.ts +95 -95
  177. package/src/migrate.ts +213 -0
  178. package/src/parser/extractor.ts +643 -256
  179. package/src/parser/index.ts +105 -205
  180. package/src/parser/langs/c.ts +860 -0
  181. package/src/parser/langs/csharp.ts +519 -0
  182. package/src/parser/langs/dart.ts +820 -0
  183. package/src/parser/langs/go.ts +437 -0
  184. package/src/parser/langs/java.ts +361 -0
  185. package/src/parser/langs/php.ts +450 -0
  186. package/src/parser/langs/python.ts +206 -0
  187. package/src/parser/langs/ruby.ts +554 -0
  188. package/src/parser/langs/rust.ts +904 -0
  189. package/src/parser/langs/swift.ts +590 -0
  190. package/src/parser/langs/typescript.ts +499 -0
  191. package/src/parser/registry.ts +88 -0
  192. package/src/parser/returns.ts +137 -0
  193. package/src/parser/spec.ts +309 -0
  194. package/src/query/arity.ts +173 -0
  195. package/src/query/callgraph.ts +473 -0
  196. package/src/query/format.ts +136 -0
  197. package/src/query/hierarchy.ts +447 -0
  198. package/src/query/resolve.ts +1972 -0
  199. package/src/query/search.ts +430 -0
  200. package/src/query/words.ts +183 -0
  201. package/src/server/index.ts +390 -79
  202. package/src/tools/context.ts +82 -0
  203. package/src/tools/explore.ts +860 -0
  204. package/src/tools/impact.ts +748 -0
  205. package/src/tools/search.ts +886 -0
  206. package/src/tools/stats.ts +633 -0
  207. package/src/types.ts +113 -45
  208. package/src/version.ts +2 -0
  209. package/src/watcher/index.ts +82 -88
  210. package/tsconfig.json +6 -3
  211. package/.mcp.json +0 -8
  212. package/commands/clast-status.md +0 -9
  213. package/dist/config/defaults.d.ts.map +0 -1
  214. package/dist/config/index.d.ts.map +0 -1
  215. package/dist/db/index.d.ts.map +0 -1
  216. package/dist/db/queries.d.ts +0 -44
  217. package/dist/db/queries.d.ts.map +0 -1
  218. package/dist/db/queries.js +0 -251
  219. package/dist/db/queries.js.map +0 -1
  220. package/dist/db/schema.d.ts +0 -3
  221. package/dist/db/schema.d.ts.map +0 -1
  222. package/dist/db/schema.js +0 -94
  223. package/dist/db/schema.js.map +0 -1
  224. package/dist/db/types.d.ts +0 -44
  225. package/dist/db/types.d.ts.map +0 -1
  226. package/dist/db/types.js +0 -2
  227. package/dist/db/types.js.map +0 -1
  228. package/dist/llm/comment-generator.d.ts.map +0 -1
  229. package/dist/llm/index.d.ts.map +0 -1
  230. package/dist/llm/prompts.d.ts.map +0 -1
  231. package/dist/parser/call-resolver.d.ts +0 -15
  232. package/dist/parser/call-resolver.d.ts.map +0 -1
  233. package/dist/parser/call-resolver.js +0 -16
  234. package/dist/parser/call-resolver.js.map +0 -1
  235. package/dist/parser/extractor.d.ts.map +0 -1
  236. package/dist/parser/index.d.ts.map +0 -1
  237. package/dist/parser/languages.d.ts +0 -11
  238. package/dist/parser/languages.d.ts.map +0 -1
  239. package/dist/parser/languages.js +0 -74
  240. package/dist/parser/languages.js.map +0 -1
  241. package/dist/server/index.d.ts.map +0 -1
  242. package/dist/server/register-tools.d.ts +0 -7
  243. package/dist/server/register-tools.d.ts.map +0 -1
  244. package/dist/server/register-tools.js +0 -17
  245. package/dist/server/register-tools.js.map +0 -1
  246. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  247. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  248. package/dist/server/tools/clast-call-graph.js +0 -70
  249. package/dist/server/tools/clast-call-graph.js.map +0 -1
  250. package/dist/server/tools/clast-comment.d.ts +0 -5
  251. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  252. package/dist/server/tools/clast-comment.js +0 -64
  253. package/dist/server/tools/clast-comment.js.map +0 -1
  254. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  255. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  256. package/dist/server/tools/clast-file-summary.js +0 -42
  257. package/dist/server/tools/clast-file-summary.js.map +0 -1
  258. package/dist/server/tools/clast-get-context.d.ts +0 -4
  259. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  260. package/dist/server/tools/clast-get-context.js +0 -80
  261. package/dist/server/tools/clast-get-context.js.map +0 -1
  262. package/dist/server/tools/clast-reindex.d.ts +0 -5
  263. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  264. package/dist/server/tools/clast-reindex.js +0 -36
  265. package/dist/server/tools/clast-reindex.js.map +0 -1
  266. package/dist/server/tools/clast-search.d.ts +0 -4
  267. package/dist/server/tools/clast-search.d.ts.map +0 -1
  268. package/dist/server/tools/clast-search.js +0 -29
  269. package/dist/server/tools/clast-search.js.map +0 -1
  270. package/dist/server/tools/clast-status.d.ts +0 -5
  271. package/dist/server/tools/clast-status.d.ts.map +0 -1
  272. package/dist/server/tools/clast-status.js +0 -23
  273. package/dist/server/tools/clast-status.js.map +0 -1
  274. package/dist/types.d.ts.map +0 -1
  275. package/dist/watcher/gitignore.d.ts +0 -9
  276. package/dist/watcher/gitignore.d.ts.map +0 -1
  277. package/dist/watcher/gitignore.js +0 -38
  278. package/dist/watcher/gitignore.js.map +0 -1
  279. package/dist/watcher/index.d.ts.map +0 -1
  280. package/skills/clast-explore/SKILL.md +0 -41
  281. package/src/db/queries.ts +0 -332
  282. package/src/db/schema.ts +0 -106
  283. package/src/db/types.ts +0 -47
  284. package/src/parser/call-resolver.ts +0 -17
  285. package/src/parser/languages.ts +0 -86
  286. package/src/server/register-tools.ts +0 -30
  287. package/src/server/tools/clast-call-graph.ts +0 -82
  288. package/src/server/tools/clast-comment.ts +0 -76
  289. package/src/server/tools/clast-file-summary.ts +0 -59
  290. package/src/server/tools/clast-get-context.ts +0 -97
  291. package/src/server/tools/clast-reindex.ts +0 -52
  292. package/src/server/tools/clast-search.ts +0 -38
  293. package/src/server/tools/clast-status.ts +0 -37
  294. package/src/watcher/gitignore.ts +0 -42
@@ -0,0 +1,450 @@
1
+ import type { Node } from 'web-tree-sitter';
2
+ import type { Ctx, LangSpec, Visitor } from '../spec.js';
3
+ import { COMPLEX_RECEIVER, field } from '../spec.js';
4
+ import type { SymbolKind } from '../../types.js';
5
+
6
+ // Namespaces are deliberately not visited: `namespace X;` and `namespace X { }`
7
+ // neither create a symbol nor a qualified-name prefix, so qnames are
8
+ // `Class.method` regardless of the namespace form.
9
+
10
+ /** `Foo` / `\A\B\Foo` / `namespace\Foo` -> `Foo`. */
11
+ function shortName(node: Node | null | undefined): string | null {
12
+ if (!node) return null;
13
+ if (node.type === 'name') return node.text;
14
+ if (node.type === 'qualified_name') {
15
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
16
+ const c = node.namedChild(i);
17
+ if (c && c.type === 'name') return c.text;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ /** Literal content of a plain (non-interpolated) string, else null. */
24
+ function literal(node: Node | null | undefined): string | null {
25
+ if (!node || (node.type !== 'string' && node.type !== 'encapsed_string')) return null;
26
+ if (node.namedChildCount !== 1) return null;
27
+ const c = node.namedChild(0);
28
+ return c && c.type === 'string_content' ? c.text : null;
29
+ }
30
+
31
+ /** First visibility keyword among the direct children, or null. */
32
+ function visibility(node: Node): string | null {
33
+ for (const c of node.namedChildren) {
34
+ if (!c) continue;
35
+ if (c.type === 'visibility_modifier') return c.text;
36
+ // Modifiers always precede the name / elements.
37
+ if (c.type === 'name' || c.type === 'property_element' || c.type === 'const_element' || c.type === 'formal_parameters') break;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function isPublic(node: Node): boolean {
43
+ const v = visibility(node);
44
+ // `public(set)`-style asymmetric visibility still reads as public.
45
+ return v === null || v.startsWith('public');
46
+ }
47
+
48
+ /** Start of the declaration proper, skipping leading `#[Attributes]`. */
49
+ function declStart(node: Node): number {
50
+ const attrs = field(node, 'attributes');
51
+ if (!attrs || attrs.startIndex !== node.startIndex) return node.startIndex;
52
+ const next = attrs.nextSibling;
53
+ return next ? next.startIndex : attrs.endIndex;
54
+ }
55
+
56
+ /** Signature: declaration text without attributes, up to the body. */
57
+ function signature(node: Node, ctx: Ctx, body: Node | null = field(node, 'body')): string {
58
+ const start = declStart(node);
59
+ let end = body && body.startIndex > start ? body.startIndex : node.endIndex;
60
+ if (end - start > 400) end = start + 400;
61
+ return ctx.src.slice(start, end).replace(/\s*[;{]\s*$/, '');
62
+ }
63
+
64
+ /** base_clause -> extends, class_interface_clause -> implements, `use Trait;` -> implements. */
65
+ function heritage(node: Node, body: Node | null, sym: number, ctx: Ctx): void {
66
+ for (const c of node.namedChildren) {
67
+ if (!c) continue;
68
+ if (c.type === 'base_clause') {
69
+ for (const n of c.namedChildren) ctx.inherit(sym, shortName(n), 'extends');
70
+ } else if (c.type === 'class_interface_clause') {
71
+ for (const n of c.namedChildren) ctx.inherit(sym, shortName(n), 'implements');
72
+ }
73
+ }
74
+ if (!body) return;
75
+ for (const m of body.namedChildren) {
76
+ if (!m) continue;
77
+ if (m.type === 'use_declaration') {
78
+ for (const n of m.namedChildren) ctx.inherit(sym, shortName(n), 'implements');
79
+ } else if (m.type === 'method_declaration') {
80
+ promotedProperties(m, ctx);
81
+ }
82
+ }
83
+ }
84
+
85
+ /** `__construct(private Foo $foo)` declares property `foo` on the class. */
86
+ function promotedProperties(method: Node, ctx: Ctx): void {
87
+ const params = field(method, 'parameters');
88
+ if (!params || params.namedChildCount === 0) return;
89
+ const name = field(method, 'name');
90
+ if (!name || name.text.toLowerCase() !== '__construct') return;
91
+ for (const p of params.namedChildren) {
92
+ if (!p || p.type !== 'property_promotion_parameter') continue;
93
+ const v = field(p, 'name');
94
+ if (!v) continue;
95
+ const vis = field(p, 'visibility')?.text ?? null;
96
+ ctx.def(p, {
97
+ name: v.text.replace(/^&?\s*\$/, ''),
98
+ kind: 'property',
99
+ sig: ctx.src.slice(declStart(p), v.endIndex),
100
+ exported: vis === null || vis.startsWith('public'),
101
+ scope: false,
102
+ });
103
+ }
104
+ }
105
+
106
+ const typeDecl = (kind: SymbolKind): Visitor => (node, ctx) => {
107
+ const name = field(node, 'name');
108
+ if (!name) return;
109
+ const body = field(node, 'body');
110
+ const sym = ctx.def(node, { name: name.text, kind, sig: signature(node, ctx, body), exported: true });
111
+ heritage(node, body, sym, ctx);
112
+ };
113
+
114
+ /** `new class(...) extends Base { ... }` (Laravel migrations, test doubles). */
115
+ const anonymousClass: Visitor = (node, ctx) => {
116
+ const body = field(node, 'body');
117
+ const sym = ctx.def(node, { name: 'class@anonymous', kind: 'class', sig: signature(node, ctx, body), exported: false });
118
+ heritage(node, body, sym, ctx);
119
+ };
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Receiver type hints
123
+ // ---------------------------------------------------------------------------
124
+
125
+ const varName = (node: Node): string => node.text.replace(/^&?\s*(\.\.\.)?\s*\$/, '');
126
+
127
+ /** Docblock types that denote a single class: no arrays, generics or shapes. */
128
+ function docType(raw: string): string | null {
129
+ return /[\[<{(]/.test(raw) ? null : raw;
130
+ }
131
+
132
+ /** Typed parameter -> local hint; a promoted constructor parameter is also a field. */
133
+ function paramHint(p: Node, ctx: Ctx): void {
134
+ const type = field(p, 'type');
135
+ if (!type) return;
136
+ const name = field(p, 'name');
137
+ if (!name) return;
138
+ const n = varName(name);
139
+ const t = type.text;
140
+ ctx.typeHint(n, t);
141
+ if (p.type === 'property_promotion_parameter') ctx.typeHint(n, t, true);
142
+ }
143
+
144
+ /**
145
+ * Hints for the parameters of a named function / method: declared types
146
+ * first, then `@param Foo $x` from the docblock for the untyped ones (the
147
+ * first hint for a name wins).
148
+ */
149
+ function signatureHints(node: Node, ctx: Ctx): void {
150
+ const params = field(node, 'parameters');
151
+ if (!params || params.namedChildCount === 0) return;
152
+ for (const p of params.namedChildren) {
153
+ if (p && (p.type === 'simple_parameter' || p.type === 'property_promotion_parameter')) paramHint(p, ctx);
154
+ }
155
+ const doc = node.previousSibling;
156
+ if (!doc || doc.type !== 'comment') return;
157
+ const text = doc.text;
158
+ if (!text.includes('@param')) return;
159
+ for (const m of text.matchAll(/@param\s+(\S+)\s+(?:\.\.\.)?\$(\w+)/g)) {
160
+ ctx.typeHint(m[2], docType(m[1]!));
161
+ }
162
+ }
163
+
164
+ /** Parameters of closures and arrow functions (named functions are handled by their own visitor). */
165
+ const parameter: Visitor = (node, ctx) => {
166
+ const owner = node.parent?.parent;
167
+ if (owner && (owner.type === 'method_declaration' || owner.type === 'function_definition')) return;
168
+ paramHint(node, ctx);
169
+ };
170
+
171
+ /** Class named by a `new Foo(...)` expression: `Foo`, or null for anonymous / dynamic classes. */
172
+ function createdClass(node: Node): string | null {
173
+ for (const c of node.namedChildren) {
174
+ if (!c) continue;
175
+ if (c.type === 'name' || c.type === 'qualified_name') return shortName(c);
176
+ if (c.type === 'arguments' || c.type === 'anonymous_class') return null;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * `$x = new Foo(...)` -> local hint; `$this->x = new Foo(...)` -> field hint;
183
+ * `$this->x = $x` -> the field takes the declared type of the parameter `$x`.
184
+ */
185
+ const assignmentExpr: Visitor = (node, ctx) => {
186
+ const right = field(node, 'right');
187
+ if (!right) return;
188
+ const isNew = right.type === 'object_creation_expression';
189
+ if (!isNew && right.type !== 'variable_name') return;
190
+ const left = field(node, 'left');
191
+ if (!left) return;
192
+ if (left.type === 'variable_name') {
193
+ if (isNew) ctx.typeHint(varName(left), createdClass(right));
194
+ } else if (left.type === 'member_access_expression') {
195
+ const obj = field(left, 'object');
196
+ const name = field(left, 'name');
197
+ if (obj && name && obj.type === 'variable_name' && name.type === 'name' && obj.text === '$this') {
198
+ ctx.typeHint(name.text, isNew ? createdClass(right) : ctx.typeOf(varName(right)), true);
199
+ }
200
+ }
201
+ };
202
+
203
+ /** `catch (FooException $e)`; multi-catch is ambiguous and skipped. */
204
+ const catchClause: Visitor = (node, ctx) => {
205
+ const type = field(node, 'type');
206
+ const name = field(node, 'name');
207
+ if (!type || !name || type.namedChildCount !== 1) return;
208
+ ctx.typeHint(varName(name), type.text);
209
+ };
210
+
211
+ /** Inline `@var Foo $x` annotations (short comments only, to stay cheap). */
212
+ const comment: Visitor = (node, ctx) => {
213
+ if (node.endIndex - node.startIndex > 120) return;
214
+ const m = /@var\s+(\S+)\s+\$(\w+)/.exec(node.text);
215
+ if (m) ctx.typeHint(m[2], docType(m[1]!));
216
+ };
217
+
218
+ const functionDef: Visitor = (node, ctx) => {
219
+ const name = field(node, 'name');
220
+ if (!name) return;
221
+ ctx.def(node, { name: name.text, kind: 'function', sig: signature(node, ctx), exported: true });
222
+ signatureHints(node, ctx);
223
+ };
224
+
225
+ const methodDecl: Visitor = (node, ctx) => {
226
+ const name = field(node, 'name');
227
+ if (!name) return;
228
+ ctx.def(node, { name: name.text, kind: 'method', sig: signature(node, ctx), exported: isPublic(node) });
229
+ signatureHints(node, ctx);
230
+ };
231
+
232
+ /** `protected static ?string $a = 1, $b;` -> one property per element. */
233
+ const propertyDecl: Visitor = (node, ctx) => {
234
+ const exported = isPublic(node);
235
+ // Declared type, else a `@var Foo` docblock (the usual style in pre-7.4 code).
236
+ let type: string | null = field(node, 'type')?.text ?? null;
237
+ if (type === null) {
238
+ const doc = node.previousSibling;
239
+ if (doc && doc.type === 'comment') {
240
+ const m = /@var\s+(\S+)/.exec(doc.text);
241
+ if (m) type = docType(m[1]!);
242
+ }
243
+ }
244
+ let prefix: string | null = null;
245
+ for (const el of node.namedChildren) {
246
+ if (!el || el.type !== 'property_element') continue;
247
+ const v = field(el, 'name') ?? el.namedChild(0);
248
+ if (!v) continue;
249
+ if (type) ctx.typeHint(varName(v), type);
250
+ if (prefix === null) prefix = ctx.src.slice(declStart(node), el.startIndex);
251
+ ctx.def(el, {
252
+ name: v.text.replace(/^&?\s*\$/, ''),
253
+ kind: 'property',
254
+ range: node,
255
+ sig: prefix + v.text,
256
+ exported,
257
+ scope: false,
258
+ });
259
+ }
260
+ };
261
+
262
+ /** `const A = 1, B = 2;` at file level or in a class / interface / enum. */
263
+ const constDecl: Visitor = (node, ctx) => {
264
+ const exported = isPublic(node);
265
+ let prefix: string | null = null;
266
+ for (const el of node.namedChildren) {
267
+ if (!el || el.type !== 'const_element') continue;
268
+ const n = el.namedChild(0);
269
+ if (!n || n.type !== 'name') continue;
270
+ if (prefix === null) prefix = ctx.src.slice(declStart(node), el.startIndex);
271
+ const text = el.endIndex - el.startIndex > 160 ? n.text : el.text;
272
+ ctx.def(el, { name: n.text, kind: 'constant', range: node, sig: prefix + text, exported, scope: false });
273
+ }
274
+ };
275
+
276
+ const enumCase: Visitor = (node, ctx) => {
277
+ const name = field(node, 'name');
278
+ if (!name) return;
279
+ ctx.def(node, { name: name.text, kind: 'constant', sig: signature(node, ctx, null), exported: true, scope: false });
280
+ };
281
+
282
+ const functionCall: Visitor = (node, ctx) => {
283
+ const fn = field(node, 'function');
284
+ if (!fn) return;
285
+ const name = shortName(fn);
286
+ if (!name) return;
287
+ if (name === 'define') {
288
+ // define('NAME', value) declares a global constant.
289
+ const stmt = node.parent;
290
+ const first = field(node, 'arguments')?.namedChild(0);
291
+ const lit = literal(first?.type === 'argument' ? first.namedChild(0) : first);
292
+ if (lit && /^[A-Za-z_\\][\w\\]*$/.test(lit)) {
293
+ const range = stmt && stmt.type === 'expression_statement' ? stmt : node;
294
+ const short = lit.slice(lit.lastIndexOf('\\') + 1);
295
+ let end = node.endIndex;
296
+ const nl = ctx.src.indexOf('\n', node.startIndex);
297
+ if (nl !== -1 && nl < end) end = nl;
298
+ ctx.def(node, {
299
+ name: short,
300
+ kind: 'constant',
301
+ range,
302
+ sig: ctx.src.slice(node.startIndex, end),
303
+ exported: true,
304
+ scope: false,
305
+ });
306
+ return;
307
+ }
308
+ }
309
+ ctx.call(name, node);
310
+ };
311
+
312
+ /**
313
+ * Receiver of a member call: `$user`, `$this`, `$this->repo` as text;
314
+ * `(new Foo(...))->bar()` -> `Foo` (resolves like a static-style call);
315
+ * anything else (call chains, indexing, ...) -> COMPLEX_RECEIVER. Never null:
316
+ * a member call must not be mistaken for a bare `foo()` call.
317
+ */
318
+ function receiverOf(node: Node | null): string {
319
+ if (!node) return COMPLEX_RECEIVER;
320
+ switch (node.type) {
321
+ case 'variable_name':
322
+ case 'member_access_expression':
323
+ case 'nullsafe_member_access_expression':
324
+ case 'scoped_property_access_expression':
325
+ case 'relative_scope': {
326
+ if (node.endIndex - node.startIndex > 48) return COMPLEX_RECEIVER;
327
+ const t = node.text.replace('?->', '->');
328
+ return /[\s(\[{'"]/.test(t) ? COMPLEX_RECEIVER : t;
329
+ }
330
+ case 'name':
331
+ case 'qualified_name':
332
+ return shortName(node) ?? COMPLEX_RECEIVER;
333
+ case 'parenthesized_expression': {
334
+ const inner = node.namedChild(0);
335
+ return (inner && inner.type === 'object_creation_expression' && createdClass(inner)) || COMPLEX_RECEIVER;
336
+ }
337
+ case 'object_creation_expression':
338
+ // PHP 8.4: `new Foo()->bar()`.
339
+ return createdClass(node) || COMPLEX_RECEIVER;
340
+ default:
341
+ return COMPLEX_RECEIVER;
342
+ }
343
+ }
344
+
345
+ const memberCall: Visitor = (node, ctx) => {
346
+ const name = field(node, 'name');
347
+ if (!name || name.type !== 'name') return;
348
+ ctx.call(name.text, node, receiverOf(field(node, 'object')));
349
+ };
350
+
351
+ const scopedCall: Visitor = (node, ctx) => {
352
+ const name = field(node, 'name');
353
+ if (!name || name.type !== 'name') return;
354
+ const scope = field(node, 'scope');
355
+ // `Foo::bar()`, `\A\Foo::bar()` -> receiver Foo; parent/self/static kept as is.
356
+ ctx.call(name.text, node, receiverOf(scope));
357
+ };
358
+
359
+ const RELATIVE = new Set(['static', 'self', 'parent']);
360
+
361
+ const objectCreation: Visitor = (node, ctx) => {
362
+ for (const c of node.namedChildren) {
363
+ if (!c) continue;
364
+ if (c.type === 'name' || c.type === 'qualified_name') {
365
+ const name = shortName(c);
366
+ if (name && !RELATIVE.has(name)) ctx.call(name, node);
367
+ return;
368
+ }
369
+ if (c.type === 'arguments' || c.type === 'anonymous_class') return;
370
+ }
371
+ };
372
+
373
+ /** `use A\B;`, `use A\B as C, D\E;`, `use A\{B, C as D};`, `use function A\f;` */
374
+ const useDecl: Visitor = (node, ctx) => {
375
+ let prefix = '';
376
+ let group: Node | null = null;
377
+ for (const c of node.namedChildren) {
378
+ if (!c) continue;
379
+ if (c.type === 'namespace_name') prefix = c.text.replace(/^\\+|\\+$/g, '') + '\\';
380
+ else if (c.type === 'namespace_use_group') group = c;
381
+ }
382
+ for (const clause of (group ?? node).namedChildren) {
383
+ if (!clause || clause.type !== 'namespace_use_clause') continue;
384
+ let target: Node | null = null;
385
+ for (const c of clause.namedChildren) {
386
+ if (c && (c.type === 'qualified_name' || c.type === 'name')) { target = c; break; }
387
+ }
388
+ if (!target) continue;
389
+ const full = prefix + target.text.replace(/^\\+/, '').replace(/\s+/g, '');
390
+ const original = full.slice(full.lastIndexOf('\\') + 1);
391
+ const alias = field(clause, 'alias')?.text;
392
+ // `use Contracts\Pipeline as PipelineContract`: bind the alias, remember what it stands for.
393
+ ctx.imp(full, node, [alias && alias !== original ? `${alias}=${original}` : original]);
394
+ }
395
+ };
396
+
397
+ /** `require 'x.php'`, `include_once __DIR__ . '/x.php'` */
398
+ const includeExpr: Visitor = (node, ctx) => {
399
+ let arg = node.namedChild(0);
400
+ if (arg && arg.type === 'parenthesized_expression') arg = arg.namedChild(0);
401
+ if (!arg) return;
402
+ const lit = literal(arg);
403
+ if (lit) {
404
+ ctx.imp(lit, node);
405
+ return;
406
+ }
407
+ if (arg.type === 'binary_expression') {
408
+ // __DIR__ . '/x.php' is relative to the including file.
409
+ const left = field(arg, 'left');
410
+ const right = literal(field(arg, 'right'));
411
+ if (right && left && left.type === 'name' && left.text === '__DIR__') {
412
+ ctx.imp('.' + (right.startsWith('/') ? right : '/' + right), node);
413
+ }
414
+ }
415
+ };
416
+
417
+ export const php: LangSpec = {
418
+ id: 'php',
419
+ grammar: 'php',
420
+ extensions: ['.php'],
421
+ identifiers: ['name'],
422
+ comments: ['comment'],
423
+ visit: {
424
+ class_declaration: typeDecl('class'),
425
+ interface_declaration: typeDecl('interface'),
426
+ trait_declaration: typeDecl('trait'),
427
+ enum_declaration: typeDecl('enum'),
428
+ anonymous_class: anonymousClass,
429
+ function_definition: functionDef,
430
+ method_declaration: methodDecl,
431
+ simple_parameter: parameter,
432
+ property_promotion_parameter: parameter,
433
+ assignment_expression: assignmentExpr,
434
+ catch_clause: catchClause,
435
+ comment,
436
+ property_declaration: propertyDecl,
437
+ const_declaration: constDecl,
438
+ enum_case: enumCase,
439
+ function_call_expression: functionCall,
440
+ member_call_expression: memberCall,
441
+ nullsafe_member_call_expression: memberCall,
442
+ scoped_call_expression: scopedCall,
443
+ object_creation_expression: objectCreation,
444
+ namespace_use_declaration: useDecl,
445
+ require_expression: includeExpr,
446
+ require_once_expression: includeExpr,
447
+ include_expression: includeExpr,
448
+ include_once_expression: includeExpr,
449
+ },
450
+ };
@@ -0,0 +1,206 @@
1
+ import type { Node } from 'web-tree-sitter';
2
+ import type { LangSpec, Visitor } from '../spec.js';
3
+ import { field, isConstName, lastSegment, simpleReceiver } from '../spec.js';
4
+
5
+ function docstring(node: Node): string | null {
6
+ const body = field(node, 'body');
7
+ const first = body?.namedChildren[0];
8
+ if (!first || first.type !== 'expression_statement') return null;
9
+ const str = first.namedChildren[0];
10
+ if (!str || str.type !== 'string') return null;
11
+ const raw = str.text.replace(/^[a-zA-Z]*("""|'''|"|')/, '').replace(/("""|'''|"|')$/, '');
12
+ const lines = raw.split(/\r?\n/);
13
+ const indent = Math.min(
14
+ ...lines.slice(1).filter(l => l.trim()).map(l => l.length - l.trimStart().length),
15
+ Infinity
16
+ );
17
+ return lines
18
+ .map((l, i) => (i === 0 ? l.trim() : l.slice(Number.isFinite(indent) ? indent : 0).trimEnd()))
19
+ .join('\n')
20
+ .trim();
21
+ }
22
+
23
+ /** Signature: decorators + header up to and including the colon (no trailing comments). */
24
+ function header(node: Node, ctx: { src: string }): string | undefined {
25
+ const start = outer(node).startIndex;
26
+ const body = field(node, 'body');
27
+ if (!body) return undefined;
28
+ let end = body.startIndex;
29
+ for (let i = node.childCount - 1; i >= 0; i--) {
30
+ const c = node.child(i);
31
+ if (c && c.type === ':' && c.endIndex <= body.startIndex) { end = c.endIndex; break; }
32
+ }
33
+ return ctx.src.slice(start, end);
34
+ }
35
+
36
+ function outer(node: Node): Node {
37
+ return node.parent && node.parent.type === 'decorated_definition' ? node.parent : node;
38
+ }
39
+
40
+ const functionDef: Visitor = (node, ctx) => {
41
+ const name = field(node, 'name');
42
+ if (!name) return;
43
+ const pk = ctx.parentKind();
44
+ ctx.def(node, {
45
+ name: name.text,
46
+ kind: pk === 'class' ? 'method' : 'function',
47
+ range: outer(node),
48
+ sig: header(node, ctx),
49
+ doc: docstring(node),
50
+ exported: !name.text.startsWith('_') && ctx.depth() <= 1,
51
+ });
52
+ };
53
+
54
+ const classDef: Visitor = (node, ctx) => {
55
+ const name = field(node, 'name');
56
+ if (!name) return;
57
+ const sym = ctx.def(node, {
58
+ name: name.text,
59
+ kind: 'class',
60
+ range: outer(node),
61
+ sig: header(node, ctx),
62
+ doc: docstring(node),
63
+ exported: !name.text.startsWith('_') && ctx.depth() === 0,
64
+ });
65
+ const supers = field(node, 'superclasses');
66
+ if (supers) {
67
+ for (const s of supers.namedChildren) {
68
+ if (!s || s.type === 'keyword_argument') continue;
69
+ if (s.type === 'call') {
70
+ // Dynamic base built from a class: `class Manager(BaseManager.from_queryset(QuerySet))`.
71
+ const fn = field(s, 'function');
72
+ if (fn && fn.type === 'attribute') ctx.inherit(sym, field(fn, 'object')?.text, 'extends');
73
+ continue;
74
+ }
75
+ // Keep the module qualifier (`forms.CharField`): it tells same-named classes apart.
76
+ ctx.inherit(sym, s.text.replace(/\[.*$/s, ''), 'extends');
77
+ }
78
+ }
79
+ };
80
+
81
+ /** Type of an assignment: its annotation, or the class being instantiated (`x = Foo(...)`). */
82
+ function assignedType(node: Node): string | null {
83
+ const annotation = field(node, 'type');
84
+ if (annotation) return annotation.text;
85
+ const right = field(node, 'right');
86
+ if (right && right.type === 'call') {
87
+ const fn = field(right, 'function');
88
+ const callee = fn?.type === 'attribute' ? field(fn, 'attribute') : fn;
89
+ if (callee && callee.type === 'identifier' && /^[A-Z]/.test(callee.text)) return callee.text;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ /** Module-level and class-level assignments become variables / fields. */
95
+ const assignment: Visitor = (node, ctx) => {
96
+ const left = field(node, 'left');
97
+ if (!left) return;
98
+ const stmt = node.parent;
99
+
100
+ // self.attr = ... inside a method: an instance attribute of the enclosing class.
101
+ if (left.type === 'attribute') {
102
+ const obj = field(left, 'object');
103
+ const attr = field(left, 'attribute');
104
+ if (!obj || !attr || obj.text !== 'self' || !ctx.inCallable()) return;
105
+ // self.repo = Repo(...) or self.repo = repo (a typed parameter)
106
+ const right = field(node, 'right');
107
+ ctx.typeHint(attr.text, assignedType(node) ?? (right?.type === 'identifier' ? ctx.typeOf(right.text) : null), true);
108
+ if (stmt && stmt.type === 'expression_statement') {
109
+ ctx.def(node, {
110
+ name: attr.text, kind: 'field', range: stmt, body: null, scope: false, inContainer: true,
111
+ exported: !attr.text.startsWith('_'),
112
+ });
113
+ }
114
+ return;
115
+ }
116
+ if (left.type !== 'identifier') return;
117
+ ctx.typeHint(left.text, assignedType(node));
118
+
119
+ if (ctx.inCallable()) return;
120
+ if (!stmt || stmt.type !== 'expression_statement') return;
121
+ const name = left.text;
122
+ const pk = ctx.parentKind();
123
+ if (pk !== null && pk !== 'class') return;
124
+ ctx.def(node, {
125
+ name,
126
+ kind: pk === 'class' ? 'field' : isConstName(name) ? 'constant' : 'variable',
127
+ range: stmt,
128
+ body: null,
129
+ exported: pk === null && !name.startsWith('_'),
130
+ });
131
+ };
132
+
133
+ const call: Visitor = (node, ctx) => {
134
+ const fn = field(node, 'function');
135
+ if (!fn) return;
136
+ if (fn.type === 'identifier') ctx.call(fn.text, node);
137
+ else if (fn.type === 'attribute') {
138
+ const obj = field(fn, 'object');
139
+ // super().method() is a call on the parent class, not on an unknown expression.
140
+ const isSuper = obj?.type === 'call' && field(obj, 'function')?.text === 'super';
141
+ ctx.call(field(fn, 'attribute')?.text, node, isSuper ? 'super' : simpleReceiver(obj));
142
+ }
143
+ };
144
+
145
+ /** `def f(x: Foo, y: Bar = ...)` */
146
+ const typedParameter: Visitor = (node, ctx) => {
147
+ const type = field(node, 'type');
148
+ const name = field(node, 'name') ?? node.namedChildren[0];
149
+ if (type && name && name.type === 'identifier') ctx.typeHint(name.text, type.text);
150
+ };
151
+
152
+ /** Applying a decorator calls it: `@register.filter` / `@cached_property`. (`@foo(...)` is a call already.) */
153
+ const decorator: Visitor = (node, ctx) => {
154
+ const expr = node.namedChildren[0];
155
+ if (!expr) return;
156
+ if (expr.type === 'identifier') ctx.call(expr.text, node);
157
+ else if (expr.type === 'attribute') {
158
+ ctx.call(field(expr, 'attribute')?.text, node, simpleReceiver(field(expr, 'object')));
159
+ }
160
+ };
161
+
162
+ const importStatement: Visitor = (node, ctx) => {
163
+ for (const c of node.namedChildren) {
164
+ if (!c) continue;
165
+ if (c.type === 'dotted_name') ctx.imp(c.text, node, [lastSegment(c.text)]);
166
+ else if (c.type === 'aliased_import') {
167
+ const name = field(c, 'name');
168
+ if (name) ctx.imp(name.text, node, [field(c, 'alias')?.text ?? lastSegment(name.text)]);
169
+ }
170
+ }
171
+ };
172
+
173
+ const importFrom: Visitor = (node, ctx) => {
174
+ const mod = field(node, 'module_name');
175
+ if (!mod) return;
176
+ const names: string[] = [];
177
+ for (const c of node.childrenForFieldName('name')) {
178
+ if (!c) continue;
179
+ // The name bound in this module is the alias when there is one.
180
+ if (c.type === 'aliased_import') {
181
+ const original = field(c, 'name')?.text ?? c.text;
182
+ const alias = field(c, 'alias')?.text;
183
+ names.push(alias && alias !== original ? `${alias}=${original}` : original);
184
+ }
185
+ else names.push(c.text);
186
+ }
187
+ ctx.imp(mod.text, node, names);
188
+ };
189
+
190
+ export const python: LangSpec = {
191
+ id: 'python',
192
+ grammar: 'python',
193
+ extensions: ['.py', '.pyi', '.pyw'],
194
+ identifiers: ['identifier'],
195
+ visit: {
196
+ function_definition: functionDef,
197
+ class_definition: classDef,
198
+ assignment,
199
+ typed_parameter: typedParameter,
200
+ typed_default_parameter: typedParameter,
201
+ decorator,
202
+ call,
203
+ import_statement: importStatement,
204
+ import_from_statement: importFrom,
205
+ },
206
+ };