impl SwiftSourceAnalyzer {
#[must_use]
pub fn new(file_path: &Path) -> Self {
Self {
items: Vec::new(),
_file_path: file_path.to_path_buf(),
source_name: file_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string(),
function_count: 0,
class_count: 0,
method_count: 0,
}
}
pub fn analyze_swift_source(mut self, source: &str) -> Result<Vec<AstItem>, String> {
if source.trim().is_empty() {
return Ok(vec![]);
}
self.extract_functions(source)?;
self.extract_classes(source)?;
self.extract_methods(source)?;
Ok(self.items)
}
fn extract_functions(&mut self, source: &str) -> Result<(), String> {
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("func ") && trimmed.contains('(') {
if let Some(func_name) = self.extract_function_name(trimmed) {
let qualified_name = self.get_qualified_name(&func_name);
self.items.push(AstItem::Function {
name: qualified_name,
visibility: self.extract_visibility(trimmed),
is_async: trimmed.contains("async"),
line: line_num + 1,
});
self.function_count += 1;
}
}
}
Ok(())
}
fn extract_classes(&mut self, source: &str) -> Result<(), String> {
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if (trimmed.starts_with("class ") || trimmed.starts_with("struct "))
&& trimmed.contains('{')
{
if let Some(class_name) = self.extract_class_name(trimmed) {
let qualified_name = self.get_qualified_name(&class_name);
self.items.push(AstItem::Struct {
name: qualified_name,
visibility: self.extract_visibility(trimmed),
fields_count: 0, derives: vec![], line: line_num + 1,
});
self.class_count += 1;
}
}
}
Ok(())
}
fn extract_methods(&mut self, source: &str) -> Result<(), String> {
let mut in_class = false;
let mut brace_count = 0;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("class ") || trimmed.starts_with("struct ") {
in_class = true;
brace_count = 0;
}
brace_count += trimmed.matches('{').count() as i32;
brace_count -= trimmed.matches('}').count() as i32;
if in_class && brace_count == 0 && trimmed.contains('}') {
in_class = false;
}
if in_class && trimmed.starts_with("func ") && trimmed.contains('(') {
if let Some(method_name) = self.extract_function_name(trimmed) {
let qualified_name = self.get_qualified_name(&method_name);
self.items.push(AstItem::Function {
name: qualified_name,
visibility: self.extract_visibility(trimmed),
is_async: trimmed.contains("async"),
line: line_num + 1,
});
self.method_count += 1;
}
}
}
Ok(())
}
fn extract_function_name(&self, line: &str) -> Option<String> {
let after_func = line.strip_prefix("func ")?.trim();
let after_func = if let Some(stripped) = after_func.strip_prefix("private ") {
stripped
} else if let Some(stripped) = after_func.strip_prefix("public ") {
stripped
} else if let Some(stripped) = after_func.strip_prefix("internal ") {
stripped
} else {
after_func
};
let name_part = after_func.split('(').next()?;
Some(name_part.trim().to_string())
}
fn extract_class_name(&self, line: &str) -> Option<String> {
let after_keyword = if let Some(stripped) = line.strip_prefix("class ") {
stripped
} else if let Some(stripped) = line.strip_prefix("struct ") {
stripped
} else {
return None;
};
let name_part = after_keyword
.split_whitespace()
.next()?
.trim_end_matches('{')
.trim_end_matches(':');
Some(name_part.trim().to_string())
}
fn extract_visibility(&self, line: &str) -> String {
if line.contains("private ") {
"private".to_string()
} else if line.contains("public ") {
"public".to_string()
} else if line.contains("internal ") {
"internal".to_string()
} else {
"internal".to_string() }
}
fn get_qualified_name(&self, symbol_name: &str) -> String {
if self.source_name.is_empty() {
symbol_name.to_string()
} else {
format!("{}::{}", self.source_name, symbol_name)
}
}
}