diff --git a/codewiki/src/be/dependency_analyzer/analyzers/cpp.py b/codewiki/src/be/dependency_analyzer/analyzers/cpp.py index dd89d1b3..9c7b9c5c 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/cpp.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/cpp.py @@ -1,3 +1,11 @@ +"""C++ dependency analyzer using tree-sitter. + +This module implements a tree-sitter based analyzer for C++ source files. +It extracts top-level components (classes, structs, functions, methods, +namespaces and global variables) as `Node` objects and detects relationships +between them (calls, inheritance, instantiation and usage) as +`CallRelationship` objects, for use by the dependency analysis pipeline. +""" import logging from typing import List, Optional, Tuple from pathlib import Path @@ -46,8 +54,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() @@ -366,3 +374,4 @@ def _class_has_method(self, class_node, method_name): def analyze_cpp_file(file_path: str, content: str, repo_path: str = None) -> Tuple[List[Node], List[CallRelationship]]: analyzer = TreeSitterCppAnalyzer(file_path, content, repo_path) return analyzer.nodes, analyzer.call_relationships + diff --git a/codewiki/src/be/dependency_analyzer/analyzers/csharp.py b/codewiki/src/be/dependency_analyzer/analyzers/csharp.py index 50500aa4..2e9951b4 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/csharp.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/csharp.py @@ -1,3 +1,9 @@ +"""C# source analyzer using tree-sitter for extracting components and call relationships. + +This module parses C# source files with tree-sitter-c-sharp to identify +top-level components (classes, interfaces, structs, enums, records, delegates) +and derive call relationships between them for dependency analysis. +""" import logging from typing import List, Optional, Tuple from pathlib import Path @@ -45,7 +51,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() @@ -295,3 +301,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 7b94e167..735832cd 100644 --- a/codewiki/src/be/dependency_analyzer/analyzers/javascript.py +++ b/codewiki/src/be/dependency_analyzer/analyzers/javascript.py @@ -1,3 +1,12 @@ +"""Tree-sitter based dependency analyzer for JavaScript and TypeScript source files. + +This module parses JS/TS files using tree-sitter grammars to extract top-level +components (classes, interfaces, functions, methods) as Node objects and to +detect call/inheritance/type relationships between them as CallRelationship +objects. It is used as part of the dependency-analysis pipeline to build the +project-wide dependency graph. +""" + import logging import os import traceback @@ -97,11 +106,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/ast_parser.py b/codewiki/src/be/dependency_analyzer/ast_parser.py index 963c46a0..4e26cbf1 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,8 +237,9 @@ def _build_namespaced_components( if not original_id: continue - # Create FQDN (namespaced component ID) - fqdn = f"{namespace}.{original_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 namespace_mapping[original_id] = fqdn @@ -314,8 +327,8 @@ def _resolve_cross_namespace_dependencies( for other_id, other_component in sorted(all_components.items()): # ✅ SORT for determinism if other_component.name == dep_name and other_id != component_id: # Extract namespaces to check if it's cross-namespace - source_namespace = component_id.split(".")[0] - target_namespace = other_id.split(".")[0] + source_namespace = component_id.split("::")[0] + target_namespace = other_id.split("::")[0] if source_namespace != target_namespace: logger.debug(f" ├─ Cross-namespace dependency: {component_id} → {other_id}") cross_deps_resolved += 1 @@ -343,8 +356,8 @@ def _build_components_from_analysis(self, call_graph_result: Dict): if not original_id: continue - # Construct FQDN: {namespace}.{original_id} - fqdn = f"{namespace}.{original_id}" + # Construct FQDN: {namespace}::{original_id} + fqdn = f"{namespace}::{original_id}" node = Node( id=fqdn, # FQDN as primary identifier @@ -443,3 +456,4 @@ def save_dependency_graph(self, output_path: str): logger.debug(f"Saved {len(self.components)} components to {output_path}") return result +