use crate::utils::path::PathNormalizer;
use crate::indexer::languages::Language;
use tree_sitter::Node;
pub struct Php {}
impl Language for Php {
fn name(&self) -> &'static str {
"php"
}
fn get_ts_language(&self) -> tree_sitter::Language {
tree_sitter_php::LANGUAGE_PHP.into()
}
fn get_meaningful_kinds(&self) -> Vec<&'static str> {
vec![
"function_definition",
"method_declaration",
"namespace_definition",
"namespace_use_declaration",
]
}
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String> {
let mut symbols = Vec::new();
match node.kind() {
"function_definition" | "method_declaration" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "name" {
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
symbols.push(name.to_string());
}
break;
}
}
}
_ => self.extract_identifiers(node, contents, &mut symbols),
}
symbols.sort();
symbols.dedup();
symbols
}
fn extract_identifiers(&self, node: Node, contents: &str, symbols: &mut Vec<String>) {
let kind = node.kind();
if kind == "name" || kind == "variable_name" {
if let Ok(text) = node.utf8_text(contents.as_bytes()) {
let t = text.trim();
let t = if let Some(stripped) = t.strip_prefix('$') {
stripped
} else {
t
};
if !t.is_empty() && !symbols.contains(&t.to_string()) {
symbols.push(t.to_string());
}
}
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
self.extract_identifiers(cursor.node(), contents, symbols);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn are_node_types_equivalent(&self, type1: &str, type2: &str) -> bool {
if type1 == type2 {
return true;
}
let semantic_groups = [
&["function_definition", "method_declaration"] as &[&str],
&[
"class_declaration",
"trait_declaration",
"interface_declaration",
],
&["property_declaration", "const_declaration"],
&["namespace_definition", "use_declaration"],
];
for group in &semantic_groups {
let contains_type1 = group.contains(&type1);
let contains_type2 = group.contains(&type2);
if contains_type1 && contains_type2 {
return true;
}
}
false
}
fn get_node_type_description(&self, node_type: &str) -> &'static str {
match node_type {
"function_definition" | "method_declaration" => "function declarations",
"class_declaration" => "class declarations",
"trait_declaration" => "trait declarations",
"interface_declaration" => "interface declarations",
"property_declaration" => "property declarations",
"const_declaration" => "constant declarations",
"namespace_definition" => "namespace declarations",
"use_declaration" => "use statements",
_ => "declarations",
}
}
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
let mut imports = Vec::new();
let mut exports = Vec::new();
match node.kind() {
"namespace_use_declaration" => {
if let Ok(use_text) = node.utf8_text(contents.as_bytes()) {
if let Some(imported_items) = parse_php_use_statement(use_text) {
imports.extend(imported_items);
}
}
}
"function_definition"
| "method_declaration"
| "class_declaration"
| "namespace_definition" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "name" {
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
exports.push(name.to_string());
break;
}
}
}
}
_ => {}
}
(imports, exports)
}
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
all_files: &[String],
) -> Option<String> {
use super::resolution_utils::{resolve_relative_path, FileRegistry};
let registry = FileRegistry::new(all_files);
if import_path.starts_with("./") || import_path.starts_with("../") {
if let Some(relative_path) = resolve_relative_path(source_file, import_path) {
return self.find_matching_php_file(&relative_path, ®istry);
}
} else if import_path.ends_with(".php") || !import_path.contains("/") {
let source_path = std::path::Path::new(source_file);
if let Some(source_dir) = source_path.parent() {
let target_path = source_dir.join(import_path);
if let Some(found) = self.find_matching_php_file(&target_path, ®istry) {
return Some(found);
}
}
let file_path = PathNormalizer::normalize_separators(import_path);
return self.resolve_namespace_import(&file_path, source_file, ®istry);
} else {
let file_path = PathNormalizer::normalize_separators(import_path);
return self.resolve_namespace_import(&file_path, source_file, ®istry);
}
None
}
fn get_file_extensions(&self) -> Vec<&'static str> {
vec!["php"]
}
}
fn parse_php_use_statement(use_text: &str) -> Option<Vec<String>> {
let mut imports = Vec::new();
let cleaned = use_text.trim();
if let Some(rest) = cleaned.strip_prefix("use ") {
let rest = rest.trim_end_matches(';');
if let Some(as_pos) = rest.find(" as ") {
let class_path = &rest[..as_pos];
if let Some(class_name) = class_path.split('\\').next_back() {
imports.push(class_name.to_string());
}
} else {
if let Some(class_name) = rest.split('\\').next_back() {
imports.push(class_name.to_string());
}
}
return Some(imports);
}
None
}
impl Php {
fn find_matching_php_file(
&self,
target_path: &std::path::Path,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
let target_str = target_path.to_string_lossy().to_string();
if let Some(exact_match) = crate::utils::path::PathNormalizer::find_path_in_collection(
&target_str,
registry.get_all_files(),
) {
return Some(exact_match.to_string());
}
let with_php_ext = if target_str.ends_with(".php") {
target_str.clone()
} else {
format!("{}.php", target_str)
};
if let Some(exact_match) = crate::utils::path::PathNormalizer::find_path_in_collection(
&with_php_ext,
registry.get_all_files(),
) {
return Some(exact_match.to_string());
}
if let Ok(canonical_target) = target_path.canonicalize() {
let canonical_str = canonical_target.to_string_lossy().to_string();
for php_file in registry.get_all_files() {
if let Ok(canonical_php) = std::path::Path::new(php_file).canonicalize() {
let canonical_php_str = canonical_php.to_string_lossy().to_string();
if canonical_str == canonical_php_str {
return Some(php_file.clone());
}
}
}
}
if let Some(target_file_name) = target_path.file_name() {
if let Some(target_parent) = target_path.parent() {
for php_file in registry.get_all_files() {
let php_path = std::path::Path::new(php_file);
if let Some(php_file_name) = php_path.file_name() {
if let Some(php_parent) = php_path.parent() {
if target_file_name == php_file_name {
if let (Some(target_parent_str), Some(php_parent_str)) =
(target_parent.to_str(), php_parent.to_str())
{
if target_parent_str.ends_with(php_parent_str)
|| php_parent_str.ends_with(target_parent_str)
{
return Some(php_file.clone());
}
}
}
}
}
}
}
}
None
}
fn resolve_namespace_import(
&self,
file_path: &str,
source_file: &str,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
let source_path = std::path::Path::new(source_file);
let source_dir = source_path.parent()?;
let namespace_parts: Vec<&str> = file_path.split('/').collect();
for end_idx in (1..=namespace_parts.len()).rev() {
let partial_path = namespace_parts[0..end_idx].join("/");
let candidates = vec![
format!("src/{}.php", partial_path),
format!("lib/{}.php", partial_path),
format!("app/{}.php", partial_path),
format!("src/{}.php", partial_path.to_lowercase()),
format!("lib/{}.php", partial_path.to_lowercase()),
format!("app/{}.php", partial_path.to_lowercase()),
format!("{}.php", partial_path),
format!("{}/index.php", partial_path),
format!("vendor/{}.php", partial_path.to_lowercase()),
format!(
"vendor/{}/src/{}.php",
namespace_parts.first().unwrap_or(&"").to_lowercase(),
namespace_parts[1..].join("/")
),
];
for candidate in &candidates {
if let Some(found) = registry.find_exact_file(candidate) {
return Some(found);
}
let relative_path = source_dir.join(candidate);
if let Some(found) = self.find_matching_php_file(&relative_path, registry) {
return Some(found);
}
}
}
None
}
}