Skip to main content

sqry_lang_java/relations/
java_common.rs

1//! Java relation extraction helpers
2//! Migrated from relations-shared/src/hooks/java/common.rs
3
4use sqry_lang_support::relations::{build_qualified_name, normalize_type_signature};
5use std::path::{Path, PathBuf};
6use tree_sitter::{Node, Tree};
7
8/// Context for Java relation extraction
9#[derive(Debug)]
10pub struct JavaRelationContext<'a> {
11    /// Path to the file currently being processed.
12    pub file: &'a Path,
13    /// Optional package declaration discovered in the file.
14    pub package_name: Option<String>,
15}
16
17impl<'a> JavaRelationContext<'a> {
18    /// Construct a new context with an optional package name.
19    #[must_use]
20    pub fn new(file: &'a Path, package_name: Option<String>) -> Self {
21        Self { file, package_name }
22    }
23}
24
25/// Package resolution for Java symbols
26#[derive(Debug, Clone)]
27pub struct PackageResolver {
28    source_root: PathBuf,
29}
30
31impl PackageResolver {
32    /// Create a resolver rooted at the provided source directory.
33    pub fn new(source_root: impl Into<PathBuf>) -> Self {
34        Self {
35            source_root: source_root.into(),
36        }
37    }
38
39    /// Convert a Java package name (e.g., `com.example.app`) into a relative path.
40    #[must_use]
41    pub fn package_to_path(&self, package_name: &str) -> PathBuf {
42        let relative = package_name.replace('.', std::path::MAIN_SEPARATOR_STR);
43        self.source_root.join(relative)
44    }
45
46    /// Extract package name directly from the AST, when present.
47    #[must_use]
48    pub fn package_from_ast(tree: &Tree, content: &[u8]) -> Option<String> {
49        let root = tree.root_node();
50        let mut cursor = root.walk();
51        for child in root.named_children(&mut cursor) {
52            if child.kind() != "package_declaration" {
53                continue;
54            }
55            if let Some(candidate) = extract_package_name(child, content) {
56                return Some(candidate);
57            }
58        }
59        None
60    }
61}
62
63fn extract_package_name(node: Node<'_>, content: &[u8]) -> Option<String> {
64    if let Some(candidate) = node
65        .child_by_field_name("name")
66        .and_then(|name_node| extract_package_candidate(name_node, content))
67    {
68        return Some(candidate);
69    }
70
71    let mut name_cursor = node.walk();
72    node.named_children(&mut name_cursor)
73        .filter(|child| matches!(child.kind(), "scoped_identifier" | "identifier"))
74        .find_map(|child| extract_package_candidate(child, content))
75}
76
77fn extract_package_candidate(node: Node<'_>, content: &[u8]) -> Option<String> {
78    node.utf8_text(content)
79        .ok()
80        .and_then(normalize_package_name)
81}
82
83fn normalize_package_name(text: &str) -> Option<String> {
84    let candidate: String = text.chars().filter(|c| !c.is_whitespace()).collect();
85    if candidate.is_empty() {
86        None
87    } else {
88        Some(candidate)
89    }
90}
91
92/// Build a member symbol (method, field)
93#[must_use]
94pub fn build_member_symbol(package: Option<&str>, class_stack: &[String], member: &str) -> String {
95    build_qualified_name(
96        package,
97        class_stack.iter().map(std::string::String::as_str),
98        member,
99    )
100}
101
102/// Build a symbol (class, interface)
103#[must_use]
104pub fn build_symbol(package: Option<&str>, class_stack: &[String], name: &str) -> String {
105    build_qualified_name(
106        package,
107        class_stack.iter().map(std::string::String::as_str),
108        name,
109    )
110}
111
112/// Normalize Java type names
113#[must_use]
114pub fn normalize_type_name(node: &Node, content: &[u8]) -> String {
115    let raw = node.utf8_text(content).unwrap_or_default();
116    normalize_type_signature(raw)
117}