attocode-code-intel 0.1.0__py3-none-any.whl

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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,2199 @@
1
+ """Unified tree-sitter parser for multi-language AST extraction.
2
+
3
+ Provides tree-sitter-based parsing for 25+ languages including Python,
4
+ JavaScript, TypeScript, Go, Rust, Java, Ruby, C/C++, C#, PHP, Swift,
5
+ Kotlin, Scala, Lua, Elixir, Haskell, Bash, HCL, Zig, and data formats
6
+ (YAML, TOML, JSON, HTML, CSS). Falls back gracefully when grammars are
7
+ not installed.
8
+
9
+ Also includes a ``GenericTreeSitterExtractor`` that can parse any language
10
+ with an installed tree-sitter grammar using heuristic node-type matching,
11
+ extending coverage to 50+ languages without per-language config.
12
+
13
+ The main entry point is ``ts_parse_file()`` which returns a ``FileAST``
14
+ (the same dataclass used by the regex parsers in ``codebase_ast.py``).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Language config: maps language name to tree-sitter grammar module name,
26
+ # and provides node type queries for each language.
27
+
28
+ _TS_AVAILABLE = False
29
+ _PARSERS: dict[str, object] = {} # lang -> tree_sitter.Parser
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class _LangConfig:
34
+ """Configuration for parsing a specific language via tree-sitter."""
35
+
36
+ grammar_module: str # e.g. "tree_sitter_python"
37
+ function_types: tuple[str, ...] # node types that represent functions
38
+ class_types: tuple[str, ...] # node types that represent classes
39
+ import_types: tuple[str, ...] # node types that represent imports
40
+ method_types: tuple[str, ...] = () # method declarations inside classes
41
+ var_types: tuple[str, ...] = () # top-level variable/constant declarations
42
+ name_field: str = "name" # tree-sitter field for the identifier
43
+ language_func: str = "language" # function name to call on grammar module
44
+
45
+
46
+ LANGUAGE_CONFIGS: dict[str, _LangConfig] = {
47
+ # ---------------------------------------------------------------
48
+ # Tier 1: Original 9 languages (production-ready)
49
+ # ---------------------------------------------------------------
50
+ "python": _LangConfig(
51
+ grammar_module="tree_sitter_python",
52
+ function_types=("function_definition",),
53
+ class_types=("class_definition",),
54
+ import_types=("import_statement", "import_from_statement"),
55
+ ),
56
+ "javascript": _LangConfig(
57
+ grammar_module="tree_sitter_javascript",
58
+ function_types=("function_declaration", "arrow_function", "function_expression", "generator_function_declaration"),
59
+ class_types=("class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"),
60
+ import_types=("import_statement", "export_statement"),
61
+ method_types=("method_definition",),
62
+ ),
63
+ "typescript": _LangConfig(
64
+ grammar_module="tree_sitter_typescript",
65
+ language_func="language_typescript",
66
+ function_types=("function_declaration", "arrow_function", "function_expression", "generator_function_declaration"),
67
+ class_types=("class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"),
68
+ import_types=("import_statement", "export_statement"),
69
+ method_types=("method_definition",),
70
+ ),
71
+ "go": _LangConfig(
72
+ grammar_module="tree_sitter_go",
73
+ function_types=("function_declaration", "method_declaration"),
74
+ class_types=("type_declaration",),
75
+ import_types=("import_declaration",),
76
+ var_types=("const_declaration", "var_declaration"),
77
+ ),
78
+ "rust": _LangConfig(
79
+ grammar_module="tree_sitter_rust",
80
+ function_types=("function_item",),
81
+ class_types=("struct_item", "enum_item", "impl_item", "trait_item"),
82
+ import_types=("use_declaration", "mod_item"),
83
+ ),
84
+ "java": _LangConfig(
85
+ grammar_module="tree_sitter_java",
86
+ function_types=("method_declaration", "constructor_declaration"),
87
+ class_types=("class_declaration", "interface_declaration", "enum_declaration"),
88
+ import_types=("import_declaration",),
89
+ ),
90
+ "ruby": _LangConfig(
91
+ grammar_module="tree_sitter_ruby",
92
+ function_types=("method",),
93
+ class_types=("class", "module"),
94
+ import_types=("call",), # require/require_relative
95
+ ),
96
+ "c": _LangConfig(
97
+ grammar_module="tree_sitter_c",
98
+ function_types=("function_definition",),
99
+ class_types=("struct_specifier", "enum_specifier", "union_specifier"),
100
+ import_types=("preproc_include",),
101
+ ),
102
+ "cpp": _LangConfig(
103
+ grammar_module="tree_sitter_cpp",
104
+ function_types=("function_definition",),
105
+ class_types=(
106
+ "class_specifier", "struct_specifier", "enum_specifier",
107
+ "namespace_definition",
108
+ ),
109
+ import_types=("preproc_include",),
110
+ ),
111
+ "csharp": _LangConfig(
112
+ grammar_module="tree_sitter_c_sharp",
113
+ function_types=("method_declaration", "constructor_declaration", "local_function_statement"),
114
+ class_types=(
115
+ "class_declaration", "struct_declaration", "interface_declaration",
116
+ "enum_declaration", "record_declaration", "namespace_declaration",
117
+ ),
118
+ import_types=("using_directive",),
119
+ ),
120
+ "php": _LangConfig(
121
+ grammar_module="tree_sitter_php",
122
+ function_types=("function_definition", "method_declaration"),
123
+ class_types=(
124
+ "class_declaration", "interface_declaration", "trait_declaration",
125
+ "enum_declaration",
126
+ ),
127
+ import_types=("namespace_use_declaration",),
128
+ method_types=("method_declaration",),
129
+ language_func="language_php",
130
+ ),
131
+ "swift": _LangConfig(
132
+ grammar_module="tree_sitter_swift",
133
+ function_types=("function_declaration", "init_declaration"),
134
+ class_types=(
135
+ "class_declaration", "struct_declaration", "protocol_declaration",
136
+ "enum_declaration", "extension_declaration",
137
+ ),
138
+ import_types=("import_declaration",),
139
+ ),
140
+ "kotlin": _LangConfig(
141
+ grammar_module="tree_sitter_kotlin",
142
+ function_types=("function_declaration",),
143
+ class_types=(
144
+ "class_declaration", "object_declaration", "interface_declaration",
145
+ "enum_class_body",
146
+ ),
147
+ import_types=("import_header", "import"),
148
+ ),
149
+ "scala": _LangConfig(
150
+ grammar_module="tree_sitter_scala",
151
+ function_types=("function_definition",),
152
+ class_types=(
153
+ "class_definition", "object_definition", "trait_definition",
154
+ "enum_definition",
155
+ ),
156
+ import_types=("import_declaration",),
157
+ ),
158
+ "lua": _LangConfig(
159
+ grammar_module="tree_sitter_lua",
160
+ function_types=("function_declaration", "function_definition"),
161
+ class_types=(), # Lua has no native classes
162
+ import_types=(), # require() is a regular function call
163
+ ),
164
+ "elixir": _LangConfig(
165
+ grammar_module="tree_sitter_elixir",
166
+ # Elixir uses macro calls for everything — handled specially in _process_node
167
+ function_types=(),
168
+ class_types=(),
169
+ import_types=(),
170
+ ),
171
+ "haskell": _LangConfig(
172
+ grammar_module="tree_sitter_haskell",
173
+ function_types=("function", "bind"),
174
+ class_types=("data_type", "newtype", "type_synonym", "class", "instance"),
175
+ import_types=("import",),
176
+ ),
177
+ "bash": _LangConfig(
178
+ grammar_module="tree_sitter_bash",
179
+ function_types=("function_definition",),
180
+ class_types=(), # No classes in bash
181
+ import_types=("command",), # source/. commands
182
+ ),
183
+ "hcl": _LangConfig(
184
+ grammar_module="tree_sitter_hcl",
185
+ function_types=(), # HCL doesn't have functions
186
+ class_types=("block",), # resource, data, module blocks
187
+ import_types=(), # No imports in HCL
188
+ ),
189
+ "zig": _LangConfig(
190
+ grammar_module="tree_sitter_zig",
191
+ function_types=("function_declaration",),
192
+ class_types=("struct_declaration", "enum_declaration", "union_declaration"),
193
+ import_types=(), # @import is a builtin call
194
+ var_types=("variable_declaration",),
195
+ ),
196
+ # Data/config languages
197
+ "yaml": _LangConfig(
198
+ grammar_module="tree_sitter_yaml",
199
+ function_types=(),
200
+ class_types=(),
201
+ import_types=(),
202
+ ),
203
+ "toml": _LangConfig(
204
+ grammar_module="tree_sitter_toml",
205
+ function_types=(),
206
+ class_types=("table",), # [section] headers
207
+ import_types=(),
208
+ ),
209
+ "json": _LangConfig(
210
+ grammar_module="tree_sitter_json",
211
+ function_types=(),
212
+ class_types=(),
213
+ import_types=(),
214
+ ),
215
+ "html": _LangConfig(
216
+ grammar_module="tree_sitter_html",
217
+ function_types=(),
218
+ class_types=("element",), # HTML elements
219
+ import_types=("script_element", "style_element"), # External resources
220
+ ),
221
+ "css": _LangConfig(
222
+ grammar_module="tree_sitter_css",
223
+ function_types=(),
224
+ class_types=("rule_set",), # CSS selectors
225
+ import_types=("import_statement",), # @import
226
+ ),
227
+ "erlang": _LangConfig(
228
+ grammar_module="tree_sitter_erlang",
229
+ function_types=("function_clause",),
230
+ class_types=("module_attribute",), # -module(name). attribute
231
+ import_types=("attribute",), # -import(Module, [Functions]).
232
+ ),
233
+ "clojure": _LangConfig(
234
+ grammar_module="tree_sitter_clojure",
235
+ # Clojure uses macro calls for everything — handled specially in _process_node
236
+ function_types=(),
237
+ class_types=(),
238
+ import_types=(),
239
+ ),
240
+ "perl": _LangConfig(
241
+ grammar_module="tree_sitter_perl",
242
+ function_types=("subroutine_declaration_statement", "function_definition"),
243
+ class_types=("package_statement",), # package Foo;
244
+ import_types=("use_statement", "require_statement"),
245
+ ),
246
+ "crystal": _LangConfig(
247
+ grammar_module="tree_sitter_crystal",
248
+ function_types=("method_def", "fun_def"),
249
+ class_types=("class_def", "module_def", "struct_def", "lib_def"),
250
+ import_types=("require",),
251
+ ),
252
+ "dart": _LangConfig(
253
+ grammar_module="tree_sitter_dart",
254
+ function_types=("function_signature", "method_signature", "function_body"),
255
+ class_types=(
256
+ "class_definition", "enum_declaration",
257
+ "mixin_declaration", "extension_declaration",
258
+ ),
259
+ import_types=("import_or_export",),
260
+ ),
261
+ "ocaml": _LangConfig(
262
+ grammar_module="tree_sitter_ocaml",
263
+ function_types=("value_definition", "let_binding"),
264
+ class_types=("type_definition", "module_definition", "class_definition"),
265
+ import_types=("open_statement",),
266
+ language_func="language_ocaml",
267
+ ),
268
+ "fsharp": _LangConfig(
269
+ grammar_module="tree_sitter_fsharp",
270
+ function_types=("function_or_value_defn", "member_defn"),
271
+ class_types=("type_definition", "module_defn", "namespace_defn"),
272
+ import_types=("open_declaration", "module_abbrev"),
273
+ ),
274
+ "julia": _LangConfig(
275
+ grammar_module="tree_sitter_julia",
276
+ function_types=("function_definition", "short_function_definition", "macro_definition"),
277
+ class_types=("struct_definition", "abstract_definition", "module_definition"),
278
+ import_types=("import_statement", "using_statement"),
279
+ ),
280
+ "nim": _LangConfig(
281
+ grammar_module="tree_sitter_nim",
282
+ function_types=(
283
+ "proc_declaration", "func_declaration", "method_declaration",
284
+ "template_declaration", "macro_declaration",
285
+ ),
286
+ class_types=("type_section", "object_declaration"),
287
+ import_types=("import_statement", "from_statement"),
288
+ ),
289
+ "r": _LangConfig(
290
+ grammar_module="tree_sitter_r",
291
+ # R functions are assignments like `f <- function() {}` — handled specially
292
+ function_types=(),
293
+ class_types=(), # R uses S4/R6 classes via function calls
294
+ import_types=("call",), # library() and require() are function calls
295
+ ),
296
+ "objc": _LangConfig(
297
+ grammar_module="tree_sitter_objc",
298
+ function_types=("function_definition",), # C-style functions
299
+ class_types=(
300
+ "class_interface", "class_implementation",
301
+ "protocol_declaration", "category_interface",
302
+ ),
303
+ import_types=("preproc_import", "preproc_include"),
304
+ method_types=("method_declaration",),
305
+ ),
306
+ }
307
+
308
+ # Aliases
309
+ LANGUAGE_CONFIGS["c++"] = LANGUAGE_CONFIGS["cpp"]
310
+ LANGUAGE_CONFIGS["c#"] = LANGUAGE_CONFIGS["csharp"]
311
+ LANGUAGE_CONFIGS["shell"] = LANGUAGE_CONFIGS["bash"]
312
+ LANGUAGE_CONFIGS["sh"] = LANGUAGE_CONFIGS["bash"]
313
+ LANGUAGE_CONFIGS["terraform"] = LANGUAGE_CONFIGS["hcl"]
314
+ LANGUAGE_CONFIGS["scss"] = LANGUAGE_CONFIGS["css"]
315
+ LANGUAGE_CONFIGS["objective-c"] = LANGUAGE_CONFIGS["objc"]
316
+ LANGUAGE_CONFIGS["objective_c"] = LANGUAGE_CONFIGS["objc"]
317
+ LANGUAGE_CONFIGS["f#"] = LANGUAGE_CONFIGS["fsharp"]
318
+ LANGUAGE_CONFIGS["metal"] = LANGUAGE_CONFIGS["cpp"]
319
+
320
+
321
+ def _try_init_tree_sitter() -> bool:
322
+ """Try to import tree-sitter. Returns True if available."""
323
+ global _TS_AVAILABLE
324
+ try:
325
+ import tree_sitter # noqa: F401
326
+
327
+ _TS_AVAILABLE = True
328
+ except ImportError:
329
+ _TS_AVAILABLE = False
330
+ return _TS_AVAILABLE
331
+
332
+
333
+ def _get_parser(language: str):
334
+ """Get or create a tree-sitter Parser for the given language.
335
+
336
+ Returns None if the grammar is not installed.
337
+ """
338
+ if language in _PARSERS:
339
+ return _PARSERS[language]
340
+
341
+ if not _TS_AVAILABLE and not _try_init_tree_sitter():
342
+ return None
343
+
344
+ config = LANGUAGE_CONFIGS.get("typescript" if language == "tsx" else language)
345
+ if config is None:
346
+ return None
347
+
348
+ try:
349
+ import importlib
350
+
351
+ import tree_sitter as ts
352
+
353
+ grammar_mod = importlib.import_module(config.grammar_module)
354
+ lang_fn = getattr(grammar_mod, "language_tsx" if language == "tsx" else config.language_func)
355
+ lang = ts.Language(lang_fn())
356
+ parser = ts.Parser(lang)
357
+ _PARSERS[language] = parser
358
+ return parser
359
+ except (ImportError, AttributeError, Exception) as e:
360
+ logger.debug("tree-sitter grammar for %s not available via module: %s", language, e)
361
+
362
+ # Fallback: try tree-sitter-language-pack
363
+ try:
364
+ import tree_sitter as ts
365
+ from tree_sitter_language_pack import get_language as _pack_get_language
366
+
367
+ lang = _pack_get_language(language)
368
+ parser = ts.Parser(lang)
369
+ _PARSERS[language] = parser
370
+ return parser
371
+ except Exception as e2:
372
+ logger.debug("tree-sitter grammar for %s not in language-pack: %s", language, e2)
373
+ _PARSERS[language] = None # Cache the failure
374
+ return None
375
+
376
+
377
+ def is_available(language: str = "") -> bool:
378
+ """Check if tree-sitter parsing is available for a language (or any language)."""
379
+ if not _TS_AVAILABLE and not _try_init_tree_sitter():
380
+ return False
381
+ if language:
382
+ return _get_parser(language) is not None
383
+ return True
384
+
385
+
386
+ def supported_languages() -> list[str]:
387
+ """Return list of languages with tree-sitter configs defined."""
388
+ return list(LANGUAGE_CONFIGS.keys())
389
+
390
+
391
+ # ---------------------------------------------------------------------------
392
+ # AST extraction helpers
393
+ # ---------------------------------------------------------------------------
394
+
395
+
396
+ def _node_text(node, source_bytes: bytes) -> str:
397
+ """Extract the text content of a tree-sitter node."""
398
+ return source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
399
+
400
+
401
+ def _find_name(node, source_bytes: bytes) -> str:
402
+ """Extract the name identifier from a node (function/class/etc)."""
403
+ # Try the standard 'name' field first
404
+ name_node = node.child_by_field_name("name")
405
+ if name_node:
406
+ return _node_text(name_node, source_bytes)
407
+
408
+ # C/C++: name is inside declarator → function_declarator → identifier
409
+ declarator = node.child_by_field_name("declarator")
410
+ if declarator:
411
+ if declarator.type == "function_declarator":
412
+ for child in declarator.children:
413
+ if child.type in ("identifier", "field_identifier", "qualified_identifier"):
414
+ return _node_text(child, source_bytes)
415
+ # pointer_declarator or other wrappers around function_declarator
416
+ for sub in declarator.children:
417
+ if sub.type == "function_declarator":
418
+ for child in sub.children:
419
+ if child.type == "identifier":
420
+ return _node_text(child, source_bytes)
421
+
422
+ # Fallback: look for identifier child
423
+ for child in node.children:
424
+ if child.type == "identifier":
425
+ return _node_text(child, source_bytes)
426
+ if child.type == "type_identifier":
427
+ return _node_text(child, source_bytes)
428
+
429
+ # Go: type_declaration → type_spec → type_identifier
430
+ for child in node.children:
431
+ if child.type == "type_spec":
432
+ for grandchild in child.children:
433
+ if grandchild.type == "type_identifier":
434
+ return _node_text(grandchild, source_bytes)
435
+
436
+ return ""
437
+
438
+
439
+ def _find_parameters(node, source_bytes: bytes) -> list[str]:
440
+ """Extract parameter names from a function node."""
441
+ params_node = node.child_by_field_name("parameters")
442
+ if params_node is None:
443
+ # Some languages use "formal_parameters"
444
+ for child in node.children:
445
+ if child.type in ("parameters", "formal_parameters", "parameter_list"):
446
+ params_node = child
447
+ break
448
+
449
+ if params_node is None:
450
+ return []
451
+
452
+ param_names: list[str] = []
453
+ for child in params_node.children:
454
+ if child.type in (
455
+ "identifier", "typed_parameter", "default_parameter",
456
+ "typed_default_parameter", "list_splat_pattern",
457
+ "dictionary_splat_pattern", "formal_parameter",
458
+ "required_parameter", "optional_parameter",
459
+ "rest_parameter", "spread_element",
460
+ ):
461
+ name = _find_name(child, source_bytes)
462
+ if not name:
463
+ name = _node_text(child, source_bytes).split(":")[0].split("=")[0].strip()
464
+ name = name.lstrip("*").lstrip("&")
465
+ if name and name not in ("self", "cls", ",", "(", ")"):
466
+ param_names.append(name)
467
+ return param_names
468
+
469
+
470
+ def _find_return_type(node, source_bytes: bytes) -> str:
471
+ """Extract return type annotation from a function node."""
472
+ ret = node.child_by_field_name("return_type")
473
+ if ret:
474
+ text = _node_text(ret, source_bytes)
475
+ return text.lstrip("->").lstrip(":").strip()
476
+ return ""
477
+
478
+
479
+ def _is_async(node, source_bytes: bytes) -> bool:
480
+ """Check if a function node is async."""
481
+ text = _node_text(node, source_bytes)
482
+ return text.lstrip().startswith("async ")
483
+
484
+
485
+ def _find_decorators(node, source_bytes: bytes) -> list[str]:
486
+ """Extract decorators from a function/class node."""
487
+ decorators: list[str] = []
488
+ # Check previous siblings for decorator nodes
489
+ prev = node.prev_named_sibling
490
+ while prev and prev.type == "decorator":
491
+ text = _node_text(prev, source_bytes).lstrip("@").strip()
492
+ decorators.insert(0, text)
493
+ prev = prev.prev_named_sibling
494
+ return decorators
495
+
496
+
497
+ def _find_go_doc_comment(node, source_bytes: bytes) -> str:
498
+ """Extract Go-style doc comment from comment lines immediately above a node.
499
+
500
+ Go doc comments are consecutive ``// Comment`` lines directly preceding a
501
+ declaration with no blank lines in between. This walks backward through
502
+ previous named siblings collecting ``comment`` nodes whose end line is
503
+ adjacent to (or the same as) the start line of the next element.
504
+ """
505
+ comment_lines: list[str] = []
506
+ expected_line = node.start_point[0] # 0-based line of the declaration
507
+
508
+ prev = node.prev_named_sibling
509
+ while prev is not None and prev.type == "comment":
510
+ # The comment must end exactly one line before the expected line
511
+ if prev.end_point[0] + 1 != expected_line:
512
+ break
513
+ text = _node_text(prev, source_bytes).strip()
514
+ # Strip leading "//" (single-line) or "/* ... */" (block)
515
+ if text.startswith("//"):
516
+ text = text[2:].strip()
517
+ elif text.startswith("/*") and text.endswith("*/"):
518
+ text = text[2:-2].strip()
519
+ comment_lines.insert(0, text)
520
+ expected_line = prev.start_point[0]
521
+ prev = prev.prev_named_sibling
522
+
523
+ return "\n".join(comment_lines) if comment_lines else ""
524
+
525
+
526
+ def _extract_go_receiver(node, source_bytes: bytes) -> str:
527
+ """Extract receiver type name from a Go method_declaration node.
528
+
529
+ Go methods look like ``func (r *Receiver) MethodName(...) ...``.
530
+ The tree-sitter Go grammar exposes a ``receiver`` field containing a
531
+ ``parameter_list`` with one ``parameter_declaration`` whose ``type``
532
+ child holds the receiver type (possibly wrapped in ``pointer_type``).
533
+ """
534
+ receiver_node = node.child_by_field_name("receiver")
535
+ if receiver_node is None:
536
+ return ""
537
+
538
+ for child in receiver_node.children:
539
+ if child.type == "parameter_declaration":
540
+ type_node = child.child_by_field_name("type")
541
+ if type_node is None:
542
+ # Fallback: look for type_identifier or pointer_type child
543
+ for sub in child.children:
544
+ if sub.type in ("type_identifier", "pointer_type"):
545
+ type_node = sub
546
+ break
547
+ if type_node is not None:
548
+ # pointer_type wraps the actual type_identifier
549
+ if type_node.type == "pointer_type":
550
+ for sub in type_node.children:
551
+ if sub.type == "type_identifier":
552
+ return _node_text(sub, source_bytes)
553
+ return _node_text(type_node, source_bytes)
554
+ return ""
555
+
556
+
557
+ def _get_visibility(name: str, language: str) -> str:
558
+ """Determine visibility from name conventions."""
559
+ if language == "python":
560
+ if name.startswith("__") and not name.endswith("__"):
561
+ return "private"
562
+ if name.startswith("_"):
563
+ return "private"
564
+ return "public"
565
+ elif language == "go":
566
+ # Go convention: uppercase first letter = exported (public)
567
+ if name and name[0].isupper():
568
+ return "public"
569
+ return "private"
570
+ elif language in ("java", "typescript", "c", "cpp", "csharp", "kotlin", "swift", "scala",
571
+ "dart", "objc", "crystal", "fsharp"):
572
+ # Would need modifier parsing; default to public
573
+ return "public"
574
+ elif language == "php":
575
+ # PHP uses explicit modifiers; convention: __ prefix is magic
576
+ if name.startswith("__"):
577
+ return "public" # magic methods
578
+ return "public"
579
+ elif language == "ruby":
580
+ return "public"
581
+ elif language == "lua":
582
+ # Lua convention: _prefix means private
583
+ if name.startswith("_"):
584
+ return "private"
585
+ return "public"
586
+ elif language == "erlang":
587
+ # Erlang: all exported functions are public; default to public
588
+ return "public"
589
+ elif language == "clojure":
590
+ # Handled by _process_clojure_call which checks defn vs defn-
591
+ return "public"
592
+ elif language == "perl":
593
+ # Perl convention: _ prefix means private
594
+ if name.startswith("_"):
595
+ return "private"
596
+ return "public"
597
+ elif language == "ocaml":
598
+ # OCaml: visibility determined by .mli files; default to public
599
+ return "public"
600
+ elif language == "julia":
601
+ # Julia convention: _ prefix means private/internal
602
+ if name.startswith("_"):
603
+ return "private"
604
+ return "public"
605
+ elif language == "nim":
606
+ # Nim: exported procs use * suffix, but that's in the type, not the name
607
+ # Convention: default to public
608
+ return "public"
609
+ elif language == "r":
610
+ # R: . prefix means hidden/private
611
+ if name.startswith("."):
612
+ return "private"
613
+ return "public"
614
+ return "public"
615
+
616
+
617
+ def _extract_import_module(node, source_bytes: bytes, language: str) -> str:
618
+ """Extract the module name from an import node."""
619
+ text = _node_text(node, source_bytes).strip()
620
+
621
+ if language == "python":
622
+ # "from X import Y" or "import X"
623
+ if text.startswith("from "):
624
+ parts = text.split()
625
+ return parts[1] if len(parts) > 1 else ""
626
+ if text.startswith("import "):
627
+ return text[7:].split(",")[0].strip().split(" as ")[0].strip()
628
+ elif language in ("javascript", "typescript"):
629
+ # "import X from 'Y'" or "import 'Y'"
630
+ if "from " in text:
631
+ parts = text.split("from ")
632
+ return parts[-1].strip().strip("'\"`;")
633
+ if text.startswith("import "):
634
+ return text[7:].strip().strip("'\"`;")
635
+ elif language == "go":
636
+ # import "fmt" or import ( "fmt" "os" )
637
+ # Tree-sitter Go grammar: import_declaration → import_spec → path
638
+ # or import_declaration → import_spec_list → import_spec* → path
639
+ for child in node.children:
640
+ if child.type == "import_spec":
641
+ path_node = child.child_by_field_name("path")
642
+ if path_node:
643
+ return _node_text(path_node, source_bytes).strip('"')
644
+ elif child.type == "import_spec_list":
645
+ # Grouped imports: return first one
646
+ for spec in child.children:
647
+ if spec.type == "import_spec":
648
+ path_node = spec.child_by_field_name("path")
649
+ if path_node:
650
+ return _node_text(path_node, source_bytes).strip('"')
651
+ # Fallback: try direct path field (some grammar versions)
652
+ mod_node = node.child_by_field_name("path")
653
+ if mod_node:
654
+ return _node_text(mod_node, source_bytes).strip('"')
655
+ elif language == "rust":
656
+ # use std::collections::HashMap;
657
+ arg_node = node.child_by_field_name("argument")
658
+ if arg_node:
659
+ return _node_text(arg_node, source_bytes).rstrip(";")
660
+ elif language == "java":
661
+ # import java.util.List;
662
+ return text.replace("import ", "").rstrip(";").strip()
663
+ elif language == "ruby":
664
+ # require 'foo' or require_relative 'foo'
665
+ if "require" in text:
666
+ parts = text.split("'")
667
+ if len(parts) >= 2:
668
+ return parts[1]
669
+ parts = text.split('"')
670
+ if len(parts) >= 2:
671
+ return parts[1]
672
+ elif language in ("c", "cpp"):
673
+ # #include <stdio.h> or #include "header.h"
674
+ for ch in ('<', '"'):
675
+ if ch in text:
676
+ return text.split(ch)[1].split('>' if ch == '<' else '"')[0]
677
+ elif language == "csharp":
678
+ # using System.Collections.Generic;
679
+ text = text.replace("using ", "").rstrip(";").strip()
680
+ # Remove "static" keyword for static usings
681
+ if text.startswith("static "):
682
+ text = text[7:]
683
+ return text
684
+ elif language == "php":
685
+ # use App\Models\User;
686
+ text = text.replace("use ", "").rstrip(";").strip()
687
+ return text
688
+ elif language == "swift":
689
+ # import Foundation
690
+ return text.replace("import ", "").strip()
691
+ elif language == "kotlin":
692
+ # import kotlin.collections.mutableListOf
693
+ return text.replace("import ", "").strip()
694
+ elif language == "scala":
695
+ # import scala.collection.mutable._
696
+ return text.replace("import ", "").strip()
697
+ elif language == "haskell":
698
+ # import Data.Map (Map, fromList)
699
+ parts = text.split()
700
+ if len(parts) >= 2:
701
+ # Handle "import qualified X as Y" or "import X"
702
+ idx = 1
703
+ if idx < len(parts) and parts[idx] == "qualified":
704
+ idx += 1
705
+ if idx < len(parts):
706
+ return parts[idx]
707
+ elif language == "erlang":
708
+ # -import(module, [func/arity, ...]). or -module(name).
709
+ # Attribute text looks like: -import(lists, [map/2]).
710
+ if text.startswith("-import("):
711
+ inner = text[8:].rstrip(").").strip()
712
+ return inner.split(",")[0].strip()
713
+ if text.startswith("-module("):
714
+ inner = text[8:].rstrip(").").strip()
715
+ return inner
716
+ # Generic attribute: extract attribute name
717
+ if text.startswith("-"):
718
+ return text.split("(")[0].lstrip("-").strip()
719
+ elif language == "clojure":
720
+ # Handled by _process_clojure_call; fallback for raw import text
721
+ return text
722
+ elif language == "perl":
723
+ # use Module::Name; or use Module::Name qw(...);
724
+ # require Module::Name;
725
+ if text.startswith("use "):
726
+ mod = text[4:].rstrip(";").strip()
727
+ return mod.split()[0].split("(")[0]
728
+ if text.startswith("require "):
729
+ mod = text[8:].rstrip(";").strip()
730
+ return mod.split()[0].strip("'\"")
731
+ elif language == "crystal":
732
+ # require "module_name"
733
+ if "require" in text:
734
+ parts = text.split('"')
735
+ if len(parts) >= 2:
736
+ return parts[1]
737
+ elif language == "dart":
738
+ # import 'package:flutter/material.dart'; or export '...';
739
+ for quote in ("'", '"'):
740
+ if quote in text:
741
+ parts = text.split(quote)
742
+ if len(parts) >= 2:
743
+ return parts[1]
744
+ elif language == "ocaml":
745
+ # open Module_name
746
+ if text.startswith("open "):
747
+ return text[5:].strip()
748
+ elif language == "fsharp":
749
+ # open System.Collections.Generic or module M = Module.Path
750
+ if text.startswith("open "):
751
+ return text[5:].strip()
752
+ if text.startswith("module ") and "=" in text:
753
+ return text.split("=")[1].strip()
754
+ elif language == "julia":
755
+ # import Module or import Module: func1, func2
756
+ # using Module or using Module: func1, func2
757
+ if text.startswith(("import ", "using ")):
758
+ mod = text.split(None, 1)[1] if len(text.split(None, 1)) > 1 else ""
759
+ return mod.split(":")[0].strip()
760
+ elif language == "nim":
761
+ # import module or from module import symbol
762
+ if text.startswith("from "):
763
+ parts = text.split()
764
+ if len(parts) >= 2:
765
+ return parts[1]
766
+ if text.startswith("import "):
767
+ return text[7:].strip().split(",")[0].strip()
768
+ elif language == "r":
769
+ # library(pkg) or require(pkg)
770
+ if "library(" in text or "require(" in text:
771
+ inner = text.split("(")[1].split(")")[0].strip() if "(" in text else ""
772
+ return inner.strip('"').strip("'")
773
+ elif language == "objc":
774
+ # #import <Foundation/Foundation.h> or #import "header.h"
775
+ # #include <stdio.h> or #include "header.h"
776
+ for ch in ('<', '"'):
777
+ if ch in text:
778
+ return text.split(ch)[1].split('>' if ch == '<' else '"')[0]
779
+
780
+ return text
781
+
782
+
783
+ def _extract_bases(node, source_bytes: bytes, language: str) -> list[str]:
784
+ """Extract base classes/interfaces from a class node."""
785
+ bases: list[str] = []
786
+
787
+ if language == "python":
788
+ args_node = node.child_by_field_name("superclasses")
789
+ if args_node:
790
+ for child in args_node.children:
791
+ if child.type in ("identifier", "attribute"):
792
+ bases.append(_node_text(child, source_bytes))
793
+ elif language in ("javascript", "typescript", "java"):
794
+ # Look for extends/implements
795
+ for child in node.children:
796
+ if child.type in ("class_heritage", "superclass", "super_interfaces"):
797
+ for ident in child.children:
798
+ if ident.type in ("identifier", "type_identifier", "generic_type"):
799
+ bases.append(_node_text(ident, source_bytes))
800
+ elif language == "rust":
801
+ # trait bounds for impl blocks
802
+ for child in node.children:
803
+ if child.type in ("trait_type", "type_identifier"):
804
+ bases.append(_node_text(child, source_bytes))
805
+ elif language == "csharp":
806
+ for child in node.children:
807
+ if child.type == "base_list":
808
+ for ident in child.children:
809
+ if ident.type in ("identifier", "generic_name", "qualified_name"):
810
+ bases.append(_node_text(ident, source_bytes))
811
+ elif language in ("kotlin", "swift"):
812
+ for child in node.children:
813
+ if child.type in ("delegation_specifier", "type_identifier", "user_type",
814
+ "inheritance_specifier"):
815
+ bases.append(_node_text(child, source_bytes))
816
+ elif language == "scala":
817
+ for child in node.children:
818
+ if child.type in ("extends_clause",):
819
+ for ident in child.children:
820
+ if ident.type in ("type_identifier", "generic_type"):
821
+ bases.append(_node_text(ident, source_bytes))
822
+ elif language == "dart":
823
+ # class Foo extends Bar implements Baz, Qux
824
+ for child in node.children:
825
+ if child.type in ("superclass", "interfaces", "mixins"):
826
+ for ident in child.children:
827
+ if ident.type in ("type_identifier", "identifier", "generic_type"):
828
+ bases.append(_node_text(ident, source_bytes))
829
+ elif language == "crystal":
830
+ # class Foo < Bar
831
+ for child in node.children:
832
+ if child.type in ("superclass", "type_identifier"):
833
+ text = _node_text(child, source_bytes).strip()
834
+ if text and text != "<":
835
+ bases.append(text)
836
+ elif language == "objc":
837
+ # @interface Foo : Bar <Protocol1, Protocol2>
838
+ for child in node.children:
839
+ if child.type in ("superclass_reference", "type_identifier",
840
+ "protocol_qualifiers", "parameterized_class_type_arguments"):
841
+ text = _node_text(child, source_bytes).strip()
842
+ if text:
843
+ bases.append(text)
844
+ elif language == "nim":
845
+ # type Foo = ref object of Bar
846
+ for child in node.children:
847
+ if child.type in ("of_clause", "type_identifier"):
848
+ text = _node_text(child, source_bytes).strip()
849
+ if text.startswith("of "):
850
+ text = text[3:].strip()
851
+ if text:
852
+ bases.append(text)
853
+
854
+ return bases
855
+
856
+
857
+ # ---------------------------------------------------------------------------
858
+ # Elixir-specific helpers (macro-based definitions)
859
+ # ---------------------------------------------------------------------------
860
+
861
+
862
+ _ELIXIR_FN_MACROS = frozenset({"def", "defp", "defmacro", "defmacrop", "defguard", "defdelegate"})
863
+ _ELIXIR_MOD_MACROS = frozenset({"defmodule", "defprotocol", "defimpl"})
864
+ _ELIXIR_IMPORT_MACROS = frozenset({"import", "use", "require", "alias"})
865
+
866
+
867
+ def _process_elixir_call(
868
+ node, source_bytes: bytes,
869
+ functions: list[dict], classes: list[dict], imports: list[dict],
870
+ parent_class: str,
871
+ ) -> str:
872
+ """Handle Elixir call nodes that represent def/defmodule/import macros.
873
+
874
+ Returns the module name if a defmodule was found (for use as parent_class
875
+ when recursing into the do_block), empty string otherwise.
876
+ """
877
+ # Get the macro name (first identifier child — Elixir AST has no "target" field)
878
+ target = node.child_by_field_name("target")
879
+ if not target:
880
+ for child in node.children:
881
+ if child.type == "identifier":
882
+ target = child
883
+ break
884
+ if not target:
885
+ return ""
886
+ macro_name = _node_text(target, source_bytes).strip()
887
+
888
+ # Get the arguments (first "arguments" child — may not be a named field)
889
+ args = node.child_by_field_name("arguments")
890
+ if not args:
891
+ for child in node.children:
892
+ if child.type == "arguments":
893
+ args = child
894
+ break
895
+
896
+ if macro_name in _ELIXIR_FN_MACROS:
897
+ # def/defp: first arg is the function head (a call node with the fn name)
898
+ if args and args.children:
899
+ head = args.children[0]
900
+ name = ""
901
+ if head.type == "call":
902
+ # Inner call also uses identifier children, not "target" field
903
+ head_target = head.child_by_field_name("target")
904
+ if not head_target:
905
+ for child in head.children:
906
+ if child.type == "identifier":
907
+ head_target = child
908
+ break
909
+ if head_target:
910
+ name = _node_text(head_target, source_bytes).strip()
911
+ elif head.type == "identifier":
912
+ name = _node_text(head, source_bytes).strip()
913
+ if name:
914
+ functions.append({
915
+ "name": name,
916
+ "parameters": [],
917
+ "return_type": "",
918
+ "start_line": node.start_point[0] + 1,
919
+ "end_line": node.end_point[0] + 1,
920
+ "is_async": False,
921
+ "decorators": [macro_name] if macro_name != "def" else [],
922
+ "visibility": "public" if macro_name in ("def", "defmacro") else "private",
923
+ "parent_class": parent_class,
924
+ })
925
+ return ""
926
+
927
+ elif macro_name in _ELIXIR_MOD_MACROS:
928
+ # defmodule: first arg is the module name (an alias like MyApp.Repo)
929
+ if args and args.children:
930
+ name_node = args.children[0]
931
+ name = _node_text(name_node, source_bytes).strip()
932
+ if name:
933
+ classes.append({
934
+ "name": name,
935
+ "bases": [],
936
+ "methods": [],
937
+ "decorators": [macro_name] if macro_name != "defmodule" else [],
938
+ "start_line": node.start_point[0] + 1,
939
+ "end_line": node.end_point[0] + 1,
940
+ })
941
+ return name
942
+ return ""
943
+
944
+ elif macro_name in _ELIXIR_IMPORT_MACROS:
945
+ # import/use/require/alias: first arg is the module reference
946
+ if args and args.children:
947
+ mod_node = args.children[0]
948
+ module = _node_text(mod_node, source_bytes).strip()
949
+ if module:
950
+ imports.append({
951
+ "module": module,
952
+ "is_from": macro_name == "alias",
953
+ "start_line": node.start_point[0] + 1,
954
+ })
955
+
956
+ return ""
957
+
958
+
959
+ # ---------------------------------------------------------------------------
960
+ # Clojure-specific helpers (macro-based definitions)
961
+ # ---------------------------------------------------------------------------
962
+
963
+ _CLOJURE_FN_MACROS = frozenset({"defn", "defn-", "defmacro", "defmulti", "defmethod", "defonce"})
964
+ _CLOJURE_CLASS_MACROS = frozenset({"defprotocol", "defrecord", "deftype"})
965
+
966
+
967
+ def _process_clojure_call(
968
+ node, source_bytes: bytes,
969
+ functions: list[dict], classes: list[dict], imports: list[dict],
970
+ parent_class: str,
971
+ ) -> str:
972
+ """Handle Clojure list nodes that represent defn/defprotocol/ns forms.
973
+
974
+ Returns the name if a namespace/protocol was found (for use as parent_class
975
+ when recursing into children), empty string otherwise.
976
+ """
977
+ # Clojure forms are lists: (defn name [args] body)
978
+ # The first child is the macro symbol
979
+ children = [c for c in node.children if c.type not in ("(", ")", "meta_lit", "metadata")]
980
+ if not children:
981
+ return ""
982
+
983
+ head = children[0]
984
+ macro_name = _node_text(head, source_bytes).strip()
985
+
986
+ if macro_name in _CLOJURE_FN_MACROS:
987
+ # (defn name [params] ...) or (defn- name [params] ...)
988
+ if len(children) >= 2:
989
+ name = _node_text(children[1], source_bytes).strip()
990
+ if name:
991
+ decorators = [macro_name] if macro_name != "defn" else []
992
+ visibility = "public" if macro_name != "defn-" else "private"
993
+ functions.append({
994
+ "name": name,
995
+ "parameters": [],
996
+ "return_type": "",
997
+ "start_line": node.start_point[0] + 1,
998
+ "end_line": node.end_point[0] + 1,
999
+ "is_async": False,
1000
+ "decorators": decorators,
1001
+ "visibility": visibility,
1002
+ "parent_class": parent_class,
1003
+ })
1004
+ return ""
1005
+
1006
+ elif macro_name in _CLOJURE_CLASS_MACROS:
1007
+ # (defprotocol Name ...) / (defrecord Name [...] ...) / (deftype Name [...] ...)
1008
+ if len(children) >= 2:
1009
+ name = _node_text(children[1], source_bytes).strip()
1010
+ if name:
1011
+ decorators = [macro_name] if macro_name != "defprotocol" else []
1012
+ classes.append({
1013
+ "name": name,
1014
+ "bases": [],
1015
+ "methods": [],
1016
+ "decorators": decorators,
1017
+ "start_line": node.start_point[0] + 1,
1018
+ "end_line": node.end_point[0] + 1,
1019
+ })
1020
+ return name
1021
+ return ""
1022
+
1023
+ elif macro_name == "ns":
1024
+ # (ns my.namespace (:require [dep1] [dep2]))
1025
+ if len(children) >= 2:
1026
+ name = _node_text(children[1], source_bytes).strip()
1027
+ if name:
1028
+ classes.append({
1029
+ "name": name,
1030
+ "bases": [],
1031
+ "methods": [],
1032
+ "decorators": [],
1033
+ "start_line": node.start_point[0] + 1,
1034
+ "end_line": node.end_point[0] + 1,
1035
+ })
1036
+ # Extract :require dependencies
1037
+ for child in children[2:]:
1038
+ child_text = _node_text(child, source_bytes).strip()
1039
+ if ":require" in child_text:
1040
+ # Extract module names from require vector
1041
+ for sub in child.children:
1042
+ sub_text = _node_text(sub, source_bytes).strip()
1043
+ if sub_text.startswith("["):
1044
+ mod = sub_text.strip("[]").split()[0] if sub_text.strip("[]") else ""
1045
+ if mod and mod != ":require":
1046
+ imports.append({
1047
+ "module": mod,
1048
+ "is_from": False,
1049
+ "start_line": sub.start_point[0] + 1,
1050
+ })
1051
+ elif sub.type == "sym_lit" and sub_text != ":require":
1052
+ imports.append({
1053
+ "module": sub_text,
1054
+ "is_from": False,
1055
+ "start_line": sub.start_point[0] + 1,
1056
+ })
1057
+ return name
1058
+ return ""
1059
+
1060
+ return ""
1061
+
1062
+
1063
+ # ---------------------------------------------------------------------------
1064
+ # R-specific helpers (function assignment detection)
1065
+ # ---------------------------------------------------------------------------
1066
+
1067
+
1068
+ def _process_r_assignment(
1069
+ node, source_bytes: bytes,
1070
+ functions: list[dict], parent_class: str,
1071
+ ) -> bool:
1072
+ """Handle R assignment nodes where RHS is a function_definition.
1073
+
1074
+ R defines functions via `f <- function(x) { ... }` or `f = function(x) { ... }`.
1075
+ Returns True if a function was extracted.
1076
+ """
1077
+ # Left side: variable name
1078
+ left = node.child_by_field_name("left")
1079
+ if not left:
1080
+ # Try first child
1081
+ children = [c for c in node.children if c.type not in ("<-", "=", "<<-")]
1082
+ if len(children) >= 2:
1083
+ left = children[0]
1084
+ else:
1085
+ return False
1086
+
1087
+ # Right side: check for function_definition
1088
+ right = node.child_by_field_name("right")
1089
+ if not right:
1090
+ children = [c for c in node.children if c.type not in ("<-", "=", "<<-")]
1091
+ if len(children) >= 2:
1092
+ right = children[-1]
1093
+ else:
1094
+ return False
1095
+
1096
+ if right.type != "function_definition":
1097
+ return False
1098
+
1099
+ name = _node_text(left, source_bytes).strip()
1100
+ if not name:
1101
+ return False
1102
+
1103
+ params = _find_parameters(right, source_bytes)
1104
+ functions.append({
1105
+ "name": name,
1106
+ "parameters": params,
1107
+ "return_type": "",
1108
+ "start_line": node.start_point[0] + 1,
1109
+ "end_line": node.end_point[0] + 1,
1110
+ "is_async": False,
1111
+ "decorators": [],
1112
+ "visibility": "public",
1113
+ "parent_class": parent_class,
1114
+ })
1115
+ return True
1116
+
1117
+
1118
+ # ---------------------------------------------------------------------------
1119
+ # Main parsing function
1120
+ # ---------------------------------------------------------------------------
1121
+
1122
+
1123
+ def ts_parse_file(file_path: str, content: str | None = None, language: str = "") -> dict | None:
1124
+ """Parse a file using tree-sitter and return structured AST data.
1125
+
1126
+ Returns a dict with keys: functions, classes, imports, top_level_vars,
1127
+ line_count — matching the fields of ``FileAST`` from ``codebase_ast.py``.
1128
+
1129
+ Returns None if tree-sitter is not available or the language is unsupported.
1130
+
1131
+ Args:
1132
+ file_path: Path to the source file.
1133
+ content: Optional file content (read from disk if not provided).
1134
+ language: Language name (auto-detected from extension if empty).
1135
+ """
1136
+ if not language:
1137
+ from attocode_intel._internal.integrations.context.codebase_ast import detect_language
1138
+ language = detect_language(file_path)
1139
+
1140
+ if language not in LANGUAGE_CONFIGS:
1141
+ return None
1142
+
1143
+ parser = _get_parser("tsx" if Path(file_path).suffix == ".tsx" else language)
1144
+ if parser is None:
1145
+ return None
1146
+
1147
+ if content is None:
1148
+ try:
1149
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
1150
+ except OSError:
1151
+ return None
1152
+
1153
+ source_bytes = content.encode("utf-8")
1154
+
1155
+ try:
1156
+ tree = parser.parse(source_bytes)
1157
+ except Exception as e:
1158
+ logger.debug("tree-sitter parse error for %s: %s", file_path, e)
1159
+ return None
1160
+
1161
+ config = LANGUAGE_CONFIGS[language]
1162
+ root = tree.root_node
1163
+
1164
+ functions: list[dict] = []
1165
+ classes: list[dict] = []
1166
+ imports: list[dict] = []
1167
+ top_level_vars: list[str] = []
1168
+
1169
+ def _process_node(node, parent_class: str = "", parent_function: str = "") -> None:
1170
+ """Recursively process tree-sitter nodes."""
1171
+ ntype = node.type
1172
+
1173
+ # A CommonJS assignment names its function through the left-hand side.
1174
+ if language in ("javascript", "typescript") and ntype == "call_expression":
1175
+ fn = node.child_by_field_name("function")
1176
+ args = node.child_by_field_name("arguments")
1177
+ if fn and _node_text(fn, source_bytes) == "require" and args:
1178
+ values = args.named_children
1179
+ if len(values) == 1 and values[0].type == "string":
1180
+ imports.append({"module": _node_text(values[0], source_bytes)[1:-1],
1181
+ "start_line": node.start_point[0] + 1})
1182
+ # Functions
1183
+ if ntype in config.function_types:
1184
+ name = _find_name(node, source_bytes)
1185
+ definition_start = node.start_point[0] + 1
1186
+ assigned_method = False
1187
+ qualified_name = parent_function + "." + name if parent_function else name
1188
+ if language in ("javascript", "typescript") and node.parent:
1189
+ parent = node.parent
1190
+ target = (parent.child_by_field_name("left") if parent.type == "assignment_expression"
1191
+ else parent.child_by_field_name("name") if parent.type == "variable_declarator"
1192
+ else parent.child_by_field_name("key") if parent.type == "pair" else None)
1193
+ if target:
1194
+ assigned_method = target.type == "member_expression"
1195
+ definition_start = target.start_point[0] + 1
1196
+ qualified_name = _node_text(target, source_bytes)
1197
+ name = qualified_name.rsplit(".", 1)[-1]
1198
+ if not name:
1199
+ return
1200
+
1201
+ params = _find_parameters(node, source_bytes)
1202
+ ret_type = _find_return_type(node, source_bytes)
1203
+ decorators = _find_decorators(node, source_bytes)
1204
+ is_async_fn = _is_async(node, source_bytes)
1205
+ visibility = _get_visibility(name, language)
1206
+
1207
+ # Go: extract receiver type for method_declaration → parent_class
1208
+ effective_parent = parent_class
1209
+ if language == "go" and ntype == "method_declaration":
1210
+ receiver_type = _extract_go_receiver(node, source_bytes)
1211
+ if receiver_type:
1212
+ effective_parent = receiver_type
1213
+
1214
+ # Go: extract doc comment from preceding // comment lines
1215
+ docstring = ""
1216
+ if language == "go":
1217
+ docstring = _find_go_doc_comment(node, source_bytes)
1218
+
1219
+ fn_data = {
1220
+ "name": name,
1221
+ "parameters": params,
1222
+ "return_type": ret_type,
1223
+ "start_line": definition_start,
1224
+ "end_line": node.end_point[0] + 1,
1225
+ "is_async": is_async_fn,
1226
+ "decorators": decorators,
1227
+ "visibility": visibility,
1228
+ "parent_class": effective_parent,
1229
+ "qualified_name": qualified_name,
1230
+ "is_nested": bool(parent_function),
1231
+ "is_method": assigned_method,
1232
+ }
1233
+ if docstring:
1234
+ fn_data["docstring"] = docstring
1235
+
1236
+ if effective_parent:
1237
+ # Will be added as method to the class
1238
+ for cls in classes:
1239
+ if cls["name"] == effective_parent:
1240
+ cls["methods"].append(fn_data)
1241
+ break
1242
+ else:
1243
+ # Go: receiver type may not have a matching type_declaration
1244
+ # in the same file; record as a standalone function with parent_class set
1245
+ functions.append(fn_data)
1246
+ else:
1247
+ functions.append(fn_data)
1248
+ body = node.child_by_field_name("body")
1249
+ if body:
1250
+ for child in body.named_children:
1251
+ _process_node(child, parent_function=(effective_parent + "." + name if effective_parent else qualified_name))
1252
+ return
1253
+
1254
+ # Methods inside classes (some languages have specific method types)
1255
+ if ntype in config.method_types and parent_class:
1256
+ name = _find_name(node, source_bytes)
1257
+ if name:
1258
+ params = _find_parameters(node, source_bytes)
1259
+ ret_type = _find_return_type(node, source_bytes)
1260
+ decorators = _find_decorators(node, source_bytes)
1261
+ is_async_fn = _is_async(node, source_bytes)
1262
+ visibility = _get_visibility(name, language)
1263
+
1264
+ method_data = {
1265
+ "name": name,
1266
+ "parameters": params,
1267
+ "return_type": ret_type,
1268
+ "start_line": node.start_point[0] + 1,
1269
+ "end_line": node.end_point[0] + 1,
1270
+ "is_async": is_async_fn,
1271
+ "decorators": decorators,
1272
+ "visibility": visibility,
1273
+ "parent_class": parent_class,
1274
+ }
1275
+ for cls in classes:
1276
+ if cls["name"] == parent_class:
1277
+ cls["methods"].append(method_data)
1278
+ break
1279
+ return
1280
+
1281
+ # Classes
1282
+ if ntype in config.class_types:
1283
+ name = _find_name(node, source_bytes)
1284
+ if language == "rust" and ntype == "impl_item":
1285
+ target_type = node.child_by_field_name("type")
1286
+ if target_type:
1287
+ name = _node_text(target_type, source_bytes)
1288
+ if not name:
1289
+ # For impl blocks in Rust, try type field
1290
+ type_node = node.child_by_field_name("type")
1291
+ if type_node:
1292
+ name = _node_text(type_node, source_bytes)
1293
+
1294
+ # HCL blocks: resource "aws_eks_cluster" "this" { ... }
1295
+ # Extract resource type as a top-level var in addition to
1296
+ # using the block keyword (resource/data/module) as class name
1297
+ if language == "hcl" and ntype == "block":
1298
+ string_lits = [
1299
+ c for c in node.children if c.type == "string_lit"
1300
+ ]
1301
+ for sl in string_lits:
1302
+ for sc in sl.children:
1303
+ if sc.type == "template_literal":
1304
+ resource_name = _node_text(sc, source_bytes)
1305
+ if resource_name and resource_name not in top_level_vars:
1306
+ top_level_vars.append(resource_name)
1307
+
1308
+ if name:
1309
+ decorators = _find_decorators(node, source_bytes)
1310
+ bases = _extract_bases(node, source_bytes, language)
1311
+
1312
+ cls_data = {
1313
+ "name": name,
1314
+ "bases": bases,
1315
+ "methods": [],
1316
+ "decorators": decorators,
1317
+ "start_line": node.start_point[0] + 1,
1318
+ "end_line": node.end_point[0] + 1,
1319
+ }
1320
+
1321
+ # Go: extract doc comment for type declarations
1322
+ if language == "go":
1323
+ docstring = _find_go_doc_comment(node, source_bytes)
1324
+ if docstring:
1325
+ cls_data["docstring"] = docstring
1326
+
1327
+ classes.append(cls_data)
1328
+
1329
+ # Process children to find methods
1330
+ body = node.child_by_field_name("body")
1331
+ if body:
1332
+ for child in body.children:
1333
+ _process_node(child, parent_class=name)
1334
+ else:
1335
+ for child in node.children:
1336
+ _process_node(child, parent_class=name)
1337
+
1338
+ # Go interface methods: type_declaration → type_spec → interface_type → method_elem
1339
+ if language == "go":
1340
+ for child in node.children:
1341
+ if child.type == "type_spec":
1342
+ for gchild in child.children:
1343
+ if gchild.type == "interface_type":
1344
+ for method in gchild.children:
1345
+ if method.type == "method_elem":
1346
+ # Name is in field_identifier child
1347
+ mname = ""
1348
+ for mc in method.children:
1349
+ if mc.type == "field_identifier":
1350
+ mname = _node_text(mc, source_bytes)
1351
+ break
1352
+ if not mname:
1353
+ mname = _find_name(method, source_bytes)
1354
+ if mname:
1355
+ params = _find_parameters(method, source_bytes)
1356
+ fn_data = {
1357
+ "name": mname, "parameters": params,
1358
+ "return_type": "",
1359
+ "start_line": method.start_point[0] + 1,
1360
+ "end_line": method.end_point[0] + 1,
1361
+ "is_async": False, "decorators": [],
1362
+ "visibility": "public",
1363
+ "parent_class": name,
1364
+ }
1365
+ cls_data["methods"].append(fn_data)
1366
+ return
1367
+
1368
+ # Imports
1369
+ if ntype in config.import_types:
1370
+ if language == "rust":
1371
+ from .syntax_evidence import rust_imports
1372
+ imports.extend(rust_imports(node, source_bytes))
1373
+ if ntype == "mod_item" and node.child_by_field_name("body"):
1374
+ for child in node.child_by_field_name("body").named_children:
1375
+ _process_node(child)
1376
+ return
1377
+ if language in ("javascript", "typescript"):
1378
+ source = node.child_by_field_name("source")
1379
+ if source:
1380
+ module = _node_text(source, source_bytes)[1:-1]
1381
+ bindings = []
1382
+ stack = list(node.named_children)
1383
+ while stack:
1384
+ child = stack.pop()
1385
+ if child.type in {"import_specifier", "export_specifier"}:
1386
+ identifiers = [item for item in child.named_children
1387
+ if item.type in {"identifier", "type_identifier"}]
1388
+ if identifiers:
1389
+ imported = _node_text(identifiers[0], source_bytes)
1390
+ local = (_node_text(identifiers[1], source_bytes)
1391
+ if len(identifiers) > 1 else imported)
1392
+ bindings.append({"module": module, "names": [imported],
1393
+ "alias": local if local != imported else "",
1394
+ "is_from": True,
1395
+ "is_reexport": ntype == "export_statement",
1396
+ "start_line": node.start_point[0] + 1})
1397
+ continue
1398
+ if child.type in {"namespace_import", "namespace_export"}:
1399
+ identifier = next((item for item in child.named_children
1400
+ if item.type == "identifier"), None)
1401
+ if identifier:
1402
+ bindings.append({"module": module, "names": ["*"],
1403
+ "alias": _node_text(identifier, source_bytes),
1404
+ "is_from": True,
1405
+ "is_reexport": ntype == "export_statement",
1406
+ "start_line": node.start_point[0] + 1})
1407
+ continue
1408
+ stack.extend(child.named_children)
1409
+ imports.extend(bindings or [{"module": module,
1410
+ "is_reexport": ntype == "export_statement",
1411
+ "start_line": node.start_point[0] + 1}])
1412
+ # Exported declarations still need their definitions extracted.
1413
+ for child in node.named_children:
1414
+ _process_node(child, parent_class)
1415
+ return
1416
+ if language == "python":
1417
+ import ast as python_ast
1418
+ try:
1419
+ statement = python_ast.parse(_node_text(node, source_bytes)).body[0]
1420
+ if isinstance(statement, python_ast.ImportFrom):
1421
+ for binding in statement.names:
1422
+ imports.append({"module": "." * statement.level + (statement.module or ""),
1423
+ "names": [binding.name], "alias": binding.asname or "",
1424
+ "is_from": True, "start_line": node.start_point[0] + 1})
1425
+ elif isinstance(statement, python_ast.Import):
1426
+ for binding in statement.names:
1427
+ imports.append({"module": binding.name, "names": [binding.name],
1428
+ "alias": binding.asname or "", "start_line": node.start_point[0] + 1})
1429
+ return
1430
+ except (SyntaxError, ValueError):
1431
+ pass
1432
+ if language == "go":
1433
+ stack = [node]
1434
+ while stack:
1435
+ item = stack.pop()
1436
+ if item.type == "import_spec":
1437
+ path = item.child_by_field_name("path")
1438
+ if path:
1439
+ imports.append({"module": _node_text(path, source_bytes).strip('"'),
1440
+ "start_line": item.start_point[0] + 1})
1441
+ else:
1442
+ stack.extend(reversed(item.named_children))
1443
+ return
1444
+ module = _extract_import_module(node, source_bytes, language)
1445
+ if module:
1446
+ is_from = language == "python" and ntype == "import_from_statement"
1447
+ imports.append({
1448
+ "module": module,
1449
+ "is_from": is_from,
1450
+ "start_line": node.start_point[0] + 1,
1451
+ })
1452
+ return
1453
+
1454
+ # Top-level variable assignments (Python-specific)
1455
+ if language == "python" and ntype == "expression_statement" and not parent_class:
1456
+ child = node.children[0] if node.children else None
1457
+ if child and child.type == "assignment":
1458
+ left = child.child_by_field_name("left")
1459
+ if left and left.type == "identifier":
1460
+ var_name = _node_text(left, source_bytes)
1461
+ if var_name.isupper() or var_name == "__all__":
1462
+ top_level_vars.append(var_name)
1463
+ return
1464
+
1465
+ # Config-driven top-level variable/constant declarations (Go, Zig, etc.)
1466
+ if config.var_types and ntype in config.var_types and not parent_class:
1467
+ if language == "go":
1468
+ # Go const/var blocks: const ( X = 1; Y = 2 )
1469
+ for child in node.children:
1470
+ if child.type in ("const_spec", "var_spec"):
1471
+ var_name = _find_name(child, source_bytes)
1472
+ if var_name:
1473
+ top_level_vars.append(var_name)
1474
+ else:
1475
+ var_name = _find_name(node, source_bytes)
1476
+ if var_name:
1477
+ top_level_vars.append(var_name)
1478
+ return
1479
+
1480
+ # Elixir: macro calls (def, defmodule, import, etc.)
1481
+ if language == "elixir" and ntype == "call":
1482
+ mod_name = _process_elixir_call(
1483
+ node, source_bytes, functions, classes, imports, parent_class,
1484
+ )
1485
+ # Recurse into do_block for nested defs inside defmodule
1486
+ inner_parent = mod_name or parent_class
1487
+ for child in node.children:
1488
+ if child.type == "do_block":
1489
+ for sub in child.children:
1490
+ _process_node(sub, parent_class=inner_parent)
1491
+ return
1492
+
1493
+ # Clojure: list nodes represent forms like (defn ...), (ns ...), etc.
1494
+ if language == "clojure" and ntype in ("list_lit", "list"):
1495
+ mod_name = _process_clojure_call(
1496
+ node, source_bytes, functions, classes, imports, parent_class,
1497
+ )
1498
+ # Recurse into children for nested definitions
1499
+ if mod_name:
1500
+ for child in node.children:
1501
+ _process_node(child, parent_class=mod_name)
1502
+ return
1503
+
1504
+ # R: detect function assignments (f <- function(...) { ... })
1505
+ if language == "r" and ntype in ("left_assignment", "equals_assignment", "binary_operator"):
1506
+ if _process_r_assignment(node, source_bytes, functions, parent_class):
1507
+ return
1508
+
1509
+ # R: filter import calls to only library() and require()
1510
+ if language == "r" and ntype == "call":
1511
+ # Only treat library() and require() as imports
1512
+ fn_node = node.child_by_field_name("function")
1513
+ if not fn_node:
1514
+ for child in node.children:
1515
+ if child.type == "identifier":
1516
+ fn_node = child
1517
+ break
1518
+ if fn_node:
1519
+ fn_name = _node_text(fn_node, source_bytes).strip()
1520
+ if fn_name in ("library", "require"):
1521
+ module = _extract_import_module(node, source_bytes, language)
1522
+ if module:
1523
+ imports.append({
1524
+ "module": module,
1525
+ "is_from": False,
1526
+ "start_line": node.start_point[0] + 1,
1527
+ })
1528
+ return
1529
+
1530
+ # Recurse into children
1531
+ for child in node.children:
1532
+ _process_node(child, parent_class=parent_class, parent_function=parent_function)
1533
+
1534
+ # Process top-level nodes
1535
+ for child in root.children:
1536
+ _process_node(child)
1537
+
1538
+ # Data/config language extraction: extract top-level keys as vars
1539
+ if language in ("yaml", "toml", "json"):
1540
+ _extract_data_symbols(root, source_bytes, language, top_level_vars, classes)
1541
+
1542
+ # HTML: extract meaningful elements (IDs, component tags)
1543
+ if language == "html":
1544
+ _extract_html_symbols(root, source_bytes, classes, imports)
1545
+
1546
+ # CSS: extract selectors as class names
1547
+ if language == "css":
1548
+ _extract_css_symbols(root, source_bytes, classes)
1549
+
1550
+ return {
1551
+ "functions": functions,
1552
+ "classes": classes,
1553
+ "imports": imports,
1554
+ "top_level_vars": top_level_vars,
1555
+ "line_count": content.count("\n") + 1,
1556
+ "language": language,
1557
+ }
1558
+
1559
+
1560
+ # ---------------------------------------------------------------------------
1561
+ # Data/config language helpers (Phase 2)
1562
+ # ---------------------------------------------------------------------------
1563
+
1564
+
1565
+ def _extract_data_symbols(
1566
+ root, source_bytes: bytes, language: str,
1567
+ top_level_vars: list[str], classes: list[dict],
1568
+ ) -> None:
1569
+ """Extract structural symbols from data/config files."""
1570
+ if language == "yaml":
1571
+ # Extract top-level mapping keys
1572
+ # Tree-sitter YAML: root → document → block_node → block_mapping → block_mapping_pair
1573
+ def _find_yaml_pairs(node, depth: int = 0):
1574
+ if depth > 5:
1575
+ return
1576
+ if node.type == "block_mapping_pair":
1577
+ key_node = node.child_by_field_name("key")
1578
+ if key_node:
1579
+ key = _node_text(key_node, source_bytes).strip()
1580
+ if key:
1581
+ top_level_vars.append(key)
1582
+ return # don't recurse into values
1583
+ for child in node.children:
1584
+ _find_yaml_pairs(child, depth + 1)
1585
+ _find_yaml_pairs(root)
1586
+ elif language == "toml":
1587
+ # Extract table names and top-level keys
1588
+ for child in root.children:
1589
+ if child.type == "table":
1590
+ # [section] headers become class-like entries
1591
+ for sub in child.children:
1592
+ if sub.type in ("bare_key", "dotted_key", "quoted_key"):
1593
+ name = _node_text(sub, source_bytes).strip("[]\"'")
1594
+ if name:
1595
+ classes.append({
1596
+ "name": name,
1597
+ "bases": [],
1598
+ "methods": [],
1599
+ "decorators": [],
1600
+ "start_line": sub.start_point[0] + 1,
1601
+ "end_line": sub.end_point[0] + 1,
1602
+ })
1603
+ elif child.type == "pair":
1604
+ key_node = child.child_by_field_name("key") or (
1605
+ child.children[0] if child.children else None
1606
+ )
1607
+ if key_node:
1608
+ key = _node_text(key_node, source_bytes).strip()
1609
+ if key:
1610
+ top_level_vars.append(key)
1611
+ elif language == "json":
1612
+ # Extract top-level object keys
1613
+ obj = root.children[0] if root.children else None
1614
+ if obj and obj.type == "object":
1615
+ for child in obj.children:
1616
+ if child.type == "pair":
1617
+ key_node = child.child_by_field_name("key")
1618
+ if key_node:
1619
+ key = _node_text(key_node, source_bytes).strip('"')
1620
+ if key:
1621
+ top_level_vars.append(key)
1622
+
1623
+
1624
+ def _extract_html_symbols(
1625
+ root, source_bytes: bytes, classes: list[dict], imports: list[dict],
1626
+ ) -> None:
1627
+ """Extract meaningful symbols from HTML."""
1628
+ def _walk_html(node) -> None:
1629
+ if node.type == "element":
1630
+ tag_node = node.child_by_field_name("tag_name")
1631
+ if not tag_node:
1632
+ # Try first child for self-closing
1633
+ for child in node.children:
1634
+ if child.type == "start_tag":
1635
+ for sub in child.children:
1636
+ if sub.type == "tag_name":
1637
+ tag_node = sub
1638
+ break
1639
+ break
1640
+
1641
+ if tag_node:
1642
+ tag = _node_text(tag_node, source_bytes)
1643
+ # Track script/link imports
1644
+ if tag in ("script", "link"):
1645
+ _extract_html_resource(node, source_bytes, imports)
1646
+ # Track elements with IDs as named symbols
1647
+ _extract_html_id(node, source_bytes, tag, classes)
1648
+
1649
+ for child in node.children:
1650
+ _walk_html(child)
1651
+
1652
+ _walk_html(root)
1653
+
1654
+
1655
+ def _extract_html_resource(node, source_bytes: bytes, imports: list[dict]) -> None:
1656
+ """Extract src/href from script/link tags."""
1657
+ for child in node.children:
1658
+ if child.type in ("start_tag", "self_closing_tag"):
1659
+ for attr in child.children:
1660
+ if attr.type == "attribute":
1661
+ name_node = attr.child_by_field_name("name")
1662
+ val_node = attr.child_by_field_name("value")
1663
+ if name_node and val_node:
1664
+ attr_name = _node_text(name_node, source_bytes)
1665
+ if attr_name in ("src", "href"):
1666
+ val = _node_text(val_node, source_bytes).strip('"\'')
1667
+ if val:
1668
+ imports.append({
1669
+ "module": val,
1670
+ "is_from": False,
1671
+ "start_line": node.start_point[0] + 1,
1672
+ })
1673
+
1674
+
1675
+ def _extract_html_id(
1676
+ node, source_bytes: bytes, tag: str, classes: list[dict],
1677
+ ) -> None:
1678
+ """Extract elements with id attributes as named symbols."""
1679
+ for child in node.children:
1680
+ if child.type in ("start_tag", "self_closing_tag"):
1681
+ for attr in child.children:
1682
+ if attr.type == "attribute":
1683
+ name_node = attr.child_by_field_name("name")
1684
+ val_node = attr.child_by_field_name("value")
1685
+ if name_node and val_node and _node_text(name_node, source_bytes) == "id":
1686
+ id_val = _node_text(val_node, source_bytes).strip('"\'')
1687
+ if id_val:
1688
+ classes.append({
1689
+ "name": f"{tag}#{id_val}",
1690
+ "bases": [],
1691
+ "methods": [],
1692
+ "decorators": [],
1693
+ "start_line": node.start_point[0] + 1,
1694
+ "end_line": node.end_point[0] + 1,
1695
+ })
1696
+
1697
+
1698
+ def _extract_css_symbols(root, source_bytes: bytes, classes: list[dict]) -> None:
1699
+ """Extract CSS selectors as symbols."""
1700
+ for child in root.children:
1701
+ if child.type == "rule_set":
1702
+ # Get the selector
1703
+ for sub in child.children:
1704
+ if sub.type == "selectors":
1705
+ selector = _node_text(sub, source_bytes).strip()
1706
+ if selector:
1707
+ classes.append({
1708
+ "name": selector,
1709
+ "bases": [],
1710
+ "methods": [],
1711
+ "decorators": [],
1712
+ "start_line": child.start_point[0] + 1,
1713
+ "end_line": child.end_point[0] + 1,
1714
+ })
1715
+ break
1716
+
1717
+
1718
+ # ---------------------------------------------------------------------------
1719
+ # Generic tree-sitter extractor (any grammar, no config needed)
1720
+ # ---------------------------------------------------------------------------
1721
+
1722
+ # Heuristic node types that commonly represent definitions across languages.
1723
+ # Used by the generic extractor to identify symbols without per-language config.
1724
+
1725
+ # Node types that indicate a "function-like" definition
1726
+ _GENERIC_FUNCTION_TYPES = frozenset({
1727
+ "function_definition", "function_declaration", "function_item",
1728
+ "method_definition", "method_declaration", "method",
1729
+ "local_function_statement", "constructor_declaration",
1730
+ "init_declaration", "arrow_function", "generator_function_declaration",
1731
+ "function", # Haskell
1732
+ "bind", # Haskell
1733
+ "proc_declaration", "template_declaration", # Nim
1734
+ "fun_def", "val_def", "let_binding", # OCaml, F#
1735
+ "defn", "defn-", "defmacro", # Clojure
1736
+ "function_clause", # Erlang
1737
+ "macro_definition", # Clojure, Elixir
1738
+ "subroutine_definition", "subroutine", # Fortran
1739
+ "class_method_definition", "instance_method_definition", # Obj-C
1740
+ "fun_declaration", # Crystal, Dart
1741
+ "abstract_method_signature", # Dart
1742
+ "short_function_definition", # Julia
1743
+ "function_assignment", # R
1744
+ })
1745
+
1746
+ # Node types that indicate a "class-like" definition
1747
+ _GENERIC_CLASS_TYPES = frozenset({
1748
+ "class_definition", "class_declaration",
1749
+ "struct_definition", "struct_declaration", "struct_specifier", "struct_item",
1750
+ "enum_definition", "enum_declaration", "enum_specifier", "enum_item",
1751
+ "interface_declaration", "trait_definition", "trait_item", "protocol_declaration",
1752
+ "type_declaration", "type_definition", "type_alias",
1753
+ "module_definition", "module_declaration", "namespace_definition",
1754
+ "object_declaration", "record_declaration",
1755
+ "data_type", "newtype", "type_synonym", # Haskell
1756
+ "union_declaration", # Zig
1757
+ "type_section", "object_type", # Nim
1758
+ "deftype", "defrecord", "defprotocol", # Clojure
1759
+ "record_definition", "type_spec", # Erlang
1760
+ "category_interface", "category_implementation", # Obj-C
1761
+ "class_interface", "class_implementation", # Obj-C
1762
+ "abstract_type_definition", # Julia
1763
+ "mixin_declaration", # Dart
1764
+ "lib_declaration", # Crystal
1765
+ })
1766
+
1767
+ # Node types that indicate imports
1768
+ _GENERIC_IMPORT_TYPES = frozenset({
1769
+ "import_statement", "import_declaration", "import_from_statement",
1770
+ "use_declaration", "preproc_include", "using_directive",
1771
+ "namespace_use_declaration", "import_header",
1772
+ "import", # Haskell
1773
+ "using_namespace_declaration", # Zig
1774
+ "open_directive", "open_statement", # OCaml, F#
1775
+ "require_expression", "require", # Clojure, Crystal
1776
+ "include_statement", "include_directive", # Nim, C
1777
+ "import_attribute", "attribute", # Erlang
1778
+ "import_header", # Obj-C
1779
+ "using_statement", # Julia
1780
+ "library_import", # R
1781
+ })
1782
+
1783
+
1784
+ class GenericTreeSitterExtractor:
1785
+ """Parse any tree-sitter grammar using heuristic node-type matching.
1786
+
1787
+ This provides "good enough" symbol extraction for any language with a
1788
+ tree-sitter grammar installed, without needing per-language config.
1789
+ """
1790
+
1791
+ def __init__(self) -> None:
1792
+ self._grammar_cache: dict[str, object | None] = {}
1793
+
1794
+ def _get_language_obj(self, grammar_module: str, language: str = ""):
1795
+ """Try to load a tree-sitter Language from a grammar module.
1796
+
1797
+ Attempts in order:
1798
+ 1. Individual PyPI grammar package (e.g. tree_sitter_perl)
1799
+ 2. tree-sitter-language-pack (170+ bundled languages)
1800
+ """
1801
+ cache_key = grammar_module or language
1802
+ if cache_key in self._grammar_cache:
1803
+ return self._grammar_cache[cache_key]
1804
+
1805
+ if not _TS_AVAILABLE and not _try_init_tree_sitter():
1806
+ self._grammar_cache[cache_key] = None
1807
+ return None
1808
+
1809
+ # Strategy 1: Individual grammar package
1810
+ if grammar_module:
1811
+ try:
1812
+ import importlib
1813
+
1814
+ import tree_sitter as ts
1815
+
1816
+ mod = importlib.import_module(grammar_module)
1817
+ # Try standard language(), fall back to language_<name>() for
1818
+ # grammars like tree_sitter_php that export language_php()
1819
+ lang_fn = getattr(mod, "language", None)
1820
+ if lang_fn is None:
1821
+ # Search for language_* functions
1822
+ for attr in dir(mod):
1823
+ if attr.startswith("language_") and callable(getattr(mod, attr)):
1824
+ lang_fn = getattr(mod, attr)
1825
+ break
1826
+ if lang_fn is not None:
1827
+ lang = ts.Language(lang_fn())
1828
+ self._grammar_cache[cache_key] = lang
1829
+ return lang
1830
+ except (ImportError, AttributeError, Exception) as e:
1831
+ logger.debug("Generic extractor: grammar %s not available: %s", grammar_module, e)
1832
+
1833
+ # Strategy 2: tree-sitter-language-pack fallback
1834
+ if language:
1835
+ try:
1836
+ from tree_sitter_language_pack import get_language
1837
+ lang_obj = get_language(language)
1838
+ self._grammar_cache[cache_key] = lang_obj
1839
+ logger.debug("Loaded %s from tree-sitter-language-pack", language)
1840
+ return lang_obj
1841
+ except (ImportError, KeyError, Exception) as e:
1842
+ logger.debug("language-pack fallback for %s not available: %s", language, e)
1843
+
1844
+ self._grammar_cache[cache_key] = None
1845
+ return None
1846
+
1847
+ def parse(
1848
+ self, file_path: str, content: str, grammar_module: str, language: str = "",
1849
+ ) -> dict | None:
1850
+ """Parse a file using generic heuristics.
1851
+
1852
+ Args:
1853
+ file_path: Path for error messages.
1854
+ content: Source code content.
1855
+ grammar_module: Python module name for the grammar (e.g. "tree_sitter_dart").
1856
+ language: Optional language label.
1857
+
1858
+ Returns:
1859
+ Dict matching ``ts_parse_file`` output format, or None on failure.
1860
+ """
1861
+ lang_obj = self._get_language_obj(grammar_module, language=language)
1862
+ if lang_obj is None:
1863
+ return None
1864
+
1865
+ try:
1866
+ import tree_sitter as ts
1867
+ parser = ts.Parser(lang_obj)
1868
+ except Exception:
1869
+ return None
1870
+
1871
+ source_bytes = content.encode("utf-8")
1872
+ try:
1873
+ tree = parser.parse(source_bytes)
1874
+ except Exception as e:
1875
+ logger.debug("Generic parse error for %s: %s", file_path, e)
1876
+ return None
1877
+
1878
+ functions: list[dict] = []
1879
+ classes: list[dict] = []
1880
+ imports: list[dict] = []
1881
+ top_level_vars: list[str] = []
1882
+
1883
+ def _walk(node, depth: int = 0) -> None:
1884
+ ntype = node.type
1885
+
1886
+ if ntype in _GENERIC_FUNCTION_TYPES:
1887
+ name = _find_name(node, source_bytes)
1888
+ if name:
1889
+ functions.append({
1890
+ "name": name,
1891
+ "parameters": _find_parameters(node, source_bytes),
1892
+ "return_type": _find_return_type(node, source_bytes),
1893
+ "start_line": node.start_point[0] + 1,
1894
+ "end_line": node.end_point[0] + 1,
1895
+ "is_async": _is_async(node, source_bytes),
1896
+ "decorators": [],
1897
+ "visibility": "public",
1898
+ "parent_class": "",
1899
+ })
1900
+ return
1901
+
1902
+ if ntype in _GENERIC_CLASS_TYPES:
1903
+ name = _find_name(node, source_bytes)
1904
+ if name:
1905
+ classes.append({
1906
+ "name": name,
1907
+ "bases": [],
1908
+ "methods": [],
1909
+ "decorators": [],
1910
+ "start_line": node.start_point[0] + 1,
1911
+ "end_line": node.end_point[0] + 1,
1912
+ })
1913
+ # Don't return — recurse into children for methods
1914
+
1915
+ if ntype in _GENERIC_IMPORT_TYPES:
1916
+ text = _node_text(node, source_bytes).strip()
1917
+ if text:
1918
+ imports.append({
1919
+ "module": text,
1920
+ "is_from": False,
1921
+ "start_line": node.start_point[0] + 1,
1922
+ })
1923
+ return
1924
+
1925
+ # Only recurse a few levels deep to avoid noise
1926
+ if depth < 5:
1927
+ for child in node.children:
1928
+ _walk(child, depth + 1)
1929
+
1930
+ for child in tree.root_node.children:
1931
+ _walk(child)
1932
+
1933
+ return {
1934
+ "functions": functions,
1935
+ "classes": classes,
1936
+ "imports": imports,
1937
+ "top_level_vars": top_level_vars,
1938
+ "line_count": content.count("\n") + 1,
1939
+ "language": language or "unknown",
1940
+ }
1941
+
1942
+
1943
+ # Singleton for use by ts_parse_file_generic
1944
+ _generic_extractor = GenericTreeSitterExtractor()
1945
+
1946
+
1947
+ # Additional grammar modules for languages beyond the 25 configured ones.
1948
+ # Maps language name → grammar module name. Users can extend this.
1949
+ EXTRA_GRAMMAR_MODULES: dict[str, str] = {
1950
+ "dart": "tree_sitter_dart",
1951
+ "r": "tree_sitter_r",
1952
+ "julia": "tree_sitter_julia",
1953
+ "perl": "tree_sitter_perl",
1954
+ "ocaml": "tree_sitter_ocaml",
1955
+ "clojure": "tree_sitter_clojure",
1956
+ "elm": "tree_sitter_elm",
1957
+ "erlang": "tree_sitter_erlang",
1958
+ "nix": "tree_sitter_nix",
1959
+ "solidity": "tree_sitter_solidity",
1960
+ "protobuf": "tree_sitter_proto",
1961
+ "graphql": "tree_sitter_graphql",
1962
+ "sql": "tree_sitter_sql",
1963
+ "latex": "tree_sitter_latex",
1964
+ "make": "tree_sitter_make",
1965
+ "cmake": "tree_sitter_cmake",
1966
+ "dockerfile": "tree_sitter_dockerfile",
1967
+ "verilog": "tree_sitter_verilog",
1968
+ "vhdl": "tree_sitter_vhdl",
1969
+ "wasm": "tree_sitter_wasm",
1970
+ "awk": "tree_sitter_awk",
1971
+ "fish": "tree_sitter_fish",
1972
+ "powershell": "tree_sitter_powershell",
1973
+ "groovy": "tree_sitter_groovy",
1974
+ "gleam": "tree_sitter_gleam",
1975
+ "odin": "tree_sitter_odin",
1976
+ "v": "tree_sitter_v",
1977
+ "pascal": "tree_sitter_pascal",
1978
+ "fortran": "tree_sitter_fortran",
1979
+ "ada": "tree_sitter_ada",
1980
+ "d": "tree_sitter_d",
1981
+ "objc": "tree_sitter_objc",
1982
+ "nim": "tree_sitter_nim",
1983
+ "fsharp": "tree_sitter_fsharp",
1984
+ "crystal": "tree_sitter_crystal",
1985
+ "haskell": "tree_sitter_haskell",
1986
+ }
1987
+
1988
+ # Extension mapping for extra grammars
1989
+ EXTRA_LANG_EXTENSIONS: dict[str, str] = {
1990
+ ".dart": "dart",
1991
+ ".r": "r",
1992
+ ".R": "r",
1993
+ ".jl": "julia",
1994
+ ".pl": "perl",
1995
+ ".pm": "perl",
1996
+ ".ml": "ocaml",
1997
+ ".mli": "ocaml",
1998
+ ".clj": "clojure",
1999
+ ".cljs": "clojure",
2000
+ ".cljc": "clojure",
2001
+ ".elm": "elm",
2002
+ ".erl": "erlang",
2003
+ ".hrl": "erlang",
2004
+ ".nix": "nix",
2005
+ ".sol": "solidity",
2006
+ ".proto": "protobuf",
2007
+ ".graphql": "graphql",
2008
+ ".gql": "graphql",
2009
+ ".sql": "sql",
2010
+ ".tex": "latex",
2011
+ ".mk": "make",
2012
+ ".cmake": "cmake",
2013
+ ".v": "verilog",
2014
+ ".sv": "verilog",
2015
+ ".vhd": "vhdl",
2016
+ ".vhdl": "vhdl",
2017
+ ".wat": "wasm",
2018
+ ".wast": "wasm",
2019
+ ".awk": "awk",
2020
+ ".fish": "fish",
2021
+ ".ps1": "powershell",
2022
+ ".psm1": "powershell",
2023
+ ".groovy": "groovy",
2024
+ ".gradle": "groovy",
2025
+ ".gleam": "gleam",
2026
+ ".odin": "odin",
2027
+ ".pas": "pascal",
2028
+ ".pp": "pascal",
2029
+ ".f90": "fortran",
2030
+ ".f95": "fortran",
2031
+ ".f03": "fortran",
2032
+ ".adb": "ada",
2033
+ ".ads": "ada",
2034
+ ".d": "d",
2035
+ ".nim": "nim",
2036
+ ".nimble": "nim",
2037
+ ".fs": "fsharp",
2038
+ ".fsi": "fsharp",
2039
+ ".fsx": "fsharp",
2040
+ ".cr": "crystal",
2041
+ ".m": "objc",
2042
+ ".mm": "objc",
2043
+ ".hs": "haskell",
2044
+ ".lhs": "haskell",
2045
+ }
2046
+
2047
+
2048
+ def ts_parse_file_generic(
2049
+ file_path: str, content: str | None = None, language: str = "",
2050
+ ) -> dict | None:
2051
+ """Parse a file using the generic tree-sitter extractor.
2052
+
2053
+ This handles languages beyond the 25 configured ones, using heuristic
2054
+ node-type matching. Falls back gracefully when grammars aren't installed.
2055
+
2056
+ Args:
2057
+ file_path: Path to the source file.
2058
+ content: Optional content (read from disk if not provided).
2059
+ language: Language name (auto-detected from extension if empty).
2060
+
2061
+ Returns:
2062
+ Dict matching ``ts_parse_file`` format, or None if grammar not available.
2063
+ """
2064
+ if not language:
2065
+ ext = Path(file_path).suffix
2066
+ language = EXTRA_LANG_EXTENSIONS.get(ext, "")
2067
+ if not language:
2068
+ return None
2069
+
2070
+ grammar_module = EXTRA_GRAMMAR_MODULES.get(language)
2071
+ if not grammar_module:
2072
+ return None
2073
+
2074
+ if content is None:
2075
+ try:
2076
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
2077
+ except OSError:
2078
+ return None
2079
+
2080
+ return _generic_extractor.parse(file_path, content, grammar_module, language)
2081
+
2082
+
2083
+ # ---------------------------------------------------------------------------
2084
+ # Universal-ctags integration (fallback for languages without grammars)
2085
+ # ---------------------------------------------------------------------------
2086
+
2087
+
2088
+ def ctags_parse_file(file_path: str, content: str | None = None) -> dict | None:
2089
+ """Parse a file using universal-ctags as a subprocess fallback.
2090
+
2091
+ Provides symbol extraction for 150+ languages when ctags is installed.
2092
+ Returns None if ctags is not available or produces no output.
2093
+ """
2094
+ import json as json_mod
2095
+ import subprocess
2096
+
2097
+ # Write content to a temp file if provided (ctags needs a file on disk)
2098
+ target_path = file_path
2099
+ if content is not None:
2100
+ import tempfile
2101
+ suffix = Path(file_path).suffix
2102
+ try:
2103
+ with tempfile.NamedTemporaryFile(
2104
+ mode="w", suffix=suffix, delete=False, encoding="utf-8",
2105
+ ) as temp_file:
2106
+ temp_file.write(content)
2107
+ temp_file.flush()
2108
+ target_path = temp_file.name
2109
+ except OSError:
2110
+ return None
2111
+
2112
+ try:
2113
+ result = subprocess.run(
2114
+ [
2115
+ "ctags", "--output-format=json", "--fields=+neKS",
2116
+ "--kinds-all=*", "-f", "-", target_path,
2117
+ ],
2118
+ capture_output=True, text=True, timeout=10,
2119
+ )
2120
+ if result.returncode != 0:
2121
+ return None
2122
+ except (FileNotFoundError, subprocess.TimeoutExpired):
2123
+ return None
2124
+ finally:
2125
+ if temp_file:
2126
+ Path(temp_file.name).unlink(missing_ok=True)
2127
+
2128
+ functions: list[dict] = []
2129
+ classes: list[dict] = []
2130
+ imports: list[dict] = []
2131
+ top_level_vars: list[str] = []
2132
+
2133
+ for line in result.stdout.strip().splitlines():
2134
+ if not line.strip():
2135
+ continue
2136
+ try:
2137
+ tag = json_mod.loads(line)
2138
+ except json_mod.JSONDecodeError:
2139
+ continue
2140
+
2141
+ name = tag.get("name", "")
2142
+ kind = tag.get("kind", "")
2143
+ line_no = tag.get("line", 0)
2144
+ end_line = tag.get("end", line_no)
2145
+
2146
+ if not name:
2147
+ continue
2148
+
2149
+ if kind in ("function", "method", "subroutine", "procedure", "def"):
2150
+ functions.append({
2151
+ "name": name,
2152
+ "parameters": [],
2153
+ "return_type": tag.get("typeref", ""),
2154
+ "start_line": line_no,
2155
+ "end_line": end_line,
2156
+ "is_async": False,
2157
+ "decorators": [],
2158
+ "visibility": "public",
2159
+ "parent_class": tag.get("scope", ""),
2160
+ })
2161
+ elif kind in ("class", "struct", "interface", "enum", "trait",
2162
+ "module", "namespace", "type", "union"):
2163
+ classes.append({
2164
+ "name": name,
2165
+ "bases": [],
2166
+ "methods": [],
2167
+ "decorators": [],
2168
+ "start_line": line_no,
2169
+ "end_line": end_line,
2170
+ })
2171
+ elif kind in ("import", "include", "using"):
2172
+ imports.append({
2173
+ "module": name,
2174
+ "is_from": False,
2175
+ "start_line": line_no,
2176
+ })
2177
+ elif kind in ("variable", "constant", "define"):
2178
+ top_level_vars.append(name)
2179
+
2180
+ if not functions and not classes and not imports and not top_level_vars:
2181
+ return None
2182
+
2183
+ # Read line count
2184
+ if content is not None:
2185
+ line_count = content.count("\n") + 1
2186
+ else:
2187
+ try:
2188
+ line_count = Path(file_path).read_text(encoding="utf-8", errors="replace").count("\n") + 1
2189
+ except OSError:
2190
+ line_count = 0
2191
+
2192
+ return {
2193
+ "functions": functions,
2194
+ "classes": classes,
2195
+ "imports": imports,
2196
+ "top_level_vars": top_level_vars,
2197
+ "line_count": line_count,
2198
+ "language": "unknown",
2199
+ }