code2llm 0.5.176__tar.gz → 0.5.183__tar.gz

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 (237) hide show
  1. {code2llm-0.5.176 → code2llm-0.5.183}/MANIFEST.in +1 -0
  2. {code2llm-0.5.176/code2llm.egg-info → code2llm-0.5.183}/PKG-INFO +23 -9
  3. {code2llm-0.5.176 → code2llm-0.5.183}/README.md +9 -5
  4. code2llm-0.5.183/VERSION +1 -0
  5. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/__init__.py +1 -1
  6. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/__init__.py +8 -0
  7. code2llm-0.5.183/code2llm/analysis/call_graph.py +16 -0
  8. code2llm-0.5.183/code2llm/analysis/call_graph_engine.py +448 -0
  9. code2llm-0.5.183/code2llm/analysis/complexity.py +285 -0
  10. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/coupling.py +17 -1
  11. code2llm-0.5.183/code2llm/analysis/native_bridge.py +285 -0
  12. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/pipeline_detector.py +21 -6
  13. code2llm-0.5.183/code2llm/analysis/smell_engine.py +363 -0
  14. code2llm-0.5.183/code2llm/analysis/smells.py +89 -0
  15. code2llm-0.5.183/code2llm/autoupdate.py +178 -0
  16. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli.py +6 -0
  17. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/code2logic.py +4 -0
  18. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/orchestrator.py +26 -4
  19. code2llm-0.5.183/code2llm/cli_exports/orchestrator_constants.py +126 -0
  20. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/analyzer.py +108 -19
  21. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/config.py +1 -1
  22. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/file_analyzer.py +2 -27
  23. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/file_cache.py +10 -5
  24. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/file_filter.py +66 -11
  25. code2llm-0.5.183/code2llm/core/lang/_calls.py +20 -0
  26. code2llm-0.5.183/code2llm/core/lang/_complexity.py +30 -0
  27. code2llm-0.5.183/code2llm/core/lang/js_analysis.py +201 -0
  28. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/typescript.py +3 -19
  29. code2llm-0.5.183/code2llm/core/models.py +227 -0
  30. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/persistent_cache.py +72 -17
  31. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/refactoring.py +69 -32
  32. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/prioritizer.py +22 -15
  33. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/flow_constants.py +4 -0
  34. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/module_list.py +1 -1
  35. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/utils.py +1 -1
  36. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/yaml_export.py +1 -1
  37. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/_render_section_helpers.py +20 -11
  38. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/helpers.py +21 -2
  39. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/renderer.py +16 -0
  40. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/mermaid/png.py +45 -26
  41. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/__init__.py +1 -1
  42. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/parsers/toon_parser.py +10 -4
  43. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/refactor/prompt_engine.py +1 -1
  44. code2llm-0.5.183/code2llm/templates/extract_method.md +22 -0
  45. code2llm-0.5.183/code2llm/templates/move_method.md +19 -0
  46. {code2llm-0.5.176 → code2llm-0.5.183/code2llm.egg-info}/PKG-INFO +23 -9
  47. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm.egg-info/SOURCES.txt +18 -1
  48. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm.egg-info/requires.txt +17 -3
  49. code2llm-0.5.183/code2llm.egg-info/top_level.txt +3 -0
  50. {code2llm-0.5.176 → code2llm-0.5.183}/pyproject.toml +37 -20
  51. {code2llm-0.5.176 → code2llm-0.5.183}/setup.py +1 -1
  52. code2llm-0.5.183/tests/test_analysis_cache_version.py +122 -0
  53. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_analyzer.py +69 -0
  54. code2llm-0.5.183/tests/test_call_graph_engine.py +130 -0
  55. code2llm-0.5.183/tests/test_complexity.py +204 -0
  56. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_edge_cases.py +33 -0
  57. code2llm-0.5.183/tests/test_export_cache_ownership.py +143 -0
  58. code2llm-0.5.183/tests/test_js_complexity_boundaries.py +115 -0
  59. code2llm-0.5.183/tests/test_native_rust_bridge.py +357 -0
  60. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_orchestrator_cache_mtime.py +3 -2
  61. code2llm-0.5.183/tests/test_perf_exporters.py +74 -0
  62. code2llm-0.5.183/tests/test_smell_engine.py +167 -0
  63. code2llm-0.5.183/tests/test_version_parity.py +38 -0
  64. code2llm-0.5.176/VERSION +0 -1
  65. code2llm-0.5.176/code2llm/analysis/call_graph.py +0 -208
  66. code2llm-0.5.176/code2llm/analysis/smells.py +0 -284
  67. code2llm-0.5.176/code2llm/cli_exports/orchestrator_constants.py +0 -55
  68. code2llm-0.5.176/code2llm/core/lang/_calls.py +0 -96
  69. code2llm-0.5.176/code2llm/core/lang/_complexity.py +0 -73
  70. code2llm-0.5.176/code2llm/core/models.py +0 -207
  71. code2llm-0.5.176/code2llm.egg-info/top_level.txt +0 -1
  72. {code2llm-0.5.176 → code2llm-0.5.183}/LICENSE +0 -0
  73. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/__main__.py +0 -0
  74. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/_data_impl.py +0 -0
  75. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/cfg.py +0 -0
  76. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/data_analysis.py +0 -0
  77. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/dfg.py +0 -0
  78. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/pipeline_classifier.py +0 -0
  79. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/pipeline_resolver.py +0 -0
  80. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/side_effects.py +0 -0
  81. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/type_inference.py +0 -0
  82. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/utils/__init__.py +0 -0
  83. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/analysis/utils/ast_helpers.py +0 -0
  84. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/api.py +0 -0
  85. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_analysis.py +0 -0
  86. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_commands.py +0 -0
  87. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/__init__.py +0 -0
  88. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/formats.py +0 -0
  89. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/orchestrator_chunked.py +0 -0
  90. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/orchestrator_handlers.py +0 -0
  91. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_exports/prompt.py +0 -0
  92. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/cli_parser.py +0 -0
  93. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/__init__.py +0 -0
  94. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/ast_registry.py +0 -0
  95. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/export_pipeline.py +0 -0
  96. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/gitignore.py +0 -0
  97. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/incremental.py +0 -0
  98. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/__init__.py +0 -0
  99. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/_c_parser.py +0 -0
  100. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/base.py +0 -0
  101. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/cpp.py +0 -0
  102. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/csharp.py +0 -0
  103. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/generic.py +0 -0
  104. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/go_lang.py +0 -0
  105. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/java.py +0 -0
  106. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/php.py +0 -0
  107. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/ruby.py +0 -0
  108. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/rust.py +0 -0
  109. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/ts_extractors.py +0 -0
  110. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/lang/ts_parser.py +0 -0
  111. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/large_repo.py +0 -0
  112. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/repo_files.py +0 -0
  113. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/source_classifier.py +0 -0
  114. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/__init__.py +0 -0
  115. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/cache.py +0 -0
  116. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/incremental.py +0 -0
  117. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/scanner.py +0 -0
  118. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming/strategies.py +0 -0
  119. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/streaming_analyzer.py +0 -0
  120. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/core/toon_size_manager.py +0 -0
  121. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/__init__.py +0 -0
  122. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/article_view.py +0 -0
  123. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/base.py +0 -0
  124. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/context_exporter.py +0 -0
  125. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/context_view.py +0 -0
  126. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/dashboard_data.py +0 -0
  127. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/dashboard_renderer.py +0 -0
  128. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/__init__.py +0 -0
  129. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/computation.py +0 -0
  130. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/constants.py +0 -0
  131. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/exclusion.py +0 -0
  132. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/render.py +0 -0
  133. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution/yaml_export.py +0 -0
  134. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/evolution_exporter.py +0 -0
  135. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/flow_exporter.py +0 -0
  136. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/flow_renderer.py +0 -0
  137. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/html_dashboard.py +0 -0
  138. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/index_generator/__init__.py +0 -0
  139. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/index_generator/renderer.py +0 -0
  140. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/index_generator/scanner.py +0 -0
  141. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/index_generator.py +0 -0
  142. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/json_exporter.py +0 -0
  143. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/llm_exporter.py +0 -0
  144. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/__init__.py +0 -0
  145. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/alerts.py +0 -0
  146. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/details.py +0 -0
  147. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map/header.py +0 -0
  148. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/map_exporter.py +0 -0
  149. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/__init__.py +0 -0
  150. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/calls.py +0 -0
  151. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/classic.py +0 -0
  152. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/compact.py +0 -0
  153. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/flow_compact.py +0 -0
  154. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/flow_detailed.py +0 -0
  155. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/flow_full.py +0 -0
  156. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid/utils.py +0 -0
  157. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid_exporter.py +0 -0
  158. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  159. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/planfile_tickets.py +0 -0
  160. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/__init__.py +0 -0
  161. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/constants.py +0 -0
  162. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/core.py +0 -0
  163. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/evolution.py +0 -0
  164. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/health.py +0 -0
  165. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/hotspots.py +0 -0
  166. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml/modules.py +0 -0
  167. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/project_yaml_exporter.py +0 -0
  168. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme/__init__.py +0 -0
  169. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme/content.py +0 -0
  170. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme/files.py +0 -0
  171. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme/insights.py +0 -0
  172. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme/sections.py +0 -0
  173. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/readme_exporter.py +0 -0
  174. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/report_generators.py +0 -0
  175. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/__init__.py +0 -0
  176. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/_render_coupling_helpers.py +0 -0
  177. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/constants.py +0 -0
  178. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/metrics.py +0 -0
  179. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/metrics_core.py +0 -0
  180. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/metrics_duplicates.py +0 -0
  181. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/metrics_health.py +0 -0
  182. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon/module_detail.py +0 -0
  183. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon.py +0 -0
  184. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/toon_view.py +0 -0
  185. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/validate_project.py +0 -0
  186. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/exporters/yaml_exporter.py +0 -0
  187. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/__init__.py +0 -0
  188. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/_utils.py +0 -0
  189. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/__init__.py +0 -0
  190. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/analysis.py +0 -0
  191. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/cli.py +0 -0
  192. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/generator.py +0 -0
  193. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/nodes.py +0 -0
  194. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/parsing.py +0 -0
  195. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow/utils.py +0 -0
  196. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_flow.py +0 -0
  197. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/llm_task.py +0 -0
  198. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/mermaid/__init__.py +0 -0
  199. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/mermaid/fix.py +0 -0
  200. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/mermaid/validation.py +0 -0
  201. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/generators/mermaid.py +0 -0
  202. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/config.py +0 -0
  203. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/entity_resolution.py +0 -0
  204. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/intent_matching.py +0 -0
  205. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/normalization.py +0 -0
  206. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/nlp/pipeline.py +0 -0
  207. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/patterns/__init__.py +0 -0
  208. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/patterns/detector.py +0 -0
  209. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm/refactor/__init__.py +0 -0
  210. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm.egg-info/dependency_links.txt +0 -0
  211. {code2llm-0.5.176 → code2llm-0.5.183}/code2llm.egg-info/entry_points.txt +0 -0
  212. {code2llm-0.5.176 → code2llm-0.5.183}/setup.cfg +0 -0
  213. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_advanced_analysis.py +0 -0
  214. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_cache_invalidation_e2e.py +0 -0
  215. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_calls_toon_export.py +0 -0
  216. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_declarative_collection.py +0 -0
  217. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_deep_analysis.py +0 -0
  218. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_dup_groups.py +0 -0
  219. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_export_cache_flags.py +0 -0
  220. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_export_profile_contract.py +0 -0
  221. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_file_analyzer_tagging.py +0 -0
  222. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_flow_exporter.py +0 -0
  223. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_format_quality.py +0 -0
  224. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_mermaid_png.py +0 -0
  225. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_multilanguage_e2e.py +0 -0
  226. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_nlp_pipeline.py +0 -0
  227. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_nonpython_cc_calls.py +0 -0
  228. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_persistent_cache.py +0 -0
  229. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_pipeline_detector.py +0 -0
  230. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_planfile_tickets_exporter.py +0 -0
  231. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_project_toon_export.py +0 -0
  232. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_prompt_engine.py +0 -0
  233. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_prompt_txt.py +0 -0
  234. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_refactoring_engine.py +0 -0
  235. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_smells_data_clumps.py +0 -0
  236. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_smells_shotgun_surgery.py +0 -0
  237. {code2llm-0.5.176 → code2llm-0.5.183}/tests/test_toon_v2.py +0 -0
