diff --git a/src/skill_seekers/cli/code_analyzer.py b/src/skill_seekers/cli/code_analyzer.py index 20ad975..ff10fc0 100644 --- a/src/skill_seekers/cli/code_analyzer.py +++ b/src/skill_seekers/cli/code_analyzer.py @@ -106,8 +106,14 @@ class CodeAnalyzer: if language == "Python": return self._analyze_python(content, file_path) elif language == "GDScript": - # GDScript is Python-like, use Python analyzer - return self._analyze_python(content, file_path) + # GDScript has Godot-specific syntax, use dedicated parser + return self._analyze_gdscript(content, file_path) + elif language == "GodotScene": + return self._analyze_godot_scene(content, file_path) + elif language == "GodotResource": + return self._analyze_godot_resource(content, file_path) + elif language == "GodotShader": + return self._analyze_godot_shader(content, file_path) elif language in ["JavaScript", "TypeScript"]: return self._analyze_javascript(content, file_path) elif language in ["C", "C++"]: @@ -1424,6 +1430,274 @@ class CodeAnalyzer: return comments + def _analyze_godot_scene(self, content: str, file_path: str) -> dict[str, Any]: + """ + Analyze Godot .tscn scene file. + + Extracts: + - Node hierarchy + - Script attachments + - External resource dependencies + - Scene metadata + """ + nodes = [] + resources = [] + scripts = [] + + # Extract external resources + for match in re.finditer(r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content): + res_type, path, res_id = match.groups() + resources.append({ + "type": res_type, + "path": path, + "id": res_id + }) + + # Track scripts separately + if res_type == "Script": + scripts.append({ + "path": path, + "id": res_id + }) + + # Extract nodes + for match in re.finditer(r'\[node name="(.+?)".*?type="(.+?)".*?\]', content): + node_name, node_type = match.groups() + + # Check if node has a script attached + script_match = re.search(rf'\[node name="{re.escape(node_name)}".*?script = ExtResource\("(.+?)"\)', content, re.DOTALL) + attached_script = script_match.group(1) if script_match else None + + nodes.append({ + "name": node_name, + "type": node_type, + "script": attached_script + }) + + return { + "file": file_path, + "nodes": nodes, + "scripts": scripts, + "resources": resources, + "scene_metadata": { + "node_count": len(nodes), + "script_count": len(scripts), + "resource_count": len(resources) + } + } + + def _analyze_godot_resource(self, content: str, file_path: str) -> dict[str, Any]: + """ + Analyze Godot .tres resource file. + + Extracts: + - Resource type and class + - Script reference + - Properties and values + - External dependencies + """ + properties = [] + resources = [] + resource_type = None + script_class = None + script_path = None + + # Extract resource header + header_match = re.search(r'\[gd_resource type="(.+?)"(?:\s+script_class="(.+?)")?\s+', content) + if header_match: + resource_type = header_match.group(1) + script_class = header_match.group(2) + + # Extract external resources + for match in re.finditer(r'\[ext_resource.*?type="(.+?)".*?path="(.+?)".*?id="(.+?)"\]', content): + res_type, path, res_id = match.groups() + resources.append({ + "type": res_type, + "path": path, + "id": res_id + }) + + if res_type == "Script": + script_path = path + + # Extract properties from [resource] section + resource_section = re.search(r'\[resource\](.*?)(?:\n\[|$)', content, re.DOTALL) + if resource_section: + prop_text = resource_section.group(1) + + for line in prop_text.strip().split('\n'): + if '=' in line: + key, value = line.split('=', 1) + properties.append({ + "name": key.strip(), + "value": value.strip() + }) + + return { + "file": file_path, + "resource_type": resource_type, + "script_class": script_class, + "script_path": script_path, + "properties": properties, + "resources": resources, + "resource_metadata": { + "property_count": len(properties), + "dependency_count": len(resources) + } + } + + def _analyze_godot_shader(self, content: str, file_path: str) -> dict[str, Any]: + """ + Analyze Godot .gdshader shader file. + + Extracts: + - Shader type (spatial, canvas_item, particles, etc.) + - Uniforms (parameters) + - Functions + - Varying variables + """ + uniforms = [] + functions = [] + varyings = [] + shader_type = None + + # Extract shader type + type_match = re.search(r'shader_type\s+(\w+)', content) + if type_match: + shader_type = type_match.group(1) + + # Extract uniforms + for match in re.finditer(r'uniform\s+(\w+)\s+(\w+)(?:\s*:\s*(.+?))?(?:\s*=\s*(.+?))?;', content): + uniform_type, name, hint, default = match.groups() + uniforms.append({ + "name": name, + "type": uniform_type, + "hint": hint, + "default": default + }) + + # Extract varying variables + for match in re.finditer(r'varying\s+(\w+)\s+(\w+)', content): + var_type, name = match.groups() + varyings.append({ + "name": name, + "type": var_type + }) + + # Extract functions + for match in re.finditer(r'void\s+(\w+)\s*\(([^)]*)\)', content): + func_name, params = match.groups() + functions.append({ + "name": func_name, + "parameters": params.strip() if params else "" + }) + + return { + "file": file_path, + "shader_type": shader_type, + "uniforms": uniforms, + "varyings": varyings, + "functions": functions, + "shader_metadata": { + "uniform_count": len(uniforms), + "function_count": len(functions) + } + } + + def _analyze_gdscript(self, content: str, file_path: str) -> dict[str, Any]: + """ + Analyze GDScript file using regex (Godot-specific syntax). + + GDScript has Python-like syntax but with Godot-specific keywords: + - class_name MyClass extends Node + - func _ready(): (functions) + - signal my_signal(param) + - @export var speed: float = 100.0 + - @onready var sprite = $Sprite2D + """ + classes = [] + functions = [] + signals = [] + exports = [] + + # Extract class definition + class_match = re.search(r'class_name\s+(\w+)(?:\s+extends\s+(\w+))?', content) + if class_match: + class_name = class_match.group(1) + extends = class_match.group(2) + classes.append({ + "name": class_name, + "bases": [extends] if extends else [], + "methods": [], + "line_number": content[: class_match.start()].count("\n") + 1 + }) + + # Extract functions + for match in re.finditer(r'func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?:', content): + func_name, params, return_type = match.groups() + + # Parse parameters + param_list = [] + if params.strip(): + for param in params.split(','): + param = param.strip() + if ':' in param: + # param_name: Type = default + parts = param.split(':') + name = parts[0].strip() + type_and_default = parts[1].strip() + + param_type = type_and_default.split('=')[0].strip() if '=' in type_and_default else type_and_default + default = type_and_default.split('=')[1].strip() if '=' in type_and_default else None + + param_list.append({ + "name": name, + "type_hint": param_type, + "default": default + }) + else: + param_list.append({ + "name": param, + "type_hint": None, + "default": None + }) + + functions.append({ + "name": func_name, + "parameters": param_list, + "return_type": return_type, + "line_number": content[: match.start()].count("\n") + 1 + }) + + # Extract signals + for match in re.finditer(r'signal\s+(\w+)(?:\(([^)]*)\))?', content): + signal_name, params = match.groups() + signals.append({ + "name": signal_name, + "parameters": params if params else "", + "line_number": content[: match.start()].count("\n") + 1 + }) + + # Extract @export variables + for match in re.finditer(r'@export(?:\(([^)]+)\))?\s+var\s+(\w+)(?:\s*:\s*(\w+))?(?:\s*=\s*(.+?))?(?:\n|$)', content): + hint, var_name, var_type, default = match.groups() + exports.append({ + "name": var_name, + "type": var_type, + "default": default, + "export_hint": hint, + "line_number": content[: match.start()].count("\n") + 1 + }) + + return { + "file": file_path, + "classes": classes, + "functions": functions, + "signals": signals, + "exports": exports + } + + if __name__ == "__main__": # Test the analyzer python_code = ''' @@ -1463,3 +1737,4 @@ def create_sprite(texture: str) -> Node2D: ] ) print(f" {method['name']}({params}) -> {method['return_type']}") + diff --git a/src/skill_seekers/cli/codebase_scraper.py b/src/skill_seekers/cli/codebase_scraper.py index 1a35369..cc4ee3f 100644 --- a/src/skill_seekers/cli/codebase_scraper.py +++ b/src/skill_seekers/cli/codebase_scraper.py @@ -69,6 +69,9 @@ LANGUAGE_EXTENSIONS = { ".c": "C", ".cs": "C#", ".gd": "GDScript", # Godot scripting language + ".tscn": "GodotScene", # Godot scene files + ".tres": "GodotResource", # Godot resource files + ".gdshader": "GodotShader", # Godot shader files ".go": "Go", ".rs": "Rust", ".java": "Java", @@ -842,7 +845,18 @@ def analyze_codebase( analysis = analyzer.analyze_file(str(file_path), content, language) # Only include files with actual analysis results - if analysis and (analysis.get("classes") or analysis.get("functions")): + # Check for any meaningful content (classes, functions, nodes, properties, etc.) + has_content = ( + analysis.get("classes") + or analysis.get("functions") + or analysis.get("nodes") # Godot scenes + or analysis.get("properties") # Godot resources + or analysis.get("uniforms") # Godot shaders + or analysis.get("signals") # GDScript signals + or analysis.get("exports") # GDScript exports + ) + + if analysis and has_content: results["files"].append( { "file": str(file_path.relative_to(directory)), diff --git a/src/skill_seekers/cli/dependency_analyzer.py b/src/skill_seekers/cli/dependency_analyzer.py index ffded19..96f01fb 100644 --- a/src/skill_seekers/cli/dependency_analyzer.py +++ b/src/skill_seekers/cli/dependency_analyzer.py @@ -111,6 +111,9 @@ class DependencyAnalyzer: elif language == "GDScript": # GDScript is Python-like, uses similar import syntax deps = self._extract_python_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) elif language in ("JavaScript", "TypeScript"): deps = self._extract_js_imports(content, file_path) elif language in ("C++", "C"): @@ -758,3 +761,48 @@ class DependencyAnalyzer: [node for node in self.graph.nodes() if self.graph.in_degree(node) == 0] ), } + + def _extract_godot_resources(self, content: str, file_path: str) -> list[DependencyInfo]: + """ + Extract resource dependencies from Godot files (.tscn, .tres, .gdshader). + + Extracts: + - ext_resource paths (scripts, scenes, textures, etc.) + - preload() and load() calls + """ + deps = [] + + # Extract ext_resource dependencies + for match in re.finditer(r'\[ext_resource.*?path="(.+?)".*?\]', content): + resource_path = match.group(1) + + # Convert res:// paths to relative paths + if resource_path.startswith("res://"): + resource_path = resource_path[6:] # Remove res:// prefix + + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=resource_path, + import_type="ext_resource", + line_number=content[: match.start()].count("\n") + 1, + ) + ) + + # Extract preload() and load() calls (in GDScript sections) + for match in re.finditer(r'(?:preload|load)\("(.+?)"\)', content): + resource_path = match.group(1) + + if resource_path.startswith("res://"): + resource_path = resource_path[6:] + + deps.append( + DependencyInfo( + source_file=file_path, + imported_module=resource_path, + import_type="preload", + line_number=content[: match.start()].count("\n") + 1, + ) + ) + + return deps