diff --git a/codewiki/src/be/dependency_analyzer/analyzers/c.py b/codewiki/src/be/dependency_analyzer/analyzers/c.py index 5177dde2..e3dce9cb 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/c.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/c.py @@ -53,7 +53,7 @@ def _get_relative_path(self) -> str: def _get_component_id(self, name: str) -> str: module_path = self._get_module_path() - return f"{module_path}.{name}" if module_path else name + return f"{module_path}::{name}" if module_path else name def _analyze(self): language_capsule = tree_sitter_c.language() diff --git a/codewiki/src/be/dependency_analyzer/analyzers/cpp.py b/codewiki/src/be/dependency_analyzer/analyzers/cpp.py index 72dce92f..20cec996 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/cpp.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/cpp.py @@ -55,8 +55,8 @@ def _get_relative_path(self) -> str: def _get_component_id(self, name: str, parent_class: str = None) -> str: module_path = self._get_module_path() if parent_class: - return f"{module_path}.{parent_class}.{name}" if module_path else f"{parent_class}.{name}" - return f"{module_path}.{name}" if module_path else name + return f"{module_path}::{parent_class}.{name}" if module_path else f"{parent_class}.{name}" + return f"{module_path}::{name}" if module_path else name def _analyze(self): language_capsule = tree_sitter_cpp.language() diff --git a/codewiki/src/be/dependency_analyzer/analyzers/csharp.py b/codewiki/src/be/dependency_analyzer/analyzers/csharp.py index 579cd070..971ea9a3 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/csharp.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/csharp.py @@ -53,7 +53,7 @@ def _get_relative_path(self) -> str: def _get_component_id(self, name: str) -> str: module_path = self._get_module_path() - return f"{module_path}.{name}" if module_path else name + return f"{module_path}::{name}" if module_path else name def _analyze(self): language_capsule = tree_sitter_c_sharp.language() @@ -303,3 +303,4 @@ def analyze_csharp_file(file_path: str, content: str, repo_path: str = None) -> analyzer = TreeSitterCSharpAnalyzer(file_path, content, repo_path) return analyzer.nodes, analyzer.call_relationships + diff --git a/codewiki/src/be/dependency_analyzer/analyzers/java.py b/codewiki/src/be/dependency_analyzer/analyzers/java.py index 26f586a1..1880f3e8 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/java.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/java.py @@ -1,3 +1,13 @@ +"""Java analyzer for the dependency analysis pipeline. + +This module uses tree-sitter to parse Java source files and extract +structural components (classes, interfaces, enums, records, annotations, +methods) as well as call/relationship information (inheritance, interface +implementation, field type usage, method invocations, and object creation). +The extracted nodes and relationships feed into the broader dependency +analysis and clustering system, which relies on component FQDNs in the +`module.path::ClassName` format. +""" import logging from typing import List, Optional, Tuple from pathlib import Path @@ -47,9 +57,9 @@ def _get_relative_path(self) -> str: def _get_component_id(self, name: str, parent_class: str = None) -> str: module_path = self._get_module_path() if parent_class: - return f"{module_path}.{parent_class}.{name}" + return f"{module_path}::{parent_class}.{name}" else: - return f"{module_path}.{name}" + return f"{module_path}::{name}" def _analyze(self): language_capsule = tree_sitter_java.language() @@ -353,4 +363,4 @@ def _find_containing_method(self, node): def analyze_java_file(file_path: str, content: str, repo_path: str = None) -> Tuple[List[Node], List[CallRelationship]]: analyzer = TreeSitterJavaAnalyzer(file_path, content, repo_path) - return analyzer.nodes, analyzer.call_relationships \ No newline at end of file + return analyzer.nodes, analyzer.call_relationships diff --git a/codewiki/src/be/dependency_analyzer/analyzers/javascript.py b/codewiki/src/be/dependency_analyzer/analyzers/javascript.py index ff14408b..3210d869 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/javascript.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/javascript.py @@ -108,11 +108,11 @@ def _get_component_id(self, name: str, class_name: str = None, is_method: bool = module_path = self._get_module_path() if is_method and class_name: - return f"{module_path}.{class_name}.{name}" + return f"{module_path}::{class_name}.{name}" elif class_name and not is_method: - return f"{module_path}.{name}" + return f"{module_path}::{name}" else: - return f"{module_path}.{name}" + return f"{module_path}::{name}" def _find_containing_class(self, node) -> Optional[str]: parent = node.parent diff --git a/codewiki/src/be/dependency_analyzer/analyzers/php.py b/codewiki/src/be/dependency_analyzer/analyzers/php.py index 2488fdf1..7d48e256 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/php.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/php.py @@ -148,18 +148,18 @@ def _get_relative_path(self) -> str: return str(self.file_path) def _get_component_id(self, name: str, parent_class: str = None) -> str: - """Generate component ID for a node.""" + """Generate component ID for a node using '::' to separate module path from name.""" # Use namespace if available if self.namespace_resolver.current_namespace: ns_prefix = self.namespace_resolver.current_namespace.replace("\\", ".") if parent_class: - return f"{ns_prefix}.{parent_class}.{name}" - return f"{ns_prefix}.{name}" + return f"{ns_prefix}::{parent_class}.{name}" + return f"{ns_prefix}::{name}" module_path = self._get_module_path() if parent_class: - return f"{module_path}.{parent_class}.{name}" - return f"{module_path}.{name}" + return f"{module_path}::{parent_class}.{name}" + return f"{module_path}::{name}" def _analyze(self): """Parse and analyze the PHP file.""" diff --git a/codewiki/src/be/dependency_analyzer/analyzers/python.py b/codewiki/src/be/dependency_analyzer/analyzers/python.py index deda7935..8346e015 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/python.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/python.py @@ -49,16 +49,17 @@ def _get_module_path(self) -> str: path = path[:-len(ext)] break return path.replace('/', '.').replace('\\', '.') - except: + except Exception as e: + logger.debug(f"Failed to compute module path for {self.file_path}: {e}") return str(self.file_path).replace('/', '.').replace('\\', '.') def _get_component_id(self, name: str) -> str: - """Generate dot-separated component ID.""" + """Generate component ID in '::' FQDN format.""" module_path = self._get_module_path() if self.current_class_name: - return f"{module_path}.{self.current_class_name}.{name}" + return f"{module_path}::{self.current_class_name}.{name}" else: - return f"{module_path}.{name}" + return f"{module_path}::{name}" def generic_visit(self, node): """Override generic_visit to continue AST traversal.""" @@ -70,7 +71,7 @@ def visit_ClassDef(self, node: ast.ClassDef): base_classes = [self._extract_base_class_name(base) for base in node.bases] base_classes = [name for name in base_classes if name is not None] - component_id = f"{self._get_module_path()}.{node.name}" + component_id = f"{self._get_module_path()}::{node.name}" relative_path = self._get_relative_path() class_node = Node( @@ -98,7 +99,7 @@ def visit_ClassDef(self, node: ast.ClassDef): if base_name in self.top_level_nodes: self.call_relationships.append(CallRelationship( caller=component_id, - callee=f"{self._get_module_path()}.{base_name}", + callee=f"{self._get_module_path()}::{base_name}", call_line=node.lineno, is_resolved=True )) @@ -126,7 +127,7 @@ def _process_function_node(self, node: ast.FunctionDef | ast.AsyncFunctionDef): """Process function definition - only add to nodes if it's top-level.""" if not self.current_class_name: - component_id = f"{self._get_module_path()}.{node.name}" + component_id = f"{self._get_module_path()}::{node.name}" relative_path = self._get_relative_path() func_node = Node( @@ -175,12 +176,12 @@ def visit_Call(self, node: ast.Call): call_name = self._get_call_name(node.func) if call_name: if self.current_class_name: - caller_id = f"{self._get_module_path()}.{self.current_class_name}" + caller_id = f"{self._get_module_path()}::{self.current_class_name}" else: - caller_id = f"{self._get_module_path()}.{self.current_function_name}" + caller_id = f"{self._get_module_path()}::{self.current_function_name}" if call_name in self.top_level_nodes: - callee_id = f"{self._get_module_path()}.{call_name}" + callee_id = f"{self._get_module_path()}::{call_name}" else: callee_id = call_name @@ -264,3 +265,4 @@ def analyze_python_file( analyzer.analyze() return analyzer.nodes, analyzer.call_relationships + diff --git a/codewiki/src/be/dependency_analyzer/analyzers/typescript.py b/codewiki/src/be/dependency_analyzer/analyzers/typescript.py index 9edde854..3da9d2df 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/typescript.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/typescript.py @@ -661,7 +661,7 @@ def _get_relative_path(self) -> str: def _get_component_id(self, name: str) -> str: module_path = self._get_module_path() - return f"{module_path}.{name}" + return f"{module_path}::{name}" def _extract_inheritance(self, node) -> List[str]: """Extract inheritance/implementation relationships.""" diff --git a/codewiki/src/be/dependency_analyzer/ast_parser.py b/codewiki/src/be/dependency_analyzer/ast_parser.py index 963c46a0..cc32defb 100644 --- a/codewiki/src/be/dependency_analyzer/ast_parser.py +++ b/codewiki/src/be/dependency_analyzer/ast_parser.py @@ -1,3 +1,16 @@ +"""AST parsing and dependency graph construction for multi-repository codebases. + +This module implements the core dependency analysis pipeline stage that: +- Parses one or more repositories (single-path or multi-path modes) into + structural and call-graph representations using the AnalysisService. +- Builds Node-based components keyed by fully-qualified domain names (FQDNs) + in the canonical `module.path::ComponentName` format. +- Namespaces components originating from multiple repositories to avoid ID + collisions and tracks module membership for each component. +- Resolves intra- and cross-namespace dependency edges between components. +- Persists the resulting dependency graph to disk for downstream consumers + (e.g., clustering, LLM-based summarization, and documentation generation). +""" import os import json import logging @@ -12,7 +25,6 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) class DependencyParser: @@ -104,7 +116,7 @@ def _parse_multiple_repositories(self, filtered_folders: List[str] = None) -> Di Parse multiple repositories and merge components with namespace prefixes. Each repository gets a namespace prefix based on its directory name. - Component IDs are prefixed to avoid collisions: {namespace}.{original_id} + Component IDs are prefixed to avoid collisions: {namespace}::{original_id} Returns: Dictionary of all components from all repositories with namespaced IDs @@ -225,7 +237,8 @@ def _build_namespaced_components( if not original_id: continue - # Create FQDN (namespaced component ID) + # Create FQDN (namespaced component ID) using '::' to separate + # the namespace/module path from the component identifier fqdn = f"{namespace}.{original_id}" # Store mapping for dependency resolution @@ -259,11 +272,16 @@ def _build_namespaced_components( components[fqdn] = node # Track module (with namespace) - if "." in original_id: - module_parts = original_id.split(".")[:-1] - module_path = ".".join(module_parts) - if module_path: - self.modules.add(f"{namespace}.{module_path}") + # original_id is '::' from the analyzers, so the + # module path is everything before '::'. (The dot-split fallback is + # for ids that predate the '::' separator.) + module_path = ( + original_id.split("::")[0] + if "::" in original_id + else ".".join(original_id.split(".")[:-1]) + ) + if module_path: + self.modules.add(f"{namespace}.{module_path}") # Second pass: Add dependencies within this namespace for rel_dict in relationships: @@ -343,7 +361,7 @@ def _build_components_from_analysis(self, call_graph_result: Dict): if not original_id: continue - # Construct FQDN: {namespace}.{original_id} + # Construct FQDN: {namespace}::{original_id} fqdn = f"{namespace}.{original_id}" node = Node( @@ -379,12 +397,17 @@ def _build_components_from_analysis(self, call_graph_result: Dict): if legacy_id and legacy_id != fqdn: component_id_mapping[legacy_id] = fqdn - if "." in original_id: - module_parts = original_id.split(".")[:-1] - module_path = ".".join(module_parts) - if module_path: - # Store module with namespace - self.modules.add(f"{namespace}.{module_path}") + # original_id is '::' from the analyzers, so the + # module path is everything before '::'. (The dot-split fallback is + # for ids that predate the '::' separator.) + module_path = ( + original_id.split("::")[0] + if "::" in original_id + else ".".join(original_id.split(".")[:-1]) + ) + if module_path: + # Store module with namespace + self.modules.add(f"{namespace}.{module_path}") processed_relationships = 0 for rel_dict in relationships: @@ -443,3 +466,4 @@ def save_dependency_graph(self, output_path: str): logger.debug(f"Saved {len(self.components)} components to {output_path}") return result +