use codegraph_parser_api::{
CallRelation, ClassEntity, FunctionEntity, ImplementationRelation, ImportRelation,
InheritanceRelation, Parameter, ParserConfig, TraitEntity,
};
use tree_sitter::Node;
pub struct PhpVisitor<'a> {
pub source: &'a [u8],
#[allow(dead_code)]
pub config: ParserConfig,
pub functions: Vec<FunctionEntity>,
pub classes: Vec<ClassEntity>,
pub traits: Vec<TraitEntity>,
pub imports: Vec<ImportRelation>,
pub calls: Vec<CallRelation>,
pub inheritance: Vec<InheritanceRelation>,
pub implementations: Vec<ImplementationRelation>,
current_namespace: Option<String>,
current_class: Option<String>,
current_function: Option<String>,
}
impl<'a> PhpVisitor<'a> {
pub fn new(source: &'a [u8], config: ParserConfig) -> Self {
Self {
source,
config,
functions: Vec::new(),
classes: Vec::new(),
traits: Vec::new(),
imports: Vec::new(),
calls: Vec::new(),
inheritance: Vec::new(),
implementations: Vec::new(),
current_namespace: None,
current_class: None,
current_function: None,
}
}
fn node_text(&self, node: Node) -> String {
node.utf8_text(self.source).unwrap_or("").to_string()
}
pub fn visit_node(&mut self, node: Node) {
let should_recurse = match node.kind() {
"function_definition" => {
self.visit_function(node);
false }
"method_declaration" => {
if self.current_class.is_none() {
self.visit_method(node);
}
false
}
"class_declaration" => {
self.visit_class(node);
false }
"interface_declaration" => {
self.visit_interface(node);
false }
"trait_declaration" => {
self.visit_trait(node);
false }
"enum_declaration" => {
self.visit_enum(node);
false }
"namespace_definition" => {
self.visit_namespace(node);
true }
"namespace_use_declaration" => {
self.visit_use(node);
false
}
"anonymous_function_creation_expression" | "arrow_function" => {
false }
"function_call_expression" | "member_call_expression" | "scoped_call_expression" => {
self.visit_call_expression(node);
true }
_ => true, };
if should_recurse {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.visit_node(child);
}
}
}
fn visit_function(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "anonymous".to_string());
let visibility = "public".to_string();
let return_type = self.extract_return_type(node);
let parameters = self.extract_parameters(node);
let doc_comment = self.extract_doc_comment(node);
let qualified_name = self.qualify_name(&name);
let func = FunctionEntity {
name: qualified_name.clone(),
signature: self.extract_function_signature(node),
visibility,
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
is_async: false,
is_test: false,
is_static: false,
is_abstract: false,
parameters,
return_type,
doc_comment,
attributes: Vec::new(),
parent_class: None,
complexity: None,
};
self.functions.push(func);
let previous_function = self.current_function.take();
self.current_function = Some(qualified_name);
if let Some(body) = node.child_by_field_name("body") {
self.visit_function_body(body);
}
self.current_function = previous_function;
}
fn visit_method(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "method".to_string());
let (visibility, is_static, is_abstract) = self.extract_method_modifiers(node);
let return_type = self.extract_return_type(node);
let parameters = self.extract_parameters(node);
let doc_comment = self.extract_doc_comment(node);
let func = FunctionEntity {
name: name.clone(),
signature: self.extract_function_signature(node),
visibility,
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
is_async: false,
is_test: false,
is_static,
is_abstract,
parameters,
return_type,
doc_comment,
attributes: Vec::new(),
parent_class: self.current_class.clone(),
complexity: None,
};
self.functions.push(func);
let previous_function = self.current_function.take();
self.current_function = Some(name);
if let Some(body) = node.child_by_field_name("body") {
self.visit_function_body(body);
}
self.current_function = previous_function;
}
fn visit_class(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "Class".to_string());
let qualified_name = self.qualify_name(&name);
let is_abstract = self.has_abstract_modifier(node);
let doc_comment = self.extract_doc_comment(node);
let mut base_classes = Vec::new();
let mut implemented_traits = Vec::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "base_clause" {
let mut bc_cursor = child.walk();
for bc_child in child.named_children(&mut bc_cursor) {
if bc_child.kind() == "name" || bc_child.kind() == "qualified_name" {
let base_name = self.node_text(bc_child);
base_classes.push(base_name.clone());
self.inheritance.push(InheritanceRelation {
child: qualified_name.clone(),
parent: base_name,
order: 0,
});
}
}
} else if child.kind() == "class_interface_clause" {
self.extract_implemented_interfaces(
child,
&qualified_name,
&mut implemented_traits,
);
}
}
let class_entity = ClassEntity {
name: qualified_name.clone(),
visibility: "public".to_string(),
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
is_abstract,
is_interface: false,
base_classes,
implemented_traits,
methods: Vec::new(),
fields: Vec::new(),
doc_comment,
attributes: Vec::new(),
type_parameters: Vec::new(),
};
self.classes.push(class_entity);
let previous_class = self.current_class.take();
self.current_class = Some(qualified_name);
if let Some(body) = node.child_by_field_name("body") {
self.visit_class_body(body);
}
self.current_class = previous_class;
}
fn visit_class_body(&mut self, node: Node) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"method_declaration" => self.visit_method(child),
"use_declaration" => self.visit_trait_use(child),
_ => {}
}
}
}
fn visit_trait_use(&mut self, node: Node) {
if let Some(class_name) = &self.current_class.clone() {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "name" || child.kind() == "qualified_name" {
let trait_name = self.node_text(child);
self.implementations.push(ImplementationRelation {
implementor: class_name.clone(),
trait_name,
});
}
}
}
}
fn visit_interface(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "Interface".to_string());
let qualified_name = self.qualify_name(&name);
let doc_comment = self.extract_doc_comment(node);
let mut parent_traits = Vec::new();
if let Some(base_clause) = node.child_by_field_name("base_clause") {
for child in base_clause.children(&mut base_clause.walk()) {
if child.kind() == "name" || child.kind() == "qualified_name" {
parent_traits.push(self.node_text(child));
}
}
}
let required_methods = self.extract_interface_methods(node);
let interface_entity = TraitEntity {
name: qualified_name.clone(),
visibility: "public".to_string(),
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
required_methods,
parent_traits,
doc_comment,
attributes: Vec::new(),
};
self.traits.push(interface_entity);
let previous_class = self.current_class.take();
self.current_class = Some(qualified_name);
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
self.visit_method(child);
}
}
}
self.current_class = previous_class;
}
fn visit_trait(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "Trait".to_string());
let qualified_name = self.qualify_name(&name);
let doc_comment = self.extract_doc_comment(node);
let trait_entity = TraitEntity {
name: qualified_name.clone(),
visibility: "public".to_string(),
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
required_methods: Vec::new(),
parent_traits: Vec::new(),
doc_comment,
attributes: Vec::new(),
};
self.traits.push(trait_entity);
let previous_class = self.current_class.take();
self.current_class = Some(qualified_name);
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
self.visit_method(child);
}
}
}
self.current_class = previous_class;
}
fn visit_enum(&mut self, node: Node) {
let name = node
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "Enum".to_string());
let qualified_name = self.qualify_name(&name);
let doc_comment = self.extract_doc_comment(node);
let enum_entity = ClassEntity {
name: qualified_name.clone(),
visibility: "public".to_string(),
line_start: node.start_position().row + 1,
line_end: node.end_position().row + 1,
is_abstract: false,
is_interface: false,
base_classes: Vec::new(),
implemented_traits: Vec::new(),
methods: Vec::new(),
fields: Vec::new(),
doc_comment,
attributes: vec!["enum".to_string()],
type_parameters: Vec::new(),
};
self.classes.push(enum_entity);
let previous_class = self.current_class.take();
self.current_class = Some(qualified_name);
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
self.visit_method(child);
}
}
}
self.current_class = previous_class;
}
fn visit_namespace(&mut self, node: Node) {
let name = node.child_by_field_name("name").map(|n| self.node_text(n));
self.current_namespace = name;
}
fn visit_use(&mut self, node: Node) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "namespace_use_clause" {
self.extract_use_clause(child);
}
}
}
fn extract_use_clause(&mut self, node: Node) {
let mut cursor = node.walk();
let mut imported = String::new();
let mut alias = None;
for child in node.children(&mut cursor) {
match child.kind() {
"qualified_name" | "name" => {
imported = self.node_text(child);
}
"namespace_aliasing_clause" => {
let mut alias_cursor = child.walk();
for alias_child in child.children(&mut alias_cursor) {
if alias_child.kind() == "name" {
alias = Some(self.node_text(alias_child));
}
}
}
_ => {}
}
}
if !imported.is_empty() {
let import = ImportRelation {
importer: self
.current_namespace
.clone()
.unwrap_or_else(|| "global".to_string()),
imported,
symbols: Vec::new(),
is_wildcard: false,
alias,
};
self.imports.push(import);
}
}
fn visit_call_expression(&mut self, node: Node) {
let caller = match &self.current_function {
Some(name) => name.clone(),
None => return,
};
let callee = self.extract_callee_name(node);
if callee.is_empty() || callee == "$this" || callee == "this" {
return;
}
let call_site_line = node.start_position().row + 1;
let call = CallRelation {
caller,
callee,
call_site_line,
is_direct: true,
};
self.calls.push(call);
}
fn extract_callee_name(&self, node: Node) -> String {
match node.kind() {
"function_call_expression" => {
if let Some(function_node) = node.child_by_field_name("function") {
match function_node.kind() {
"name" => self.node_text(function_node),
"qualified_name" => {
self.node_text(function_node)
}
"variable_name" => {
self.node_text(function_node)
}
_ => self.node_text(function_node),
}
} else {
String::new()
}
}
"member_call_expression" => {
if let Some(name_node) = node.child_by_field_name("name") {
self.node_text(name_node)
} else {
String::new()
}
}
"scoped_call_expression" => {
if let Some(name_node) = node.child_by_field_name("name") {
let scope = node
.child_by_field_name("scope")
.map(|n| self.node_text(n))
.unwrap_or_default();
let method = self.node_text(name_node);
if scope.is_empty() || scope == "self" || scope == "static" {
method
} else {
format!("{}::{}", scope, method)
}
} else {
String::new()
}
}
_ => String::new(),
}
}
fn visit_function_body(&mut self, node: Node) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_call_expression"
| "member_call_expression"
| "scoped_call_expression" => {
self.visit_call_expression(child);
self.visit_function_body(child);
}
_ => {
self.visit_function_body(child);
}
}
}
}
fn extract_implemented_interfaces(
&mut self,
node: Node,
class_name: &str,
implemented_traits: &mut Vec<String>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "name" || child.kind() == "qualified_name" {
let interface_name = self.node_text(child);
implemented_traits.push(interface_name.clone());
self.implementations.push(ImplementationRelation {
implementor: class_name.to_string(),
trait_name: interface_name,
});
}
}
}
fn extract_interface_methods(&self, node: Node) -> Vec<FunctionEntity> {
let mut methods = Vec::new();
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() == "method_declaration" {
let name = child
.child_by_field_name("name")
.map(|n| self.node_text(n))
.unwrap_or_else(|| "method".to_string());
let (visibility, is_static, _is_abstract) =
self.extract_method_modifiers(child);
let return_type = self.extract_return_type(child);
let parameters = self.extract_parameters(child);
let func = FunctionEntity {
name,
signature: self.extract_function_signature(child),
visibility,
line_start: child.start_position().row + 1,
line_end: child.end_position().row + 1,
is_async: false,
is_test: false,
is_static,
is_abstract: true, parameters,
return_type,
doc_comment: None,
attributes: Vec::new(),
parent_class: None,
complexity: None,
};
methods.push(func);
}
}
}
methods
}
fn extract_return_type(&self, node: Node) -> Option<String> {
node.child_by_field_name("return_type")
.map(|n| self.node_text(n))
}
fn extract_parameters(&self, node: Node) -> Vec<Parameter> {
let mut params = Vec::new();
if let Some(params_node) = node.child_by_field_name("parameters") {
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
if child.kind() == "simple_parameter" || child.kind() == "variadic_parameter" {
if let Some(name_node) = child.child_by_field_name("name") {
let name = self.node_text(name_node);
let is_variadic = child.kind() == "variadic_parameter";
let type_annotation =
child.child_by_field_name("type").map(|n| self.node_text(n));
let default_value = child
.child_by_field_name("default_value")
.map(|n| self.node_text(n));
let mut param = Parameter::new(name);
if let Some(t) = type_annotation {
param = param.with_type(t);
}
if let Some(d) = default_value {
param = param.with_default(d);
}
if is_variadic {
param = param.variadic();
}
params.push(param);
}
}
}
}
params
}
fn extract_function_signature(&self, node: Node) -> String {
self.node_text(node)
.lines()
.next()
.unwrap_or("")
.to_string()
}
fn extract_method_modifiers(&self, node: Node) -> (String, bool, bool) {
let mut visibility = "public".to_string();
let mut is_static = false;
let mut is_abstract = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"visibility_modifier" => {
visibility = self.node_text(child);
}
"static_modifier" => {
is_static = true;
}
"abstract_modifier" => {
is_abstract = true;
}
_ => {}
}
}
(visibility, is_static, is_abstract)
}
fn has_abstract_modifier(&self, node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "abstract_modifier" {
return true;
}
}
false
}
fn extract_doc_comment(&self, node: Node) -> Option<String> {
if let Some(prev) = node.prev_sibling() {
if prev.kind() == "comment" {
let comment = self.node_text(prev);
if comment.starts_with("/**") {
return Some(comment);
}
}
}
None
}
fn qualify_name(&self, name: &str) -> String {
if let Some(ref ns) = self.current_namespace {
format!("{}\\{}", ns, name)
} else {
name.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_and_visit(source: &[u8]) -> PhpVisitor<'_> {
use tree_sitter::Parser;
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_php::language_php())
.unwrap();
let tree = parser.parse(source, None).unwrap();
let mut visitor = PhpVisitor::new(source, ParserConfig::default());
visitor.visit_node(tree.root_node());
visitor
}
#[test]
fn test_visitor_basics() {
let visitor = PhpVisitor::new(b"<?php", ParserConfig::default());
assert_eq!(visitor.functions.len(), 0);
assert_eq!(visitor.classes.len(), 0);
assert_eq!(visitor.traits.len(), 0);
}
#[test]
fn test_visitor_function_extraction() {
let source = b"<?php\nfunction greet(string $name): string { return \"Hello\"; }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.functions.len(), 1);
assert_eq!(visitor.functions[0].name, "greet");
}
#[test]
fn test_visitor_class_extraction() {
let source = b"<?php\nclass Person { public string $name; }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.classes[0].name, "Person");
}
#[test]
fn test_visitor_interface_extraction() {
let source = b"<?php\ninterface Reader { public function read(): string; }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.traits.len(), 1);
assert_eq!(visitor.traits[0].name, "Reader");
}
#[test]
fn test_visitor_trait_extraction() {
let source = b"<?php\ntrait Loggable { public function log(string $msg): void {} }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.traits.len(), 1);
assert_eq!(visitor.traits[0].name, "Loggable");
}
#[test]
fn test_visitor_method_extraction() {
let source = b"<?php\nclass Calculator { public function add(int $a, int $b): int { return $a + $b; } }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.functions.len(), 1);
assert_eq!(visitor.functions[0].name, "add");
assert_eq!(
visitor.functions[0].parent_class,
Some("Calculator".to_string())
);
}
#[test]
fn test_visitor_use_extraction() {
let source = b"<?php\nuse App\\Models\\User;\nuse App\\Services\\AuthService;";
let visitor = parse_and_visit(source);
assert_eq!(visitor.imports.len(), 2);
assert_eq!(visitor.imports[0].imported, "App\\Models\\User");
assert_eq!(visitor.imports[1].imported, "App\\Services\\AuthService");
}
#[test]
fn test_visitor_use_with_alias() {
let source = b"<?php\nuse App\\Models\\User as UserModel;";
let visitor = parse_and_visit(source);
assert_eq!(visitor.imports.len(), 1);
assert_eq!(visitor.imports[0].imported, "App\\Models\\User");
assert_eq!(visitor.imports[0].alias, Some("UserModel".to_string()));
}
#[test]
fn test_visitor_inheritance() {
let source = b"<?php\nclass Animal {}\nclass Dog extends Animal {}";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 2);
assert_eq!(visitor.inheritance.len(), 1);
assert_eq!(visitor.inheritance[0].child, "Dog");
assert_eq!(visitor.inheritance[0].parent, "Animal");
}
#[test]
fn test_visitor_implements() {
let source =
b"<?php\ninterface Shape { public function area(): float; }\nclass Circle implements Shape { public function area(): float { return 0.0; } }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.traits.len(), 1);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.implementations.len(), 1);
assert_eq!(visitor.implementations[0].implementor, "Circle");
assert_eq!(visitor.implementations[0].trait_name, "Shape");
}
#[test]
fn test_visitor_enum() {
let source = b"<?php\nenum Status: string { case Pending = 'pending'; }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.classes[0].name, "Status");
assert!(visitor.classes[0].attributes.contains(&"enum".to_string()));
}
#[test]
fn test_visitor_namespace() {
let source = b"<?php\nnamespace App\\Controllers;\nclass HomeController {}";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.classes[0].name, "App\\Controllers\\HomeController");
}
#[test]
fn test_visitor_abstract_class() {
let source =
b"<?php\nabstract class BaseController { abstract public function handle(): void; }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert!(visitor.classes[0].is_abstract);
}
#[test]
fn test_visitor_static_method() {
let source =
b"<?php\nclass Helper { public static function format(string $s): string { return $s; } }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.functions.len(), 1);
assert!(visitor.functions[0].is_static);
}
#[test]
fn test_visitor_visibility_modifiers() {
let source = b"<?php\nclass Foo { private function bar(): void {} protected function baz(): void {} public function qux(): void {} }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.functions.len(), 3);
assert_eq!(visitor.functions[0].visibility, "private");
assert_eq!(visitor.functions[1].visibility, "protected");
assert_eq!(visitor.functions[2].visibility, "public");
}
#[test]
fn test_visitor_trait_use() {
let source =
b"<?php\ntrait Loggable {}\nclass Logger { use Loggable; public function log(): void {} }";
let visitor = parse_and_visit(source);
assert_eq!(visitor.traits.len(), 1);
assert_eq!(visitor.classes.len(), 1);
assert!(visitor
.implementations
.iter()
.any(|i| i.implementor == "Logger" && i.trait_name == "Loggable"));
}
#[test]
fn test_visitor_function_call_extraction() {
let source = b"<?php
function caller() {
helper();
another_func();
}
function helper() {}
function another_func() {}
";
let visitor = parse_and_visit(source);
assert_eq!(visitor.functions.len(), 3);
assert_eq!(visitor.calls.len(), 2);
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "caller" && c.callee == "helper"));
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "caller" && c.callee == "another_func"));
}
#[test]
fn test_visitor_method_call_extraction() {
let source = b"<?php
class MyClass {
public function caller() {
$this->helper();
$this->process();
}
public function helper() {}
public function process() {}
}
";
let visitor = parse_and_visit(source);
assert_eq!(visitor.classes.len(), 1);
assert_eq!(visitor.functions.len(), 3);
assert_eq!(visitor.calls.len(), 2);
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "caller" && c.callee == "helper"));
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "caller" && c.callee == "process"));
}
#[test]
fn test_visitor_static_call_extraction() {
let source = b"<?php
class Calculator {
public function calculate() {
self::helper();
Helper::format();
}
public static function helper() {}
}
";
let visitor = parse_and_visit(source);
assert_eq!(visitor.calls.len(), 2);
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "calculate" && c.callee == "helper"));
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "calculate" && c.callee == "Helper::format"));
}
#[test]
fn test_visitor_nested_calls() {
let source = b"<?php
function outer() {
process(helper());
}
function helper() {}
function process($x) {}
";
let visitor = parse_and_visit(source);
assert_eq!(visitor.calls.len(), 2);
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "outer" && c.callee == "process"));
assert!(visitor
.calls
.iter()
.any(|c| c.caller == "outer" && c.callee == "helper"));
}
#[test]
fn test_visitor_call_line_numbers() {
let source = b"<?php
function caller() {
helper();
}
function helper() {}
";
let visitor = parse_and_visit(source);
assert_eq!(visitor.calls.len(), 1);
assert_eq!(visitor.calls[0].caller, "caller");
assert_eq!(visitor.calls[0].callee, "helper");
assert_eq!(visitor.calls[0].call_site_line, 3);
assert!(visitor.calls[0].is_direct);
}
}