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,1461 @@
1
+ """Codebase AST analysis using tree-sitter.
2
+
3
+ Provides structural code analysis for Python, JavaScript, and TypeScript
4
+ using tree-sitter parsers. Falls back to regex-based extraction when
5
+ tree-sitter is not available.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Parameter and property detail types
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class ParamDef:
22
+ """Detailed parameter definition for a function/method."""
23
+
24
+ name: str
25
+ type_annotation: str = ""
26
+ default_value: str = ""
27
+ is_rest: bool = False # *args
28
+ is_kwonly: bool = False # after *
29
+ is_kwargs: bool = False # **kwargs
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class PropertyDef:
34
+ """A class property (attribute) definition."""
35
+
36
+ name: str
37
+ start_line: int
38
+ type_annotation: str = ""
39
+ has_default: bool = False
40
+ visibility: str = "public" # public / private (_prefix) / name_mangled (__prefix)
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Core AST node types
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class FunctionDef:
50
+ """A function definition."""
51
+
52
+ name: str
53
+ start_line: int
54
+ end_line: int
55
+ params: list[str] = field(default_factory=list)
56
+ return_type: str = ""
57
+ decorators: list[str] = field(default_factory=list)
58
+ is_async: bool = False
59
+ is_method: bool = False
60
+ docstring: str = ""
61
+ # --- Extended fields ---
62
+ parameters: list[ParamDef] = field(default_factory=list)
63
+ visibility: str = "public"
64
+ is_generator: bool = False
65
+ is_staticmethod: bool = False
66
+ is_classmethod: bool = False
67
+ is_property: bool = False
68
+ type_params: list[str] = field(default_factory=list)
69
+ qualified_name: str = ""
70
+ is_nested: bool = False
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class ClassDef:
75
+ """A class definition."""
76
+
77
+ name: str
78
+ start_line: int
79
+ end_line: int
80
+ bases: list[str] = field(default_factory=list)
81
+ methods: list[FunctionDef] = field(default_factory=list)
82
+ decorators: list[str] = field(default_factory=list)
83
+ docstring: str = ""
84
+ # --- Extended fields ---
85
+ properties: list[PropertyDef] = field(default_factory=list)
86
+ is_abstract: bool = False
87
+ metaclass: str = ""
88
+
89
+
90
+ @dataclass(slots=True)
91
+ class ImportDef:
92
+ """An import statement."""
93
+
94
+ module: str
95
+ names: list[str] = field(default_factory=list)
96
+ alias: str = ""
97
+ is_from: bool = False
98
+ is_reexport: bool = False
99
+ line: int = 0
100
+
101
+
102
+ @dataclass(slots=True)
103
+ class FileAST:
104
+ """AST summary for a single file."""
105
+
106
+ path: str
107
+ language: str
108
+ functions: list[FunctionDef] = field(default_factory=list)
109
+ classes: list[ClassDef] = field(default_factory=list)
110
+ imports: list[ImportDef] = field(default_factory=list)
111
+ top_level_vars: list[str] = field(default_factory=list)
112
+ line_count: int = 0
113
+ parsing_tier: str = "" # "tree_sitter", "regex", "generic_ts", "ctags", "minimal"
114
+
115
+ @property
116
+ def symbol_count(self) -> int:
117
+ return len(self.functions) + len(self.classes) + len(self.top_level_vars)
118
+
119
+ def get_symbols(self) -> list[str]:
120
+ """Get all top-level symbol names (functions, classes, and constants)."""
121
+ symbols = [f.name for f in self.functions]
122
+ symbols.extend(c.name for c in self.classes)
123
+ symbols.extend(self.top_level_vars)
124
+ return symbols
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Change tracking types (for incremental updates)
129
+ # ---------------------------------------------------------------------------
130
+
131
+
132
+ @dataclass(slots=True)
133
+ class DependencyChanges:
134
+ """Changes to a file's import dependencies."""
135
+
136
+ added: list[ImportDef] = field(default_factory=list)
137
+ removed: list[ImportDef] = field(default_factory=list)
138
+
139
+
140
+ @dataclass(slots=True)
141
+ class SymbolChange:
142
+ """A change to a symbol detected by AST diffing."""
143
+
144
+ kind: Literal["added", "removed", "modified"]
145
+ symbol_name: str
146
+ symbol_kind: str # 'function', 'class', 'method'
147
+ file_path: str
148
+ previous: FunctionDef | ClassDef | None = None
149
+
150
+
151
+ @dataclass(slots=True)
152
+ class FileChangeResult:
153
+ """Result of incrementally updating a single file's AST."""
154
+
155
+ file_path: str
156
+ symbol_changes: list[SymbolChange] = field(default_factory=list)
157
+ dependency_changes: DependencyChanges = field(default_factory=DependencyChanges)
158
+ was_incremental: bool = True
159
+
160
+
161
+ # Language detection
162
+ LANG_EXTENSIONS: dict[str, str] = {
163
+ ".py": "python",
164
+ ".pyi": "python",
165
+ ".js": "javascript",
166
+ ".jsx": "javascript",
167
+ ".mjs": "javascript",
168
+ ".cjs": "javascript",
169
+ ".ts": "typescript",
170
+ ".tsx": "typescript",
171
+ ".mts": "typescript",
172
+ ".cts": "typescript",
173
+ ".go": "go",
174
+ ".rs": "rust",
175
+ ".java": "java",
176
+ ".rb": "ruby",
177
+ ".c": "c",
178
+ ".h": "c",
179
+ ".cpp": "cpp",
180
+ ".hpp": "cpp",
181
+ ".cc": "cpp",
182
+ ".cxx": "cpp",
183
+ ".hxx": "cpp",
184
+ ".hh": "cpp",
185
+ ".metal": "cpp",
186
+ ".cs": "csharp",
187
+ ".php": "php",
188
+ ".swift": "swift",
189
+ ".kt": "kotlin",
190
+ ".kts": "kotlin",
191
+ ".scala": "scala",
192
+ ".sc": "scala",
193
+ ".lua": "lua",
194
+ ".ex": "elixir",
195
+ ".exs": "elixir",
196
+ ".hs": "haskell",
197
+ ".lhs": "haskell",
198
+ ".sh": "bash",
199
+ ".bash": "bash",
200
+ ".zsh": "bash",
201
+ ".tf": "hcl",
202
+ ".hcl": "hcl",
203
+ ".tfvars": "hcl",
204
+ ".zig": "zig",
205
+ ".nim": "nim",
206
+ ".fs": "fsharp",
207
+ ".fsi": "fsharp",
208
+ ".fsx": "fsharp",
209
+ ".cr": "crystal",
210
+ ".m": "objc",
211
+ ".mm": "objc",
212
+ # Data/config languages
213
+ ".yaml": "yaml",
214
+ ".yml": "yaml",
215
+ ".toml": "toml",
216
+ ".json": "json",
217
+ ".jsonc": "json",
218
+ ".html": "html",
219
+ ".htm": "html",
220
+ ".css": "css",
221
+ ".scss": "css",
222
+ }
223
+
224
+
225
+ def detect_language(file_path: str) -> str:
226
+ """Detect language from file extension."""
227
+ ext = Path(file_path).suffix.lower()
228
+ return LANG_EXTENSIONS.get(ext, "unknown")
229
+
230
+
231
+ # Regex-based fallback parsers (no tree-sitter dependency)
232
+
233
+
234
+ def _parse_python_params(params_str: str) -> tuple[list[str], list[ParamDef]]:
235
+ """Parse a Python function parameter string into both simple names and detailed ParamDefs.
236
+
237
+ Handles type annotations, default values, *args, **kwargs, and keyword-only params.
238
+
239
+ Returns:
240
+ Tuple of (simple name list for backward compat, detailed ParamDef list).
241
+ """
242
+ simple: list[str] = []
243
+ detailed: list[ParamDef] = []
244
+
245
+ if not params_str.strip():
246
+ return simple, detailed
247
+
248
+ # Split parameters respecting brackets and string literals
249
+ params_raw: list[str] = []
250
+ depth = 0
251
+ current: list[str] = []
252
+ i = 0
253
+ while i < len(params_str):
254
+ ch = params_str[i]
255
+ if ch in ('"', "'"):
256
+ # Skip string literals
257
+ quote = ch
258
+ current.append(ch)
259
+ i += 1
260
+ while i < len(params_str) and params_str[i] != quote:
261
+ if params_str[i] == "\\":
262
+ current.append(params_str[i])
263
+ i += 1
264
+ if i < len(params_str):
265
+ current.append(params_str[i])
266
+ i += 1
267
+ continue
268
+ current.append(params_str[i])
269
+ i += 1
270
+ if i < len(params_str):
271
+ current.append(params_str[i]) # closing quote
272
+ i += 1
273
+ continue
274
+ if ch in ("(", "[", "{"):
275
+ depth += 1
276
+ current.append(ch)
277
+ elif ch in (")", "]", "}"):
278
+ depth -= 1
279
+ current.append(ch)
280
+ elif ch == "," and depth == 0:
281
+ params_raw.append("".join(current).strip())
282
+ current = []
283
+ else:
284
+ current.append(ch)
285
+ i += 1
286
+ if current:
287
+ params_raw.append("".join(current).strip())
288
+
289
+ seen_star = False
290
+ for raw in params_raw:
291
+ raw = raw.strip()
292
+ if not raw:
293
+ continue
294
+
295
+ # Bare * separator
296
+ if raw == "*":
297
+ seen_star = True
298
+ continue
299
+
300
+ is_rest = raw.startswith("*") and not raw.startswith("**")
301
+ is_kwargs = raw.startswith("**")
302
+
303
+ # Strip * / **
304
+ cleaned = raw.lstrip("*")
305
+
306
+ # Split name : type = default
307
+ name = cleaned
308
+ type_ann = ""
309
+ default_val = ""
310
+
311
+ if "=" in cleaned:
312
+ before_eq, default_val = cleaned.split("=", 1)
313
+ default_val = default_val.strip()
314
+ cleaned = before_eq.strip()
315
+
316
+ if ":" in cleaned:
317
+ name, type_ann = cleaned.split(":", 1)
318
+ name = name.strip()
319
+ type_ann = type_ann.strip()
320
+ else:
321
+ name = cleaned.strip()
322
+
323
+ simple.append(name)
324
+ detailed.append(ParamDef(
325
+ name=name,
326
+ type_annotation=type_ann,
327
+ default_value=default_val,
328
+ is_rest=is_rest,
329
+ is_kwonly=seen_star and not is_rest and not is_kwargs,
330
+ is_kwargs=is_kwargs,
331
+ ))
332
+
333
+ if is_rest:
334
+ seen_star = True
335
+
336
+ return simple, detailed
337
+
338
+
339
+ def _detect_visibility(name: str) -> str:
340
+ """Detect Python visibility from naming convention."""
341
+ if name.startswith("__") and not name.endswith("__"):
342
+ return "name_mangled"
343
+ if name.startswith("_") and not name.startswith("__"):
344
+ return "private"
345
+ return "public"
346
+
347
+
348
+ def _check_generator(lines: list[str], start: int, end: int, indent: int) -> bool:
349
+ """Check if a function body contains yield/yield from statements."""
350
+ for j in range(start + 1, min(end, len(lines))):
351
+ stripped = lines[j].strip()
352
+ if stripped.startswith(("yield ", "yield\n", "yield\r")) or "yield " in stripped:
353
+ # Verify it's at the right indentation level (not in a nested def)
354
+ line_indent = len(lines[j]) - len(lines[j].lstrip())
355
+ if line_indent > indent:
356
+ return True
357
+ return False
358
+
359
+
360
+ def _extract_class_properties(
361
+ lines: list[str], class_start: int, class_end: int,
362
+ ) -> list[PropertyDef]:
363
+ """Extract class properties from __init__ body and class-level annotations."""
364
+ props: list[PropertyDef] = []
365
+ seen_names: set[str] = set()
366
+
367
+ for j in range(class_start, min(class_end, len(lines))):
368
+ stripped = lines[j].strip()
369
+
370
+ # self.x = ... or self.x: type = ...
371
+ init_match = re.match(r"self\.(\w+)\s*(?::\s*(\S+))?\s*=", stripped)
372
+ if init_match:
373
+ attr_name = init_match.group(1)
374
+ if attr_name not in seen_names:
375
+ seen_names.add(attr_name)
376
+ props.append(PropertyDef(
377
+ name=attr_name,
378
+ start_line=j + 1,
379
+ type_annotation=init_match.group(2) or "",
380
+ has_default=True,
381
+ visibility=_detect_visibility(attr_name),
382
+ ))
383
+
384
+ # Class-level annotation: name: type (no self.)
385
+ # Must be at one indent level inside class
386
+ if not stripped.startswith("self.") and not stripped.startswith("def "):
387
+ ann_match = re.match(r"^(\w+)\s*:\s*(\S+)(?:\s*=\s*(.+))?$", stripped)
388
+ if ann_match:
389
+ line_indent = len(lines[j]) - len(lines[j].lstrip())
390
+ # Must be indented (inside class body)
391
+ if line_indent > 0:
392
+ attr_name = ann_match.group(1)
393
+ if attr_name not in seen_names and attr_name not in ("class", "def", "return"):
394
+ seen_names.add(attr_name)
395
+ props.append(PropertyDef(
396
+ name=attr_name,
397
+ start_line=j + 1,
398
+ type_annotation=ann_match.group(2) or "",
399
+ has_default=ann_match.group(3) is not None,
400
+ visibility=_detect_visibility(attr_name),
401
+ ))
402
+
403
+ return props
404
+
405
+
406
+ _STRINGS_AND_COMMENTS_RE = re.compile(
407
+ r'#.*$|"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', re.MULTILINE,
408
+ )
409
+
410
+
411
+ def _count_parens(line: str) -> int:
412
+ """Count unbalanced parentheses, ignoring those inside strings and comments."""
413
+ stripped = _STRINGS_AND_COMMENTS_RE.sub("", line)
414
+ return stripped.count("(") - stripped.count(")")
415
+
416
+
417
+ def _join_multiline_defs(lines: list[str]) -> list[tuple[int, int, str]]:
418
+ """Preprocess lines to join multi-line ``def`` and ``class`` signatures.
419
+
420
+ Detects lines that start a ``def`` or ``class`` statement but have
421
+ unbalanced parentheses, then accumulates subsequent lines until
422
+ parentheses balance and the line ends with ``:``.
423
+
424
+ Returns:
425
+ A list of ``(start_line_index, end_line_index, joined_line)`` tuples.
426
+ *start_line_index* is the original line where the statement begins.
427
+ *end_line_index* is the last raw line consumed (equal to start for
428
+ single-line statements). Lines that were folded into a preceding
429
+ signature are omitted (their content is merged into the signature
430
+ line).
431
+ """
432
+ result: list[tuple[int, int, str]] = []
433
+ i = 0
434
+ n = len(lines)
435
+
436
+ while i < n:
437
+ line = lines[i]
438
+ stripped = line.strip()
439
+
440
+ # Quick check: does this line start a def or class?
441
+ is_def = bool(re.match(r"\s*(async\s+)?def\s+\w+", line))
442
+ is_class = bool(re.match(r"\s*class\s+\w+", stripped))
443
+
444
+ if (is_def or is_class) and "(" in line:
445
+ # Count parentheses balance on this line (ignoring strings/comments)
446
+ depth = _count_parens(line)
447
+ if depth > 0:
448
+ # Multi-line signature — accumulate until balanced
449
+ # Strip comments from each line before joining so embedded
450
+ # comments don't confuse downstream parsing.
451
+ start_idx = i
452
+ parts = [_STRINGS_AND_COMMENTS_RE.sub(
453
+ lambda m: m.group() if m.group()[0] != "#" else "", line,
454
+ ).rstrip()]
455
+ i += 1
456
+ while i < n and depth > 0:
457
+ continuation = lines[i]
458
+ depth += _count_parens(continuation)
459
+ cleaned = _STRINGS_AND_COMMENTS_RE.sub(
460
+ lambda m: m.group() if m.group()[0] != "#" else "", continuation,
461
+ ).strip()
462
+ parts.append(cleaned)
463
+ i += 1
464
+ # i now points to the line AFTER the last continuation line
465
+ end_idx = i - 1
466
+ joined = " ".join(parts)
467
+ result.append((start_idx, end_idx, joined))
468
+ continue
469
+
470
+ # Regular line — pass through
471
+ result.append((i, i, line))
472
+ i += 1
473
+
474
+ return result
475
+
476
+
477
+ def _match_function_def(line: str) -> re.Match[str] | None:
478
+ """Match a function definition with balanced-paren-aware parameter extraction.
479
+
480
+ Handles nested parens in default values like ``def foo(x: int = max(0, 1))``.
481
+ Returns a match-like object with groups:
482
+ 1: indent, 2: async prefix or None, 3: name,
483
+ 4: type params or "", 5: params string, 6: return type or ""
484
+ """
485
+ m = re.match(r"^(\s*)(async\s+)?def\s+(\w+)\s*(?:\[([^\]]*)\])?\s*\(", line)
486
+ if not m:
487
+ return None
488
+ # Find the matching closing paren by counting depth
489
+ paren_start = m.end() - 1 # index of the opening '('
490
+ depth = 1
491
+ i = paren_start + 1
492
+ while i < len(line) and depth > 0:
493
+ ch = line[i]
494
+ if ch == "(":
495
+ depth += 1
496
+ elif ch == ")":
497
+ depth -= 1
498
+ elif ch in ('"', "'"):
499
+ # Skip string literals
500
+ quote = ch
501
+ i += 1
502
+ while i < len(line) and line[i] != quote:
503
+ if line[i] == "\\":
504
+ i += 1
505
+ i += 1
506
+ i += 1
507
+ if depth != 0:
508
+ return None
509
+ paren_end = i - 1 # index of closing ')'
510
+ params_str = line[paren_start + 1:paren_end]
511
+ rest = line[paren_end + 1:]
512
+ # Match optional return type and colon
513
+ ret_match = re.match(r"\s*(?:->\s*(.+?))?\s*:\s*$", rest)
514
+ if not ret_match:
515
+ return None
516
+ return_type = ret_match.group(1) or ""
517
+ # Build a synthetic match via a simple namespace
518
+ class _FuncMatch:
519
+ def group(self, n: int) -> str | None:
520
+ return [
521
+ line, # 0: full match
522
+ m.group(1), # 1: indent
523
+ m.group(2), # 2: async prefix
524
+ m.group(3), # 3: name
525
+ m.group(4) or "", # 4: type params
526
+ params_str, # 5: params
527
+ return_type, # 6: return type
528
+ ][n]
529
+ return _FuncMatch()
530
+
531
+
532
+ def parse_python(content: str, path: str = "") -> FileAST:
533
+ """Parse Python source using regex patterns.
534
+
535
+ Extracts functions, classes, imports, and top-level variables.
536
+ Populates extended fields: ParamDef details, visibility, decorator flags,
537
+ generator detection, class properties, abstract detection, metaclass.
538
+ """
539
+ lines = content.split("\n")
540
+ ast = FileAST(path=path, language="python", line_count=len(lines))
541
+
542
+ # Parse imports
543
+ for i, line in enumerate(lines):
544
+ stripped = line.strip()
545
+ if stripped.startswith("from "):
546
+ match = re.match(r"from\s+([\w.]+)\s+import\s+(.+)", stripped)
547
+ if match:
548
+ module = match.group(1)
549
+ names = [n.strip().split(" as ")[0].strip() for n in match.group(2).split(",")]
550
+ ast.imports.append(ImportDef(
551
+ module=module, names=names, is_from=True, line=i + 1,
552
+ ))
553
+ elif stripped.startswith("import "):
554
+ match = re.match(r"import\s+(.+)", stripped)
555
+ if match:
556
+ for part in match.group(1).split(","):
557
+ parts = part.strip().split(" as ")
558
+ ast.imports.append(ImportDef(
559
+ module=parts[0].strip(),
560
+ alias=parts[1].strip() if len(parts) > 1 else "",
561
+ line=i + 1,
562
+ ))
563
+
564
+ # Preprocess lines to join multi-line def/class signatures
565
+ preprocessed = _join_multiline_defs(lines)
566
+
567
+ # Parse classes and functions
568
+ current_class: ClassDef | None = None
569
+ decorators: list[str] = []
570
+
571
+ for orig_line_idx, sig_end_idx, line in preprocessed:
572
+ i = orig_line_idx
573
+ stripped = line.strip()
574
+
575
+ # Decorators
576
+ if stripped.startswith("@"):
577
+ decorators.append(stripped[1:].split("(")[0])
578
+ continue
579
+
580
+ # Class definition
581
+ class_match = re.match(r"^class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:", stripped)
582
+ if class_match and not line.startswith((" ", "\t")):
583
+ class_name = class_match.group(1)
584
+ bases_str = class_match.group(2) or ""
585
+ bases = [b.strip() for b in bases_str.split(",") if b.strip()] if bases_str else []
586
+
587
+ # Extract metaclass from bases (metaclass=ABCMeta)
588
+ metaclass = ""
589
+ filtered_bases: list[str] = []
590
+ for b in bases:
591
+ mc_match = re.match(r"metaclass\s*=\s*(\w+)", b)
592
+ if mc_match:
593
+ metaclass = mc_match.group(1)
594
+ else:
595
+ filtered_bases.append(b)
596
+
597
+ # Find end of class (next non-indented line)
598
+ # Start scanning from after the last line of the signature
599
+ body_start = sig_end_idx + 1
600
+ end_line = body_start
601
+ for j in range(body_start, len(lines)):
602
+ if lines[j].strip() and not lines[j].startswith((" ", "\t")):
603
+ end_line = j
604
+ break
605
+ else:
606
+ end_line = len(lines)
607
+
608
+ # Extract docstring (body starts after the signature)
609
+ docstring = _extract_docstring(lines, body_start)
610
+
611
+ # Detect abstract class
612
+ is_abstract = (
613
+ "ABC" in filtered_bases
614
+ or "ABCMeta" in metaclass
615
+ or "abstractmethod" in decorators
616
+ )
617
+
618
+ # Extract properties
619
+ properties = _extract_class_properties(lines, body_start, end_line)
620
+
621
+ current_class = ClassDef(
622
+ name=class_name,
623
+ start_line=i + 1,
624
+ end_line=end_line,
625
+ bases=filtered_bases,
626
+ decorators=decorators,
627
+ docstring=docstring,
628
+ properties=properties,
629
+ is_abstract=is_abstract,
630
+ metaclass=metaclass,
631
+ )
632
+ ast.classes.append(current_class)
633
+ decorators = []
634
+ continue
635
+
636
+ # Function definition (with optional PEP 695 type params)
637
+ # Use balanced-paren-aware extraction instead of [^)]*
638
+ func_match = _match_function_def(line)
639
+ if func_match:
640
+ indent = func_match.group(1)
641
+ is_async = func_match.group(2) is not None
642
+ func_name = func_match.group(3)
643
+ type_params_str = func_match.group(4) or ""
644
+ params_str = func_match.group(5)
645
+ return_type = (func_match.group(6) or "").strip().rstrip(":")
646
+
647
+ # Parse type params (PEP 695)
648
+ type_params = [t.strip() for t in type_params_str.split(",") if t.strip()] if type_params_str else []
649
+
650
+ # Parse parameters (both simple and detailed)
651
+ params, parameters = _parse_python_params(params_str)
652
+
653
+ # Find end of function
654
+ # Start scanning from after the last line of the signature
655
+ func_indent = len(indent)
656
+ func_body_start = sig_end_idx + 1
657
+ end_line = func_body_start
658
+ for j in range(func_body_start, len(lines)):
659
+ line = lines[j]
660
+ if line.strip() and not line.startswith(" " * (func_indent + 1)) and not line.startswith("\t" * (func_indent // 4 + 1)) and (len(line) - len(line.lstrip())) <= func_indent:
661
+ end_line = j
662
+ break
663
+ else:
664
+ end_line = len(lines)
665
+
666
+ docstring = _extract_docstring(lines, func_body_start)
667
+
668
+ # Detect decorator-based flags
669
+ is_staticmethod = "staticmethod" in decorators
670
+ is_classmethod = "classmethod" in decorators
671
+ is_property_flag = "property" in decorators
672
+ is_abstract_method = "abstractmethod" in decorators
673
+
674
+ # Detect visibility
675
+ visibility = _detect_visibility(func_name)
676
+
677
+ # Detect generator
678
+ is_generator = _check_generator(lines, i, end_line, func_indent)
679
+
680
+ # If abstract method found on a function inside a class, mark class abstract
681
+ if is_abstract_method and current_class and not current_class.is_abstract:
682
+ current_class.is_abstract = True
683
+
684
+ func = FunctionDef(
685
+ name=func_name,
686
+ start_line=i + 1,
687
+ end_line=end_line,
688
+ params=params,
689
+ return_type=return_type,
690
+ decorators=decorators,
691
+ is_async=is_async,
692
+ is_method=func_indent > 0,
693
+ docstring=docstring,
694
+ parameters=parameters,
695
+ visibility=visibility,
696
+ is_generator=is_generator,
697
+ is_staticmethod=is_staticmethod,
698
+ is_classmethod=is_classmethod,
699
+ is_property=is_property_flag,
700
+ type_params=type_params,
701
+ )
702
+
703
+ if current_class and func_indent > 0:
704
+ current_class.methods.append(func)
705
+ else:
706
+ ast.functions.append(func)
707
+ current_class = None
708
+
709
+ decorators = []
710
+ continue
711
+
712
+ # Top-level variable
713
+ if not line.startswith((" ", "\t")) and "=" in stripped and not stripped.startswith(("#", "def ", "class ", "@", "if ", "for ", "while ", "return ")):
714
+ var_match = re.match(r"^([A-Z][A-Za-z_0-9]*|[A-Z_][A-Z_0-9]*)\s*[=:]", stripped)
715
+ if var_match:
716
+ ast.top_level_vars.append(var_match.group(1))
717
+
718
+ if stripped and not stripped.startswith(("#", "@")):
719
+ decorators = []
720
+
721
+ return ast
722
+
723
+
724
+ def _parse_js_params(params_str: str) -> tuple[list[str], list[ParamDef]]:
725
+ """Parse a JS/TS parameter string into param names and ParamDef list."""
726
+ params: list[str] = []
727
+ parameters: list[ParamDef] = []
728
+ if not params_str.strip():
729
+ return params, parameters
730
+ for p in params_str.split(","):
731
+ p = p.strip()
732
+ if not p:
733
+ continue
734
+ is_rest = p.startswith("...")
735
+ p_cleaned = p.lstrip(".")
736
+ name = p_cleaned
737
+ type_ann = ""
738
+ default_val = ""
739
+ if "=" in p_cleaned:
740
+ name, default_val = p_cleaned.split("=", 1)
741
+ name = name.strip()
742
+ default_val = default_val.strip()
743
+ if ":" in name:
744
+ name, type_ann = name.split(":", 1)
745
+ name = name.strip()
746
+ type_ann = type_ann.strip()
747
+ # Strip optional ? from name
748
+ name = name.rstrip("?")
749
+ params.append(name)
750
+ parameters.append(ParamDef(
751
+ name=name,
752
+ type_annotation=type_ann,
753
+ default_value=default_val,
754
+ is_rest=is_rest,
755
+ ))
756
+ return params, parameters
757
+
758
+
759
+ def parse_javascript(content: str, path: str = "") -> FileAST:
760
+ """Parse JavaScript/TypeScript source using regex patterns.
761
+
762
+ Extracts functions with parameter details, classes with methods and end
763
+ lines, and TypeScript visibility modifiers.
764
+ """
765
+ lines = content.split("\n")
766
+ lang = "typescript" if path.endswith((".ts", ".tsx")) else "javascript"
767
+ ast = FileAST(path=path, language=lang, line_count=len(lines))
768
+
769
+ decorators: list[str] = []
770
+ # Track class body for method extraction
771
+ current_class: ClassDef | None = None
772
+ class_brace_depth: int = 0
773
+
774
+ # Method pattern: matches class methods like `async foo(x: number): void {`
775
+ # including optional visibility, static, abstract, get/set modifiers.
776
+ _method_re = re.compile(
777
+ r"^(?:(?:public|private|protected|readonly)\s+)*"
778
+ r"(?:static\s+)?(?:abstract\s+)?(?:async\s+)?"
779
+ r"(?:get\s+|set\s+)?"
780
+ r"(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)"
781
+ r"(?:\s*:\s*([^{]+?))?\s*\{"
782
+ )
783
+
784
+ for i, line in enumerate(lines):
785
+ stripped = line.strip()
786
+
787
+ # Track brace depth inside a class body
788
+ if current_class is not None:
789
+ class_brace_depth += stripped.count("{") - stripped.count("}")
790
+ if class_brace_depth <= 0:
791
+ current_class = None
792
+ class_brace_depth = 0
793
+
794
+ # Decorators (TypeScript)
795
+ if stripped.startswith("@"):
796
+ decorators.append(stripped[1:].split("(")[0])
797
+ continue
798
+
799
+ # Import statements
800
+ import_match = re.match(r"import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+))\s+from\s+['\"]([^'\"]+)['\"]", stripped)
801
+ if import_match:
802
+ names = []
803
+ if import_match.group(1):
804
+ names = [n.strip().split(" as ")[0].strip() for n in import_match.group(1).split(",")]
805
+ elif import_match.group(2):
806
+ names = [import_match.group(2)]
807
+ ast.imports.append(ImportDef(
808
+ module=import_match.group(3),
809
+ names=names,
810
+ is_from=True,
811
+ line=i + 1,
812
+ ))
813
+
814
+ # Function definitions (top-level `function` keyword)
815
+ func_match = re.match(
816
+ r"^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)"
817
+ r"(?:\s*:\s*([^{]+?))?\s*\{",
818
+ stripped,
819
+ )
820
+ if func_match:
821
+ func_name = func_match.group(1)
822
+ params, parameters = _parse_js_params(func_match.group(2))
823
+ return_type = (func_match.group(3) or "").strip()
824
+
825
+ # Find end line via brace matching
826
+ end_line = i + 1
827
+ brace_count = stripped.count("{") - stripped.count("}")
828
+ if brace_count > 0:
829
+ for j in range(i + 1, min(i + 500, len(lines))):
830
+ brace_count += lines[j].count("{") - lines[j].count("}")
831
+ if brace_count <= 0:
832
+ end_line = j + 1
833
+ break
834
+
835
+ ast.functions.append(FunctionDef(
836
+ name=func_name,
837
+ start_line=i + 1,
838
+ end_line=end_line,
839
+ params=params,
840
+ parameters=parameters,
841
+ return_type=return_type,
842
+ is_async="async " in stripped,
843
+ decorators=decorators,
844
+ ))
845
+ decorators = []
846
+ continue
847
+
848
+ # Class definitions
849
+ class_match = re.match(
850
+ r"^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?\s*\{",
851
+ stripped,
852
+ )
853
+ if class_match:
854
+ bases = [class_match.group(2)] if class_match.group(2) else []
855
+ is_abstract = "abstract " in stripped.split("class")[0]
856
+
857
+ # Find end line via brace matching
858
+ end_line = i + 1
859
+ brace_count = stripped.count("{") - stripped.count("}")
860
+ if brace_count > 0:
861
+ for j in range(i + 1, min(i + 1000, len(lines))):
862
+ brace_count += lines[j].count("{") - lines[j].count("}")
863
+ if brace_count <= 0:
864
+ end_line = j + 1
865
+ break
866
+
867
+ current_class = ClassDef(
868
+ name=class_match.group(1),
869
+ start_line=i + 1,
870
+ end_line=end_line,
871
+ bases=bases,
872
+ decorators=decorators,
873
+ is_abstract=is_abstract,
874
+ )
875
+ ast.classes.append(current_class)
876
+ # Track brace depth: start at 1 (the opening brace of the class)
877
+ class_brace_depth = stripped.count("{") - stripped.count("}")
878
+ decorators = []
879
+ continue
880
+
881
+ # Method definitions inside a class body
882
+ if current_class is not None and class_brace_depth > 0:
883
+ method_match = _method_re.match(stripped)
884
+ if method_match:
885
+ method_name = method_match.group(1)
886
+ # Skip constructor as a special case — still include it
887
+ params, parameters = _parse_js_params(method_match.group(2))
888
+ return_type = (method_match.group(3) or "").strip()
889
+
890
+ # Find method end line via brace matching
891
+ end_line = i + 1
892
+ m_brace = stripped.count("{") - stripped.count("}")
893
+ if m_brace > 0:
894
+ for j in range(i + 1, min(i + 500, len(lines))):
895
+ m_brace += lines[j].count("{") - lines[j].count("}")
896
+ if m_brace <= 0:
897
+ end_line = j + 1
898
+ break
899
+
900
+ current_class.methods.append(FunctionDef(
901
+ name=method_name,
902
+ start_line=i + 1,
903
+ end_line=end_line,
904
+ params=params,
905
+ parameters=parameters,
906
+ return_type=return_type,
907
+ is_async="async " in stripped.split(method_name)[0],
908
+ decorators=decorators,
909
+ ))
910
+ decorators = []
911
+ continue
912
+
913
+ if stripped and not stripped.startswith("@"):
914
+ decorators = []
915
+
916
+ return ast
917
+
918
+
919
+ def parse_rust(content: str, path: str = "") -> FileAST:
920
+ """Parse Rust source using regex patterns.
921
+
922
+ Extracts functions, structs, enums, traits, impl blocks, imports (use/mod).
923
+ """
924
+ lines = content.split("\n")
925
+ ast = FileAST(path=path, language="rust", line_count=len(lines))
926
+
927
+ for i, line in enumerate(lines):
928
+ stripped = line.strip()
929
+
930
+ # Imports: use ... ;
931
+ use_match = re.match(r"^(?:pub\s+)?use\s+(.+);", stripped)
932
+ if use_match:
933
+ module = use_match.group(1).strip()
934
+ # Extract the base path (before :: { ... })
935
+ base = re.sub(r"\s*::\s*\{[^}]*\}", "", module)
936
+ base = re.sub(r"\s*::\s*\w+$", "", base)
937
+ ast.imports.append(ImportDef(module=module, is_from=True, line=i + 1))
938
+ continue
939
+
940
+ # mod declarations: mod foo;
941
+ mod_match = re.match(r"^(?:pub\s+)?mod\s+(\w+)\s*;", stripped)
942
+ if mod_match:
943
+ ast.imports.append(ImportDef(
944
+ module=mod_match.group(1), is_from=False, line=i + 1,
945
+ ))
946
+ continue
947
+
948
+ # Functions: (pub)? (async)? fn name(...)
949
+ fn_match = re.match(
950
+ r"^(?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+\"[^\"]*\"\s+)?fn\s+(\w+)",
951
+ stripped,
952
+ )
953
+ if fn_match:
954
+ func_name = fn_match.group(1)
955
+ # Find end via brace matching
956
+ end_line = i + 1
957
+ brace_count = stripped.count("{") - stripped.count("}")
958
+ if brace_count > 0:
959
+ for j in range(i + 1, min(i + 500, len(lines))):
960
+ brace_count += lines[j].count("{") - lines[j].count("}")
961
+ if brace_count <= 0:
962
+ end_line = j + 1
963
+ break
964
+ is_async = "async " in stripped.split("fn")[0]
965
+ ast.functions.append(FunctionDef(
966
+ name=func_name,
967
+ start_line=i + 1,
968
+ end_line=end_line,
969
+ is_async=is_async,
970
+ ))
971
+ continue
972
+
973
+ # Structs: (pub)? struct Name
974
+ struct_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?struct\s+(\w+)", stripped)
975
+ if struct_match:
976
+ end_line = i + 1
977
+ brace_count = stripped.count("{") - stripped.count("}")
978
+ if brace_count > 0:
979
+ for j in range(i + 1, min(i + 500, len(lines))):
980
+ brace_count += lines[j].count("{") - lines[j].count("}")
981
+ if brace_count <= 0:
982
+ end_line = j + 1
983
+ break
984
+ ast.classes.append(ClassDef(
985
+ name=struct_match.group(1),
986
+ start_line=i + 1,
987
+ end_line=end_line,
988
+ ))
989
+ continue
990
+
991
+ # Enums: (pub)? enum Name
992
+ enum_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?enum\s+(\w+)", stripped)
993
+ if enum_match:
994
+ end_line = i + 1
995
+ brace_count = stripped.count("{") - stripped.count("}")
996
+ if brace_count > 0:
997
+ for j in range(i + 1, min(i + 500, len(lines))):
998
+ brace_count += lines[j].count("{") - lines[j].count("}")
999
+ if brace_count <= 0:
1000
+ end_line = j + 1
1001
+ break
1002
+ ast.classes.append(ClassDef(
1003
+ name=enum_match.group(1),
1004
+ start_line=i + 1,
1005
+ end_line=end_line,
1006
+ ))
1007
+ continue
1008
+
1009
+ # Traits: (pub)? trait Name
1010
+ trait_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?(?:unsafe\s+)?trait\s+(\w+)", stripped)
1011
+ if trait_match:
1012
+ end_line = i + 1
1013
+ brace_count = stripped.count("{") - stripped.count("}")
1014
+ if brace_count > 0:
1015
+ for j in range(i + 1, min(i + 500, len(lines))):
1016
+ brace_count += lines[j].count("{") - lines[j].count("}")
1017
+ if brace_count <= 0:
1018
+ end_line = j + 1
1019
+ break
1020
+ ast.classes.append(ClassDef(
1021
+ name=trait_match.group(1),
1022
+ start_line=i + 1,
1023
+ end_line=end_line,
1024
+ ))
1025
+ continue
1026
+
1027
+ # Top-level constants: (pub)? const NAME: ...
1028
+ const_match = re.match(r"^(?:pub\s+)?(?:const|static)\s+(\w+)\s*:", stripped)
1029
+ if const_match:
1030
+ ast.top_level_vars.append(const_match.group(1))
1031
+
1032
+ return ast
1033
+
1034
+
1035
+ def parse_go(content: str, path: str = "") -> FileAST:
1036
+ """Parse Go source using regex patterns.
1037
+
1038
+ Extracts functions, methods, structs, interfaces, imports.
1039
+ """
1040
+ lines = content.split("\n")
1041
+ ast = FileAST(path=path, language="go", line_count=len(lines))
1042
+
1043
+ i = 0
1044
+ while i < len(lines):
1045
+ stripped = lines[i].strip()
1046
+
1047
+ # Import block: import ( ... )
1048
+ if stripped == "import (" or stripped.startswith("import ("):
1049
+ i += 1
1050
+ while i < len(lines):
1051
+ imp_line = lines[i].strip()
1052
+ if imp_line == ")":
1053
+ break
1054
+ # Parse "pkg" or alias "pkg"
1055
+ imp_match = re.match(r'^(?:\w+\s+)?"([^"]+)"', imp_line)
1056
+ if imp_match:
1057
+ ast.imports.append(ImportDef(
1058
+ module=imp_match.group(1), is_from=False, line=i + 1,
1059
+ ))
1060
+ i += 1
1061
+ i += 1
1062
+ continue
1063
+
1064
+ # Single import: import "pkg"
1065
+ single_imp = re.match(r'^import\s+(?:\w+\s+)?"([^"]+)"', stripped)
1066
+ if single_imp:
1067
+ ast.imports.append(ImportDef(
1068
+ module=single_imp.group(1), is_from=False, line=i + 1,
1069
+ ))
1070
+ i += 1
1071
+ continue
1072
+
1073
+ # Method: func (receiver) Name(...)
1074
+ method_match = re.match(r"^func\s+\([^)]+\)\s+(\w+)\s*\(", stripped)
1075
+ if method_match:
1076
+ func_name = method_match.group(1)
1077
+ end_line = i + 1
1078
+ brace_count = stripped.count("{") - stripped.count("}")
1079
+ if brace_count > 0:
1080
+ for j in range(i + 1, min(i + 500, len(lines))):
1081
+ brace_count += lines[j].count("{") - lines[j].count("}")
1082
+ if brace_count <= 0:
1083
+ end_line = j + 1
1084
+ break
1085
+ ast.functions.append(FunctionDef(
1086
+ name=func_name,
1087
+ start_line=i + 1,
1088
+ end_line=end_line,
1089
+ is_method=True,
1090
+ ))
1091
+ i += 1
1092
+ continue
1093
+
1094
+ # Function: func Name(...)
1095
+ fn_match = re.match(r"^func\s+(\w+)\s*\(", stripped)
1096
+ if fn_match:
1097
+ func_name = fn_match.group(1)
1098
+ end_line = i + 1
1099
+ brace_count = stripped.count("{") - stripped.count("}")
1100
+ if brace_count > 0:
1101
+ for j in range(i + 1, min(i + 500, len(lines))):
1102
+ brace_count += lines[j].count("{") - lines[j].count("}")
1103
+ if brace_count <= 0:
1104
+ end_line = j + 1
1105
+ break
1106
+ ast.functions.append(FunctionDef(
1107
+ name=func_name,
1108
+ start_line=i + 1,
1109
+ end_line=end_line,
1110
+ ))
1111
+ i += 1
1112
+ continue
1113
+
1114
+ # Struct: type Name struct {
1115
+ struct_match = re.match(r"^type\s+(\w+)\s+struct\s*\{", stripped)
1116
+ if struct_match:
1117
+ end_line = i + 1
1118
+ brace_count = stripped.count("{") - stripped.count("}")
1119
+ if brace_count > 0:
1120
+ for j in range(i + 1, min(i + 500, len(lines))):
1121
+ brace_count += lines[j].count("{") - lines[j].count("}")
1122
+ if brace_count <= 0:
1123
+ end_line = j + 1
1124
+ break
1125
+ ast.classes.append(ClassDef(
1126
+ name=struct_match.group(1),
1127
+ start_line=i + 1,
1128
+ end_line=end_line,
1129
+ ))
1130
+ i += 1
1131
+ continue
1132
+
1133
+ # Interface: type Name interface {
1134
+ iface_match = re.match(r"^type\s+(\w+)\s+interface\s*\{", stripped)
1135
+ if iface_match:
1136
+ end_line = i + 1
1137
+ brace_count = stripped.count("{") - stripped.count("}")
1138
+ if brace_count > 0:
1139
+ for j in range(i + 1, min(i + 500, len(lines))):
1140
+ brace_count += lines[j].count("{") - lines[j].count("}")
1141
+ if brace_count <= 0:
1142
+ end_line = j + 1
1143
+ break
1144
+ ast.classes.append(ClassDef(
1145
+ name=iface_match.group(1),
1146
+ start_line=i + 1,
1147
+ end_line=end_line,
1148
+ ))
1149
+ i += 1
1150
+ continue
1151
+
1152
+ # Top-level constants
1153
+ const_match = re.match(r"^(?:var|const)\s+(\w+)\s+", stripped)
1154
+ if const_match:
1155
+ ast.top_level_vars.append(const_match.group(1))
1156
+
1157
+ i += 1
1158
+
1159
+ return ast
1160
+
1161
+
1162
+ def _ts_result_to_file_ast(result: dict, file_path: str) -> FileAST:
1163
+ """Convert tree-sitter parse result dict into a FileAST dataclass."""
1164
+ language = result.get("language", "unknown")
1165
+
1166
+ # Convert function dicts to FunctionDef
1167
+ functions: list[FunctionDef] = []
1168
+ for fn in result.get("functions", []):
1169
+ params = fn.get("parameters", [])
1170
+ param_defs = [
1171
+ ParamDef(name=p) if isinstance(p, str) else p
1172
+ for p in params
1173
+ ]
1174
+ functions.append(FunctionDef(
1175
+ name=fn["name"],
1176
+ qualified_name=fn.get("qualified_name", ""),
1177
+ is_nested=fn.get("is_nested", False),
1178
+ is_method=fn.get("is_method", False),
1179
+ start_line=fn.get("start_line", 0),
1180
+ end_line=fn.get("end_line", 0),
1181
+ parameters=param_defs,
1182
+ return_type=fn.get("return_type", ""),
1183
+ decorators=fn.get("decorators", []),
1184
+ is_async=fn.get("is_async", False),
1185
+ visibility=fn.get("visibility", "public"),
1186
+ docstring=fn.get("docstring", ""),
1187
+ ))
1188
+
1189
+ # Convert class dicts to ClassDef
1190
+ classes: list[ClassDef] = []
1191
+ for cls in result.get("classes", []):
1192
+ methods: list[FunctionDef] = []
1193
+ for m in cls.get("methods", []):
1194
+ m_params = m.get("parameters", [])
1195
+ m_param_defs = [
1196
+ ParamDef(name=p) if isinstance(p, str) else p
1197
+ for p in m_params
1198
+ ]
1199
+ methods.append(FunctionDef(
1200
+ name=m["name"],
1201
+ start_line=m.get("start_line", 0),
1202
+ end_line=m.get("end_line", 0),
1203
+ parameters=m_param_defs,
1204
+ return_type=m.get("return_type", ""),
1205
+ decorators=m.get("decorators", []),
1206
+ is_async=m.get("is_async", False),
1207
+ visibility=m.get("visibility", "public"),
1208
+ docstring=m.get("docstring", ""),
1209
+ ))
1210
+ classes.append(ClassDef(
1211
+ name=cls["name"],
1212
+ start_line=cls.get("start_line", 0),
1213
+ end_line=cls.get("end_line", 0),
1214
+ bases=cls.get("bases", []),
1215
+ methods=methods,
1216
+ decorators=cls.get("decorators", []),
1217
+ docstring=cls.get("docstring", ""),
1218
+ ))
1219
+
1220
+ # Convert import dicts to ImportDef
1221
+ imports: list[ImportDef] = []
1222
+ for imp in result.get("imports", []):
1223
+ imports.append(ImportDef(
1224
+ module=imp.get("module", ""),
1225
+ is_from=imp.get("is_from", False),
1226
+ names=imp.get("names", []),
1227
+ alias=imp.get("alias", ""),
1228
+ is_reexport=imp.get("is_reexport", False),
1229
+ line=imp.get("start_line", 0),
1230
+ ))
1231
+
1232
+ return FileAST(
1233
+ path=file_path,
1234
+ language=language,
1235
+ functions=functions,
1236
+ classes=classes,
1237
+ imports=imports,
1238
+ top_level_vars=result.get("top_level_vars", []),
1239
+ line_count=result.get("line_count", 0),
1240
+ )
1241
+
1242
+
1243
+ def parse_file(file_path: str, content: str | None = None) -> FileAST:
1244
+ """Parse a source file and return its AST.
1245
+
1246
+ Uses a tiered fallback strategy:
1247
+ 1. Tree-sitter with per-language config (25 languages)
1248
+ 2. Generic tree-sitter heuristics (30+ additional languages)
1249
+ 3. Regex-based parsers for Python/JS/TS/Rust/Go
1250
+ 4. Universal-ctags subprocess (150+ languages)
1251
+ 5. Minimal FileAST for truly unknown languages
1252
+
1253
+ Returns a FileAST for any input file.
1254
+ """
1255
+ if content is None:
1256
+ try:
1257
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
1258
+ except OSError:
1259
+ return FileAST(path=file_path, language="unknown")
1260
+
1261
+ lang = detect_language(file_path)
1262
+
1263
+ # Tier 1: Tree-sitter with per-language config (25 languages)
1264
+ try:
1265
+ from attocode_intel._internal.integrations.context.ts_parser import ts_parse_file
1266
+
1267
+ ts_result = ts_parse_file(file_path, content=content, language=lang)
1268
+ if ts_result is not None:
1269
+ result = _ts_result_to_file_ast(ts_result, file_path)
1270
+ result.parsing_tier = "tree_sitter"
1271
+ return result
1272
+ except ImportError:
1273
+ pass # tree-sitter not installed
1274
+
1275
+ # Tier 2: Regex fallback for Python, JavaScript/TypeScript, Rust, Go
1276
+ regex_result: FileAST | None = None
1277
+ if lang == "python":
1278
+ regex_result = parse_python(content, file_path)
1279
+ elif lang in ("javascript", "typescript"):
1280
+ regex_result = parse_javascript(content, file_path)
1281
+ elif lang == "rust":
1282
+ regex_result = parse_rust(content, file_path)
1283
+ elif lang == "go":
1284
+ regex_result = parse_go(content, file_path)
1285
+ if regex_result is not None:
1286
+ regex_result.parsing_tier = "regex"
1287
+ return regex_result
1288
+
1289
+ # Tier 3: Generic tree-sitter heuristics (30+ additional languages)
1290
+ try:
1291
+ from attocode_intel._internal.integrations.context.ts_parser import ts_parse_file_generic
1292
+
1293
+ generic_result = ts_parse_file_generic(file_path, content=content, language=lang)
1294
+ if generic_result is not None:
1295
+ result = _ts_result_to_file_ast(generic_result, file_path)
1296
+ result.parsing_tier = "generic_ts"
1297
+ return result
1298
+ except ImportError:
1299
+ pass
1300
+
1301
+ # Tier 4: Universal-ctags fallback (150+ languages)
1302
+ try:
1303
+ from attocode_intel._internal.integrations.context.ts_parser import ctags_parse_file
1304
+
1305
+ ctags_result = ctags_parse_file(file_path, content=content)
1306
+ if ctags_result is not None:
1307
+ if lang:
1308
+ ctags_result["language"] = lang
1309
+ result = _ts_result_to_file_ast(ctags_result, file_path)
1310
+ result.parsing_tier = "ctags"
1311
+ return result
1312
+ except Exception:
1313
+ pass # ctags not installed or other error
1314
+
1315
+ # Tier 5: Minimal parsing for unknown languages
1316
+ return FileAST(
1317
+ path=file_path,
1318
+ language=lang if lang != "unknown" else lang,
1319
+ line_count=content.count("\n") + 1,
1320
+ parsing_tier="minimal",
1321
+ )
1322
+
1323
+
1324
+ def diff_file_ast(old: FileAST, new: FileAST) -> list[SymbolChange]:
1325
+ """Compute symbol-level diff between two versions of a file's AST.
1326
+
1327
+ Compares functions and classes by name and detects additions,
1328
+ removals, and modifications (signature or line range changes).
1329
+
1330
+ Args:
1331
+ old: Previous AST for the file.
1332
+ new: Current AST for the file.
1333
+
1334
+ Returns:
1335
+ List of SymbolChange objects describing what changed.
1336
+ """
1337
+ changes: list[SymbolChange] = []
1338
+ file_path = new.path or old.path
1339
+
1340
+ # --- Functions ---
1341
+ old_funcs = {f.name: f for f in old.functions}
1342
+ new_funcs = {f.name: f for f in new.functions}
1343
+
1344
+ for name in new_funcs:
1345
+ if name not in old_funcs:
1346
+ changes.append(SymbolChange(
1347
+ kind="added", symbol_name=name, symbol_kind="function",
1348
+ file_path=file_path,
1349
+ ))
1350
+ else:
1351
+ of, nf = old_funcs[name], new_funcs[name]
1352
+ if (of.params != nf.params or of.return_type != nf.return_type
1353
+ or of.decorators != nf.decorators or of.is_async != nf.is_async
1354
+ or of.start_line != nf.start_line or of.end_line != nf.end_line):
1355
+ changes.append(SymbolChange(
1356
+ kind="modified", symbol_name=name, symbol_kind="function",
1357
+ file_path=file_path, previous=of,
1358
+ ))
1359
+
1360
+ for name in old_funcs:
1361
+ if name not in new_funcs:
1362
+ changes.append(SymbolChange(
1363
+ kind="removed", symbol_name=name, symbol_kind="function",
1364
+ file_path=file_path, previous=old_funcs[name],
1365
+ ))
1366
+
1367
+ # --- Classes ---
1368
+ old_classes = {c.name: c for c in old.classes}
1369
+ new_classes = {c.name: c for c in new.classes}
1370
+
1371
+ for name in new_classes:
1372
+ if name not in old_classes:
1373
+ changes.append(SymbolChange(
1374
+ kind="added", symbol_name=name, symbol_kind="class",
1375
+ file_path=file_path,
1376
+ ))
1377
+ else:
1378
+ oc, nc = old_classes[name], new_classes[name]
1379
+ old_method_names = {m.name for m in oc.methods}
1380
+ new_method_names = {m.name for m in nc.methods}
1381
+ if (oc.bases != nc.bases or oc.decorators != nc.decorators
1382
+ or old_method_names != new_method_names
1383
+ or oc.start_line != nc.start_line or oc.end_line != nc.end_line):
1384
+ changes.append(SymbolChange(
1385
+ kind="modified", symbol_name=name, symbol_kind="class",
1386
+ file_path=file_path, previous=oc,
1387
+ ))
1388
+ # Check individual method changes
1389
+ old_methods = {m.name: m for m in oc.methods}
1390
+ new_methods = {m.name: m for m in nc.methods}
1391
+ for mname in new_methods:
1392
+ if mname not in old_methods:
1393
+ changes.append(SymbolChange(
1394
+ kind="added", symbol_name=f"{name}.{mname}",
1395
+ symbol_kind="method", file_path=file_path,
1396
+ ))
1397
+ else:
1398
+ om, nm = old_methods[mname], new_methods[mname]
1399
+ if (om.params != nm.params or om.return_type != nm.return_type
1400
+ or om.start_line != nm.start_line):
1401
+ changes.append(SymbolChange(
1402
+ kind="modified", symbol_name=f"{name}.{mname}",
1403
+ symbol_kind="method", file_path=file_path, previous=om,
1404
+ ))
1405
+ for mname in old_methods:
1406
+ if mname not in new_methods:
1407
+ changes.append(SymbolChange(
1408
+ kind="removed", symbol_name=f"{name}.{mname}",
1409
+ symbol_kind="method", file_path=file_path, previous=old_methods[mname],
1410
+ ))
1411
+
1412
+ return changes
1413
+
1414
+
1415
+ def diff_imports(old: FileAST, new: FileAST) -> DependencyChanges:
1416
+ """Compute import-level diff between two versions of a file's AST.
1417
+
1418
+ Args:
1419
+ old: Previous AST for the file.
1420
+ new: Current AST for the file.
1421
+
1422
+ Returns:
1423
+ DependencyChanges with added and removed imports.
1424
+ """
1425
+ old_set = {(i.module, tuple(i.names), i.is_from) for i in old.imports}
1426
+ new_set = {(i.module, tuple(i.names), i.is_from) for i in new.imports}
1427
+
1428
+ added = [
1429
+ ImportDef(module=m, names=list(n), is_from=f)
1430
+ for m, n, f in (new_set - old_set)
1431
+ ]
1432
+ removed = [
1433
+ ImportDef(module=m, names=list(n), is_from=f)
1434
+ for m, n, f in (old_set - new_set)
1435
+ ]
1436
+
1437
+ return DependencyChanges(added=added, removed=removed)
1438
+
1439
+
1440
+ def _extract_docstring(lines: list[str], start_idx: int) -> str:
1441
+ """Extract a Python docstring starting at the given line index."""
1442
+ if start_idx >= len(lines):
1443
+ return ""
1444
+
1445
+ # Look for triple-quoted string
1446
+ for i in range(start_idx, min(start_idx + 3, len(lines))):
1447
+ stripped = lines[i].strip()
1448
+ if stripped.startswith('"""') or stripped.startswith("'''"):
1449
+ quote = stripped[:3]
1450
+ if stripped.endswith(quote) and len(stripped) > 6:
1451
+ return stripped[3:-3].strip()
1452
+ # Multi-line docstring
1453
+ doc_lines = [stripped[3:]]
1454
+ for j in range(i + 1, len(lines)):
1455
+ line = lines[j].strip()
1456
+ if quote in line:
1457
+ doc_lines.append(line.split(quote)[0])
1458
+ return "\n".join(doc_lines).strip()
1459
+ doc_lines.append(line)
1460
+ break
1461
+ return ""