codeboarding 0.12.3__tar.gz → 0.12.4__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 (191) hide show
  1. {codeboarding-0.12.3/codeboarding.egg-info → codeboarding-0.12.4}/PKG-INFO +1 -1
  2. {codeboarding-0.12.3 → codeboarding-0.12.4/codeboarding.egg-info}/PKG-INFO +1 -1
  3. {codeboarding-0.12.3 → codeboarding-0.12.4}/pyproject.toml +1 -1
  4. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/analysis_cache.py +77 -83
  5. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/analysis_result.py +44 -0
  6. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/graph.py +8 -2
  7. codeboarding-0.12.4/static_analyzer/incremental_orchestrator.py +338 -0
  8. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/scanner.py +1 -1
  9. codeboarding-0.12.3/static_analyzer/incremental_orchestrator.py +0 -125
  10. {codeboarding-0.12.3 → codeboarding-0.12.4}/LICENSE +0 -0
  11. {codeboarding-0.12.3 → codeboarding-0.12.4}/PYPI.md +0 -0
  12. {codeboarding-0.12.3 → codeboarding-0.12.4}/README.md +0 -0
  13. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/__init__.py +0 -0
  14. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/abstraction_agent.py +0 -0
  15. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/agent.py +0 -0
  16. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/agent_responses.py +0 -0
  17. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/change_status.py +0 -0
  18. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/cluster_budget.py +0 -0
  19. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/cluster_methods_mixin.py +0 -0
  20. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/constants.py +0 -0
  21. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/dependency_discovery.py +0 -0
  22. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/details_agent.py +0 -0
  23. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/incremental_agent.py +0 -0
  24. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/llm_config.py +0 -0
  25. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/meta_agent.py +0 -0
  26. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/model_capabilities.py +0 -0
  27. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/planner_agent.py +0 -0
  28. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/__init__.py +0 -0
  29. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/abstract_prompt_factory.py +0 -0
  30. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/claude_prompts.py +0 -0
  31. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/deepseek_prompts.py +0 -0
  32. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/gemini_flash_prompts.py +0 -0
  33. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/glm_prompts.py +0 -0
  34. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/gpt_prompts.py +0 -0
  35. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/kimi_prompts.py +0 -0
  36. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/prompt_factory.py +0 -0
  37. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/retry.py +0 -0
  38. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/__init__.py +0 -0
  39. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/base.py +0 -0
  40. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/get_external_deps.py +0 -0
  41. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/get_method_invocations.py +0 -0
  42. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_cfg.py +0 -0
  43. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_docs.py +0 -0
  44. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_file.py +0 -0
  45. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_file_structure.py +0 -0
  46. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_packages.py +0 -0
  47. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_source.py +0 -0
  48. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_structure.py +0 -0
  49. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/toolkit.py +0 -0
  50. {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/validation.py +0 -0
  51. {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/__init__.py +0 -0
  52. {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/cache.py +0 -0
  53. {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/details_cache.py +0 -0
  54. {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/meta_cache.py +0 -0
  55. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/SOURCES.txt +0 -0
  56. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/dependency_links.txt +0 -0
  57. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/entry_points.txt +0 -0
  58. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/requires.txt +0 -0
  59. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/top_level.txt +0 -0
  60. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/__init__.py +0 -0
  61. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/bootstrap.py +0 -0
  62. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/__init__.py +0 -0
  63. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/full_analysis.py +0 -0
  64. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/incremental_analysis.py +0 -0
  65. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/partial_analysis.py +0 -0
  66. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/__init__.py +0 -0
  67. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/analysis.py +0 -0
  68. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/orchestration.py +0 -0
  69. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/rendering.py +0 -0
  70. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/__init__.py +0 -0
  71. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/local.py +0 -0
  72. {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/remote.py +0 -0
  73. {codeboarding-0.12.3 → codeboarding-0.12.4}/constants.py +0 -0
  74. {codeboarding-0.12.3 → codeboarding-0.12.4}/core/__init__.py +0 -0
  75. {codeboarding-0.12.3 → codeboarding-0.12.4}/core/plugin_loader.py +0 -0
  76. {codeboarding-0.12.3 → codeboarding-0.12.4}/core/protocols.py +0 -0
  77. {codeboarding-0.12.3 → codeboarding-0.12.4}/core/registry.py +0 -0
  78. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/__init__.py +0 -0
  79. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/analysis_json.py +0 -0
  80. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/cluster_delta.py +0 -0
  81. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/cluster_snapshot.py +0 -0
  82. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/diagram_generator.py +0 -0
  83. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/exceptions.py +0 -0
  84. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/file_coverage.py +0 -0
  85. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/io_utils.py +0 -0
  86. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/run_context.py +0 -0
  87. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/run_mode.py +0 -0
  88. {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/version.py +0 -0
  89. {codeboarding-0.12.3 → codeboarding-0.12.4}/github_action.py +0 -0
  90. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/__init__.py +0 -0
  91. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/__init__.py +0 -0
  92. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/circular_deps.py +0 -0
  93. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/cohesion.py +0 -0
  94. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/coupling.py +0 -0
  95. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/function_size.py +0 -0
  96. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/god_class.py +0 -0
  97. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/inheritance.py +0 -0
  98. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/instability.py +0 -0
  99. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/unused_code_diagnostics.py +0 -0
  100. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/config.py +0 -0
  101. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/models.py +0 -0
  102. {codeboarding-0.12.3 → codeboarding-0.12.4}/health/runner.py +0 -0
  103. {codeboarding-0.12.3 → codeboarding-0.12.4}/install.py +0 -0
  104. {codeboarding-0.12.3 → codeboarding-0.12.4}/logging_config.py +0 -0
  105. {codeboarding-0.12.3 → codeboarding-0.12.4}/main.py +0 -0
  106. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/__init__.py +0 -0
  107. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/callbacks.py +0 -0
  108. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/context.py +0 -0
  109. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/mixin.py +0 -0
  110. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/paths.py +0 -0
  111. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/stats.py +0 -0
  112. {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/writers.py +0 -0
  113. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/__init__.py +0 -0
  114. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/html.py +0 -0
  115. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/html_template.py +0 -0
  116. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/markdown.py +0 -0
  117. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/mdx.py +0 -0
  118. {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/sphinx.py +0 -0
  119. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/__init__.py +0 -0
  120. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/change_detector.py +0 -0
  121. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/diff_parser.py +0 -0
  122. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/errors.py +0 -0
  123. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/git_ops.py +0 -0
  124. {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/ignore.py +0 -0
  125. {codeboarding-0.12.3 → codeboarding-0.12.4}/setup.cfg +0 -0
  126. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/__init__.py +0 -0
  127. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cfg_skip_planner.py +0 -0
  128. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cluster_helpers.py +0 -0
  129. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cluster_relations.py +0 -0
  130. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/constants.py +0 -0
  131. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/csharp_config_scanner.py +0 -0
  132. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/dotnet_sdk.py +0 -0
  133. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/__init__.py +0 -0
  134. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/__init__.py +0 -0
  135. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/csharp_adapter.py +0 -0
  136. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/go_adapter.py +0 -0
  137. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/java_adapter.py +0 -0
  138. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/php_adapter.py +0 -0
  139. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/python_adapter.py +0 -0
  140. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
  141. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
  142. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/call_graph_builder.py +0 -0
  143. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/edge_build_context.py +0 -0
  144. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/edge_builder.py +0 -0
  145. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/hierarchy_builder.py +0 -0
  146. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/language_adapter.py +0 -0
  147. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/lsp_client.py +0 -0
  148. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/lsp_constants.py +0 -0
  149. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/models.py +0 -0
  150. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/progress.py +0 -0
  151. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/protocols.py +0 -0
  152. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/result_converter.py +0 -0
  153. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/source_inspector.py +0 -0
  154. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/symbol_table.py +0 -0
  155. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/utils.py +0 -0
  156. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/java_config_scanner.py +0 -0
  157. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/java_utils.py +0 -0
  158. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/language_results.py +0 -0
  159. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/leiden_utils.py +0 -0
  160. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/lsp_client/__init__.py +0 -0
  161. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/lsp_client/diagnostics.py +0 -0
  162. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/node.py +0 -0
  163. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/programming_language.py +0 -0
  164. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/reference_resolve_mixin.py +0 -0
  165. {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/typescript_config_scanner.py +0 -0
  166. {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/__init__.py +0 -0
  167. {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/device_id.py +0 -0
  168. {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/events.py +0 -0
  169. {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/schemas.py +0 -0
  170. {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/service.py +0 -0
  171. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_cli_parser.py +0 -0
  172. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_github_action.py +0 -0
  173. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_install.py +0 -0
  174. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_logging_config.py +0 -0
  175. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_main.py +0 -0
  176. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_pyproject_packages.py +0 -0
  177. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_registry_coverage.py +0 -0
  178. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_telemetry_events.py +0 -0
  179. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_tool_registry.py +0 -0
  180. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_user_config.py +0 -0
  181. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_vscode_constants.py +0 -0
  182. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_windows_compatibility.py +0 -0
  183. {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_windows_encoding.py +0 -0
  184. {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/__init__.py +0 -0
  185. {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/installers.py +0 -0
  186. {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/manifest.py +0 -0
  187. {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/paths.py +0 -0
  188. {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/registry.py +0 -0
  189. {codeboarding-0.12.3 → codeboarding-0.12.4}/user_config.py +0 -0
  190. {codeboarding-0.12.3 → codeboarding-0.12.4}/utils.py +0 -0
  191. {codeboarding-0.12.3 → codeboarding-0.12.4}/vscode_constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.12.3
3
+ Version: 0.12.4
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeboarding
3
- Version: 0.12.3
3
+ Version: 0.12.4
4
4
  Summary: Interactive Diagrams for Code
5
5
  Author: CodeBoarding Team
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeboarding"
7
- version = "0.12.3"
7
+ version = "0.12.4"
8
8
  description = "Interactive Diagrams for Code"
9
9
  readme = "PYPI.md"
10
10
  license = "MIT"
@@ -29,7 +29,8 @@ from typing import TYPE_CHECKING, Any
29
29
 
30
30
  from filelock import FileLock
31
31
 
32
- from static_analyzer.graph import CallGraph
32
+ from static_analyzer.analysis_result import AnalysisData, InvalidatedAnalysis, InvalidatedEdge
33
+ from static_analyzer.graph import Edge
33
34
  from static_analyzer.lsp_client.diagnostics import FileDiagnosticsMap
34
35
  from static_analyzer.node import Node
35
36
  from utils import to_absolute_path, to_relative_path
@@ -332,67 +333,67 @@ def _atomic_copy(src: Path, dest: Path) -> None:
332
333
  raise
333
334
 
334
335
 
335
- def invalidate_files(analysis_result: dict[str, Any], changed_files: set[Path]) -> dict[str, Any]:
336
+ def invalidate_files(analysis_result: dict[str, Any], changed_files: set[Path]) -> InvalidatedAnalysis:
336
337
  """Return a copy of *analysis_result* with every entry from *changed_files* removed.
337
338
 
338
339
  Drops nodes whose ``file_path`` is in the change set, cascades edges that
339
- reference dropped nodes, drops class hierarchies and references from the
340
- same files, prunes package relations to surviving files, and filters
341
- ``source_files`` / ``diagnostics`` accordingly. Raises ``ValueError`` if
342
- the result has dangling edges or references after filtering.
340
+ reference dropped nodes, remembers cross-boundary edges for later LSP
341
+ validation, drops class hierarchies and references from the same files,
342
+ prunes package relations to surviving files, and filters ``source_files`` /
343
+ ``diagnostics`` accordingly. Raises ``ValueError`` if the result has
344
+ dangling edges or references after filtering.
343
345
  """
344
346
  changed_file_strs = {str(path) for path in changed_files}
345
347
 
346
- call_graph: CallGraph = analysis_result["call_graph"]
347
- filtered_cg = call_graph.filter(lambda node: node.file_path not in changed_file_strs)
348
+ cached = AnalysisData.from_dict(analysis_result)
349
+ call_graph = cached.call_graph
350
+ invalidated_edges: list[InvalidatedEdge] = []
351
+ filtered_cg = call_graph.filter(
352
+ lambda node: node.file_path not in changed_file_strs,
353
+ on_dropped_edge=lambda edge: _collect_invalidated_edge(edge, changed_file_strs, invalidated_edges),
354
+ )
348
355
 
349
- updated_result: dict[str, Any] = {
350
- "call_graph": filtered_cg,
351
- "class_hierarchies": {},
352
- "package_relations": {},
353
- "references": [],
354
- "source_files": [],
355
- }
356
+ diagnostics = None
357
+ if cached.diagnostics is not None:
358
+ diagnostics = {fp: diags for fp, diags in cached.diagnostics.items() if fp not in changed_file_strs}
356
359
 
357
- if "diagnostics" in analysis_result:
358
- updated_result["diagnostics"] = {
359
- fp: diags for fp, diags in analysis_result["diagnostics"].items() if fp not in changed_file_strs
360
- }
360
+ class_hierarchies = {
361
+ class_name: class_info.copy()
362
+ for class_name, class_info in cached.class_hierarchies.items()
363
+ if class_info.get("file_path", "") not in changed_file_strs
364
+ }
361
365
 
362
- class_hierarchies: dict[str, Any] = analysis_result["class_hierarchies"]
363
- for class_name, class_info in class_hierarchies.items():
364
- if class_info.get("file_path", "") not in changed_file_strs:
365
- updated_result["class_hierarchies"][class_name] = class_info.copy()
366
-
367
- package_relations: dict[str, Any] = analysis_result["package_relations"]
368
- for package_name, package_info in package_relations.items():
369
- original_files = package_info.get("files", [])
370
- remaining = [f for f in original_files if f not in changed_file_strs]
371
- if remaining:
372
- updated_package_info = package_info.copy()
373
- updated_package_info["files"] = remaining
374
- updated_result["package_relations"][package_name] = updated_package_info
375
-
376
- references: list[Node] = analysis_result["references"]
377
- for ref in references:
378
- if ref.file_path not in changed_file_strs:
379
- updated_result["references"].append(ref)
380
-
381
- source_files: list[Path] = analysis_result["source_files"]
382
- for file_path in source_files:
383
- if str(file_path) not in changed_file_strs:
384
- updated_result["source_files"].append(file_path)
366
+ package_relations: dict[str, Any] = {}
367
+ for package_name, package_info in cached.package_relations.items():
368
+ remaining_files = [f for f in package_info.get("files", []) if f not in changed_file_strs]
369
+ if remaining_files:
370
+ package_relations[package_name] = {**package_info, "files": remaining_files}
371
+
372
+ references = [ref for ref in cached.references if ref.file_path not in changed_file_strs]
373
+ source_files = [file_path for file_path in cached.source_files if str(file_path) not in changed_file_strs]
374
+
375
+ updated_result = AnalysisData(
376
+ call_graph=filtered_cg,
377
+ class_hierarchies=class_hierarchies,
378
+ package_relations=package_relations,
379
+ references=references,
380
+ source_files=source_files,
381
+ diagnostics=diagnostics,
382
+ )
385
383
 
386
384
  _validate_no_dangling_references(updated_result)
387
385
 
388
386
  logger.info(
389
387
  f"Invalidated {len(changed_files)} files: kept {len(filtered_cg.nodes)} nodes, "
390
- f"{len(filtered_cg.edges)} edges, {len(updated_result['references'])} references"
388
+ f"{len(filtered_cg.edges)} edges, {len(updated_result.references)} references"
391
389
  )
392
- return updated_result
390
+ return InvalidatedAnalysis(updated_result, invalidated_edges, changed_file_strs)
393
391
 
394
392
 
395
- def merge_results(cached_result: dict[str, Any], new_result: dict[str, Any]) -> dict[str, Any]:
393
+ def merge_results(
394
+ cached_result: AnalysisData,
395
+ new_result: dict[str, Any],
396
+ ) -> AnalysisData:
396
397
  """Union ``cached_result`` (post-invalidation) with ``new_result`` (fresh re-LSP).
397
398
 
398
399
  For overlapping keys (same file appearing in both), the new result wins
@@ -400,51 +401,44 @@ def merge_results(cached_result: dict[str, Any], new_result: dict[str, Any]) ->
400
401
  nodes from both sides merge; edges from either side that reference
401
402
  nodes present in the merged graph are kept.
402
403
  """
403
- merged_result: dict[str, Any] = {
404
- "call_graph": cached_result["call_graph"].union(new_result["call_graph"]),
405
- "class_hierarchies": {},
406
- "package_relations": {},
407
- "references": [],
408
- "source_files": [],
409
- }
410
-
411
- merged_result["class_hierarchies"].update(cached_result["class_hierarchies"])
412
- merged_result["class_hierarchies"].update(new_result["class_hierarchies"])
413
-
414
- merged_result["package_relations"].update(cached_result["package_relations"])
415
- merged_result["package_relations"].update(new_result["package_relations"])
416
-
417
- new_source_files: list[Path] = new_result.get("source_files", [])
418
- new_file_paths = {str(path) for path in new_source_files}
419
-
420
- for ref in cached_result["references"]:
421
- if ref.file_path not in new_file_paths:
422
- merged_result["references"].append(ref)
423
- merged_result["references"].extend(new_result["references"])
424
-
425
- for file_path in cached_result["source_files"]:
426
- if str(file_path) not in new_file_paths:
427
- merged_result["source_files"].append(file_path)
428
- merged_result["source_files"].extend(new_source_files)
429
-
430
- cached_diagnostics: FileDiagnosticsMap = cached_result.get("diagnostics", {})
431
- new_diagnostics: FileDiagnosticsMap = new_result.get("diagnostics", {})
404
+ new = AnalysisData.from_dict(new_result)
405
+ new_file_paths = {str(path) for path in new.source_files}
406
+ cached_diagnostics = cached_result.diagnostics or {}
407
+ new_diagnostics = new.diagnostics or {}
432
408
  merged_diagnostics: FileDiagnosticsMap = {
433
409
  fp: diags for fp, diags in cached_diagnostics.items() if fp not in new_file_paths
434
410
  }
435
411
  merged_diagnostics.update(new_diagnostics)
436
- if merged_diagnostics:
437
- merged_result["diagnostics"] = merged_diagnostics
438
412
 
439
- return merged_result
413
+ merged = AnalysisData(
414
+ call_graph=cached_result.call_graph.union(new.call_graph),
415
+ class_hierarchies={**cached_result.class_hierarchies, **new.class_hierarchies},
416
+ package_relations={**cached_result.package_relations, **new.package_relations},
417
+ references=[ref for ref in cached_result.references if ref.file_path not in new_file_paths] + new.references,
418
+ source_files=[path for path in cached_result.source_files if str(path) not in new_file_paths]
419
+ + new.source_files,
420
+ diagnostics=merged_diagnostics or None,
421
+ )
422
+ return merged
423
+
424
+
425
+ def _collect_invalidated_edge(
426
+ edge: Edge, changed_file_strs: set[str], invalidated_edges: list[InvalidatedEdge]
427
+ ) -> None:
428
+ src_node = edge.src_node
429
+ dst_node = edge.dst_node
430
+ src_changed = src_node.file_path in changed_file_strs
431
+ dst_changed = dst_node.file_path in changed_file_strs
432
+ if src_changed != dst_changed:
433
+ invalidated_edges.append((edge.get_source(), edge.get_destination(), src_node, dst_node))
440
434
 
441
435
 
442
- def _validate_no_dangling_references(analysis_result: dict[str, Any]) -> None:
436
+ def _validate_no_dangling_references(analysis_result: AnalysisData) -> None:
443
437
  """Sanity-check: every edge reaches existing nodes, every reference / class /
444
438
  package points at a file in ``source_files``. Raises on violations."""
445
- call_graph: CallGraph = analysis_result["call_graph"]
439
+ call_graph = analysis_result.call_graph
446
440
  existing_nodes = set(call_graph.nodes.keys())
447
- source_file_strs = {str(path) for path in analysis_result["source_files"]}
441
+ source_file_strs = {str(path) for path in analysis_result.source_files}
448
442
  errors: list[str] = []
449
443
 
450
444
  for edge in call_graph.edges:
@@ -455,16 +449,16 @@ def _validate_no_dangling_references(analysis_result: dict[str, Any]) -> None:
455
449
  if dst_name not in existing_nodes:
456
450
  errors.append(f"Edge destination '{dst_name}' references non-existent node")
457
451
 
458
- for ref in analysis_result["references"]:
452
+ for ref in analysis_result.references:
459
453
  if ref.file_path not in source_file_strs:
460
454
  errors.append(f"Reference '{ref.fully_qualified_name}' from '{ref.file_path}' references non-existent file")
461
455
 
462
- for class_name, class_info in analysis_result["class_hierarchies"].items():
456
+ for class_name, class_info in analysis_result.class_hierarchies.items():
463
457
  class_file_path = class_info.get("file_path", "")
464
458
  if class_file_path and class_file_path not in source_file_strs:
465
459
  errors.append(f"Class hierarchy '{class_name}' references non-existent file '{class_file_path}'")
466
460
 
467
- for package_name, package_info in analysis_result["package_relations"].items():
461
+ for package_name, package_info in analysis_result.package_relations.items():
468
462
  for package_file in package_info.get("files", []):
469
463
  if package_file not in source_file_strs:
470
464
  errors.append(f"Package '{package_name}' references non-existent file '{package_file}'")
@@ -2,6 +2,8 @@ import logging
2
2
  import re
3
3
  from collections.abc import Iterator
4
4
  from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
5
7
 
6
8
  from static_analyzer.constants import Language
7
9
  from static_analyzer.graph import CallGraph
@@ -32,6 +34,48 @@ _WORD_RE = re.compile(r"\b([a-z]+)\b")
32
34
  # Used to detect generic type params like T or E in lowercased method signatures.
33
35
  _STANDALONE_SINGLE_LETTER_RE = re.compile(r"(?<![a-z])([a-z])(?!\w)")
34
36
 
37
+ InvalidatedEdge = tuple[str, str, Node, Node]
38
+
39
+
40
+ @dataclass
41
+ class AnalysisData:
42
+ call_graph: CallGraph
43
+ class_hierarchies: dict[str, Any]
44
+ package_relations: dict[str, Any]
45
+ references: list[Node]
46
+ source_files: list[Path]
47
+ diagnostics: FileDiagnosticsMap | None = None
48
+
49
+ @classmethod
50
+ def from_dict(cls, analysis: dict[str, Any]) -> "AnalysisData":
51
+ return cls(
52
+ call_graph=analysis["call_graph"],
53
+ class_hierarchies=analysis["class_hierarchies"],
54
+ package_relations=analysis["package_relations"],
55
+ references=analysis["references"],
56
+ source_files=analysis["source_files"],
57
+ diagnostics=analysis.get("diagnostics"),
58
+ )
59
+
60
+ def to_dict(self) -> dict[str, Any]:
61
+ analysis: dict[str, Any] = {
62
+ "call_graph": self.call_graph,
63
+ "class_hierarchies": self.class_hierarchies,
64
+ "package_relations": self.package_relations,
65
+ "references": self.references,
66
+ "source_files": self.source_files,
67
+ }
68
+ if self.diagnostics is not None:
69
+ analysis["diagnostics"] = self.diagnostics
70
+ return analysis
71
+
72
+
73
+ @dataclass
74
+ class InvalidatedAnalysis:
75
+ analysis: AnalysisData
76
+ invalidated_edges: list[InvalidatedEdge]
77
+ invalidated_files: set[str]
78
+
35
79
 
36
80
  def _strip_java_generics(name: str) -> str:
37
81
  """Remove Java generic type params from a (already lowercased) qualified name.
@@ -171,13 +171,17 @@ class CallGraph:
171
171
 
172
172
  self.nodes[src_name].added_method_called_by_me(self.nodes[dst_name])
173
173
 
174
- def filter(self, keep_node: Callable[[Node], bool]) -> "CallGraph":
174
+ def filter(
175
+ self,
176
+ keep_node: Callable[[Node], bool],
177
+ on_dropped_edge: Callable[[Edge], None],
178
+ ) -> "CallGraph":
175
179
  """Return a new CallGraph keeping only nodes matching ``keep_node`` and connecting edges.
176
180
 
177
181
  ``_cluster_cache`` is preserved and pruned to the surviving qnames so
178
182
  a warm-start invalidation/filter step doesn't silently drop the prior
179
183
  clustering. Edges whose endpoints both survive are re-added; edges
180
- with a dropped endpoint are cascaded out.
184
+ with a dropped endpoint are cascaded out and optionally collected.
181
185
  """
182
186
  out = CallGraph(language=self.language)
183
187
  for node in self.nodes.values():
@@ -190,6 +194,8 @@ class CallGraph:
190
194
  out.add_edge(src, dst)
191
195
  except ValueError as e:
192
196
  logger.warning(f"Failed to add edge {src} -> {dst} during filter: {e}")
197
+ else:
198
+ on_dropped_edge(edge)
193
199
  out._cluster_cache = self._prune_cluster_cache(out.nodes)
194
200
  return out
195
201
 
@@ -0,0 +1,338 @@
1
+ """Pkl warm-start updater: bring cached per-language analysis up to date.
2
+
3
+ Warm-start flow:
4
+ 1. Keep unchanged files from the pkl and invalidate changed/deleted files.
5
+ 2. Re-LSP existing changed files and merge their fresh nodes/references back in.
6
+ 3. Rebuild inbound edges: keep ``unchanged -> changed`` only when references still prove it.
7
+ 4. Rebuild outbound edges: resolve changed-file call sites with definitions.
8
+ 5. Keep unchanged-only edges cached and let ``StaticAnalyzer`` persist the new pkl.
9
+ """
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from repo_utils.ignore import RepoIgnoreManager
16
+ from static_analyzer.analysis_result import AnalysisData, InvalidatedEdge
17
+ from static_analyzer.analysis_cache import (
18
+ invalidate_files,
19
+ merge_results,
20
+ )
21
+ from static_analyzer.constants import NodeType
22
+ from static_analyzer.engine.call_graph_builder import CallGraphBuilder
23
+ from static_analyzer.engine.language_adapter import LanguageAdapter
24
+ from static_analyzer.engine.lsp_client import LSPClient
25
+ from static_analyzer.engine.result_converter import convert_to_codeboarding_format
26
+ from static_analyzer.engine.source_inspector import SourceInspector
27
+ from static_analyzer.engine.utils import uri_to_path
28
+ from static_analyzer.graph import CallGraph
29
+ from static_analyzer.node import Node
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def update_cfg_for_changed_files(
35
+ cached_analysis: dict[str, Any],
36
+ changed_files: set[Path],
37
+ adapter: LanguageAdapter,
38
+ project_path: Path,
39
+ engine_client: LSPClient,
40
+ ignore_manager: RepoIgnoreManager,
41
+ ) -> dict[str, Any]:
42
+ """Apply *changed_files* to *cached_analysis* via re-LSP-and-merge.
43
+
44
+ Steps:
45
+
46
+ 1. ``invalidate_files`` drops every node/edge/reference/class/package
47
+ entry sourced from a changed file, leaving the cached state of every
48
+ *unchanged* file intact.
49
+ 2. The LSP re-analyses just the changed files (existing ones; deleted
50
+ files contribute nothing).
51
+ 3. ``merge_results`` unions the kept-from-cache state with the fresh
52
+ per-file result.
53
+ 4. Surviving entries are filtered against the live filesystem so a
54
+ deleted file's references / classes / package members are removed
55
+ from the merged dict.
56
+
57
+ Returns a fresh dict with the same shape as ``cached_analysis``. The
58
+ caller stuffs it into ``StaticAnalysisResults`` and saves the pkl
59
+ tagged with the *current* source SHA.
60
+ """
61
+ if not changed_files:
62
+ return cached_analysis
63
+
64
+ existing_files = {f for f in changed_files if f.exists()}
65
+ deleted_files = {f for f in changed_files if not f.exists()}
66
+ logger.info(
67
+ "update_cfg_for_changed_files: %d changed (%d existing, %d deleted)",
68
+ len(changed_files),
69
+ len(existing_files),
70
+ len(deleted_files),
71
+ )
72
+
73
+ updated_cache = invalidate_files(cached_analysis, changed_files)
74
+
75
+ changed_source_files = [
76
+ f for f in existing_files if f.suffix in adapter.file_extensions and not ignore_manager.should_ignore(f)
77
+ ]
78
+
79
+ if changed_source_files:
80
+ builder = CallGraphBuilder(engine_client, adapter, project_path)
81
+ engine_result = builder.build(changed_source_files)
82
+ new_analysis = convert_to_codeboarding_format(builder.symbol_table, engine_result, adapter)
83
+ else:
84
+ new_analysis = {
85
+ "call_graph": CallGraph(language=adapter.language),
86
+ "class_hierarchies": {},
87
+ "package_relations": {},
88
+ "references": [],
89
+ "source_files": [],
90
+ "diagnostics": {},
91
+ }
92
+
93
+ fresh_diagnostics = engine_client.get_collected_diagnostics()
94
+ if fresh_diagnostics:
95
+ new_analysis["diagnostics"] = fresh_diagnostics
96
+
97
+ merged_analysis = merge_results(updated_cache.analysis, new_analysis)
98
+ _rebuild_changed_file_edges(
99
+ merged_analysis,
100
+ updated_cache.invalidated_edges,
101
+ updated_cache.invalidated_files,
102
+ changed_source_files,
103
+ adapter,
104
+ engine_client,
105
+ )
106
+ return _filter_to_live_files(merged_analysis).to_dict()
107
+
108
+
109
+ def _rebuild_changed_file_edges(
110
+ merged_analysis: AnalysisData,
111
+ invalidated_edges: list[InvalidatedEdge],
112
+ changed_file_strs: set[str],
113
+ changed_source_files: list[Path],
114
+ adapter: LanguageAdapter,
115
+ engine_client: LSPClient,
116
+ ) -> None:
117
+ _restore_inbound_edges(
118
+ merged_analysis.call_graph, invalidated_edges, changed_file_strs, adapter, engine_client, SourceInspector()
119
+ )
120
+ _add_outbound_edges_from_changed_files(merged_analysis.call_graph, changed_source_files, engine_client)
121
+
122
+
123
+ def _restore_inbound_edges(
124
+ call_graph: CallGraph,
125
+ invalidated_edges: list[InvalidatedEdge],
126
+ changed_file_strs: set[str],
127
+ adapter: LanguageAdapter,
128
+ engine_client: LSPClient,
129
+ source_inspector: SourceInspector,
130
+ ) -> None:
131
+ if not invalidated_edges:
132
+ return
133
+
134
+ restored = 0
135
+ checked = 0
136
+ references_cache: dict[str, list[dict]] = {}
137
+
138
+ for src_name, dst_name, old_src_node, old_dst_node in invalidated_edges:
139
+ if old_src_node.file_path in changed_file_strs or old_dst_node.file_path not in changed_file_strs:
140
+ continue
141
+ if not call_graph.has_node(src_name) or not call_graph.has_node(dst_name):
142
+ continue
143
+
144
+ src_node = call_graph.nodes[src_name]
145
+ dst_node = call_graph.nodes[dst_name]
146
+
147
+ checked += 1
148
+ refs = references_cache.get(dst_name)
149
+ if refs is None:
150
+ try:
151
+ engine_client.did_open(Path(dst_node.file_path), adapter.language_id)
152
+ refs = engine_client.references(Path(dst_node.file_path), dst_node.line_start - 1, dst_node.col_start)
153
+ except Exception:
154
+ logger.debug("Failed to validate references for %s", dst_name, exc_info=True)
155
+ refs = []
156
+ references_cache[dst_name] = refs
157
+
158
+ # Inbound: unchanged B -> changed A is kept only if references(A) still lands inside B.
159
+ if _edge_reference_still_exists(src_node, dst_node, refs, adapter, source_inspector):
160
+ try:
161
+ call_graph.add_edge(src_name, dst_name)
162
+ restored += 1
163
+ except ValueError:
164
+ logger.debug("Failed to restore edge %s -> %s", src_name, dst_name, exc_info=True)
165
+
166
+ logger.info(
167
+ "Validated %d inbound cached edge(s), restored %d/%d",
168
+ len(invalidated_edges),
169
+ restored,
170
+ checked,
171
+ )
172
+
173
+
174
+ def _edge_reference_still_exists(
175
+ src_node: Node,
176
+ dst_node: Node,
177
+ refs: list[dict],
178
+ adapter: LanguageAdapter,
179
+ source_inspector: SourceInspector,
180
+ ) -> bool:
181
+ for ref in refs:
182
+ ref_file = uri_to_path(ref.get("uri", ""))
183
+ if ref_file is None or str(ref_file) != src_node.file_path:
184
+ continue
185
+
186
+ ref_range = ref.get("range", {})
187
+ ref_start = ref_range.get("start", {})
188
+ ref_end = ref_range.get("end", {})
189
+ ref_line = ref_start.get("line", -1)
190
+ ref_char = ref_start.get("character", -1)
191
+ ref_end_char = ref_end.get("character", -1)
192
+ if not _position_inside_node(src_node, ref_line, ref_char):
193
+ continue
194
+ if not _reference_matches_edge_kind(
195
+ dst_node, ref_file, ref_line, ref_char, ref_end_char, adapter, source_inspector
196
+ ):
197
+ continue
198
+ return True
199
+ return False
200
+
201
+
202
+ def _add_outbound_edges_from_changed_files(
203
+ call_graph: CallGraph,
204
+ changed_source_files: list[Path],
205
+ engine_client: LSPClient,
206
+ ) -> None:
207
+ if not changed_source_files:
208
+ return
209
+
210
+ source_inspector = SourceInspector()
211
+ added = 0
212
+
213
+ for file_path in changed_source_files:
214
+ call_sites = source_inspector.find_call_sites(file_path)
215
+ if not call_sites:
216
+ continue
217
+ queries = [(file_path, line, char) for line, char in call_sites]
218
+ try:
219
+ definition_results, _ = engine_client.send_definition_batch(queries)
220
+ except Exception:
221
+ logger.debug("Failed to resolve outbound definitions for %s", file_path, exc_info=True)
222
+ continue
223
+
224
+ for (line, char), definitions in zip(call_sites, definition_results):
225
+ containing_nodes = _containing_callable_nodes(call_graph, file_path, line, char)
226
+ if not containing_nodes:
227
+ continue
228
+ src_node = max(
229
+ containing_nodes, key=lambda node: (node.line_start, node.col_start, len(node.fully_qualified_name))
230
+ )
231
+ for definition in definitions:
232
+ for dst_node in _definition_nodes(call_graph, definition):
233
+ if dst_node.fully_qualified_name == src_node.fully_qualified_name:
234
+ continue
235
+ try:
236
+ before = len(call_graph.edges)
237
+ call_graph.add_edge(src_node.fully_qualified_name, dst_node.fully_qualified_name)
238
+ if len(call_graph.edges) > before:
239
+ added += 1
240
+ except ValueError:
241
+ logger.debug(
242
+ "Failed to add outbound edge %s -> %s",
243
+ src_node.fully_qualified_name,
244
+ dst_node.fully_qualified_name,
245
+ exc_info=True,
246
+ )
247
+
248
+ if added:
249
+ logger.info("Added %d new outbound edge(s) from changed files", added)
250
+
251
+
252
+ def _containing_callable_nodes(call_graph: CallGraph, file_path: Path, line: int, char: int) -> list[Node]:
253
+ return [
254
+ node
255
+ for node in call_graph.nodes.values()
256
+ if node.is_callable() and node.file_path == str(file_path) and _position_inside_node(node, line, char)
257
+ ]
258
+
259
+
260
+ def _definition_nodes(call_graph: CallGraph, definition: dict) -> list[Node]:
261
+ return [node for node in call_graph.nodes.values() if _definition_points_to_node(definition, node)]
262
+
263
+
264
+ def _definition_points_to_node(definition: dict, dst_node: Node) -> bool:
265
+ uri = definition.get("targetUri", definition.get("uri", ""))
266
+ file_path = uri_to_path(uri)
267
+ if file_path is None or str(file_path) != dst_node.file_path:
268
+ return False
269
+ selection_range = definition.get("targetSelectionRange", definition.get("targetRange", definition.get("range", {})))
270
+ start = selection_range.get("start", {})
271
+ line = start.get("line", -1)
272
+ character = start.get("character", -1)
273
+ return _position_inside_node(dst_node, line, character)
274
+
275
+
276
+ def _position_inside_node(node: Node, zero_based_line: int, character: int) -> bool:
277
+ line = zero_based_line + 1
278
+ if line < node.line_start or line > node.line_end:
279
+ return False
280
+ if line == node.line_start and character < node.col_start:
281
+ return False
282
+ return True
283
+
284
+
285
+ def _reference_matches_edge_kind(
286
+ dst_node: Node,
287
+ ref_file: Path,
288
+ ref_line: int,
289
+ ref_char: int,
290
+ ref_end_char: int,
291
+ adapter: LanguageAdapter,
292
+ source_inspector: SourceInspector,
293
+ ) -> bool:
294
+ if adapter.is_class_like(dst_node.type) and not source_inspector.is_invocation(ref_file, ref_line, ref_end_char):
295
+ return False
296
+ if dst_node.type == NodeType.CONSTANT and not source_inspector.is_invocation(ref_file, ref_line, ref_end_char):
297
+ return False
298
+ if dst_node.type == NodeType.VARIABLE and not source_inspector.is_callable_usage(
299
+ ref_file, ref_line, ref_char, ref_end_char
300
+ ):
301
+ return False
302
+ return True
303
+
304
+
305
+ def _filter_to_live_files(merged_analysis: AnalysisData) -> AnalysisData:
306
+ """Drop entries whose file no longer exists on disk.
307
+
308
+ A file in ``source_files`` may have been re-LSPed earlier in the run and
309
+ then removed by a subsequent edit; this final filter keeps the merged
310
+ dict consistent with the live filesystem.
311
+ """
312
+ # Normalize: ``merge_results`` may contain a mix of Path (from the cached
313
+ # side) and str (from the LSP-rebuilt new side); coerce before ``.exists()``.
314
+ all_existing = {Path(f) for f in merged_analysis.source_files if Path(f).exists()}
315
+ existing_file_strs = {str(f) for f in all_existing}
316
+
317
+ merged_analysis.source_files = list(all_existing)
318
+ merged_analysis.references = [ref for ref in merged_analysis.references if ref.file_path in existing_file_strs]
319
+
320
+ merged_analysis.call_graph = merged_analysis.call_graph.filter(
321
+ lambda node: node.file_path in existing_file_strs,
322
+ on_dropped_edge=lambda _edge: None,
323
+ )
324
+
325
+ merged_analysis.class_hierarchies = {
326
+ name: info
327
+ for name, info in merged_analysis.class_hierarchies.items()
328
+ if info.get("file_path") in existing_file_strs
329
+ }
330
+
331
+ filtered_packages: dict[str, Any] = {}
332
+ for pkg_name, pkg_info in merged_analysis.package_relations.items():
333
+ existing_pkg_files = [f for f in pkg_info.get("files", []) if f in existing_file_strs]
334
+ if existing_pkg_files:
335
+ filtered_packages[pkg_name] = {**pkg_info, "files": existing_pkg_files}
336
+ merged_analysis.package_relations = filtered_packages
337
+
338
+ return merged_analysis