use std::{collections::HashMap, path::Path};
use crate::relations::java_common::{PackageResolver, build_member_symbol, build_symbol};
use crate::relations::local_scopes::{self, JavaScopeTree, ResolutionOutcome};
use sqry_core::graph::unified::StagingGraph;
use sqry_core::graph::unified::build::helper::GraphBuildHelper;
use sqry_core::graph::unified::edge::FfiConvention;
use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
use tree_sitter::{Node, Tree};
const DEFAULT_SCOPE_DEPTH: usize = 4;
const FILE_MODULE_NAME: &str = "<file_module>";
#[derive(Debug, Clone, Copy)]
pub struct JavaGraphBuilder {
max_scope_depth: usize,
}
impl Default for JavaGraphBuilder {
fn default() -> Self {
Self {
max_scope_depth: DEFAULT_SCOPE_DEPTH,
}
}
}
impl JavaGraphBuilder {
#[must_use]
pub fn new(max_scope_depth: usize) -> Self {
Self { max_scope_depth }
}
}
impl GraphBuilder for JavaGraphBuilder {
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
let mut scope_tree = local_scopes::build(tree.root_node(), content, Some(file))?;
for context in ast_graph.contexts() {
let qualified_name = context.qualified_name();
let span = Span::from_bytes(context.span.0, context.span.1);
if context.is_constructor {
helper.add_method_with_visibility(
qualified_name,
Some(span),
false,
false,
context.visibility.as_deref(),
);
} else {
helper.add_method_with_signature(
qualified_name,
Some(span),
false,
context.is_static,
context.visibility.as_deref(),
context.return_type.as_deref(),
);
if context.is_native {
build_jni_native_method_edge(context, &mut helper);
}
}
}
add_field_typeof_edges(&ast_graph, &mut helper);
let root = tree.root_node();
walk_tree_for_edges(
root,
content,
&ast_graph,
&mut scope_tree,
&mut helper,
tree,
)?;
Ok(())
}
fn language(&self) -> Language {
Language::Java
}
}
#[derive(Debug)]
struct ASTGraph {
contexts: Vec<MethodContext>,
field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
import_map: HashMap<String, String>,
has_jna_import: bool,
has_panama_import: bool,
jna_library_interfaces: Vec<String>,
}
impl ASTGraph {
fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
let package_name = PackageResolver::package_from_ast(tree, content);
let mut contexts = Vec::new();
let mut class_stack = Vec::new();
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.expect("Failed to load recursion limits");
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.expect("Invalid file_ops_depth configuration");
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.expect("Failed to create recursion guard");
if let Err(e) = extract_java_contexts(
tree.root_node(),
content,
&mut contexts,
&mut class_stack,
package_name.as_deref(),
0,
max_depth,
&mut guard,
) {
eprintln!("Warning: Java AST traversal hit recursion limit: {e}");
}
let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
let jna_library_interfaces = find_jna_library_interfaces(tree.root_node(), content);
Self {
contexts,
field_types,
import_map,
has_jna_import,
has_panama_import,
jna_library_interfaces,
}
}
fn contexts(&self) -> &[MethodContext] {
&self.contexts
}
fn find_enclosing(&self, byte_pos: usize) -> Option<&MethodContext> {
self.contexts
.iter()
.filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
.max_by_key(|ctx| ctx.depth)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] struct MethodContext {
qualified_name: String,
span: (usize, usize),
depth: usize,
is_static: bool,
#[allow(dead_code)] is_synchronized: bool,
is_constructor: bool,
#[allow(dead_code)] is_native: bool,
package_name: Option<String>,
class_stack: Vec<String>,
return_type: Option<String>,
visibility: Option<String>,
}
impl MethodContext {
fn qualified_name(&self) -> &str {
&self.qualified_name
}
}
fn extract_java_contexts(
node: Node,
content: &[u8],
contexts: &mut Vec<MethodContext>,
class_stack: &mut Vec<String>,
package_name: Option<&str>,
depth: usize,
max_depth: usize,
guard: &mut sqry_core::query::security::RecursionGuard,
) -> Result<(), sqry_core::query::security::RecursionError> {
guard.enter()?;
if depth > max_depth {
guard.exit();
return Ok(());
}
match node.kind() {
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration" => {
if let Some(name_node) = node.child_by_field_name("name") {
let class_name = extract_identifier(name_node, content);
class_stack.push(class_name.clone());
if let Some(body_node) = node.child_by_field_name("body") {
extract_methods_from_body(
body_node,
content,
class_stack,
package_name,
contexts,
depth + 1,
max_depth,
guard,
)?;
for i in 0..body_node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = body_node.child(i as u32) {
extract_java_contexts(
child,
content,
contexts,
class_stack,
package_name,
depth + 1,
max_depth,
guard,
)?;
}
}
}
class_stack.pop();
guard.exit();
return Ok(());
}
}
_ => {}
}
for i in 0..node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = node.child(i as u32) {
extract_java_contexts(
child,
content,
contexts,
class_stack,
package_name,
depth,
max_depth,
guard,
)?;
}
}
guard.exit();
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn extract_methods_from_body(
body_node: Node,
content: &[u8],
class_stack: &[String],
package_name: Option<&str>,
contexts: &mut Vec<MethodContext>,
depth: usize,
_max_depth: usize,
_guard: &mut sqry_core::query::security::RecursionGuard,
) -> Result<(), sqry_core::query::security::RecursionError> {
for i in 0..body_node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = body_node.child(i as u32) {
match child.kind() {
"method_declaration" => {
if let Some(method_context) =
extract_method_context(child, content, class_stack, package_name, depth)
{
contexts.push(method_context);
}
}
"constructor_declaration" | "compact_constructor_declaration" => {
let constructor_context = extract_constructor_context(
child,
content,
class_stack,
package_name,
depth,
);
contexts.push(constructor_context);
}
_ => {}
}
}
}
Ok(())
}
fn extract_method_context(
method_node: Node,
content: &[u8],
class_stack: &[String],
package_name: Option<&str>,
depth: usize,
) -> Option<MethodContext> {
let name_node = method_node.child_by_field_name("name")?;
let method_name = extract_identifier(name_node, content);
let is_static = has_modifier(method_node, "static", content);
let is_synchronized = has_modifier(method_node, "synchronized", content);
let is_native = has_modifier(method_node, "native", content);
let visibility = extract_visibility(method_node, content);
let return_type = method_node
.child_by_field_name("type")
.map(|type_node| extract_full_return_type(type_node, content));
let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
Some(MethodContext {
qualified_name,
span: (method_node.start_byte(), method_node.end_byte()),
depth,
is_static,
is_synchronized,
is_constructor: false,
is_native,
package_name: package_name.map(std::string::ToString::to_string),
class_stack: class_stack.to_vec(),
return_type,
visibility,
})
}
fn extract_constructor_context(
constructor_node: Node,
content: &[u8],
class_stack: &[String],
package_name: Option<&str>,
depth: usize,
) -> MethodContext {
let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
let visibility = extract_visibility(constructor_node, content);
MethodContext {
qualified_name,
span: (constructor_node.start_byte(), constructor_node.end_byte()),
depth,
is_static: false,
is_synchronized: false,
is_constructor: true,
is_native: false,
package_name: package_name.map(std::string::ToString::to_string),
class_stack: class_stack.to_vec(),
return_type: None, visibility,
}
}
fn walk_tree_for_edges(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
tree: &Tree,
) -> GraphResult<()> {
match node.kind() {
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration" => {
return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
}
"method_declaration" | "constructor_declaration" => {
handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
if node.kind() == "method_declaration"
&& let Some((http_method, path)) = extract_spring_route_info(node, content)
{
let full_path =
if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
let prefix = class_prefix.trim_end_matches('/');
let suffix = path.trim_start_matches('/');
if suffix.is_empty() {
class_prefix
} else {
format!("{prefix}/{suffix}")
}
} else {
path
};
let qualified_name = format!("route::{http_method}::{full_path}");
let span = Span::from_bytes(node.start_byte(), node.end_byte());
let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
let byte_pos = node.start_byte();
if let Some(context) = ast_graph.find_enclosing(byte_pos) {
let method_id = helper.ensure_method(
context.qualified_name(),
Some(Span::from_bytes(context.span.0, context.span.1)),
false,
context.is_static,
);
helper.add_contains_edge(endpoint_id, method_id);
}
}
}
"compact_constructor_declaration" => {
handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
}
"method_invocation" => {
handle_method_invocation(node, content, ast_graph, helper);
}
"object_creation_expression" => {
handle_constructor_call(node, content, ast_graph, helper);
}
"import_declaration" => {
handle_import_declaration(node, content, helper);
}
"local_variable_declaration" => {
handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
}
"enhanced_for_statement" => {
handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
}
"catch_clause" => {
handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
}
"lambda_expression" => {
handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
}
"try_with_resources_statement" => {
handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
}
"instanceof_expression" => {
handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
}
"switch_label" => {
handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
}
"identifier" => {
handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
}
_ => {}
}
for i in 0..node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = node.child(i as u32) {
walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
}
}
Ok(())
}
fn handle_type_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
tree: &Tree,
) -> GraphResult<()> {
let Some(name_node) = node.child_by_field_name("name") else {
return Ok(());
};
let class_name = extract_identifier(name_node, content);
let span = Span::from_bytes(node.start_byte(), node.end_byte());
let package = PackageResolver::package_from_ast(tree, content);
let class_stack = extract_declaration_class_stack(node, content);
let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
if is_public(node, content) {
export_from_file_module(helper, class_node_id);
}
process_inheritance(node, content, package.as_deref(), class_node_id, helper);
if node.kind() == "class_declaration" {
process_implements(node, content, package.as_deref(), class_node_id, helper);
}
if node.kind() == "interface_declaration" {
process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
}
if let Some(body_node) = node.child_by_field_name("body") {
let is_interface = node.kind() == "interface_declaration";
process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
for i in 0..body_node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = body_node.child(i as u32) {
walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
}
}
}
Ok(())
}
fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
let mut class_stack = Vec::new();
let mut current_node = Some(node);
while let Some(current) = current_node {
if matches!(
current.kind(),
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
) && let Some(name_node) = current.child_by_field_name("name")
{
class_stack.push(extract_identifier(name_node, content));
}
current_node = current.parent();
}
class_stack.reverse();
class_stack
}
fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
let scope = class_stack
.split_last()
.map_or(&[][..], |(_, parent_stack)| parent_stack);
build_symbol(package, scope, class_name)
}
fn add_type_node(
helper: &mut GraphBuildHelper,
kind: &str,
qualified_name: &str,
span: Span,
) -> sqry_core::graph::unified::node::NodeId {
match kind {
"interface_declaration" => helper.add_interface(qualified_name, Some(span)),
_ => helper.add_class(qualified_name, Some(span)),
}
}
fn handle_method_invocation(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) {
if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
if is_ffi {
return;
}
}
process_method_call_unified(node, content, ast_graph, helper);
}
fn handle_constructor_call(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) {
process_constructor_call_unified(node, content, ast_graph, helper);
}
fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
process_import_unified(node, content, helper);
}
fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
let field_id = if *is_final {
if let Some(vis) = visibility {
helper.add_constant_with_static_and_visibility(
field_name,
None,
*is_static,
Some(vis.as_str()),
)
} else {
helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
}
} else {
if let Some(vis) = visibility {
helper.add_property_with_static_and_visibility(
field_name,
None,
*is_static,
Some(vis.as_str()),
)
} else {
helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
}
};
let type_id = helper.add_class(type_fqn, None);
helper.add_typeof_edge(field_id, type_id);
}
}
fn extract_method_parameters(
method_node: Node,
content: &[u8],
qualified_method_name: &str,
helper: &mut GraphBuildHelper,
import_map: &HashMap<String, String>,
scope_tree: &mut JavaScopeTree,
) {
let mut cursor = method_node.walk();
for child in method_node.children(&mut cursor) {
if child.kind() == "formal_parameters" {
let mut param_cursor = child.walk();
for param_child in child.children(&mut param_cursor) {
match param_child.kind() {
"formal_parameter" => {
handle_formal_parameter(
param_child,
content,
qualified_method_name,
helper,
import_map,
scope_tree,
);
}
"spread_parameter" => {
handle_spread_parameter(
param_child,
content,
qualified_method_name,
helper,
import_map,
scope_tree,
);
}
"receiver_parameter" => {
handle_receiver_parameter(
param_child,
content,
qualified_method_name,
helper,
import_map,
scope_tree,
);
}
_ => {}
}
}
}
}
}
fn handle_formal_parameter(
param_node: Node,
content: &[u8],
method_name: &str,
helper: &mut GraphBuildHelper,
import_map: &HashMap<String, String>,
scope_tree: &mut JavaScopeTree,
) {
use sqry_core::graph::unified::node::NodeKind;
let Some(type_node) = param_node.child_by_field_name("type") else {
return;
};
let Some(name_node) = param_node.child_by_field_name("name") else {
return;
};
let type_text = extract_type_name(type_node, content);
let param_name = extract_identifier(name_node, content);
if type_text.is_empty() || param_name.is_empty() {
return;
}
let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
let qualified_param = format!("{method_name}::{param_name}");
let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(param_id, type_id);
}
fn handle_spread_parameter(
param_node: Node,
content: &[u8],
method_name: &str,
helper: &mut GraphBuildHelper,
import_map: &HashMap<String, String>,
scope_tree: &mut JavaScopeTree,
) {
use sqry_core::graph::unified::node::NodeKind;
let mut type_text = String::new();
let mut param_name = String::new();
let mut param_name_node = None;
let mut cursor = param_node.walk();
for child in param_node.children(&mut cursor) {
match child.kind() {
"type_identifier" | "generic_type" | "scoped_type_identifier" => {
type_text = extract_type_name(child, content);
}
"variable_declarator" => {
if let Some(name_node) = child.child_by_field_name("name") {
param_name = extract_identifier(name_node, content);
param_name_node = Some(name_node);
}
}
_ => {}
}
}
if type_text.is_empty() || param_name.is_empty() {
return;
}
let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
let qualified_param = format!("{method_name}::{param_name}");
let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
if let Some(name_node) = param_name_node {
scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
}
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(param_id, type_id);
}
fn handle_receiver_parameter(
param_node: Node,
content: &[u8],
method_name: &str,
helper: &mut GraphBuildHelper,
import_map: &HashMap<String, String>,
_scope_tree: &mut JavaScopeTree,
) {
use sqry_core::graph::unified::node::NodeKind;
let mut type_text = String::new();
let mut cursor = param_node.walk();
for child in param_node.children(&mut cursor) {
if matches!(
child.kind(),
"type_identifier" | "generic_type" | "scoped_type_identifier"
) {
type_text = extract_type_name(child, content);
break;
}
}
if type_text.is_empty() {
return;
}
let param_name = "this";
let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
let qualified_param = format!("{method_name}::{param_name}");
let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(param_id, type_id);
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum FieldAccessRole {
Default,
ExplicitThisOrSuper,
Skip,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum FieldResolutionMode {
Default,
CurrentOnly,
}
fn field_access_role(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &JavaScopeTree,
identifier_text: &str,
) -> FieldAccessRole {
let Some(parent) = node.parent() else {
return FieldAccessRole::Default;
};
if parent.kind() == "field_access" {
if let Some(field_node) = parent.child_by_field_name("field")
&& field_node.id() == node.id()
&& let Some(object_node) = parent.child_by_field_name("object")
{
if is_explicit_this_or_super(object_node, content) {
return FieldAccessRole::ExplicitThisOrSuper;
}
return FieldAccessRole::Skip;
}
if let Some(object_node) = parent.child_by_field_name("object")
&& object_node.id() == node.id()
&& !scope_tree.has_local_binding(identifier_text, node.start_byte())
&& is_static_type_identifier(identifier_text, ast_graph, scope_tree)
{
return FieldAccessRole::Skip;
}
}
if parent.kind() == "method_invocation"
&& let Some(object_node) = parent.child_by_field_name("object")
&& object_node.id() == node.id()
&& !scope_tree.has_local_binding(identifier_text, node.start_byte())
&& is_static_type_identifier(identifier_text, ast_graph, scope_tree)
{
return FieldAccessRole::Skip;
}
if parent.kind() == "method_reference"
&& let Some(object_node) = parent.child_by_field_name("object")
&& object_node.id() == node.id()
&& !scope_tree.has_local_binding(identifier_text, node.start_byte())
&& is_static_type_identifier(identifier_text, ast_graph, scope_tree)
{
return FieldAccessRole::Skip;
}
FieldAccessRole::Default
}
fn is_static_type_identifier(
identifier_text: &str,
ast_graph: &ASTGraph,
scope_tree: &JavaScopeTree,
) -> bool {
ast_graph.import_map.contains_key(identifier_text)
|| scope_tree.is_known_type_name(identifier_text)
}
fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
if matches!(node.kind(), "this" | "super") {
return true;
}
if node.kind() == "identifier" {
let text = extract_identifier(node, content);
return matches!(text.as_str(), "this" | "super");
}
if node.kind() == "field_access"
&& let Some(field) = node.child_by_field_name("field")
{
let text = extract_identifier(field, content);
if matches!(text.as_str(), "this" | "super") {
return true;
}
}
false
}
#[allow(clippy::too_many_lines)]
fn is_declaration_context(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() == "variable_declarator" {
let mut cursor = parent.walk();
for (idx, child) in parent.children(&mut cursor).enumerate() {
if child.id() == node.id() {
#[allow(clippy::cast_possible_truncation)]
if let Some(field_name) = parent.field_name_for_child(idx as u32) {
return field_name == "name";
}
break;
}
}
if let Some(grandparent) = parent.parent()
&& grandparent.kind() == "spread_parameter"
{
return true;
}
return false;
}
if parent.kind() == "formal_parameter" {
let mut cursor = parent.walk();
for (idx, child) in parent.children(&mut cursor).enumerate() {
if child.id() == node.id() {
#[allow(clippy::cast_possible_truncation)]
if let Some(field_name) = parent.field_name_for_child(idx as u32) {
return field_name == "name";
}
break;
}
}
return false;
}
if parent.kind() == "enhanced_for_statement" {
let mut cursor = parent.walk();
for (idx, child) in parent.children(&mut cursor).enumerate() {
if child.id() == node.id() {
#[allow(clippy::cast_possible_truncation)]
if let Some(field_name) = parent.field_name_for_child(idx as u32) {
return field_name == "name";
}
break;
}
}
return false;
}
if parent.kind() == "lambda_expression" {
if let Some(params) = parent.child_by_field_name("parameters") {
return params.id() == node.id();
}
return false;
}
if parent.kind() == "inferred_parameters" {
return true;
}
if parent.kind() == "resource" {
if let Some(name_node) = parent.child_by_field_name("name")
&& name_node.id() == node.id()
{
let has_type = parent.child_by_field_name("type").is_some();
let has_value = parent.child_by_field_name("value").is_some();
return has_type || has_value;
}
return false;
}
if parent.kind() == "type_pattern" {
if let Some((name_node, _type_node)) = typed_pattern_parts(parent) {
return name_node.id() == node.id();
}
return false;
}
if parent.kind() == "instanceof_expression" {
let mut cursor = parent.walk();
for (idx, child) in parent.children(&mut cursor).enumerate() {
if child.id() == node.id() {
#[allow(clippy::cast_possible_truncation)]
if let Some(field_name) = parent.field_name_for_child(idx as u32) {
return field_name == "name";
}
break;
}
}
return false;
}
if parent.kind() == "record_pattern_component" {
let mut cursor = parent.walk();
for child in parent.children(&mut cursor) {
if child.id() == node.id() && child.kind() == "identifier" {
return true;
}
}
return false;
}
if parent.kind() == "record_component" {
if let Some(name_node) = parent.child_by_field_name("name") {
return name_node.id() == node.id();
}
return false;
}
matches!(
parent.kind(),
"method_declaration"
| "constructor_declaration"
| "compact_constructor_declaration"
| "class_declaration"
| "interface_declaration"
| "enum_declaration"
| "field_declaration"
| "catch_formal_parameter"
)
}
fn is_method_invocation_name(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() != "method_invocation" {
return false;
}
parent
.child_by_field_name("name")
.is_some_and(|name_node| name_node.id() == node.id())
}
fn is_method_reference_name(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() != "method_reference" {
return false;
}
parent
.child_by_field_name("name")
.is_some_and(|name_node| name_node.id() == node.id())
}
fn is_label_identifier(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent.kind() == "labeled_statement" {
return true;
}
if matches!(parent.kind(), "break_statement" | "continue_statement")
&& let Some(label) = parent.child_by_field_name("label")
{
return label.id() == node.id();
}
false
}
fn is_class_literal(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
parent.kind() == "class_literal"
}
fn is_type_identifier_context(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
matches!(
parent.kind(),
"type_identifier"
| "scoped_type_identifier"
| "scoped_identifier"
| "generic_type"
| "type_argument"
| "type_bound"
)
}
fn add_reference_edge_for_target(
usage_node: Node,
identifier_text: &str,
target_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
let usage_id = helper.add_node(
&format!("{}@{}", identifier_text, usage_node.start_byte()),
Some(usage_span),
sqry_core::graph::unified::node::NodeKind::Variable,
);
helper.add_reference_edge(usage_id, target_id);
}
fn resolve_field_reference(
node: Node,
identifier_text: &str,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
mode: FieldResolutionMode,
) {
let context = ast_graph.find_enclosing(node.start_byte());
let mut candidates = Vec::new();
if let Some(ctx) = context
&& !ctx.class_stack.is_empty()
{
if mode == FieldResolutionMode::CurrentOnly {
let class_path = ctx.class_stack.join("::");
candidates.push(format!("{class_path}::{identifier_text}"));
} else {
let stack_len = ctx.class_stack.len();
for idx in (1..=stack_len).rev() {
let class_path = ctx.class_stack[..idx].join("::");
candidates.push(format!("{class_path}::{identifier_text}"));
}
}
}
if mode != FieldResolutionMode::CurrentOnly {
candidates.push(identifier_text.to_string());
}
for candidate in candidates {
if ast_graph.field_types.contains_key(&candidate) {
add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
return;
}
}
}
fn add_field_reference(
node: Node,
identifier_text: &str,
field_name: &str,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) {
let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
let usage_id = helper.add_node(
&format!("{}@{}", identifier_text, node.start_byte()),
Some(usage_span),
sqry_core::graph::unified::node::NodeKind::Variable,
);
let field_metadata = ast_graph.field_types.get(field_name);
let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
if *is_final {
if let Some(vis) = visibility {
helper.add_constant_with_static_and_visibility(
field_name,
None,
*is_static,
Some(vis.as_str()),
)
} else {
helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
}
} else if let Some(vis) = visibility {
helper.add_property_with_static_and_visibility(
field_name,
None,
*is_static,
Some(vis.as_str()),
)
} else {
helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
}
} else {
helper.add_property_with_static_and_visibility(field_name, None, false, None)
};
helper.add_reference_edge(usage_id, field_id);
}
#[allow(clippy::similar_names)]
fn handle_identifier_for_reference(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let identifier_text = extract_identifier(node, content);
if identifier_text.is_empty() {
return;
}
if is_declaration_context(node) {
return;
}
if is_method_invocation_name(node)
|| is_method_reference_name(node)
|| is_label_identifier(node)
|| is_class_literal(node)
{
return;
}
if is_type_identifier_context(node) {
return;
}
let field_access_role =
field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
if matches!(field_access_role, FieldAccessRole::Skip) {
return;
}
let allow_local = matches!(field_access_role, FieldAccessRole::Default);
let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
FieldResolutionMode::CurrentOnly
} else {
FieldResolutionMode::Default
};
if allow_local {
match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
ResolutionOutcome::Local(binding) => {
let target_id = if let Some(node_id) = binding.node_id {
node_id
} else {
let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
var_id
};
add_reference_edge_for_target(node, &identifier_text, target_id, helper);
return;
}
ResolutionOutcome::Member { qualified_name } => {
if let Some(field_name) = qualified_name {
add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
}
return;
}
ResolutionOutcome::Ambiguous => {
return;
}
ResolutionOutcome::NoMatch => {}
}
}
if !allow_field {
return;
}
resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
}
fn handle_method_declaration_parameters(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let byte_pos = node.start_byte();
if let Some(context) = ast_graph.find_enclosing(byte_pos) {
let qualified_method_name = &context.qualified_name;
extract_method_parameters(
node,
content,
qualified_method_name,
helper,
&ast_graph.import_map,
scope_tree,
);
}
}
fn handle_local_variable_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let Some(type_node) = node.child_by_field_name("type") else {
return;
};
let type_text = extract_type_name(type_node, content);
if type_text.is_empty() {
return;
}
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or_else(|| type_text.clone());
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
{
let var_name = extract_identifier(name_node, content);
let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
}
}
fn handle_enhanced_for_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let Some(type_node) = node.child_by_field_name("type") else {
return;
};
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Some(body_node) = node.child_by_field_name("body") else {
return;
};
let type_text = extract_type_name(type_node, content);
let var_name = extract_identifier(name_node, content);
if type_text.is_empty() || var_name.is_empty() {
return;
}
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
fn handle_catch_parameter_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let Some(param_node) = node
.child_by_field_name("parameter")
.or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
.or_else(|| first_child_of_kind(node, "formal_parameter"))
else {
return;
};
let Some(name_node) = param_node
.child_by_field_name("name")
.or_else(|| first_child_of_kind(param_node, "identifier"))
else {
return;
};
let var_name = extract_identifier(name_node, content);
if var_name.is_empty() {
return;
}
let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
if let Some(type_node) = param_node
.child_by_field_name("type")
.or_else(|| first_child_of_kind(param_node, "type_identifier"))
.or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
.or_else(|| first_child_of_kind(param_node, "generic_type"))
{
add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
}
}
fn add_typeof_for_catch_type(
type_node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
var_id: sqry_core::graph::unified::node::NodeId,
) {
if type_node.kind() == "union_type" {
let mut cursor = type_node.walk();
for child in type_node.children(&mut cursor) {
if matches!(
child.kind(),
"type_identifier" | "scoped_type_identifier" | "generic_type"
) {
let type_text = extract_type_name(child, content);
if !type_text.is_empty() {
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
}
}
return;
}
let type_text = extract_type_name(type_node, content);
if type_text.is_empty() {
return;
}
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
fn handle_lambda_parameter_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
use sqry_core::graph::unified::node::NodeKind;
let Some(params_node) = node.child_by_field_name("parameters") else {
return;
};
let lambda_prefix = format!("lambda@{}", node.start_byte());
if params_node.kind() == "identifier" {
let name = extract_identifier(params_node, content);
if name.is_empty() {
return;
}
let qualified_param = format!("{lambda_prefix}::{name}");
let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
return;
}
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
match child.kind() {
"identifier" => {
let name = extract_identifier(child, content);
if name.is_empty() {
continue;
}
let qualified_param = format!("{lambda_prefix}::{name}");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
scope_tree.attach_node_id(&name, child.start_byte(), param_id);
}
"formal_parameter" => {
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Some(type_node) = child.child_by_field_name("type") else {
continue;
};
let name = extract_identifier(name_node, content);
if name.is_empty() {
continue;
}
let type_text = extract_type_name(type_node, content);
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let qualified_param = format!("{lambda_prefix}::{name}");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(param_id, type_id);
}
_ => {}
}
}
}
fn handle_try_with_resources_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let Some(resources) = node.child_by_field_name("resources") else {
return;
};
let mut cursor = resources.walk();
for resource in resources.children(&mut cursor) {
if resource.kind() != "resource" {
continue;
}
let name_node = resource.child_by_field_name("name");
let type_node = resource.child_by_field_name("type");
let value_node = resource.child_by_field_name("value");
if let Some(name_node) = name_node {
if type_node.is_none() && value_node.is_none() {
continue;
}
let name = extract_identifier(name_node, content);
if name.is_empty() {
continue;
}
let qualified_var = format!("{}@{}", name, name_node.start_byte());
let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
if let Some(type_node) = type_node {
let type_text = extract_type_name(type_node, content);
if !type_text.is_empty() {
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
}
}
}
}
fn handle_instanceof_pattern_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let mut patterns = Vec::new();
collect_pattern_declarations(node, &mut patterns);
for (name_node, type_node) in patterns {
let name = extract_identifier(name_node, content);
if name.is_empty() {
continue;
}
let qualified_var = format!("{}@{}", name, name_node.start_byte());
let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
if let Some(type_node) = type_node {
let type_text = extract_type_name(type_node, content);
if !type_text.is_empty() {
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
}
}
}
fn handle_switch_pattern_declaration(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
let mut patterns = Vec::new();
collect_pattern_declarations(node, &mut patterns);
for (name_node, type_node) in patterns {
let name = extract_identifier(name_node, content);
if name.is_empty() {
continue;
}
let qualified_var = format!("{}@{}", name, name_node.start_byte());
let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
let var_id = helper.add_variable(&qualified_var, Some(span));
scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
if let Some(type_node) = type_node {
let type_text = extract_type_name(type_node, content);
if !type_text.is_empty() {
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(var_id, type_id);
}
}
}
}
fn handle_compact_constructor_parameters(
node: Node,
content: &[u8],
ast_graph: &ASTGraph,
scope_tree: &mut JavaScopeTree,
helper: &mut GraphBuildHelper,
) {
use sqry_core::graph::unified::node::NodeKind;
let Some(record_node) = node
.parent()
.and_then(|parent| find_record_declaration(parent))
else {
return;
};
let Some(record_name_node) = record_node.child_by_field_name("name") else {
return;
};
let record_name = extract_identifier(record_name_node, content);
if record_name.is_empty() {
return;
}
let mut components = Vec::new();
collect_record_components_nodes(record_node, &mut components);
for component in components {
let Some(name_node) = component.child_by_field_name("name") else {
continue;
};
let Some(type_node) = component.child_by_field_name("type") else {
continue;
};
let name = extract_identifier(name_node, content);
if name.is_empty() {
continue;
}
let type_text = extract_type_name(type_node, content);
if type_text.is_empty() {
continue;
}
let resolved_type = ast_graph
.import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text);
let qualified_param = format!("{record_name}.<init>::{name}");
let span = Span::from_bytes(component.start_byte(), component.end_byte());
let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
let type_id = helper.add_class(&resolved_type, None);
helper.add_typeof_edge(param_id, type_id);
}
}
fn collect_pattern_declarations<'a>(
node: Node<'a>,
output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
) {
if node.kind() == "instanceof_expression"
&& !node_has_direct_child_kind(node, "type_pattern")
&& let Some(name_node) = node.child_by_field_name("name")
{
let type_node = first_type_like_child(node);
output.push((name_node, type_node));
}
if node.kind() == "type_pattern"
&& let Some((name_node, type_node)) = typed_pattern_parts(node)
{
output.push((name_node, type_node));
}
if node.kind() == "record_pattern_component"
&& let Some((name_node, type_node)) = typed_pattern_parts(node)
{
output.push((name_node, type_node));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_pattern_declarations(child, output);
}
}
fn node_has_direct_child_kind(node: Node, kind: &str) -> bool {
let mut cursor = node.walk();
node.children(&mut cursor).any(|child| child.kind() == kind)
}
fn typed_pattern_parts(node: Node) -> Option<(Node, Option<Node>)> {
let mut name_node = None;
let mut type_node = None;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "identifier" | "_reserved_identifier") {
name_node = Some(child);
} else if matches!(
child.kind(),
"type_identifier" | "scoped_type_identifier" | "generic_type"
) {
type_node = Some(child);
}
}
name_node.map(|name| (name, type_node))
}
fn first_type_like_child(node: Node) -> Option<Node> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(
child.kind(),
"type_identifier" | "scoped_type_identifier" | "generic_type"
) {
return Some(child);
}
}
None
}
fn find_record_declaration(node: Node) -> Option<Node> {
if node.kind() == "record_declaration" {
return Some(node);
}
node.parent().and_then(find_record_declaration)
}
fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
if let Some(parameters) = node.child_by_field_name("parameters") {
let mut cursor = parameters.walk();
for child in parameters.children(&mut cursor) {
if matches!(child.kind(), "formal_parameter" | "record_component") {
output.push(child);
}
}
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "record_component" {
output.push(child);
}
}
}
fn process_method_call_unified(
call_node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) {
let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
return;
};
let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
return;
};
let callee_qualified =
resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
let caller_method_id = ensure_caller_method(helper, caller_context);
let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
add_call_edge(helper, caller_method_id, target_method_id, call_node);
}
fn process_constructor_call_unified(
new_node: Node,
content: &[u8],
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) {
let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
return;
};
let Some(type_node) = new_node.child_by_field_name("type") else {
return;
};
let class_name = extract_type_name(type_node, content);
if class_name.is_empty() {
return;
}
let qualified_class = qualify_constructor_class(&class_name, caller_context);
let constructor_name = format!("{qualified_class}.<init>");
let caller_method_id = ensure_caller_method(helper, caller_context);
let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
add_call_edge(helper, caller_method_id, target_method_id, new_node);
}
fn count_call_arguments(call_node: Node<'_>) -> u8 {
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return 255;
};
let count = args_node.named_child_count();
if count <= 254 {
u8::try_from(count).unwrap_or(u8::MAX)
} else {
u8::MAX
}
}
fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
let has_asterisk = import_has_wildcard(import_node);
let Some(mut imported_name) = extract_import_name(import_node, content) else {
return;
};
if has_asterisk {
imported_name = format!("{imported_name}.*");
}
let module_id = helper.add_module("<module>", None);
let external_id = helper.add_import(
&imported_name,
Some(Span::from_bytes(
import_node.start_byte(),
import_node.end_byte(),
)),
);
helper.add_import_edge(module_id, external_id);
}
fn ensure_caller_method(
helper: &mut GraphBuildHelper,
caller_context: &MethodContext,
) -> sqry_core::graph::unified::node::NodeId {
helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
)
}
fn resolve_callee_qualified(
call_node: &Node,
content: &[u8],
ast_graph: &ASTGraph,
caller_context: &MethodContext,
callee_name: &str,
) -> String {
if let Some(object_node) = call_node.child_by_field_name("object") {
let object_text = extract_node_text(object_node, content);
return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
}
build_member_symbol(
caller_context.package_name.as_deref(),
&caller_context.class_stack,
callee_name,
)
}
fn resolve_member_call_target(
object_text: &str,
ast_graph: &ASTGraph,
caller_context: &MethodContext,
callee_name: &str,
) -> String {
if object_text.contains('.') {
return format!("{object_text}.{callee_name}");
}
if object_text == "this" {
return build_member_symbol(
caller_context.package_name.as_deref(),
&caller_context.class_stack,
callee_name,
);
}
if let Some(class_name) = caller_context.class_stack.last() {
let qualified_field = format!("{class_name}::{object_text}");
if let Some((field_type, _is_final, _visibility, _is_static)) =
ast_graph.field_types.get(&qualified_field)
{
return format!("{field_type}.{callee_name}");
}
}
if let Some((field_type, _is_final, _visibility, _is_static)) =
ast_graph.field_types.get(object_text)
{
return format!("{field_type}.{callee_name}");
}
if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
return format!("{type_fqn}.{callee_name}");
}
format!("{object_text}.{callee_name}")
}
fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
if class_name.contains('.') {
class_name.to_string()
} else if let Some(pkg) = caller_context.package_name.as_deref() {
format!("{pkg}.{class_name}")
} else {
class_name.to_string()
}
}
fn add_call_edge(
helper: &mut GraphBuildHelper,
caller_method_id: sqry_core::graph::unified::node::NodeId,
target_method_id: sqry_core::graph::unified::node::NodeId,
call_node: Node,
) {
let argument_count = count_call_arguments(call_node);
let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
helper.add_call_edge_full_with_span(
caller_method_id,
target_method_id,
argument_count,
false,
vec![call_span],
);
}
fn import_has_wildcard(import_node: Node) -> bool {
let mut cursor = import_node.walk();
import_node
.children(&mut cursor)
.any(|child| child.kind() == "asterisk")
}
fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
let mut cursor = import_node.walk();
for child in import_node.children(&mut cursor) {
if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
return Some(extract_full_identifier(child, content));
}
}
None
}
fn process_inheritance(
class_node: Node,
content: &[u8],
package_name: Option<&str>,
child_class_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
let parent_type_name = extract_type_from_superclass(superclass_node, content);
if !parent_type_name.is_empty() {
let parent_qualified = qualify_type_name(&parent_type_name, package_name);
let parent_id = helper.add_class(&parent_qualified, None);
helper.add_inherits_edge(child_class_id, parent_id);
}
}
}
fn process_implements(
class_node: Node,
content: &[u8],
package_name: Option<&str>,
class_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
let interfaces_node = class_node
.child_by_field_name("interfaces")
.or_else(|| class_node.child_by_field_name("super_interfaces"));
if let Some(node) = interfaces_node {
extract_interface_types(node, content, package_name, class_id, helper);
return;
}
let mut cursor = class_node.walk();
for child in class_node.children(&mut cursor) {
if child.kind() == "super_interfaces" {
extract_interface_types(child, content, package_name, class_id, helper);
return;
}
}
}
fn process_interface_extends(
interface_node: Node,
content: &[u8],
package_name: Option<&str>,
interface_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
let mut cursor = interface_node.walk();
for child in interface_node.children(&mut cursor) {
if child.kind() == "extends_interfaces" {
extract_parent_interfaces_for_inherits(
child,
content,
package_name,
interface_id,
helper,
);
return;
}
}
}
fn extract_parent_interfaces_for_inherits(
extends_node: Node,
content: &[u8],
package_name: Option<&str>,
child_interface_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
let mut cursor = extends_node.walk();
for child in extends_node.children(&mut cursor) {
match child.kind() {
"type_identifier" => {
let type_name = extract_identifier(child, content);
if !type_name.is_empty() {
let parent_qualified = qualify_type_name(&type_name, package_name);
let parent_id = helper.add_interface(&parent_qualified, None);
helper.add_inherits_edge(child_interface_id, parent_id);
}
}
"type_list" => {
let mut type_cursor = child.walk();
for type_child in child.children(&mut type_cursor) {
if let Some(type_name) = extract_type_identifier(type_child, content)
&& !type_name.is_empty()
{
let parent_qualified = qualify_type_name(&type_name, package_name);
let parent_id = helper.add_interface(&parent_qualified, None);
helper.add_inherits_edge(child_interface_id, parent_id);
}
}
}
"generic_type" | "scoped_type_identifier" => {
if let Some(type_name) = extract_type_identifier(child, content)
&& !type_name.is_empty()
{
let parent_qualified = qualify_type_name(&type_name, package_name);
let parent_id = helper.add_interface(&parent_qualified, None);
helper.add_inherits_edge(child_interface_id, parent_id);
}
}
_ => {}
}
}
}
fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
if superclass_node.kind() == "type_identifier" {
return extract_identifier(superclass_node, content);
}
let mut cursor = superclass_node.walk();
for child in superclass_node.children(&mut cursor) {
if let Some(name) = extract_type_identifier(child, content) {
return name;
}
}
extract_identifier(superclass_node, content)
}
fn extract_interface_types(
interfaces_node: Node,
content: &[u8],
package_name: Option<&str>,
implementor_id: sqry_core::graph::unified::node::NodeId,
helper: &mut GraphBuildHelper,
) {
let mut cursor = interfaces_node.walk();
for child in interfaces_node.children(&mut cursor) {
match child.kind() {
"type_identifier" => {
let type_name = extract_identifier(child, content);
if !type_name.is_empty() {
let interface_qualified = qualify_type_name(&type_name, package_name);
let interface_id = helper.add_interface(&interface_qualified, None);
helper.add_implements_edge(implementor_id, interface_id);
}
}
"type_list" => {
let mut type_cursor = child.walk();
for type_child in child.children(&mut type_cursor) {
if let Some(type_name) = extract_type_identifier(type_child, content)
&& !type_name.is_empty()
{
let interface_qualified = qualify_type_name(&type_name, package_name);
let interface_id = helper.add_interface(&interface_qualified, None);
helper.add_implements_edge(implementor_id, interface_id);
}
}
}
"generic_type" | "scoped_type_identifier" => {
if let Some(type_name) = extract_type_identifier(child, content)
&& !type_name.is_empty()
{
let interface_qualified = qualify_type_name(&type_name, package_name);
let interface_id = helper.add_interface(&interface_qualified, None);
helper.add_implements_edge(implementor_id, interface_id);
}
}
_ => {}
}
}
}
fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
match node.kind() {
"type_identifier" => Some(extract_identifier(node, content)),
"generic_type" => {
if let Some(name_node) = node.child_by_field_name("name") {
Some(extract_identifier(name_node, content))
} else {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "type_identifier" {
return Some(extract_identifier(child, content));
}
}
None
}
}
"scoped_type_identifier" => {
Some(extract_full_identifier(node, content))
}
_ => None,
}
}
fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
if type_name.contains('.') {
return type_name.to_string();
}
if let Some(pkg) = package_name {
format!("{pkg}.{type_name}")
} else {
type_name.to_string()
}
}
#[allow(clippy::type_complexity)]
fn extract_field_and_import_types(
node: Node,
content: &[u8],
) -> (
HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
HashMap<String, String>,
) {
let import_map = extract_import_map(node, content);
let mut field_types = HashMap::new();
let mut class_stack = Vec::new();
extract_field_types_recursive(
node,
content,
&import_map,
&mut field_types,
&mut class_stack,
);
(field_types, import_map)
}
fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
let mut import_map = HashMap::new();
collect_import_map_recursive(node, content, &mut import_map);
import_map
}
fn collect_import_map_recursive(
node: Node,
content: &[u8],
import_map: &mut HashMap<String, String>,
) {
if node.kind() == "import_declaration" {
let full_path = node.utf8_text(content).unwrap_or("");
if let Some(path_start) = full_path.find("import ") {
let after_import = &full_path[path_start + 7..].trim();
if let Some(path_end) = after_import.find(';') {
let import_path = &after_import[..path_end].trim();
if let Some(simple_name) = import_path.rsplit('.').next() {
import_map.insert(simple_name.to_string(), (*import_path).to_string());
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_import_map_recursive(child, content, import_map);
}
}
fn extract_field_types_recursive(
node: Node,
content: &[u8],
import_map: &HashMap<String, String>,
field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
class_stack: &mut Vec<String>,
) {
if matches!(
node.kind(),
"class_declaration" | "interface_declaration" | "enum_declaration" | "record_declaration"
) && let Some(name_node) = node.child_by_field_name("name")
{
let class_name = extract_identifier(name_node, content);
class_stack.push(class_name);
if let Some(body_node) = node.child_by_field_name("body") {
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
extract_field_types_recursive(child, content, import_map, field_types, class_stack);
}
}
class_stack.pop();
return; }
if node.kind() == "field_declaration" {
let is_final = has_modifier(node, "final", content);
let is_static = has_modifier(node, "static", content);
let visibility = if has_modifier(node, "public", content) {
Some(sqry_core::schema::Visibility::Public)
} else {
Some(sqry_core::schema::Visibility::Private)
};
if let Some(type_node) = node.child_by_field_name("type") {
let type_text = extract_type_name_internal(type_node, content);
if !type_text.is_empty() {
let resolved_type = import_map
.get(&type_text)
.cloned()
.unwrap_or(type_text.clone());
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
{
let field_name = extract_identifier(name_node, content);
let qualified_field = if class_stack.is_empty() {
field_name
} else {
let class_path = class_stack.join("::");
format!("{class_path}::{field_name}")
};
field_types.insert(
qualified_field,
(resolved_type.clone(), is_final, visibility, is_static),
);
}
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_field_types_recursive(child, content, import_map, field_types, class_stack);
}
}
fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
match type_node.kind() {
"generic_type" => {
if let Some(name_node) = type_node.child_by_field_name("name") {
extract_identifier(name_node, content)
} else {
extract_identifier(type_node, content)
}
}
"scoped_type_identifier" => {
extract_full_identifier(type_node, content)
}
_ => extract_identifier(type_node, content),
}
}
fn extract_identifier(node: Node, content: &[u8]) -> String {
node.utf8_text(content).unwrap_or("").to_string()
}
fn extract_node_text(node: Node, content: &[u8]) -> String {
node.utf8_text(content).unwrap_or("").to_string()
}
fn extract_full_identifier(node: Node, content: &[u8]) -> String {
node.utf8_text(content).unwrap_or("").to_string()
}
fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
let mut cursor = node.walk();
node.children(&mut cursor)
.find(|&child| child.kind() == kind)
}
fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
if let Some(name_node) = call_node.child_by_field_name("name") {
Ok(extract_identifier(name_node, content))
} else {
let mut cursor = call_node.walk();
for child in call_node.children(&mut cursor) {
if child.kind() == "identifier" {
return Ok(extract_identifier(child, content));
}
}
Err(GraphBuilderError::ParseError {
span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
reason: "Method invocation missing name".into(),
})
}
}
fn extract_type_name(type_node: Node, content: &[u8]) -> String {
match type_node.kind() {
"generic_type" => {
if let Some(name_node) = type_node.child_by_field_name("name") {
extract_identifier(name_node, content)
} else {
extract_identifier(type_node, content)
}
}
"scoped_type_identifier" => {
extract_full_identifier(type_node, content)
}
_ => extract_identifier(type_node, content),
}
}
fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
type_node.utf8_text(content).unwrap_or("").to_string()
}
fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifiers" {
let mut mod_cursor = child.walk();
for modifier_child in child.children(&mut mod_cursor) {
if extract_identifier(modifier_child, content) == modifier {
return true;
}
}
}
}
false
}
#[allow(clippy::unnecessary_wraps)]
fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
if has_modifier(node, "public", content) {
Some("public".to_string())
} else if has_modifier(node, "private", content) {
Some("private".to_string())
} else if has_modifier(node, "protected", content) {
Some("protected".to_string())
} else {
Some("package-private".to_string())
}
}
fn is_public(node: Node, content: &[u8]) -> bool {
has_modifier(node, "public", content)
}
fn is_private(node: Node, content: &[u8]) -> bool {
has_modifier(node, "private", content)
}
fn export_from_file_module(
helper: &mut GraphBuildHelper,
exported: sqry_core::graph::unified::node::NodeId,
) {
let module_id = helper.add_module(FILE_MODULE_NAME, None);
helper.add_export_edge(module_id, exported);
}
fn process_class_member_exports(
body_node: Node,
content: &[u8],
class_qualified_name: &str,
helper: &mut GraphBuildHelper,
is_interface: bool,
) {
for i in 0..body_node.child_count() {
#[allow(clippy::cast_possible_truncation)]
if let Some(child) = body_node.child(i as u32) {
match child.kind() {
"method_declaration" => {
let should_export = if is_interface {
!is_private(child, content)
} else {
is_public(child, content)
};
if should_export && let Some(name_node) = child.child_by_field_name("name") {
let method_name = extract_identifier(name_node, content);
let qualified_name = format!("{class_qualified_name}.{method_name}");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let is_static = has_modifier(child, "static", content);
let method_id =
helper.add_method(&qualified_name, Some(span), false, is_static);
export_from_file_module(helper, method_id);
}
}
"constructor_declaration" => {
if is_public(child, content) {
let qualified_name = format!("{class_qualified_name}.<init>");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let method_id =
helper.add_method(&qualified_name, Some(span), false, false);
export_from_file_module(helper, method_id);
}
}
"field_declaration" => {
if is_public(child, content) {
let mut cursor = child.walk();
for field_child in child.children(&mut cursor) {
if field_child.kind() == "variable_declarator"
&& let Some(name_node) = field_child.child_by_field_name("name")
{
let field_name = extract_identifier(name_node, content);
let qualified_name = format!("{class_qualified_name}.{field_name}");
let span = Span::from_bytes(
field_child.start_byte(),
field_child.end_byte(),
);
let is_final = has_modifier(child, "final", content);
let field_id = if is_final {
helper.add_constant(&qualified_name, Some(span))
} else {
helper.add_variable(&qualified_name, Some(span))
};
export_from_file_module(helper, field_id);
}
}
}
}
"constant_declaration" => {
let mut cursor = child.walk();
for const_child in child.children(&mut cursor) {
if const_child.kind() == "variable_declarator"
&& let Some(name_node) = const_child.child_by_field_name("name")
{
let const_name = extract_identifier(name_node, content);
let qualified_name = format!("{class_qualified_name}.{const_name}");
let span =
Span::from_bytes(const_child.start_byte(), const_child.end_byte());
let const_id = helper.add_constant(&qualified_name, Some(span));
export_from_file_module(helper, const_id);
}
}
}
"enum_constant" => {
if let Some(name_node) = child.child_by_field_name("name") {
let const_name = extract_identifier(name_node, content);
let qualified_name = format!("{class_qualified_name}.{const_name}");
let span = Span::from_bytes(child.start_byte(), child.end_byte());
let const_id = helper.add_constant(&qualified_name, Some(span));
export_from_file_module(helper, const_id);
}
}
_ => {}
}
}
}
}
fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
let mut has_jna = false;
let mut has_panama = false;
detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
(has_jna, has_panama)
}
fn detect_ffi_imports_recursive(
node: Node,
content: &[u8],
has_jna: &mut bool,
has_panama: &mut bool,
) {
if node.kind() == "import_declaration" {
let import_text = node.utf8_text(content).unwrap_or("");
if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
*has_jna = true;
}
if import_text.contains("java.lang.foreign") {
*has_panama = true;
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
detect_ffi_imports_recursive(child, content, has_jna, has_panama);
}
}
fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
let mut jna_interfaces = Vec::new();
find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
jna_interfaces
}
fn find_jna_library_interfaces_recursive(
node: Node,
content: &[u8],
jna_interfaces: &mut Vec<String>,
) {
if node.kind() == "interface_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
let interface_name = extract_identifier(name_node, content);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "extends_interfaces" {
let extends_text = child.utf8_text(content).unwrap_or("");
if extends_text.contains("Library") {
jna_interfaces.push(interface_name.clone());
}
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
find_jna_library_interfaces_recursive(child, content, jna_interfaces);
}
}
fn build_ffi_call_edge(
call_node: Node,
content: &[u8],
caller_context: &MethodContext,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
) -> bool {
let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
return false;
};
if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
let library_name = extract_jna_library_name(call_node, content);
build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
return true;
}
if ast_graph.has_jna_import
&& let Some(object_node) = call_node.child_by_field_name("object")
{
let object_text = extract_node_text(object_node, content);
let field_type = if let Some(class_name) = caller_context.class_stack.last() {
let qualified_field = format!("{class_name}::{object_text}");
ast_graph
.field_types
.get(&qualified_field)
.or_else(|| ast_graph.field_types.get(&object_text))
} else {
ast_graph.field_types.get(&object_text)
};
if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
let simple_type = simple_type_name(type_name);
if ast_graph.jna_library_interfaces.contains(&simple_type) {
build_jna_method_call_edge(
caller_context,
&simple_type,
&method_name,
call_node,
helper,
);
return true;
}
}
}
if ast_graph.has_panama_import {
if let Some(object_node) = call_node.child_by_field_name("object") {
let object_text = extract_node_text(object_node, content);
if object_text == "Linker" && method_name == "nativeLinker" {
build_panama_linker_edge(caller_context, call_node, helper);
return true;
}
if object_text == "SymbolLookup" && method_name == "libraryLookup" {
let library_name = extract_first_string_arg(call_node, content);
build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
return true;
}
if method_name == "invokeExact" || method_name == "invoke" {
if is_potential_panama_invoke(call_node, content) {
build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
return true;
}
}
}
if method_name == "nativeLinker" {
let full_text = call_node.utf8_text(content).unwrap_or("");
if full_text.contains("Linker") {
build_panama_linker_edge(caller_context, call_node, helper);
return true;
}
}
}
false
}
fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
if method_name != "load" && method_name != "loadLibrary" {
return false;
}
if let Some(object_node) = call_node.child_by_field_name("object") {
let object_text = extract_node_text(object_node, content);
return object_text == "Native" || object_text == "com.sun.jna.Native";
}
false
}
fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
if let Some(args_node) = call_node.child_by_field_name("arguments") {
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
if child.kind() == "string_literal" {
let text = child.utf8_text(content).unwrap_or("\"unknown\"");
return text.trim_matches('"').to_string();
}
}
}
"unknown".to_string()
}
fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
if let Some(args_node) = call_node.child_by_field_name("arguments") {
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
if child.kind() == "string_literal" {
let text = child.utf8_text(content).unwrap_or("\"unknown\"");
return text.trim_matches('"').to_string();
}
}
}
"unknown".to_string()
}
fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
if let Some(object_node) = call_node.child_by_field_name("object") {
let object_text = extract_node_text(object_node, content);
let lower = object_text.to_lowercase();
return lower.contains("handle")
|| lower.contains("downcall")
|| lower.contains("mh")
|| lower.contains("foreign");
}
false
}
fn simple_type_name(type_name: &str) -> String {
type_name
.rsplit('.')
.next()
.unwrap_or(type_name)
.to_string()
}
fn build_jna_native_load_edge(
caller_context: &MethodContext,
library_name: &str,
call_node: Node,
helper: &mut GraphBuildHelper,
) {
let caller_id = helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
);
let target_name = format!("native::{library_name}");
let target_id = helper.add_function(
&target_name,
Some(Span::from_bytes(
call_node.start_byte(),
call_node.end_byte(),
)),
false,
false,
);
helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
}
fn build_jna_method_call_edge(
caller_context: &MethodContext,
interface_name: &str,
method_name: &str,
call_node: Node,
helper: &mut GraphBuildHelper,
) {
let caller_id = helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
);
let target_name = format!("native::{interface_name}::{method_name}");
let target_id = helper.add_function(
&target_name,
Some(Span::from_bytes(
call_node.start_byte(),
call_node.end_byte(),
)),
false,
false,
);
helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
}
fn build_panama_linker_edge(
caller_context: &MethodContext,
call_node: Node,
helper: &mut GraphBuildHelper,
) {
let caller_id = helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
);
let target_name = "native::panama::nativeLinker";
let target_id = helper.add_function(
target_name,
Some(Span::from_bytes(
call_node.start_byte(),
call_node.end_byte(),
)),
false,
false,
);
helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
}
fn build_panama_library_lookup_edge(
caller_context: &MethodContext,
library_name: &str,
call_node: Node,
helper: &mut GraphBuildHelper,
) {
let caller_id = helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
);
let target_name = format!("native::panama::{library_name}");
let target_id = helper.add_function(
&target_name,
Some(Span::from_bytes(
call_node.start_byte(),
call_node.end_byte(),
)),
false,
false,
);
helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
}
fn build_panama_invoke_edge(
caller_context: &MethodContext,
method_name: &str,
call_node: Node,
helper: &mut GraphBuildHelper,
) {
let caller_id = helper.ensure_method(
caller_context.qualified_name(),
Some(Span::from_bytes(
caller_context.span.0,
caller_context.span.1,
)),
false,
caller_context.is_static,
);
let target_name = format!("native::panama::{method_name}");
let target_id = helper.add_function(
&target_name,
Some(Span::from_bytes(
call_node.start_byte(),
call_node.end_byte(),
)),
false,
false,
);
helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
}
fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
let method_id = helper.ensure_method(
method_context.qualified_name(),
Some(Span::from_bytes(
method_context.span.0,
method_context.span.1,
)),
false,
method_context.is_static,
);
let native_target = format!("native::jni::{}", method_context.qualified_name());
let target_id = helper.add_function(&native_target, None, false, false);
helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
}
fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
let mut cursor = method_node.walk();
let modifiers_node = method_node
.children(&mut cursor)
.find(|child| child.kind() == "modifiers")?;
let mut mod_cursor = modifiers_node.walk();
for annotation_node in modifiers_node.children(&mut mod_cursor) {
if annotation_node.kind() != "annotation" {
continue;
}
let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
continue;
};
let http_method: String = match annotation_name.as_str() {
"GetMapping" => "GET".to_string(),
"PostMapping" => "POST".to_string(),
"PutMapping" => "PUT".to_string(),
"DeleteMapping" => "DELETE".to_string(),
"PatchMapping" => "PATCH".to_string(),
"RequestMapping" => {
extract_request_mapping_method(annotation_node, content)
.unwrap_or_else(|| "GET".to_string())
}
_ => continue,
};
let Some(path) = extract_annotation_path(annotation_node, content) else {
continue;
};
return Some((http_method, path));
}
None
}
fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
let mut cursor = annotation_node.walk();
for child in annotation_node.children(&mut cursor) {
match child.kind() {
"identifier" => {
return Some(extract_identifier(child, content));
}
"scoped_identifier" => {
let full_text = extract_identifier(child, content);
return full_text.rsplit('.').next().map(String::from);
}
_ => {}
}
}
None
}
fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
let mut cursor = annotation_node.walk();
let args_node = annotation_node
.children(&mut cursor)
.find(|child| child.kind() == "annotation_argument_list")?;
let mut args_cursor = args_node.walk();
for arg_child in args_node.children(&mut args_cursor) {
match arg_child.kind() {
"string_literal" => {
return extract_string_content(arg_child, content);
}
"element_value_pair" => {
if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
return Some(path);
}
}
_ => {}
}
}
None
}
fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
let mut cursor = annotation_node.walk();
let args_node = annotation_node
.children(&mut cursor)
.find(|child| child.kind() == "annotation_argument_list")?;
let mut args_cursor = args_node.walk();
for arg_child in args_node.children(&mut args_cursor) {
if arg_child.kind() != "element_value_pair" {
continue;
}
let Some(key_node) = arg_child.child_by_field_name("key") else {
continue;
};
let key_text = extract_identifier(key_node, content);
if key_text != "method" {
continue;
}
let Some(value_node) = arg_child.child_by_field_name("value") else {
continue;
};
let value_text = extract_identifier(value_node, content);
if let Some(method) = value_text.rsplit('.').next() {
let method_upper = method.to_uppercase();
if matches!(
method_upper.as_str(),
"GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
) {
return Some(method_upper);
}
}
}
None
}
fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
let key_node = pair_node.child_by_field_name("key")?;
let key_text = extract_identifier(key_node, content);
if key_text != "path" && key_text != "value" {
return None;
}
let value_node = pair_node.child_by_field_name("value")?;
if value_node.kind() == "string_literal" {
return extract_string_content(value_node, content);
}
None
}
fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
let mut current = method_node.parent()?;
loop {
if current.kind() == "class_declaration" {
break;
}
current = current.parent()?;
}
let mut cursor = current.walk();
let modifiers = current
.children(&mut cursor)
.find(|child| child.kind() == "modifiers")?;
let mut mod_cursor = modifiers.walk();
for annotation in modifiers.children(&mut mod_cursor) {
if annotation.kind() != "annotation" {
continue;
}
let Some(name) = extract_annotation_name(annotation, content) else {
continue;
};
if name == "RequestMapping" {
return extract_annotation_path(annotation, content);
}
}
None
}
fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
let text = string_node.utf8_text(content).ok()?;
let trimmed = text.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
Some(trimmed[1..trimmed.len() - 1].to_string())
} else {
None
}
}