@@ -2,3 +2,4 @@ include VERSION
2
2
  include README.md
3
3
  include LICENSE
4
4
  recursive-include code2llm *.py
5
+ recursive-include code2llm/templates *.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.176
3
+ Version: 0.5.183
4
4
  Summary: High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries
5
5
  Home-page: https://github.com/wronai/stts
6
6
  Author: Tom Sapletta
@@ -36,7 +36,7 @@ Requires-Dist: psutil>=5.9
36
36
  Requires-Dist: code2logic
37
37
  Requires-Dist: vulture>=2.10
38
38
  Requires-Dist: tiktoken>=0.5
39
- Requires-Dist: tree-sitter>=0.21
39
+ Requires-Dist: tree-sitter>=0.25
40
40
  Requires-Dist: tree-sitter-python>=0.21
41
41
  Requires-Dist: tree-sitter-javascript>=0.21
42
42
  Requires-Dist: tree-sitter-typescript>=0.21
@@ -48,6 +48,14 @@ Requires-Dist: tree-sitter-cpp>=0.22
48
48
  Requires-Dist: tree-sitter-c-sharp>=0.21
49
49
  Requires-Dist: tree-sitter-php>=0.22
50
50
  Requires-Dist: tree-sitter-ruby>=0.21
51
+ Provides-Extra: native
52
+ Requires-Dist: code2llm-rust>=0.1.0; extra == "native"
53
+ Provides-Extra: graph
54
+ Requires-Dist: code2graph>=0.1.0; extra == "graph"
55
+ Provides-Extra: toon
56
+ Requires-Dist: code2toon>=0.1.0; extra == "toon"
57
+ Provides-Extra: diagrams
58
+ Requires-Dist: code2flow>=0.1.0; extra == "diagrams"
51
59
  Provides-Extra: dev
