impl PhpScriptAnalyzer {
#[must_use]
pub fn new(file_path: &Path) -> Self {
Self {
items: Vec::new(),
_file_path: file_path.to_path_buf(),
script_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_php_script(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("function ") && 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: "public".to_string(),
is_async: false,
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.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: "public".to_string(),
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 current_class: Option<String> = None;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("class ") && trimmed.contains('{') {
current_class = self.extract_class_name(trimmed);
}
if let Some(item) = parse_php_method_line(trimmed, line_num, ¤t_class) {
self.items.push(item);
self.method_count += 1;
}
}
Ok(())
}
fn extract_function_name(&self, line: &str) -> Option<String> {
let after_function = line.strip_prefix("function ")?.trim();
let name_part = after_function.split('(').next()?;
Some(name_part.trim().to_string())
}
fn extract_class_name(&self, line: &str) -> Option<String> {
let after_class = line.strip_prefix("class ")?.trim();
let name_part = after_class.split_whitespace().next()?.trim_end_matches('{');
Some(name_part.trim().to_string())
}
fn extract_method_name(&self, line: &str) -> Option<String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 && parts[1] == "function" {
let name_part = parts[2].split('(').next()?;
Some(name_part.trim().to_string())
} else {
None
}
}
fn get_qualified_name(&self, symbol_name: &str) -> String {
if self.script_name.is_empty() {
symbol_name.to_string()
} else {
format!("{}::{}", self.script_name, symbol_name)
}
}
}
fn parse_php_method_line(trimmed: &str, line_num: usize, current_class: &Option<String>) -> Option<AstItem> {
let is_method = (trimmed.starts_with("public function ")
|| trimmed.starts_with("private function ")
|| trimmed.starts_with("protected function "))
&& trimmed.contains('(');
if !is_method {
return None;
}
let after_vis = trimmed
.strip_prefix("public function ")
.or_else(|| trimmed.strip_prefix("private function "))
.or_else(|| trimmed.strip_prefix("protected function "))?;
let method_name = after_vis.split('(').next()?.trim().to_string();
let qualified_name = match current_class {
Some(class) => format!("{}::{}", class, method_name),
None => method_name,
};
let visibility = if trimmed.starts_with("private") {
"private"
} else if trimmed.starts_with("protected") {
"protected"
} else {
"public"
};
Some(AstItem::Function {
name: qualified_name,
visibility: visibility.to_string(),
is_async: false,
line: line_num + 1,
})
}