feat: Add comprehensive Godot file type support
Complete support for all Godot file types:
- GDScript (.gd) - Regex-based parser for Godot-specific syntax
- Godot Scenes (.tscn) - Node hierarchy and script attachments
- Godot Resources (.tres) - Properties and dependencies
- Godot Shaders (.gdshader) - Uniforms and shader functions
Implementation details:
- Added 4 new analyzer methods to CodeAnalyzer class
- _analyze_gdscript(): Functions, signals, @export vars, class_name
- _analyze_godot_scene(): Node hierarchy, scripts, resources
- _analyze_godot_resource(): Resource type, properties, script refs
- _analyze_godot_shader(): Shader type, uniforms, varyings, functions
- Updated dependency_analyzer.py
- Added _extract_godot_resources() for ext_resource and preload()
- Fixed DependencyInfo calls (removed invalid 'alias' parameter)
- Updated codebase_scraper.py
- Added Godot file extensions to LANGUAGE_EXTENSIONS
- Extended content filter to accept Godot-specific keys
(nodes, properties, uniforms, signals, exports)
Tested on Cosmic Ideler Godot project:
- 443/452 files successfully analyzed (98%)
- 265 GDScript, 118 .tscn, 38 .tres, 9 .gdshader, 13 .cs
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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']}")
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user