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.
- {codeboarding-0.12.3/codeboarding.egg-info → codeboarding-0.12.4}/PKG-INFO +1 -1
- {codeboarding-0.12.3 → codeboarding-0.12.4/codeboarding.egg-info}/PKG-INFO +1 -1
- {codeboarding-0.12.3 → codeboarding-0.12.4}/pyproject.toml +1 -1
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/analysis_cache.py +77 -83
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/analysis_result.py +44 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/graph.py +8 -2
- codeboarding-0.12.4/static_analyzer/incremental_orchestrator.py +338 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/scanner.py +1 -1
- codeboarding-0.12.3/static_analyzer/incremental_orchestrator.py +0 -125
- {codeboarding-0.12.3 → codeboarding-0.12.4}/LICENSE +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/PYPI.md +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/README.md +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/abstraction_agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/agent_responses.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/change_status.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/cluster_budget.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/cluster_methods_mixin.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/constants.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/dependency_discovery.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/details_agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/incremental_agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/llm_config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/meta_agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/model_capabilities.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/planner_agent.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/abstract_prompt_factory.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/claude_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/deepseek_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/gemini_flash_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/glm_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/gpt_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/kimi_prompts.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/prompts/prompt_factory.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/retry.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/base.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/get_external_deps.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/get_method_invocations.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_cfg.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_docs.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_file.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_file_structure.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_packages.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_source.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/read_structure.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/tools/toolkit.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/agents/validation.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/cache.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/details_cache.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/caching/meta_cache.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/SOURCES.txt +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/dependency_links.txt +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/entry_points.txt +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/requires.txt +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding.egg-info/top_level.txt +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/bootstrap.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/full_analysis.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/incremental_analysis.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_cli/commands/partial_analysis.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/analysis.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/orchestration.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/rendering.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/local.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/codeboarding_workflows/sources/remote.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/constants.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/core/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/core/plugin_loader.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/core/protocols.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/core/registry.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/analysis_json.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/cluster_delta.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/cluster_snapshot.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/diagram_generator.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/exceptions.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/file_coverage.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/io_utils.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/run_context.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/run_mode.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/diagram_analysis/version.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/github_action.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/circular_deps.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/cohesion.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/coupling.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/function_size.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/god_class.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/inheritance.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/instability.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/checks/unused_code_diagnostics.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/models.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/health/runner.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/install.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/logging_config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/main.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/callbacks.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/context.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/mixin.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/paths.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/stats.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/monitoring/writers.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/html.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/html_template.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/markdown.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/mdx.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/output_generators/sphinx.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/change_detector.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/diff_parser.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/errors.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/git_ops.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/repo_utils/ignore.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/setup.cfg +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cfg_skip_planner.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cluster_helpers.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/cluster_relations.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/constants.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/csharp_config_scanner.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/dotnet_sdk.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/csharp_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/go_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/java_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/php_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/python_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/call_graph_builder.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/edge_build_context.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/edge_builder.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/hierarchy_builder.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/language_adapter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/lsp_client.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/lsp_constants.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/models.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/progress.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/protocols.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/result_converter.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/source_inspector.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/symbol_table.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/engine/utils.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/java_config_scanner.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/java_utils.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/language_results.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/leiden_utils.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/lsp_client/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/lsp_client/diagnostics.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/node.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/programming_language.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/reference_resolve_mixin.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/static_analyzer/typescript_config_scanner.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/device_id.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/events.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/schemas.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/telemetry/service.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_cli_parser.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_github_action.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_install.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_logging_config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_main.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_pyproject_packages.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_registry_coverage.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_telemetry_events.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_tool_registry.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_user_config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_vscode_constants.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_windows_compatibility.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tests/test_windows_encoding.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/__init__.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/installers.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/manifest.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/paths.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/tool_registry/registry.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/user_config.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/utils.py +0 -0
- {codeboarding-0.12.3 → codeboarding-0.12.4}/vscode_constants.py +0 -0
|
@@ -29,7 +29,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
29
29
|
|
|
30
30
|
from filelock import FileLock
|
|
31
31
|
|
|
32
|
-
from static_analyzer.
|
|
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]) ->
|
|
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,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
363
|
-
for
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|