save progress
This commit is contained in:
282
tools/slntools/lib/dependency_graph.py
Normal file
282
tools/slntools/lib/dependency_graph.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
Project dependency graph utilities.
|
||||
|
||||
Provides functions to:
|
||||
- Build a dependency graph from parsed projects
|
||||
- Get transitive dependencies
|
||||
- Classify dependencies as internal or external to a module
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .models import CsprojProject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_dependency_graph(
|
||||
projects: list[CsprojProject],
|
||||
) -> dict[Path, set[Path]]:
|
||||
"""
|
||||
Build a dependency graph from a list of projects.
|
||||
|
||||
Args:
|
||||
projects: List of parsed CsprojProject objects
|
||||
|
||||
Returns:
|
||||
Dictionary mapping project path to set of dependency paths
|
||||
"""
|
||||
graph: dict[Path, set[Path]] = {}
|
||||
|
||||
for project in projects:
|
||||
graph[project.path] = set(project.project_references)
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
def get_transitive_dependencies(
|
||||
project_path: Path,
|
||||
graph: dict[Path, set[Path]],
|
||||
visited: Optional[set[Path]] = None,
|
||||
) -> set[Path]:
|
||||
"""
|
||||
Get all transitive dependencies for a project.
|
||||
|
||||
Handles circular dependencies gracefully by tracking visited nodes.
|
||||
|
||||
Args:
|
||||
project_path: Path to the project
|
||||
graph: Dependency graph from build_dependency_graph
|
||||
visited: Set of already visited paths (for cycle detection)
|
||||
|
||||
Returns:
|
||||
Set of all transitive dependency paths
|
||||
"""
|
||||
if visited is None:
|
||||
visited = set()
|
||||
|
||||
if project_path in visited:
|
||||
return set() # Cycle detected
|
||||
|
||||
visited.add(project_path)
|
||||
all_deps: set[Path] = set()
|
||||
|
||||
direct_deps = graph.get(project_path, set())
|
||||
all_deps.update(direct_deps)
|
||||
|
||||
for dep in direct_deps:
|
||||
transitive = get_transitive_dependencies(dep, graph, visited.copy())
|
||||
all_deps.update(transitive)
|
||||
|
||||
return all_deps
|
||||
|
||||
|
||||
def classify_dependencies(
|
||||
project: CsprojProject,
|
||||
module_dir: Path,
|
||||
src_root: Path,
|
||||
) -> dict[str, list[Path]]:
|
||||
"""
|
||||
Classify project dependencies as internal or external.
|
||||
|
||||
Args:
|
||||
project: The project to analyze
|
||||
module_dir: Root directory of the module
|
||||
src_root: Root of the src/ directory
|
||||
|
||||
Returns:
|
||||
Dictionary with keys:
|
||||
- 'internal': Dependencies within module_dir
|
||||
- '__Libraries': Dependencies from src/__Libraries/
|
||||
- '<ModuleName>': Dependencies from other modules
|
||||
"""
|
||||
result: dict[str, list[Path]] = {"internal": []}
|
||||
|
||||
module_dir = module_dir.resolve()
|
||||
src_root = src_root.resolve()
|
||||
|
||||
for ref_path in project.project_references:
|
||||
ref_path = ref_path.resolve()
|
||||
|
||||
# Check if internal to module
|
||||
try:
|
||||
ref_path.relative_to(module_dir)
|
||||
result["internal"].append(ref_path)
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# External - classify by source module
|
||||
category = _get_external_category(ref_path, src_root)
|
||||
if category not in result:
|
||||
result[category] = []
|
||||
result[category].append(ref_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _get_external_category(ref_path: Path, src_root: Path) -> str:
|
||||
"""
|
||||
Determine the category for an external dependency.
|
||||
|
||||
Args:
|
||||
ref_path: Path to the referenced project
|
||||
src_root: Root of the src/ directory
|
||||
|
||||
Returns:
|
||||
Category name (e.g., '__Libraries', 'Authority', 'Scanner')
|
||||
"""
|
||||
try:
|
||||
rel_path = ref_path.relative_to(src_root)
|
||||
except ValueError:
|
||||
# Outside of src/ - use 'External'
|
||||
return "External"
|
||||
|
||||
parts = rel_path.parts
|
||||
|
||||
if len(parts) == 0:
|
||||
return "External"
|
||||
|
||||
# First part is the module or __Libraries/__Tests etc.
|
||||
first_part = parts[0]
|
||||
|
||||
if first_part == "__Libraries":
|
||||
return "__Libraries"
|
||||
elif first_part == "__Tests":
|
||||
return "__Tests"
|
||||
elif first_part == "__Analyzers":
|
||||
return "__Analyzers"
|
||||
else:
|
||||
# It's a module name
|
||||
return first_part
|
||||
|
||||
|
||||
def collect_all_external_dependencies(
|
||||
projects: list[CsprojProject],
|
||||
module_dir: Path,
|
||||
src_root: Path,
|
||||
project_map: dict[Path, CsprojProject],
|
||||
) -> dict[str, list[CsprojProject]]:
|
||||
"""
|
||||
Collect all external dependencies for a module's projects.
|
||||
|
||||
Includes transitive dependencies.
|
||||
|
||||
Args:
|
||||
projects: List of projects in the module
|
||||
module_dir: Root directory of the module
|
||||
src_root: Root of the src/ directory
|
||||
project_map: Map from path to CsprojProject for all known projects
|
||||
|
||||
Returns:
|
||||
Dictionary mapping category to list of external CsprojProject objects
|
||||
"""
|
||||
# Build dependency graph for all known projects
|
||||
all_projects = list(project_map.values())
|
||||
graph = build_dependency_graph(all_projects)
|
||||
|
||||
module_dir = module_dir.resolve()
|
||||
src_root = src_root.resolve()
|
||||
|
||||
# Collect all external dependencies
|
||||
external_deps: dict[str, set[Path]] = {}
|
||||
|
||||
for project in projects:
|
||||
# Get all transitive dependencies
|
||||
all_deps = get_transitive_dependencies(project.path, graph)
|
||||
|
||||
for dep_path in all_deps:
|
||||
dep_path = dep_path.resolve()
|
||||
|
||||
# Skip if internal to module
|
||||
try:
|
||||
dep_path.relative_to(module_dir)
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# External - classify
|
||||
category = _get_external_category(dep_path, src_root)
|
||||
if category not in external_deps:
|
||||
external_deps[category] = set()
|
||||
external_deps[category].add(dep_path)
|
||||
|
||||
# Convert paths to CsprojProject objects
|
||||
result: dict[str, list[CsprojProject]] = {}
|
||||
for category, paths in external_deps.items():
|
||||
result[category] = []
|
||||
for path in sorted(paths):
|
||||
if path in project_map:
|
||||
result[category].append(project_map[path])
|
||||
else:
|
||||
logger.warning(f"External dependency not in project map: {path}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_module_projects(
|
||||
module_dir: Path,
|
||||
all_projects: list[CsprojProject],
|
||||
) -> list[CsprojProject]:
|
||||
"""
|
||||
Get all projects that belong to a module.
|
||||
|
||||
Args:
|
||||
module_dir: Root directory of the module
|
||||
all_projects: List of all projects
|
||||
|
||||
Returns:
|
||||
List of projects within the module directory
|
||||
"""
|
||||
module_dir = module_dir.resolve()
|
||||
result: list[CsprojProject] = []
|
||||
|
||||
for project in all_projects:
|
||||
try:
|
||||
project.path.relative_to(module_dir)
|
||||
result.append(project)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def detect_circular_dependencies(
|
||||
graph: dict[Path, set[Path]],
|
||||
) -> list[list[Path]]:
|
||||
"""
|
||||
Detect circular dependencies in the project graph.
|
||||
|
||||
Args:
|
||||
graph: Dependency graph
|
||||
|
||||
Returns:
|
||||
List of cycles (each cycle is a list of paths)
|
||||
"""
|
||||
cycles: list[list[Path]] = []
|
||||
visited: set[Path] = set()
|
||||
rec_stack: set[Path] = set()
|
||||
|
||||
def dfs(node: Path, path: list[Path]) -> None:
|
||||
visited.add(node)
|
||||
rec_stack.add(node)
|
||||
path.append(node)
|
||||
|
||||
for neighbor in graph.get(node, set()):
|
||||
if neighbor not in visited:
|
||||
dfs(neighbor, path.copy())
|
||||
elif neighbor in rec_stack:
|
||||
# Found a cycle
|
||||
cycle_start = path.index(neighbor)
|
||||
cycle = path[cycle_start:] + [neighbor]
|
||||
cycles.append(cycle)
|
||||
|
||||
rec_stack.remove(node)
|
||||
|
||||
for node in graph:
|
||||
if node not in visited:
|
||||
dfs(node, [])
|
||||
|
||||
return cycles
|
||||
Reference in New Issue
Block a user