use crate::core::deep_queries::{ImportInfo, ImportKind};
const SCENE_SOURCE_EXTS: [&str; 2] = ["gd", "tscn"];
pub fn extract_scene_imports(content: &str) -> Vec<ImportInfo> {
let mut imports = Vec::new();
for (idx, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if !trimmed.starts_with("[ext_resource") {
continue;
}
let Some(path) = extract_attr(trimmed, "path") else {
continue;
};
if !is_scene_source(&path) {
continue;
}
imports.push(ImportInfo {
source: path,
names: Vec::new(),
kind: ImportKind::SideEffect,
line: idx + 1,
is_type_only: false,
});
}
imports
}
fn extract_attr(line: &str, key: &str) -> Option<String> {
let needle = format!("{key}=\"");
let bytes = line.as_bytes();
let value_start = line.match_indices(&needle).find_map(|(i, _)| {
let boundary = i == 0 || bytes[i - 1] == b' ' || bytes[i - 1] == b'[';
boundary.then_some(i + needle.len())
})?;
let rest = &line[value_start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn is_scene_source(path: &str) -> bool {
std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| SCENE_SOURCE_EXTS.contains(&e.to_ascii_lowercase().as_str()))
}
#[cfg(test)]
mod tests {
use super::*;
const SCENE: &str = r#"[gd_scene load_steps=4 format=3 uid="uid://abc123"]
[ext_resource type="Script" path="res://actors/Player.gd" id="1_script"]
[ext_resource type="PackedScene" uid="uid://def456" path="res://scenes/Enemy.tscn" id="2_scene"]
[ext_resource type="Texture2D" path="res://art/player.png" id="3_tex"]
[node name="Player" type="CharacterBody2D"]
script = ExtResource("1_script")
[node name="Enemy" parent="." instance=ExtResource("2_scene")]
"#;
#[test]
fn extracts_script_and_scene_paths() {
let imports = extract_scene_imports(SCENE);
let sources: Vec<&str> = imports.iter().map(|i| i.source.as_str()).collect();
assert!(
sources.contains(&"res://actors/Player.gd"),
"got {sources:?}"
);
assert!(
sources.contains(&"res://scenes/Enemy.tscn"),
"got {sources:?}"
);
}
#[test]
fn skips_binary_assets() {
let imports = extract_scene_imports(SCENE);
let sources: Vec<&str> = imports.iter().map(|i| i.source.as_str()).collect();
assert!(
!sources.contains(&"res://art/player.png"),
"binary assets must not become graph edges; got {sources:?}"
);
}
#[test]
fn records_line_numbers_and_side_effect_kind() {
let imports = extract_scene_imports(SCENE);
let script = imports
.iter()
.find(|i| i.source == "res://actors/Player.gd")
.unwrap();
assert_eq!(script.line, 3, "1-based line of the ext_resource header");
assert_eq!(script.kind, ImportKind::SideEffect);
}
#[test]
fn ignores_files_without_ext_resources() {
let imports = extract_scene_imports("[gd_scene format=3]\n[node name=\"Root\"]\n");
assert!(imports.is_empty());
}
}