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:
yusyus
2026-02-02 21:36:56 +03:00
parent 583a774b00
commit b252f43d0e
3 changed files with 340 additions and 3 deletions

View File

@@ -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']}")

View File

@@ -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)),

View File

@@ -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