52
60
  Requires-Dist: pytest>=6.2; extra == "dev"
53
61
  Requires-Dist: pytest-cov>=2.12; extra == "dev"
@@ -55,9 +63,11 @@ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
55
63
  Requires-Dist: black>=21.0; extra == "dev"
56
64
  Requires-Dist: flake8>=3.9; extra == "dev"
57
65
  Requires-Dist: mypy>=0.910; extra == "dev"
58
- Requires-Dist: goal>=2.1.218; extra == "dev"
59
66
  Requires-Dist: costs>=0.1.20; extra == "dev"
60
- Requires-Dist: pfix>=0.1.60; extra == "dev"
67
+ Requires-Dist: code2llm-rust>=0.1.0; extra == "dev"
68
+ Requires-Dist: code2graph>=0.1.0; extra == "dev"
69
+ Requires-Dist: code2toon>=0.1.0; extra == "dev"
70
+ Requires-Dist: code2flow>=0.1.0; extra == "dev"
61
71
  Dynamic: author
62
72
  Dynamic: home-page
63
73
  Dynamic: license-file
@@ -67,13 +77,13 @@ Dynamic: requires-python
67
77
 
68
78
  ## AI Cost Tracking
69
79
 
70
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.176-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
71
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$8.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-105.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
80
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.183-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
81
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.57-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-140.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
72
82
 
