diff --git a/src/skill_seekers/cli/dependency_analyzer.py b/src/skill_seekers/cli/dependency_analyzer.py index 96f01fb..1365885 100644 --- a/src/skill_seekers/cli/dependency_analyzer.py +++ b/src/skill_seekers/cli/dependency_analyzer.py @@ -3,7 +3,7 @@ Dependency Graph Analyzer (C2.6) Analyzes import/require/include/use statements to build dependency graphs. -Supports 9 programming languages with language-specific extraction. +Supports 10 programming languages + Godot ecosystem with language-specific extraction. Features: - Multi-language import extraction (Python AST, others regex-based) @@ -14,6 +14,8 @@ Features: Supported Languages: - Python: import, from...import, relative imports (AST-based) +- GDScript: preload(), load(), extends (regex-based, Godot game engine) +- Godot Files: .tscn, .tres, .gdshader ext_resource parsing - JavaScript/TypeScript: ES6 import, CommonJS require (regex-based) - C/C++: #include directives (regex-based) - C#: using statements (regex, based on MS C# spec) @@ -101,7 +103,8 @@ class DependencyAnalyzer: Args: file_path: Path to source file content: File content - language: Programming language (Python, JavaScript, TypeScript, C, C++, C#, Go, Rust, Java, Ruby, PHP) + language: Programming language (Python, GDScript, GodotScene, GodotResource, GodotShader, + JavaScript, TypeScript, C, C++, C#, Go, Rust, Java, Ruby, PHP) Returns: List of DependencyInfo objects @@ -109,8 +112,8 @@ class DependencyAnalyzer: if language == "Python": deps = self._extract_python_imports(content, file_path) elif language == "GDScript": - # GDScript is Python-like, uses similar import syntax - deps = self._extract_python_imports(content, file_path) + # GDScript uses preload/load, not Python imports + deps = self._extract_gdscript_imports(content, file_path) elif language in ("GodotScene", "GodotResource", "GodotShader"): # Godot resource files use ext_resource references deps = self._extract_godot_resources(content, file_path) @@ -195,6 +198,125 @@ class DependencyAnalyzer: return deps + def _extract_gdscript_imports(self, content: str, file_path: str) -> list[DependencyInfo]: + """ + Extract GDScript import/preload/load statements. + + Handles: + - const MyClass = preload("res://path/to/file.gd") + - var scene = load("res://path/to/scene.tscn") + - extends "res://path/to/base.gd" + - extends MyBaseClass (implicit dependency) + + Note: GDScript uses res:// paths which are converted to relative paths. + """ + deps = [] + + # Extract preload() calls: const/var NAME = preload("path") + preload_pattern = r'(?:const|var)\s+\w+\s*=\s*preload\("(.+?)"\)' + for match in re.finditer(preload_pattern, content): + resource_path = match.group(1) + line_num = content[: match.start()].count("\n") + 1 + + # Convert res:// paths to relative + if resource_path.startswith("res://"): + resource_path = resource_path[6:] + + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=resource_path, + import_type="preload", + is_relative=True, + line_number=line_num, + ) + ) + + # Extract load() calls: var/const NAME = load("path") + load_pattern = r'(?:const|var)\s+\w+\s*=\s*load\("(.+?)"\)' + for match in re.finditer(load_pattern, content): + resource_path = match.group(1) + line_num = content[: match.start()].count("\n") + 1 + + if resource_path.startswith("res://"): + resource_path = resource_path[6:] + + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=resource_path, + import_type="load", + is_relative=True, + line_number=line_num, + ) + ) + + # Extract extends with string path: extends "res://path/to/base.gd" + extends_path_pattern = r'extends\s+"(.+?)"' + for match in re.finditer(extends_path_pattern, content): + resource_path = match.group(1) + line_num = content[: match.start()].count("\n") + 1 + + if resource_path.startswith("res://"): + resource_path = resource_path[6:] + + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=resource_path, + import_type="extends", + is_relative=True, + line_number=line_num, + ) + ) + + # Extract extends with class name: extends MyBaseClass + # Note: This creates a symbolic dependency that may not resolve to a file + extends_class_pattern = r'extends\s+([A-Z]\w+)' + for match in re.finditer(extends_class_pattern, content): + class_name = match.group(1) + line_num = content[: match.start()].count("\n") + 1 + + # Skip built-in Godot classes (Node, Resource, etc.) + if class_name not in ( + "Node", + "Node2D", + "Node3D", + "Resource", + "RefCounted", + "Object", + "Control", + "Area2D", + "Area3D", + "CharacterBody2D", + "CharacterBody3D", + "RigidBody2D", + "RigidBody3D", + "StaticBody2D", + "StaticBody3D", + "Camera2D", + "Camera3D", + "Sprite2D", + "Sprite3D", + "Label", + "Button", + "Panel", + "Container", + "VBoxContainer", + "HBoxContainer", + ): + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=class_name, + import_type="extends", + is_relative=False, + line_number=line_num, + ) + ) + + return deps + def _extract_js_imports(self, content: str, file_path: str) -> list[DependencyInfo]: """ Extract JavaScript/TypeScript import statements.