use std::collections::HashMap;
use std::path::Path;
use sqry_core::graph::unified::build::helper::CalleeKindHint;
use sqry_core::graph::unified::edge::kind::{FfiConvention, TypeOfContext};
use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
use sqry_core::graph::{
GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
};
use tree_sitter::{Node, Tree};
use super::phpdoc_parser::{extract_phpdoc_comment, parse_phpdoc_tags};
use super::type_extractor::{canonical_type_string, extract_type_names};
const DEFAULT_MAX_SCOPE_DEPTH: usize = 5;
#[derive(Debug)]
pub struct PhpGraphBuilder {
pub max_scope_depth: usize,
}
impl Default for PhpGraphBuilder {
fn default() -> Self {
Self {
max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
}
}
}
impl GraphBuilder for PhpGraphBuilder {
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::Php);
let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
GraphBuilderError::ParseError {
span: Span::default(),
reason: e,
}
})?;
let mut node_map = HashMap::new();
for context in ast_graph.contexts() {
let qualified_name = &context.qualified_name;
let span = Span::from_bytes(context.span.0, context.span.1);
let node_id = match &context.kind {
ContextKind::Function { is_async } => helper.add_function_with_signature(
qualified_name,
Some(span),
*is_async,
false, None, context.return_type.as_deref(),
),
ContextKind::Method {
is_async,
is_static,
visibility: _,
} => {
helper.add_method_with_signature(
qualified_name,
Some(span),
*is_async,
*is_static,
None, context.return_type.as_deref(),
)
}
ContextKind::Class => helper.add_class(qualified_name, Some(span)),
};
node_map.insert(qualified_name.clone(), node_id);
}
let root = tree.root_node();
walk_tree_for_edges(root, content, &ast_graph, &mut helper, &mut node_map)?;
process_oop_relationships(root, content, &mut helper, &mut node_map);
process_exports(root, content, &mut helper, &mut node_map);
process_phpdoc_annotations(root, content, &mut helper)?;
Ok(())
}
fn language(&self) -> Language {
Language::Php
}
fn detect_cross_language_edges(
&self,
_snapshot: &GraphSnapshot,
) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
Ok(vec![])
}
}
#[derive(Debug, Clone)]
enum ContextKind {
Function {
is_async: bool,
},
Method {
is_async: bool,
is_static: bool,
#[allow(dead_code)] visibility: Option<String>,
},
Class,
}
#[derive(Debug, Clone)]
struct CallContext {
qualified_name: String,
span: (usize, usize),
kind: ContextKind,
class_name: Option<String>,
return_type: Option<String>,
}
struct ASTGraph {
contexts: Vec<CallContext>,
node_to_context: HashMap<usize, usize>,
}
impl ASTGraph {
fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
let mut contexts = Vec::new();
let mut node_to_context = HashMap::new();
let mut scope_stack: Vec<String> = Vec::new();
let mut class_stack: Vec<String> = Vec::new();
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.map_err(|e| format!("Failed to load recursion limits: {e}"))?;
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.map_err(|e| format!("Failed to create recursion guard: {e}"))?;
let mut walk_ctx = WalkContext {
contexts: &mut contexts,
node_to_context: &mut node_to_context,
scope_stack: &mut scope_stack,
class_stack: &mut class_stack,
max_depth,
};
walk_ast(tree.root_node(), content, &mut walk_ctx, &mut guard)?;
Ok(Self {
contexts,
node_to_context,
})
}
fn contexts(&self) -> &[CallContext] {
&self.contexts
}
fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
self.node_to_context
.get(&node_id)
.and_then(|idx| self.contexts.get(*idx))
}
}
#[allow(
clippy::too_many_lines,
reason = "PHP namespace and scope handling requires a large, unified traversal."
)]
struct WalkContext<'a> {
contexts: &'a mut Vec<CallContext>,
node_to_context: &'a mut HashMap<usize, usize>,
scope_stack: &'a mut Vec<String>,
class_stack: &'a mut Vec<String>,
max_depth: usize,
}
#[allow(clippy::too_many_lines)]
fn walk_ast(
node: Node,
content: &[u8],
ctx: &mut WalkContext,
guard: &mut sqry_core::query::security::RecursionGuard,
) -> Result<(), String> {
guard
.enter()
.map_err(|e| format!("Recursion limit exceeded: {e}"))?;
if ctx.scope_stack.len() > ctx.max_depth {
guard.exit();
return Ok(());
}
match node.kind() {
"program" => {
let mut active_namespace_parts: Vec<String> = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "namespace_definition" {
let has_body = child
.children(&mut child.walk())
.any(|c| matches!(c.kind(), "compound_statement" | "declaration_list"));
let ns_name = child
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
if has_body {
for _ in 0..active_namespace_parts.len() {
ctx.scope_stack.pop();
}
active_namespace_parts.clear();
let ns_parts: Vec<String> = if ns_name.is_empty() {
Vec::new()
} else {
ns_name.split('\\').map(ToString::to_string).collect()
};
for part in &ns_parts {
ctx.scope_stack.push(part.clone());
}
for ns_child in child.children(&mut child.walk()) {
if matches!(ns_child.kind(), "compound_statement" | "declaration_list")
{
for body_child in ns_child.children(&mut ns_child.walk()) {
walk_ast(body_child, content, ctx, guard)?;
}
}
}
for _ in 0..ns_parts.len() {
ctx.scope_stack.pop();
}
} else {
for _ in 0..active_namespace_parts.len() {
ctx.scope_stack.pop();
}
active_namespace_parts = if ns_name.is_empty() {
Vec::new()
} else {
ns_name.split('\\').map(ToString::to_string).collect()
};
for part in &active_namespace_parts {
ctx.scope_stack.push(part.clone());
}
}
} else {
walk_ast(child, content, ctx, guard)?;
}
}
for _ in 0..active_namespace_parts.len() {
ctx.scope_stack.pop();
}
guard.exit();
return Ok(());
}
"namespace_definition" => {
let namespace_name = node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
let namespace_parts: Vec<String> = if namespace_name.is_empty() {
Vec::new()
} else {
namespace_name
.split('\\')
.map(ToString::to_string)
.collect()
};
let parts_count = namespace_parts.len();
for part in &namespace_parts {
ctx.scope_stack.push(part.clone());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "compound_statement" | "declaration_list") {
let mut body_cursor = child.walk();
for body_child in child.children(&mut body_cursor) {
walk_ast(body_child, content, ctx, guard)?;
}
}
}
for _ in 0..parts_count {
ctx.scope_stack.pop();
}
}
"class_declaration" => {
let name_node = node
.child_by_field_name("name")
.ok_or_else(|| "class_declaration missing name".to_string())?;
let class_name = name_node
.utf8_text(content)
.map_err(|_| "failed to read class name".to_string())?;
let qualified_class = if ctx.scope_stack.is_empty() {
class_name.to_string()
} else {
format!("{}\\{}", ctx.scope_stack.join("\\"), class_name)
};
ctx.class_stack.push(qualified_class.clone());
ctx.scope_stack.push(class_name.to_string());
let _context_idx = ctx.contexts.len();
ctx.contexts.push(CallContext {
qualified_name: qualified_class.clone(),
span: (node.start_byte(), node.end_byte()),
kind: ContextKind::Class,
class_name: Some(qualified_class),
return_type: None, });
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "declaration_list" {
let mut body_cursor = child.walk();
for body_child in child.children(&mut body_cursor) {
walk_ast(body_child, content, ctx, guard)?;
}
}
}
ctx.class_stack.pop();
ctx.scope_stack.pop();
}
"function_definition" | "method_declaration" => {
let name_node = node
.child_by_field_name("name")
.ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
let func_name = name_node
.utf8_text(content)
.map_err(|_| "failed to read function name".to_string())?;
let is_async = false;
let is_static = node
.children(&mut node.walk())
.any(|child| child.kind() == "static_modifier");
let visibility = extract_visibility(&node, content);
let return_type = extract_return_type(&node, content);
let is_method = !ctx.class_stack.is_empty();
let class_name = ctx.class_stack.last().cloned();
let qualified_func = if is_method {
if let Some(ref class) = class_name {
format!("{class}::{func_name}")
} else {
func_name.to_string()
}
} else {
if ctx.scope_stack.is_empty() {
func_name.to_string()
} else {
format!("{}\\{}", ctx.scope_stack.join("\\"), func_name)
}
};
let kind = if is_method {
ContextKind::Method {
is_async,
is_static,
visibility: visibility.clone(),
}
} else {
ContextKind::Function { is_async }
};
let context_idx = ctx.contexts.len();
ctx.contexts.push(CallContext {
qualified_name: qualified_func.clone(),
span: (node.start_byte(), node.end_byte()),
kind,
class_name,
return_type,
});
if let Some(body) = node.child_by_field_name("body") {
associate_descendants(body, context_idx, ctx.node_to_context);
}
ctx.scope_stack.push(func_name.to_string());
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
walk_ast(child, content, ctx, guard)?;
}
}
ctx.scope_stack.pop();
}
_ => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_ast(child, content, ctx, guard)?;
}
}
}
guard.exit();
Ok(())
}
fn associate_descendants(
node: Node,
context_idx: usize,
node_to_context: &mut HashMap<usize, usize>,
) {
node_to_context.insert(node.id(), context_idx);
let mut stack = vec![node];
while let Some(current) = stack.pop() {
node_to_context.insert(current.id(), context_idx);
let mut cursor = current.walk();
for child in current.children(&mut cursor) {
stack.push(child);
}
}
}
#[allow(clippy::only_used_in_recursion)]
fn walk_tree_for_edges(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) -> GraphResult<()> {
match node.kind() {
"function_call_expression" => {
process_function_call(node, content, ast_graph, helper, node_map);
}
"member_call_expression" | "nullsafe_member_call_expression" => {
process_member_call(node, content, ast_graph, helper, node_map);
}
"scoped_call_expression" => {
process_static_call(node, content, ast_graph, helper, node_map);
}
"namespace_use_declaration" => {
process_namespace_use(node, content, helper);
}
"expression_statement" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"require_expression"
| "require_once_expression"
| "include_expression"
| "include_once_expression" => {
process_file_include(child, content, helper);
}
_ => {}
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_tree_for_edges(child, content, ast_graph, helper, node_map)?;
}
Ok(())
}
fn process_function_call(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(function_node) = node.child_by_field_name("function") else {
return;
};
let Ok(callee_name) = function_node.utf8_text(content) else {
return;
};
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let source_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let call_span = span_from_node(node);
let target_id = *node_map
.entry(callee_name.to_string())
.or_insert_with(|| helper.ensure_callee(callee_name, call_span, CalleeKindHint::Function));
let argument_count = count_call_arguments(node);
helper.add_call_edge_full_with_span(
source_id,
target_id,
argument_count,
false,
vec![call_span],
);
}
fn process_member_call(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(method_node) = node.child_by_field_name("name") else {
return;
};
let Ok(method_name) = method_node.utf8_text(content) else {
return;
};
if let Some(object_node) = node.child_by_field_name("object")
&& is_php_ffi_call(object_node, content)
{
process_ffi_member_call(node, method_name, ast_graph, helper, node_map);
return;
}
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let callee_qualified = if let Some(class_name) = &call_context.class_name {
format!("{class_name}::{method_name}")
} else {
method_name.to_string()
};
let source_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let call_span = span_from_node(node);
let target_id = *node_map.entry(callee_qualified.clone()).or_insert_with(|| {
helper.ensure_callee(&callee_qualified, call_span, CalleeKindHint::Method)
});
let argument_count = count_call_arguments(node);
helper.add_call_edge_full_with_span(
source_id,
target_id,
argument_count,
false,
vec![call_span],
);
}
fn process_static_call(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(scope_node) = node.child_by_field_name("scope") else {
return;
};
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(class_name) = scope_node.utf8_text(content) else {
return;
};
let Ok(method_name) = name_node.utf8_text(content) else {
return;
};
if is_ffi_static_call(class_name, method_name) {
process_ffi_static_call(node, method_name, ast_graph, helper, node_map, content);
return;
}
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let callee_qualified = format!("{class_name}::{method_name}");
let source_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let call_span = span_from_node(node);
let target_id = *node_map.entry(callee_qualified.clone()).or_insert_with(|| {
helper.ensure_callee(&callee_qualified, call_span, CalleeKindHint::Method)
});
let argument_count = count_call_arguments(node);
helper.add_call_edge_full_with_span(
source_id,
target_id,
argument_count,
false,
vec![call_span],
);
}
fn process_namespace_use(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
let file_path = helper.file_path().to_string();
let importer_id = helper.add_module(&file_path, None);
let mut prefix = String::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "namespace_name"
&& let Ok(ns) = child.utf8_text(content)
{
prefix = ns.trim().to_string();
break;
}
}
cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"namespace_use_clause" => {
process_use_clause(child, content, helper, importer_id);
}
"namespace_use_group" => {
process_use_group(child, content, helper, importer_id, &prefix);
}
_ => {}
}
}
}
fn process_use_clause(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
import_source_id: NodeId,
) {
process_use_clause_with_prefix(node, content, helper, import_source_id, None);
}
fn process_use_clause_with_prefix(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
import_source_id: NodeId,
prefix: Option<&str>,
) {
let mut qualified_name = None;
let mut alias = None;
let mut found_as = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"qualified_name" => {
if let Ok(name) = child.utf8_text(content) {
qualified_name = Some(name.trim().to_string());
}
}
"namespace_name" => {
if qualified_name.is_none()
&& let Ok(name) = child.utf8_text(content)
{
qualified_name = Some(name.trim().to_string());
}
}
"name" => {
if found_as {
if let Ok(alias_text) = child.utf8_text(content) {
alias = Some(alias_text.trim().to_string());
}
} else if qualified_name.is_none() {
if let Ok(name) = child.utf8_text(content) {
qualified_name = Some(name.trim().to_string());
}
}
}
"as" => {
found_as = true;
}
_ => {}
}
}
if let Some(name) = qualified_name
&& !name.is_empty()
{
let full_name = if let Some(pfx) = prefix {
format!("{pfx}\\{name}")
} else {
name
};
let span = span_from_node(node);
let import_node_id = helper.add_import(&full_name, Some(span));
if let Some(alias_str) = alias {
helper.add_import_edge_full(import_source_id, import_node_id, Some(&alias_str), false);
} else {
helper.add_import_edge(import_source_id, import_node_id);
}
}
}
fn process_use_group(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
import_source_id: NodeId,
prefix: &str,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "namespace_use_clause" {
process_use_clause_with_prefix(child, content, helper, import_source_id, Some(prefix));
}
}
}
fn process_file_include(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
let file_path = helper.file_path().to_string();
let import_source_id = helper.add_module(&file_path, None);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "string"
|| child.kind() == "encapsed_string"
|| child.kind() == "binary_expression"
{
if let Ok(path_text) = child.utf8_text(content) {
let cleaned_path = path_text
.trim()
.trim_start_matches(['\'', '"'])
.trim_end_matches(['\'', '"'])
.to_string();
if !cleaned_path.is_empty() {
let span = span_from_node(node);
let import_node_id = helper.add_import(&cleaned_path, Some(span));
helper.add_import_edge(import_source_id, import_node_id);
}
}
break;
}
}
}
fn process_oop_relationships(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let kind = node.kind();
if kind == "class_declaration" {
process_class_oop(node, content, helper, node_map);
} else if kind == "interface_declaration" {
process_interface_inheritance(node, content, helper, node_map);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
process_oop_relationships(child, content, helper, node_map);
}
}
fn process_class_oop(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(class_name) = name_node.utf8_text(content) else {
return;
};
let class_name = class_name.trim();
let span = span_from_node(node);
let class_id = *node_map
.entry(class_name.to_string())
.or_insert_with(|| helper.add_class(class_name, Some(span)));
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"base_clause" => {
process_extends_clause(child, content, helper, node_map, class_id);
}
"class_interface_clause" => {
process_implements_clause(child, content, helper, node_map, class_id);
}
"declaration_list" => {
process_class_body_traits(child, content, helper, node_map, class_id);
}
_ => {}
}
}
}
fn process_extends_clause(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
class_id: NodeId,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "name"
|| child.kind() == "qualified_name"
|| child.kind() == "namespace_name"
{
if let Ok(parent_name) = child.utf8_text(content) {
let parent_name = parent_name.trim();
if !parent_name.is_empty() {
let span = span_from_node(child);
let parent_id = *node_map
.entry(parent_name.to_string())
.or_insert_with(|| helper.add_class(parent_name, Some(span)));
helper.add_inherits_edge(class_id, parent_id);
}
}
break;
}
}
}
fn process_implements_clause(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
class_id: NodeId,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "name" | "qualified_name" | "namespace_name")
&& let Ok(interface_name) = child.utf8_text(content)
{
let interface_name = interface_name.trim();
if !interface_name.is_empty() {
let span = span_from_node(child);
let interface_id = *node_map
.entry(interface_name.to_string())
.or_insert_with(|| helper.add_interface(interface_name, Some(span)));
helper.add_implements_edge(class_id, interface_id);
}
}
}
}
fn process_class_body_traits(
declaration_list: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
class_id: NodeId,
) {
let mut cursor = declaration_list.walk();
for child in declaration_list.children(&mut cursor) {
if child.kind() == "use_declaration" {
process_trait_use(child, content, helper, node_map, class_id);
}
}
}
fn process_trait_use(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
class_id: NodeId,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "name" | "qualified_name" | "namespace_name")
&& let Ok(trait_name) = child.utf8_text(content)
{
let trait_name = trait_name.trim();
if !trait_name.is_empty() {
let span = span_from_node(child);
let trait_id = *node_map.entry(trait_name.to_string()).or_insert_with(|| {
helper.add_node(
trait_name,
Some(span),
sqry_core::graph::unified::node::NodeKind::Trait,
)
});
helper.add_implements_edge(class_id, trait_id);
}
}
}
}
fn process_interface_inheritance(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(interface_name) = name_node.utf8_text(content) else {
return;
};
let interface_name = interface_name.trim();
let span = span_from_node(node);
let interface_id = *node_map
.entry(interface_name.to_string())
.or_insert_with(|| helper.add_interface(interface_name, Some(span)));
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "base_clause" {
let mut base_cursor = child.walk();
for base_child in child.children(&mut base_cursor) {
if matches!(
base_child.kind(),
"name" | "qualified_name" | "namespace_name"
) && let Ok(parent_name) = base_child.utf8_text(content)
{
let parent_name = parent_name.trim();
if !parent_name.is_empty() {
let span = span_from_node(base_child);
let parent_id = *node_map
.entry(parent_name.to_string())
.or_insert_with(|| helper.add_interface(parent_name, Some(span)));
helper.add_inherits_edge(interface_id, parent_id);
}
}
}
}
}
}
fn process_exports(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let file_path = helper.file_path().to_string();
let module_id = helper.add_module(&file_path, None);
if node.kind() != "program" {
return;
}
let mut active_namespace = String::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
process_top_level_for_export(
child,
content,
helper,
node_map,
module_id,
&mut active_namespace,
);
}
}
fn process_top_level_for_export(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
module_id: NodeId,
active_namespace: &mut String,
) {
match node.kind() {
"namespace_definition" => {
let ns_name = node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
let has_body = node
.children(&mut node.walk())
.any(|c| matches!(c.kind(), "compound_statement" | "declaration_list"));
if has_body {
active_namespace.clear();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "compound_statement" | "declaration_list") {
let mut body_cursor = child.walk();
for body_child in child.children(&mut body_cursor) {
export_declaration_if_exportable(
body_child, content, helper, node_map, module_id, &ns_name,
);
}
}
}
} else {
*active_namespace = ns_name;
}
}
"class_declaration"
| "interface_declaration"
| "trait_declaration"
| "enum_declaration"
| "function_definition" => {
export_declaration_if_exportable(
node,
content,
helper,
node_map,
module_id,
active_namespace,
);
}
_ => {
}
}
}
fn lookup_or_create_node<F>(
node_map: &mut HashMap<String, NodeId>,
qualified_name: &str,
simple_name: &str,
namespace_prefix: &str,
create_fn: F,
) -> NodeId
where
F: FnOnce() -> NodeId,
{
if let Some(&id) = node_map.get(qualified_name) {
return id;
}
if namespace_prefix.is_empty()
&& let Some(&id) = node_map.get(simple_name)
{
return id;
}
let id = create_fn();
node_map.insert(qualified_name.to_string(), id);
id
}
#[allow(clippy::too_many_lines)] fn export_declaration_if_exportable(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
module_id: NodeId,
namespace_prefix: &str,
) {
match node.kind() {
"class_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(class_name) = name_node.utf8_text(content)
{
let simple_name = class_name.trim().to_string();
let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
let span = span_from_node(node);
let class_id = lookup_or_create_node(
node_map,
&qualified_name,
&simple_name,
namespace_prefix,
|| helper.add_class(&qualified_name, Some(span)),
);
helper.add_export_edge(module_id, class_id);
export_public_methods_from_class(
node,
content,
helper,
node_map,
module_id,
&qualified_name,
);
}
}
"interface_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(interface_name) = name_node.utf8_text(content)
{
let simple_name = interface_name.trim().to_string();
let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
let span = span_from_node(node);
let interface_id = lookup_or_create_node(
node_map,
&qualified_name,
&simple_name,
namespace_prefix,
|| helper.add_interface(&qualified_name, Some(span)),
);
helper.add_export_edge(module_id, interface_id);
}
}
"trait_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(trait_name) = name_node.utf8_text(content)
{
let simple_name = trait_name.trim().to_string();
let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
let span = span_from_node(node);
let trait_id = lookup_or_create_node(
node_map,
&qualified_name,
&simple_name,
namespace_prefix,
|| {
helper.add_node(
&qualified_name,
Some(span),
sqry_core::graph::unified::node::NodeKind::Trait,
)
},
);
helper.add_export_edge(module_id, trait_id);
}
}
"enum_declaration" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(enum_name) = name_node.utf8_text(content)
{
let simple_name = enum_name.trim().to_string();
let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
let span = span_from_node(node);
let enum_id = lookup_or_create_node(
node_map,
&qualified_name,
&simple_name,
namespace_prefix,
|| helper.add_enum(&qualified_name, Some(span)),
);
helper.add_export_edge(module_id, enum_id);
}
}
"function_definition" => {
if let Some(name_node) = node.child_by_field_name("name")
&& let Ok(func_name) = name_node.utf8_text(content)
{
let simple_name = func_name.trim().to_string();
let qualified_name = build_qualified_name(namespace_prefix, &simple_name);
let span = span_from_node(node);
let func_id = lookup_or_create_node(
node_map,
&qualified_name,
&simple_name,
namespace_prefix,
|| helper.add_function(&qualified_name, Some(span), false, false),
);
helper.add_export_edge(module_id, func_id);
}
}
_ => {
}
}
}
fn build_qualified_name(namespace_prefix: &str, name: &str) -> String {
if namespace_prefix.is_empty() {
name.to_string()
} else {
format!("{namespace_prefix}\\{name}")
}
}
fn span_from_node(node: Node<'_>) -> Span {
let start = node.start_position();
let end = node.end_position();
Span::new(
sqry_core::graph::node::Position::new(start.row, start.column),
sqry_core::graph::node::Position::new(end.row, end.column),
)
}
fn count_call_arguments(call_node: Node<'_>) -> u8 {
let args_node = call_node
.child_by_field_name("arguments")
.or_else(|| call_node.child_by_field_name("argument_list"))
.or_else(|| {
let mut cursor = call_node.walk();
call_node
.children(&mut cursor)
.find(|child| child.kind() == "argument_list")
});
let Some(args_node) = args_node else {
return 255;
};
let count = args_node.named_child_count();
if count <= 254 {
u8::try_from(count).unwrap_or(u8::MAX)
} else {
255
}
}
fn extract_visibility(node: &Node, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"visibility_modifier" => {
if let Ok(vis_text) = child.utf8_text(content) {
return Some(vis_text.trim().to_string());
}
}
"public" | "private" | "protected" => {
if let Ok(vis_text) = child.utf8_text(content) {
return Some(vis_text.trim().to_string());
}
}
_ => {}
}
}
None
}
fn export_public_methods_from_class(
class_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
module_id: NodeId,
class_qualified_name: &str,
) {
let mut cursor = class_node.walk();
for child in class_node.children(&mut cursor) {
if child.kind() == "declaration_list" {
let mut body_cursor = child.walk();
for body_child in child.children(&mut body_cursor) {
if body_child.kind() == "method_declaration" {
let visibility = extract_visibility(&body_child, content);
let is_public = visibility.as_deref() == Some("public") || visibility.is_none();
if is_public {
if let Some(name_node) = body_child.child_by_field_name("name")
&& let Ok(method_name) = name_node.utf8_text(content)
{
let method_name = method_name.trim();
let qualified_method_name =
format!("{class_qualified_name}::{method_name}");
if let Some(&method_id) = node_map.get(&qualified_method_name) {
helper.add_export_edge(module_id, method_id);
}
}
}
}
}
break;
}
}
}
fn extract_return_type(node: &Node, content: &[u8]) -> Option<String> {
let mut found_colon = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if found_colon && child.is_named() {
return extract_type_from_node(&child, content);
}
if child.kind() == ":" {
found_colon = true;
}
}
None
}
fn extract_type_from_node(type_node: &Node, content: &[u8]) -> Option<String> {
match type_node.kind() {
"primitive_type" => {
type_node
.utf8_text(content)
.ok()
.map(|s| s.trim().to_string())
}
"optional_type" => {
let mut cursor = type_node.walk();
for child in type_node.children(&mut cursor) {
if child.kind() != "?" && child.is_named() {
return extract_type_from_node(&child, content);
}
}
None
}
"union_type" => {
type_node
.named_child(0)
.and_then(|first_type| extract_type_from_node(&first_type, content))
}
"named_type" | "qualified_name" => {
type_node
.utf8_text(content)
.ok()
.map(|s| s.trim().to_string())
}
"intersection_type" => {
type_node
.named_child(0)
.and_then(|first_type| extract_type_from_node(&first_type, content))
}
_ => {
type_node
.utf8_text(content)
.ok()
.map(|s| {
let trimmed = s.trim();
trimmed
.split(&['|', '&'][..])
.next()
.unwrap_or(trimmed)
.trim()
.trim_start_matches('(')
.trim_end_matches(')')
.trim()
.to_string()
})
.filter(|s| !s.is_empty())
}
}
}
fn process_phpdoc_annotations(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
match node.kind() {
"function_definition" => {
process_function_phpdoc(node, content, helper)?;
}
"method_declaration" => {
process_method_phpdoc(node, content, helper)?;
}
"property_declaration" => {
process_property_phpdoc(node, content, helper)?;
}
"simple_property" => {
process_property_phpdoc(node, content, helper)?;
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
process_phpdoc_annotations(child, content, helper)?;
}
Ok(())
}
fn process_function_phpdoc(
func_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(phpdoc_text) = extract_phpdoc_comment(func_node, content) else {
return Ok(());
};
let tags = parse_phpdoc_tags(&phpdoc_text);
let Some(name_node) = func_node.child_by_field_name("name") else {
return Ok(());
};
let function_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(func_node),
reason: "failed to read function name".to_string(),
})?
.trim()
.to_string();
if function_name.is_empty() {
return Ok(());
}
let func_node_id = helper.ensure_callee(
&function_name,
span_from_node(func_node),
CalleeKindHint::Function,
);
let _ast_params = extract_ast_parameters(func_node, content);
for (param_idx, param_tag) in tags.params.iter().enumerate() {
let canonical_type = canonical_type_string(¶m_tag.type_str);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
func_node_id,
type_node_id,
Some(TypeOfContext::Parameter),
param_idx.try_into().ok(), Some(¶m_tag.name),
);
let type_names = extract_type_names(¶m_tag.type_str);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(func_node_id, ref_type_id);
}
}
if let Some(return_type) = &tags.returns {
let canonical_type = canonical_type_string(return_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
func_node_id,
type_node_id,
Some(TypeOfContext::Return),
Some(0),
None,
);
let type_names = extract_type_names(return_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(func_node_id, ref_type_id);
}
}
Ok(())
}
fn process_method_phpdoc(
method_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(phpdoc_text) = extract_phpdoc_comment(method_node, content) else {
return Ok(());
};
let tags = parse_phpdoc_tags(&phpdoc_text);
let Some(name_node) = method_node.child_by_field_name("name") else {
return Ok(());
};
let method_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(method_node),
reason: "failed to read method name".to_string(),
})?
.trim()
.to_string();
if method_name.is_empty() {
return Ok(());
}
let class_name = get_enclosing_class_name(method_node, content)?;
let Some(class_name) = class_name else {
return Ok(());
};
let qualified_name = format!("{class_name}.{method_name}");
let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
let _ast_params = extract_ast_parameters(method_node, content);
for (param_idx, param_tag) in tags.params.iter().enumerate() {
let canonical_type = canonical_type_string(¶m_tag.type_str);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
method_node_id,
type_node_id,
Some(TypeOfContext::Parameter),
param_idx.try_into().ok(),
Some(¶m_tag.name),
);
let type_names = extract_type_names(¶m_tag.type_str);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(method_node_id, ref_type_id);
}
}
if let Some(return_type) = &tags.returns {
let canonical_type = canonical_type_string(return_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
method_node_id,
type_node_id,
Some(TypeOfContext::Return),
Some(0),
None,
);
let type_names = extract_type_names(return_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(method_node_id, ref_type_id);
}
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn process_property_phpdoc(
prop_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(phpdoc_text) = extract_phpdoc_comment(prop_node, content) else {
return Ok(());
};
let tags = parse_phpdoc_tags(&phpdoc_text);
let Some(var_type) = &tags.var_type else {
return Ok(());
};
let property_names = extract_property_names(prop_node, content);
if property_names.is_empty() {
let generic_name = format!("property_{:?}", prop_node.id());
let prop_node_id = helper.add_variable(&generic_name, None);
let canonical_type = canonical_type_string(var_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
prop_node_id,
type_node_id,
Some(TypeOfContext::Variable),
None,
Some(&generic_name),
);
let type_names = extract_type_names(var_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(prop_node_id, ref_type_id);
}
return Ok(());
}
for prop_name in property_names {
let prop_node_id = helper.add_variable(&prop_name, None);
let canonical_type = canonical_type_string(var_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
prop_node_id,
type_node_id,
Some(TypeOfContext::Variable),
None,
Some(&prop_name),
);
let type_names = extract_type_names(var_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(prop_node_id, ref_type_id);
}
}
Ok(())
}
fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
let mut params = Vec::new();
let Some(params_node) = func_node.child_by_field_name("parameters") else {
return params;
};
let mut index = 0;
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
if !child.is_named() {
continue;
}
match child.kind() {
"simple_parameter" => {
let mut param_cursor = child.walk();
for param_child in child.children(&mut param_cursor) {
if param_child.kind() == "variable_name"
&& let Ok(param_text) = param_child.utf8_text(content)
{
params.push((index, param_text.trim().to_string()));
index += 1;
break;
}
}
}
"variadic_parameter" => {
let mut param_cursor = child.walk();
for param_child in child.children(&mut param_cursor) {
if param_child.kind() == "variable_name"
&& let Ok(param_text) = param_child.utf8_text(content)
{
params.push((index, param_text.trim().to_string()));
index += 1;
break;
}
}
}
_ => {}
}
}
params
}
#[allow(clippy::unnecessary_wraps)]
fn get_enclosing_class_name(node: Node, content: &[u8]) -> GraphResult<Option<String>> {
let mut current = node;
while let Some(parent) = current.parent() {
if parent.kind() == "class_declaration" {
if let Some(name_node) = parent.child_by_field_name("name")
&& let Ok(name_text) = name_node.utf8_text(content)
{
return Ok(Some(name_text.trim().to_string()));
}
return Ok(None);
}
current = parent;
}
Ok(None)
}
fn extract_property_names(prop_node: Node, content: &[u8]) -> Vec<String> {
let mut names = Vec::new();
match prop_node.kind() {
"property_declaration" => {
let mut cursor = prop_node.walk();
for child in prop_node.children(&mut cursor) {
match child.kind() {
"property_initializer" => {
if let Some(var_node) = child.child_by_field_name("name")
&& let Ok(var_text) = var_node.utf8_text(content)
{
names.push(var_text.trim().to_string());
}
}
"simple_property" => {
if let Some(var_node) = child.child_by_field_name("name")
&& let Ok(var_text) = var_node.utf8_text(content)
{
names.push(var_text.trim().to_string());
}
}
_ => {}
}
}
}
"simple_property" => {
if let Some(var_node) = prop_node.child_by_field_name("name")
&& let Ok(var_text) = var_node.utf8_text(content)
{
names.push(var_text.trim().to_string());
}
}
_ => {}
}
names
}
fn process_ffi_member_call(
node: Node,
method_name: &str,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
) {
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let source_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let ffi_name = format!("native::ffi::{method_name}");
let call_span = span_from_node(node);
let target_id = helper.add_module(&ffi_name, Some(call_span));
helper.add_ffi_edge(source_id, target_id, FfiConvention::C);
}
fn process_ffi_static_call(
node: Node,
method_name: &str,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
node_map: &mut HashMap<String, NodeId>,
content: &[u8],
) {
let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
return;
};
let source_id = *node_map
.entry(call_context.qualified_name.clone())
.or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
let library_name = extract_php_ffi_library_name(node, content, method_name == "cdef")
.map_or_else(
|| "unknown".to_string(),
|lib| php_ffi_library_simple_name(&lib),
);
let ffi_name = format!("native::{library_name}");
let call_span = span_from_node(node);
let target_id = helper.add_module(&ffi_name, Some(call_span));
helper.add_ffi_edge(source_id, target_id, FfiConvention::C);
}
fn is_php_ffi_call(object_node: Node, content: &[u8]) -> bool {
if object_node.kind() == "scoped_call_expression"
&& let Some(scope_node) = object_node.child_by_field_name("scope")
&& let Some(name_node) = object_node.child_by_field_name("name")
&& let Ok(scope_text) = scope_node.utf8_text(content)
&& let Ok(name_text) = name_node.utf8_text(content)
&& is_ffi_static_call(scope_text, name_text)
{
return true;
}
if object_node.kind() == "parenthesized_expression"
&& let Some(inner) = object_node.named_child(0)
&& inner.kind() == "scoped_call_expression"
&& let Some(scope_node) = inner.child_by_field_name("scope")
&& let Some(name_node) = inner.child_by_field_name("name")
&& let Ok(scope_text) = scope_node.utf8_text(content)
&& let Ok(name_text) = name_node.utf8_text(content)
&& is_ffi_static_call(scope_text, name_text)
{
return true;
}
let Ok(object_text) = object_node.utf8_text(content) else {
return false;
};
let object_text = object_text.trim();
if object_text == "$ffi" || object_text == "$_ffi" {
return true;
}
if object_text.ends_with("->ffi")
|| object_text.ends_with("::$ffi")
|| object_text.ends_with("->_ffi")
|| object_text.ends_with("::$_ffi")
{
return true;
}
false
}
fn is_ffi_static_call(scope_text: &str, method_text: &str) -> bool {
(scope_text == "FFI" || scope_text == "\\FFI")
&& (method_text == "cdef" || method_text == "load")
}
fn extract_php_ffi_library_name(call_node: Node, content: &[u8], is_cdef: bool) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let args_vec: Vec<Node> = args
.children(&mut cursor)
.filter(|child| !matches!(child.kind(), "(" | ")" | ","))
.collect();
let target_arg_name = if is_cdef { "lib" } else { "filename" };
if let Some(named_arg) = find_named_argument(&args_vec, target_arg_name, content) {
return extract_string_from_argument(named_arg, content);
}
if is_cdef {
args_vec
.get(1)
.and_then(|arg| extract_string_from_argument(*arg, content))
} else {
args_vec
.first()
.and_then(|arg| extract_string_from_argument(*arg, content))
}
}
fn find_named_argument<'a>(args: &'a [Node], param_name: &str, content: &[u8]) -> Option<Node<'a>> {
for arg in args {
if arg.kind() != "argument" {
continue;
}
if arg.named_child_count() < 2 {
continue;
}
if let Some(name_node) = arg.child_by_field_name("name")
&& let Ok(name_text) = name_node.utf8_text(content)
&& name_text == param_name
{
return Some(*arg);
} else if let Some(name_node) = arg.named_child(0)
&& let Ok(name_text) = name_node.utf8_text(content)
&& name_text == param_name
{
return Some(*arg);
}
}
None
}
fn extract_string_from_argument(arg_node: Node, content: &[u8]) -> Option<String> {
let value_node = unwrap_argument_node(arg_node)?;
if !is_string_literal_node(value_node) {
return None;
}
if is_interpolated_string(value_node) {
return None;
}
extract_php_string_content(value_node, content)
}
fn unwrap_argument_node(node: Node) -> Option<Node> {
if node.kind() != "argument" {
return Some(node);
}
let name_field_node = node.child_by_field_name("name");
let ref_modifier_field_node = node.child_by_field_name("reference_modifier");
for i in 0..node.named_child_count() {
#[allow(clippy::cast_possible_truncation)] if let Some(child) = node.named_child(i as u32) {
let is_name_field = name_field_node.is_some_and(|n| n.id() == child.id());
let is_ref_modifier = ref_modifier_field_node.is_some_and(|n| n.id() == child.id());
if !is_name_field && !is_ref_modifier {
return Some(child);
}
}
}
None
}
fn is_string_literal_node(node: Node) -> bool {
matches!(
node.kind(),
"string" | "encapsed_string" | "heredoc" | "nowdoc"
)
}
fn is_interpolated_string(node: Node) -> bool {
if !matches!(node.kind(), "encapsed_string" | "heredoc") {
return false;
}
has_variable_node(node)
}
fn has_variable_node(node: Node) -> bool {
if matches!(
node.kind(),
"variable_name" | "simple_variable" | "variable" | "complex_variable"
| "dynamic_variable_name"
| "subscript_expression" | "member_access_expression" | "member_call_expression"
| "function_call_expression"
| "scoped_call_expression" | "scoped_property_access_expression"
| "class_constant_access_expression"
| "nullsafe_member_access_expression" | "nullsafe_member_call_expression"
) {
return true;
}
for i in 0..node.child_count() {
#[allow(clippy::cast_possible_truncation)] if let Some(child) = node.child(i as u32)
&& has_variable_node(child)
{
return true;
}
}
false
}
fn extract_php_string_content(string_node: Node, content: &[u8]) -> Option<String> {
let Ok(text) = string_node.utf8_text(content) else {
return None;
};
let text = text.trim();
if ((text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\'')))
&& text.len() >= 2
{
return Some(text[1..text.len() - 1].to_string());
}
Some(text.to_string())
}
fn php_ffi_library_simple_name(library_path: &str) -> String {
use std::path::Path;
let filename = Path::new(library_path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(library_path);
if let Some(so_pos) = filename.find(".so.") {
return filename[..so_pos].to_string();
}
if let Some(dot_pos) = filename.find('.') {
let extension = &filename[dot_pos + 1..];
if extension == "so"
|| extension == "dll"
|| extension == "dylib"
|| extension == "h"
|| extension == "hpp"
{
return filename[..dot_pos].to_string();
}
}
filename.to_string()
}