73
- - 🤖 **LLM usage:** $8.3176 (263 commits)
74
- - 👤 **Human dev:** ~$10535 (105.3h @ $100/h, 30min dedup)
83
+ - 🤖 **LLM usage:** $0.5710 (336 commits)
84
+ - 👤 **Human dev:** ~$14034 (140.3h @ $100/h, 30min dedup)
75
85
 
76
- Generated on 2026-08-06 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
86
+ Generated on 2026-10-18 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
77
87
 
78
88
  ---
79
89
 
@@ -418,3 +428,7 @@ For more information about code2llm, visit: https://github.com/tom-sapletta/code
418
428
  ## License
419
429
 
420
430
  Licensed under Apache-2.0.
431
+
432
+ ## Dependency maintenance
433
+
434
+ See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## AI Cost Tracking
4
4
 
5
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.176-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
6
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$8.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-105.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
5
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.183-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
6
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.57-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-140.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
7
7
 
8
- - 🤖 **LLM usage:** $8.3176 (263 commits)
9
- - 👤 **Human dev:** ~$10535 (105.3h @ $100/h, 30min dedup)
8
+ - 🤖 **LLM usage:** $0.5710 (336 commits)
9
+ - 👤 **Human dev:** ~$14034 (140.3h @ $100/h, 30min dedup)
10
10
 
11
- Generated on 2026-08-06 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
11
+ Generated on 2026-10-18 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
12
12
 
13
13
  ---
14
14
 
@@ -353,3 +353,7 @@ For more information about code2llm, visit: https://github.com/tom-sapletta/code
353
353
  ## License
354
354
 
355
355
  Licensed under Apache-2.0.
356
+
357
+ ## Dependency maintenance
358
+
359
+ See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
@@ -0,0 +1 @@
1
+ 0.5.183
@@ -8,7 +8,7 @@ Includes NLP Processing Pipeline for query normalization, intent matching,
8
8
  and entity resolution with multilingual support.
9
9
  """
10
10
 
11
- __version__ = "0.5.176"
11
+ __version__ = "0.5.183"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -12,6 +12,10 @@ __all__ = [
12
12
  "PipelineDetector",
13
13
  "PipelineResolver",
14
14
  "PipelineClassifier",
15
+ "calculate_complexity_regex",
16
+ "calculate_python_complexity",
17
+ "compute_cyclomatic_complexity",
18
+ "extract_function_body",
15
19
  ]
16
20
 
17
21
 
@@ -29,6 +33,10 @@ def __getattr__(name):
29
33
  "PipelineDetector": ".pipeline_detector",
30
34
  "PipelineResolver": ".pipeline_resolver",
31
35
  "PipelineClassifier": ".pipeline_classifier",
36
+ "calculate_complexity_regex": ".complexity",
37
+ "calculate_python_complexity": ".complexity",
38
+ "compute_cyclomatic_complexity": ".complexity",
39
+ "extract_function_body": ".complexity",
32
40
  }
33
41
  if name in _imports:
34
42
  import importlib
@@ -0,0 +1,16 @@
1
+ """Call graph extractor using AST.
2
+
3
+ Backward-compatibility facade delegating to code2llm.analysis.call_graph_engine.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from code2llm.analysis.call_graph_engine import (
9
+ CallGraphExtractor,
10
+ calculate_call_metrics,
11
+ )
12
+
13
+ __all__ = [
14
+ "CallGraphExtractor",
15
+ "calculate_call_metrics",
16
+ ]
@@ -0,0 +1,448 @@
1
+ """Call graph extraction and symbol call resolution engine.
2
+
3
+ Atomized module supporting:
4
+ - Multi-language call extraction via regex and native Rust acceleration (code2llm-fast-core)
5
+ - Python AST call graph extraction with caller/callee resolution
6
+ - Call graph metrics computation (fan-in, fan-out, called_by, call edges)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import json
13
+ import re
14
+ import subprocess
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from code2llm.analysis.complexity import _find_rust_binary, extract_function_body
19
+ from code2llm.analysis.utils import ast_unparse, qualified_name
20
+ from code2llm.core.config import Config
21
+ from code2llm.core.models import AnalysisResult, FlowEdge
22
+
23
+ CALL_PATTERN_C_FAMILY = re.compile(
24
+ r"(?<!\bfunction\b\s)"
25
+ r"(?<!\bclass\b\s)"
26
+ r"\b([a-zA-Z_]\w*)\s*\("
27
+ r"|"
28
+ r"(?:this|self)\s*\.\s*(\w+)\s*\("
29
+ r"|"
30
+ r"\b(\w+)\s*\.\s*(\w+)\s*\("
31
+ )
32
+
33
+ _CALL_KEYWORDS = frozenset(
34
+ {
35
+ "if",
36
+ "for",
37
+ "while",
38
+ "switch",
39
+ "catch",
40
+ "return",
41
+ "throw",
42
+ "new",
43
+ "typeof",
44
+ "instanceof",
45
+ "import",
46
+ "export",
47
+ "require",
48
+ "console",
49
+ "super",
50
+ "class",
51
+ "function",
52
+ "async",
53
+ "await",
54
+ "delete",
55
+ "void",
56
+ "case",
57
+ "default",
58
+ }
59
+ )
60
+
61
+
62
+ def _resolve_call(
63
+ simple_call: str,
64
+ func_qname: str,
65
+ module_name: str,
66
+ known_simple: Dict[str, List[str]],
67
+ calls_seen: set[str],
68
+ func_info: Any,
69
+ ) -> None:
70
+ """Resolve a single call name and append to func_info.calls if novel."""
71
+ if simple_call in known_simple:
72
+ candidates = known_simple[simple_call]
73
+ my_module = func_qname.rsplit(".", 1)[0]
74
+ resolved = next(
75
+ (c for c in candidates if c.rsplit(".", 1)[0] == my_module),
76
+ candidates[0],
77
+ )
78
+ if resolved != func_qname and resolved not in calls_seen:
79
+ func_info.calls.append(resolved)
80
+ calls_seen.add(resolved)
81
+ else:
82
+ ext_name = f"{module_name}.{simple_call}"
83
+ if ext_name not in calls_seen:
84
+ func_info.calls.append(ext_name)
85
+ calls_seen.add(ext_name)
86
+
87
+
88
+ def _rust_batch_calls(
89
+ lines: list[int],
90
+ file_path: str | None = None,
91
+ content: str | None = None,
92
+ ) -> dict[int, list[str]] | None:
93
+ """Call native Rust extension or binary for fast batch call extraction."""
94
+ if not lines:
95
+ return None
96
+
97
+ # 1. Native PyO3 extension (zero-subprocess in-memory execution)
98
+ from code2llm.analysis.native_bridge import native_batch_calls
99
+
100
+ resolved_content = content
101
+ if resolved_content is None and file_path and Path(file_path).is_file():
102
+ try:
103
+ resolved_content = Path(file_path).read_text(encoding="utf-8", errors="ignore")
104
+ except Exception:
105
+ resolved_content = None
106
+
107
+ if resolved_content is not None:
108
+ native_res = native_batch_calls(resolved_content, lines)
109
+ if native_res is not None:
110
+ return native_res
111
+
112
+ # 2. Secondary fallback: CLI binary
113
+ binary = _find_rust_binary()
114
+ if not binary:
115
+ return None
116
+
117
+ lines_arg = ",".join(str(l) for l in lines)
118
+ cmd = [str(binary), "--calls", "--lines", lines_arg]
119
+
120
+ try:
121
+ if file_path and Path(file_path).is_file():
122
+ cmd.extend(["--file", str(file_path)])
123
+ proc = subprocess.run(
124
+ cmd,
125
+ capture_output=True,
126
+ text=True,
127
+ check=True,
128
+ timeout=5,
129
+ )
130
+ elif content is not None:
131
+ proc = subprocess.run(
132
+ cmd,
133
+ input=content,
134
+ capture_output=True,
135
+ text=True,
136
+ check=True,
137
+ timeout=5,
138
+ )
139
+ else:
140
+ return None
141
+
142
+ data = json.loads(proc.stdout.strip())
143
+ result: dict[int, list[str]] = {}
144
+ for item in data:
145
+ result[item["line"]] = item.get("calls", [])
146
+ return result
147
+ except Exception:
148
+ return None
149
+
150
+
151
+ def extract_calls_regex(
152
+ content: str,
153
+ module_name: str,
154
+ result: Dict[str, Any],
155
+ file_path: str | None = None,
156
+ ) -> None:
157
+ """Extract function calls from function bodies using regex or native Rust acceleration."""
158
+ functions = result.get("functions", {})
159
+ if not functions:
160
+ return
161
+
162
+ known_simple: Dict[str, List[str]] = {}
163
+ for qname in functions:
164
+ simple = qname.rsplit(".", 1)[-1]
165
+ known_simple.setdefault(simple, []).append(qname)
166
+
167
+ lines = [f.line for f in functions.values() if getattr(f, "line", None)]
168
+ rust_calls = _rust_batch_calls(lines, file_path=file_path, content=content)
169
+
170
+ for func_qname, func_info in functions.items():
171
+ line = getattr(func_info, "line", None)
172
+ body = extract_function_body(content, line or 1)
173
+ if not body:
174
+ continue
175
+ calls_seen: set[str] = set()
176
+
177
+ if rust_calls and line in rust_calls:
178
+ for simple_call in rust_calls[line]:
179
+ _resolve_call(
180
+ simple_call,
181
+ func_qname,
182
+ module_name,
183
+ known_simple,
184
+ calls_seen,
185
+ func_info,
186
+ )
187
+ else:
188
+ for m in CALL_PATTERN_C_FAMILY.finditer(body):
189
+ simple_call = m.group(1) or m.group(2) or m.group(4)
190
+ if not simple_call or simple_call in _CALL_KEYWORDS:
191
+ continue
192
+ _resolve_call(
193
+ simple_call,
194
+ func_qname,
195
+ module_name,
196
+ known_simple,
197
+ calls_seen,
198
+ func_info,
199
+ )
200
+
201
+
202
+ def calculate_call_metrics(
203
+ functions: Dict[str, Any],
204
+ metrics: Dict[str, Any] | None = None,
205
+ ) -> Dict[str, Dict[str, Any]]:
206
+ """Calculate fan-in, fan-out and populate called_by for all functions."""
207
+ from code2llm.analysis.native_bridge import native_calculate_call_metrics
208
+
209
+ res_metrics = metrics if metrics is not None else {}
210
+
211
+ # Fast path: native Rust
212
+ fn_tuples = []
213
+ for name, fi in functions.items():
214
+ calls = list(getattr(fi, "calls", []))
215
+ called_by = list(getattr(fi, "called_by", []))
216
+ raw_cc = getattr(fi, "complexity", 1.0)
217
+ cc_val = float(raw_cc.get("cyclomatic", 1.0) if isinstance(raw_cc, dict) else (raw_cc or 1.0))
218
+ fn_tuples.append((name, calls, called_by, cc_val))
219
+
220
+ native_res = native_calculate_call_metrics(fn_tuples)
221
+ if native_res is not None:
222
+ computed_metrics, called_by_map = native_res
223
+ for name, callers in called_by_map.items():
224
+ if name in functions:
225
+ fi = functions[name]
226
+ if hasattr(fi, "called_by"):
227
+ fi.called_by = callers
228
+ for name, (fan_in, fan_out, cc) in computed_metrics.items():
229
+ res_metrics[name] = {
230
+ "fan_in": fan_in,
231
+ "fan_out": fan_out,
232
+ "complexity": cc,
233
+ }
234
+ return res_metrics
235
+
236
+ # Fallback: Python implementation
237
+ for caller_name, caller_info in functions.items():
238
+ calls = getattr(caller_info, "calls", [])
239
+ for callee_name in calls:
240
+ if callee_name in functions:
241
+ callee_info = functions[callee_name]
242
+ called_by = getattr(callee_info, "called_by", None)
243
+ if called_by is not None and caller_name not in called_by:
244
+ called_by.append(caller_name)
245
+
246
+ for func_name, func_info in functions.items():
247
+ calls = getattr(func_info, "calls", [])
248
+ called_by = getattr(func_info, "called_by", [])
249
+ fan_out = len(set(calls))
250
+ fan_in = len(set(called_by))
251
+ res_metrics[func_name] = {
252
+ "fan_in": fan_in,
253
+ "fan_out": fan_out,
254
+ "complexity": getattr(func_info, "complexity", 1),
255
+ }
256
+
257
+ return res_metrics
258
+
259
+
260
+ class CallGraphExtractor(ast.NodeVisitor):
261
+ """Extract call graph from AST with fast resolution and optional astroid inference."""
262
+
263
+ def __init__(self, config: Config):
264
+ self.config = config
265
+ self.result = AnalysisResult()
266
+ self.module_name = ""
267
+ self.file_path = ""
268
+
269
+ # Context
270
+ self.function_stack: List[str] = []
271
+ self.class_stack: List[str] = []
272
+ self.imports: Dict[str, str] = {}
273
+ self.astroid_tree = None
274
+
275
+ def extract(
276
+ self, tree: ast.AST, module_name: str, file_path: str
277
+ ) -> AnalysisResult:
278
+ """Extract call graph from AST."""
279
+ self.result = AnalysisResult()
280
+ self.module_name = module_name
281
+ self.file_path = file_path
282
+ self.function_stack = []
283
+ self.class_stack = []
284
+ self.imports = {}
285
+
286
+ # Lazy astroid tree import (only when not skipped)
287
+ if not getattr(self.config.performance, "skip_astroid_inference", True):
288
+ try:
289
+ import astroid as _astroid
290
+
291
+ self.astroid_tree = _astroid.MANAGER.ast_from_file(file_path)
292
+ except Exception:
293
+ self.astroid_tree = None
294
+ else:
295
+ self.astroid_tree = None
296
+
297
+ self.visit(tree)
298
+ self._calculate_metrics()
299
+ return self.result
300
+
301
+ def _calculate_metrics(self) -> None:
302
+ """Calculate fan-in and fan-out metrics (backward compatibility)."""
303
+ calculate_call_metrics(self.result.functions, self.result.metrics)
304
+
305
+ def visit_Import(self, node: ast.Import) -> None:
306
+ """Track imports."""
307
+ for alias in node.names:
308
+ name = alias.asname if alias.asname else alias.name
309
+ self.imports[name] = alias.name
310
+ if hasattr(self.result, "imports"):
311
+ self.result.imports[name] = alias.name
312
+
313
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
314
+ """Track from imports."""
315
+ module = node.module or ""
316
+ for alias in node.names:
317
+ name = alias.asname if alias.asname else alias.name
318
+ full_name = f"{module}.{alias.name}" if module else alias.name
319
+ self.imports[name] = full_name
320
+ if hasattr(self.result, "imports"):
321
+ self.result.imports[name] = full_name
322
+
323
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
324
+ """Visit class definition."""
325
+ self.class_stack.append(node.name)
326
+
327
+ self.result.classes[node.name] = {
328
+ "file": self.file_path,
329
+ "line": node.lineno,
330
+ "methods": [m.name for m in node.body if isinstance(m, ast.FunctionDef)],
331
+ "bases": [self._expr_to_str(b) for b in node.bases],
332
+ }
333
+
334
+ for stmt in node.body:
335
+ self.visit(stmt)
336
+
337
+ self.class_stack.pop()
338
+
339
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
340
+ """Visit function definition and track calls within it."""
341
+ func_name = qualified_name(self.module_name, self.class_stack, node.name)
342
+ self.function_stack.append(func_name)
343
+
344
+ for stmt in node.body:
345
+ self.visit(stmt)
346
+
347
+ self.function_stack.pop()
348
+
349
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
350
+ """Visit async function."""
351
+ self.visit_FunctionDef(node)
352
+
353
+ def visit_Call(self, node: ast.Call) -> None:
354
+ """Track function calls."""
355
+ if not self.function_stack:
356
+ self.generic_visit(node)
357
+ return
358
+
359
+ caller = self.function_stack[-1]
360
+ callee = self._resolve_call(node.func)
361
+
362
+ # If ast-based resolution returned None or None.sth, try astroid if loaded
363
+ if (not callee or "None." in callee) and self.astroid_tree:
364
+ astroid_callee = self._resolve_with_astroid(node)
365
+ if astroid_callee:
366
+ callee = astroid_callee
367
+
368
+ if callee and caller in self.result.functions:
369
+ self.result.functions[caller].calls.append(callee)
370
+
371
+ edge = FlowEdge(
372
+ source=str(-1),
373
+ target=str(-1),
374
+ edge_type="call",
375
+ label=f"{caller}->{callee}",
376
+ )
377
+ if hasattr(self.result, "call_edges"):
378
+ self.result.call_edges.append(edge)
379
+ else:
380
+ self.result.edges.append(edge)
381
+
382
+ self.generic_visit(node)
383
+
384
+ def _resolve_call(self, node: ast.AST) -> Optional[str]:
385
+ """Resolve a call to its full name."""
386
+ if isinstance(node, ast.Name):
387
+ if node.id in self.imports:
388
+ return self.imports[node.id]
389
+ return f"{self.module_name}.{node.id}"
390
+
391
+ if isinstance(node, ast.Attribute):
392
+ parts: List[str] = []
393
+ current = node
394
+
395
+ while isinstance(current, ast.Attribute):
396
+ parts.append(current.attr)
397
+ current = current.value
398
+
399
+ if isinstance(current, ast.Name):
400
+ parts.append(current.id)
401
+ parts.reverse()
402
+
403
+ root = parts[0]
404
+ if root in self.imports:
405
+ return f"{self.imports[root]}.{'.'.join(parts[1:])}"
406
+
407
+ if root in ("self", "cls") and self.class_stack:
408
+ return f"{self.module_name}.{self.class_stack[-1]}.{'.'.join(parts[1:])}"
409
+
410
+ return f"{self.module_name}.{'.'.join(parts)}"
411
+
412
+ return None
413
+
414
+ def _resolve_with_astroid(self, node: ast.Call) -> Optional[str]:
415
+ """Use astroid to infer the call target."""
416
+ if not self.astroid_tree:
417
+ return None
418
+
419
+ try:
420
+ import astroid as _astroid
421
+
422
+ for astroid_node in self.astroid_tree.nodes_of_class(_astroid.Call):
423
+ if (
424
+ astroid_node.lineno == node.lineno
425
+ and astroid_node.col_offset == node.col_offset
426
+ ):
427
+ inferred = astroid_node.func.infer()
428
+ for target in inferred:
429
+ if hasattr(target, "qname"):
430
+ return target.qname()
431
+ break
432
+ except Exception:
433
+ pass
434
+ return None
435
+
436
+ def _expr_to_str(self, node: ast.AST) -> str:
437
+ """Unparse an AST expression node to a source string."""
438
+ return ast_unparse(node, default_none="")
439
+
440
+
441
+ __all__ = [
442
+ "CALL_PATTERN_C_FAMILY",
443
+ "CallGraphExtractor",
444
+ "calculate_call_metrics",
445
+ "extract_calls_regex",
446
+ "_resolve_call",
447
+ "_rust_batch_calls",
448
+ ]