use crate::ir::reference::{RefKind, UnresolvedRef};
use crate::ir::{Symbol, SymbolId, SymbolKind, TextRange};
use crate::languages::LanguageAnalyzer;
use crate::languages::util::{byte_to_line, line_offsets};
pub struct SpecJavaAnalyzer;
impl SpecJavaAnalyzer {
pub fn new() -> Self {
Self
}
}
impl Default for SpecJavaAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl LanguageAnalyzer for SpecJavaAnalyzer {
fn language(&self) -> &'static str {
"java"
}
fn symbols_in_file(&self, path: &str, source: &str) -> Vec<Symbol> {
use regex::Regex;
use std::collections::HashSet;
let mut out = Vec::new();
let mut seen_ids: HashSet<String> = HashSet::new();
let mut class_names: HashSet<String> = HashSet::new();
let offs = line_offsets(source);
let re_type = Regex::new(
r"(?m)^[ \t]*(?:public|protected|private|abstract|final|static|sealed|non-sealed|strictfp|\s)*\b(class|interface|enum)\s+([A-Za-z_][A-Za-z0-9_]*)",
)
.expect("valid java type regex");
for caps in re_type.captures_iter(source) {
let Some(full) = caps.get(0) else {
continue;
};
let Some(kind_cap) = caps.get(1) else {
continue;
};
let Some(name_cap) = caps.get(2) else {
continue;
};
let kind = match kind_cap.as_str() {
"class" => SymbolKind::Struct,
"interface" => SymbolKind::Trait,
"enum" => SymbolKind::Enum,
_ => continue,
};
let name = name_cap.as_str();
class_names.insert(name.to_string());
let sl = byte_to_line(&offs, full.start());
let end_byte = find_decl_block_end(source, full.start()).unwrap_or(full.end());
let el = byte_to_line(&offs, end_byte.saturating_sub(1)).max(sl);
let id = SymbolId::new("java", path, &kind, name, sl);
if !seen_ids.insert(id.0.clone()) {
continue;
}
out.push(Symbol {
id,
name: name.to_string(),
kind,
file: path.to_string(),
range: TextRange {
start_line: sl,
end_line: el,
},
language: "java".to_string(),
});
}
let re_method = Regex::new(
r"(?m)^[ \t]*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s*)*(?:public|protected|private|static|final|abstract|synchronized|native|strictfp|default|\s)*(?:<[^>\n]+>\s*)?(?:[A-Za-z_][A-Za-z0-9_<>\[\].?]*(?:\s+[A-Za-z_][A-Za-z0-9_<>\[\].?]*)*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*(?:throws [^{;]+)?\{",
)
.expect("valid java method regex");
for caps in re_method.captures_iter(source) {
let Some(full) = caps.get(0) else {
continue;
};
let Some(name_cap) = caps.get(1) else {
continue;
};
let name = name_cap.as_str();
if is_java_control_like(name) {
continue;
}
let sl = byte_to_line(&offs, full.start());
let end_byte = find_decl_block_end(source, full.start()).unwrap_or(full.end());
let el = byte_to_line(&offs, end_byte.saturating_sub(1)).max(sl);
let id = SymbolId::new("java", path, &SymbolKind::Method, name, sl);
if !seen_ids.insert(id.0.clone()) {
continue;
}
out.push(Symbol {
id,
name: name.to_string(),
kind: SymbolKind::Method,
file: path.to_string(),
range: TextRange {
start_line: sl,
end_line: el,
},
language: "java".to_string(),
});
}
let re_ctor = Regex::new(
r"(?m)^[ \t]*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s*)*(?:public|protected|private|\s)+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*(?:throws [^{;]+)?\{",
)
.expect("valid java ctor regex");
for caps in re_ctor.captures_iter(source) {
let Some(full) = caps.get(0) else {
continue;
};
let Some(name_cap) = caps.get(1) else {
continue;
};
let name = name_cap.as_str();
if !class_names.contains(name) {
continue;
}
let sl = byte_to_line(&offs, full.start());
let end_byte = find_decl_block_end(source, full.start()).unwrap_or(full.end());
let el = byte_to_line(&offs, end_byte.saturating_sub(1)).max(sl);
let id = SymbolId::new("java", path, &SymbolKind::Method, name, sl);
if !seen_ids.insert(id.0.clone()) {
continue;
}
out.push(Symbol {
id,
name: name.to_string(),
kind: SymbolKind::Method,
file: path.to_string(),
range: TextRange {
start_line: sl,
end_line: el,
},
language: "java".to_string(),
});
}
out
}
fn unresolved_refs(&self, path: &str, source: &str) -> Vec<UnresolvedRef> {
use regex::Regex;
use std::collections::HashSet;
let mut out = Vec::new();
let mut seen: HashSet<(u32, String, Option<String>, bool)> = HashSet::new();
let offs = line_offsets(source);
let scan_source = mask_java_non_code(source);
let imports = self.imports_in_file(path, source);
let import_aliases: HashSet<String> = imports.keys().cloned().collect();
let syms = self.symbols_in_file(path, source);
let decl_lines: HashSet<u32> = syms
.iter()
.filter(|s| matches!(s.kind, SymbolKind::Method | SymbolKind::Function))
.map(|s| s.range.start_line)
.collect();
let class_name_set: HashSet<String> = syms
.iter()
.filter(|s| matches!(s.kind, SymbolKind::Struct | SymbolKind::Trait | SymbolKind::Enum))
.map(|s| s.name.clone())
.collect();
#[derive(Clone)]
struct JavaClassScope {
start_line: u32,
end_line: u32,
methods: std::collections::HashSet<String>,
}
let mut class_scopes: Vec<JavaClassScope> = syms
.iter()
.filter(|s| matches!(s.kind, SymbolKind::Struct | SymbolKind::Trait | SymbolKind::Enum))
.map(|s| JavaClassScope {
start_line: s.range.start_line,
end_line: s.range.end_line,
methods: std::collections::HashSet::new(),
})
.collect();
for m in syms
.iter()
.filter(|s| matches!(s.kind, SymbolKind::Method | SymbolKind::Function))
{
if let Some(scope) = class_scopes
.iter_mut()
.filter(|c| m.range.start_line >= c.start_line && m.range.start_line <= c.end_line)
.max_by_key(|c| c.start_line)
{
scope.methods.insert(m.name.clone());
}
}
let re_qualified_call =
Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)+)\s*\(")
.expect("valid java qualified call regex");
for caps in re_qualified_call.captures_iter(&scan_source) {
let Some(m) = caps.get(0) else {
continue;
};
let Some(chain_cap) = caps.get(1) else {
continue;
};
let ln = byte_to_line(&offs, m.start());
if decl_lines.contains(&ln) {
continue;
}
let compact = chain_cap.as_str().replace([' ', '\t', '\n'], "");
let mut parts = compact.split('.').collect::<Vec<_>>();
if parts.len() < 2 {
continue;
}
let Some(name) = parts.pop() else {
continue;
};
let qual = parts.join(".");
let first = qual.split('.').next().unwrap_or("");
let is_method = if first == "this" || first == "super" {
true
} else if import_aliases.contains(first) {
false
} else {
!first
.chars()
.next()
.map(|c| c.is_ascii_uppercase())
.unwrap_or(false)
};
let key = (ln, name.to_string(), Some(qual.clone()), is_method);
if !seen.insert(key) {
continue;
}
out.push(UnresolvedRef {
name: name.to_string(),
kind: RefKind::Call,
file: path.to_string(),
line: ln,
qualifier: Some(qual),
is_method,
});
}
let re_method_ref = Regex::new(
r"\b([A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*)\s*::\s*([A-Za-z_][A-Za-z0-9_]*)",
)
.expect("valid java method ref regex");
for caps in re_method_ref.captures_iter(&scan_source) {
let Some(m) = caps.get(0) else {
continue;
};
let Some(qual_cap) = caps.get(1) else {
continue;
};
let Some(name_cap) = caps.get(2) else {
continue;
};
let ln = byte_to_line(&offs, m.start());
if decl_lines.contains(&ln) {
continue;
}
let qual = qual_cap.as_str().replace([' ', '\t', '\n'], "");
let name = name_cap.as_str();
let first = qual.split('.').next().unwrap_or("");
let is_method = if first == "this" || first == "super" {
true
} else if import_aliases.contains(first) {
false
} else {
!first
.chars()
.next()
.map(|c| c.is_ascii_uppercase())
.unwrap_or(false)
};
let key = (ln, name.to_string(), Some(qual.clone()), is_method);
if !seen.insert(key) {
continue;
}
out.push(UnresolvedRef {
name: name.to_string(),
kind: RefKind::Call,
file: path.to_string(),
line: ln,
qualifier: Some(qual),
is_method,
});
}
let re_bare_call =
Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(").expect("valid java bare call regex");
let re_method_decl_line = Regex::new(
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s*)*(?:public|protected|private|static|final|abstract|synchronized|native|strictfp|default|\s)*(?:<[^>\n]+>\s*)?(?:[A-Za-z_][A-Za-z0-9_<>\[\].?]*(?:\s+[A-Za-z_][A-Za-z0-9_<>\[\].?]*)*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*(?:throws [^{;]+)?\{",
)
.expect("valid java method declaration line regex");
let re_ctor_decl_line = Regex::new(
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s*)*(?:public|protected|private|\s)+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;{]*\)\s*(?:throws [^{;]+)?\{",
)
.expect("valid java constructor declaration line regex");
for caps in re_bare_call.captures_iter(&scan_source) {
let Some(m) = caps.get(0) else {
continue;
};
let Some(name_cap) = caps.get(1) else {
continue;
};
let name = name_cap.as_str();
if is_java_control_like(name) {
continue;
}
if m.start() > 0 && source.as_bytes()[m.start().saturating_sub(1)] == b'.' {
continue;
}
let ln = byte_to_line(&offs, m.start());
if decl_lines.contains(&ln) {
continue;
}
if class_name_set.contains(name) {
continue;
}
let line_start = source[..m.start()].rfind('\n').map_or(0, |i| i + 1);
let line_end = source[m.start()..]
.find('\n')
.map_or(source.len(), |i| m.start() + i);
let line_text = &source[line_start..line_end];
if re_method_decl_line
.captures(line_text)
.and_then(|c| c.get(1).map(|m| m.as_str()))
.is_some_and(|decl_name| decl_name == name)
{
continue;
}
if re_ctor_decl_line
.captures(line_text)
.and_then(|c| c.get(1).map(|m| m.as_str()))
.is_some_and(|decl_name| decl_name == name)
{
continue;
}
let key = (ln, name.to_string(), None, false);
if !seen.insert(key) {
continue;
}
out.push(UnresolvedRef {
name: name.to_string(),
kind: RefKind::Call,
file: path.to_string(),
line: ln,
qualifier: None,
is_method: false,
});
}
if !class_scopes.is_empty() {
for r in out.clone() {
if r.is_method || r.qualifier.is_some() {
continue;
}
if decl_lines.contains(&r.line) {
continue;
}
let Some(scope) = class_scopes
.iter()
.filter(|c| r.line >= c.start_line && r.line <= c.end_line)
.max_by_key(|c| c.start_line)
else {
continue;
};
if !scope.methods.contains(&r.name) {
continue;
}
let key = (r.line, r.name.clone(), Some("this".to_string()), true);
if !seen.insert(key) {
continue;
}
out.push(UnresolvedRef {
name: r.name,
kind: RefKind::Call,
file: path.to_string(),
line: r.line,
qualifier: Some("this".to_string()),
is_method: true,
});
}
}
out
}
fn imports_in_file(
&self,
_path: &str,
source: &str,
) -> std::collections::HashMap<String, String> {
use regex::Regex;
use std::collections::HashMap;
let mut map = HashMap::new();
let re_import =
Regex::new(r"(?m)^\s*import\s+(static\s+)?([A-Za-z_][A-Za-z0-9_.]*)(\.\*)?\s*;")
.expect("valid java import regex");
for caps in re_import.captures_iter(source) {
let Some(path_cap) = caps.get(2) else {
continue;
};
let path = path_cap.as_str();
let norm = path.replace('.', "::");
let wildcard = caps.get(3).is_some();
if wildcard {
map.insert(format!("__glob__{norm}"), norm);
continue;
}
let alias = default_import_alias(path);
if alias.is_empty() {
continue;
}
map.insert(alias, norm);
}
map
}
}
fn mask_java_non_code(source: &str) -> String {
#[derive(Clone, Copy)]
enum State {
Code,
LineComment,
BlockComment,
String,
Char,
TextBlock,
}
let src = source.as_bytes();
let mut out = src.to_vec();
let mut i = 0usize;
let mut state = State::Code;
while i < src.len() {
match state {
State::Code => {
if i + 1 < src.len() && src[i] == b'/' && src[i + 1] == b'/' {
out[i] = b' ';
out[i + 1] = b' ';
i += 2;
state = State::LineComment;
continue;
}
if i + 1 < src.len() && src[i] == b'/' && src[i + 1] == b'*' {
out[i] = b' ';
out[i + 1] = b' ';
i += 2;
state = State::BlockComment;
continue;
}
if i + 2 < src.len() && src[i] == b'"' && src[i + 1] == b'"' && src[i + 2] == b'"'
{
out[i] = b' ';
out[i + 1] = b' ';
out[i + 2] = b' ';
i += 3;
state = State::TextBlock;
continue;
}
if src[i] == b'"' {
out[i] = b' ';
i += 1;
state = State::String;
continue;
}
if src[i] == b'\'' {
out[i] = b' ';
i += 1;
state = State::Char;
continue;
}
i += 1;
}
State::LineComment => {
if src[i] == b'\n' {
state = State::Code;
} else {
out[i] = b' ';
}
i += 1;
}
State::BlockComment => {
if i + 1 < src.len() && src[i] == b'*' && src[i + 1] == b'/' {
out[i] = b' ';
out[i + 1] = b' ';
i += 2;
state = State::Code;
continue;
}
if src[i] != b'\n' {
out[i] = b' ';
}
i += 1;
}
State::String => {
if src[i] == b'\\' {
out[i] = b' ';
if i + 1 < src.len() {
if src[i + 1] != b'\n' {
out[i + 1] = b' ';
}
i += 2;
} else {
i += 1;
}
continue;
}
if src[i] == b'"' {
out[i] = b' ';
i += 1;
state = State::Code;
continue;
}
if src[i] != b'\n' {
out[i] = b' ';
}
i += 1;
}
State::Char => {
if src[i] == b'\\' {
out[i] = b' ';
if i + 1 < src.len() {
if src[i + 1] != b'\n' {
out[i + 1] = b' ';
}
i += 2;
} else {
i += 1;
}
continue;
}
if src[i] == b'\'' {
out[i] = b' ';
i += 1;
state = State::Code;
continue;
}
if src[i] != b'\n' {
out[i] = b' ';
}
i += 1;
}
State::TextBlock => {
if i + 2 < src.len() && src[i] == b'"' && src[i + 1] == b'"' && src[i + 2] == b'"'
{
out[i] = b' ';
out[i + 1] = b' ';
out[i + 2] = b' ';
i += 3;
state = State::Code;
continue;
}
if src[i] != b'\n' {
out[i] = b' ';
}
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn is_java_control_like(name: &str) -> bool {
matches!(
name,
"if" | "for"
| "while"
| "switch"
| "catch"
| "return"
| "new"
| "throw"
| "synchronized"
| "assert"
| "try"
| "do"
| "else"
)
}
fn find_decl_block_end(source: &str, decl_start: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut i = decl_start;
while i < bytes.len() && bytes[i] != b'{' {
i += 1;
}
if i >= bytes.len() {
return None;
}
let mut depth = 0i32;
while i < bytes.len() {
let b = bytes[i];
if b == b'{' {
depth += 1;
} else if b == b'}' {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
i += 1;
}
Some(bytes.len())
}
fn default_import_alias(path: &str) -> String {
path.rsplit('.').next().unwrap_or(path).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_java_symbols_types_methods_and_ctor() {
let src = r#"package demo;
public class Service {
public Service() {}
public void handle(int v) {
helper(v);
}
static int helper(int v) {
return v;
}
}
interface Runner {
void run();
}
enum Mode {
ON, OFF;
}
"#;
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("src/Service.java", src);
assert!(
syms.iter()
.any(|s| s.name == "Service" && matches!(s.kind, SymbolKind::Struct))
);
assert!(
syms.iter()
.any(|s| s.name == "Runner" && matches!(s.kind, SymbolKind::Trait))
);
assert!(
syms.iter()
.any(|s| s.name == "Mode" && matches!(s.kind, SymbolKind::Enum))
);
assert!(
syms.iter()
.any(|s| s.name == "handle" && matches!(s.kind, SymbolKind::Method))
);
assert!(
syms.iter()
.any(|s| s.name == "helper" && matches!(s.kind, SymbolKind::Method))
);
}
#[test]
fn extract_java_unresolved_refs_method_and_qualified_calls() {
let src = r#"package demo;
import java.util.Collections;
class Service {
void run() {
this.handle();
Collections.sort(items);
helper();
}
void handle() {}
}
"#;
let ana = SpecJavaAnalyzer::new();
let refs = ana.unresolved_refs("src/Service.java", src);
assert!(refs.iter().any(|r| {
r.name == "handle"
&& r.qualifier.as_deref() == Some("this")
&& r.is_method
&& r.line == 7
}));
assert!(refs.iter().any(|r| {
r.name == "sort"
&& r.qualifier.as_deref() == Some("Collections")
&& !r.is_method
&& r.line == 8
}));
assert!(refs.iter().any(|r| {
r.name == "helper" && r.qualifier.is_none() && !r.is_method && r.line == 9
}));
}
#[test]
fn extract_java_imports_single_static_and_wildcard() {
let src = r#"package demo;
import java.util.List;
import java.util.*;
import static java.util.Collections.sort;
import static java.util.Collections.*;
"#;
let ana = SpecJavaAnalyzer::new();
let imports = ana.imports_in_file("src/Service.java", src);
assert_eq!(
imports.get("List").map(String::as_str),
Some("java::util::List")
);
assert_eq!(
imports.get("__glob__java::util").map(String::as_str),
Some("java::util")
);
assert_eq!(
imports.get("sort").map(String::as_str),
Some("java::util::Collections::sort")
);
assert_eq!(
imports
.get("__glob__java::util::Collections")
.map(String::as_str),
Some("java::util::Collections")
);
}
#[test]
fn java_unresolved_refs_ignores_comment_and_string_call_patterns() {
let src = r#"package demo;
class Demo {
String helper() { return "ok"; }
String run() {
String text = "helper()";
// helper()
/* helper() */
return text;
}
}
"#;
let ana = SpecJavaAnalyzer::new();
let refs = ana.unresolved_refs("demo/Demo.java", src);
assert!(
!refs.iter().any(|r| r.name == "helper"),
"refs={:?}",
refs.iter()
.map(|r| (&r.name, r.qualifier.as_deref(), r.line))
.collect::<Vec<_>>()
);
}
#[test]
fn extract_java_unresolved_refs_static_import_and_nested_qualified_call() {
let src = r#"package demo;
import static demo.Ops.pick;
public class Main {
static int run() {
return pick(1) + Outer.Inner.compute();
}
}
"#;
let ana = SpecJavaAnalyzer::new();
let refs = ana.unresolved_refs("demo/Main.java", src);
assert!(
refs.iter()
.any(|r| r.name == "pick" && r.qualifier.is_none() && !r.is_method)
);
assert!(refs.iter().any(|r| {
r.name == "compute" && r.qualifier.as_deref() == Some("Outer.Inner") && !r.is_method
}));
}
#[test]
fn java_hard_case_fixture_overload_static_import_nested() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/Parser.java", src);
let parse_count = syms
.iter()
.filter(|s| s.name == "parse" && matches!(s.kind, SymbolKind::Method))
.count();
assert_eq!(parse_count, 2, "expected overloaded parse methods");
assert!(
syms.iter()
.any(|s| s.name == "run" && matches!(s.kind, SymbolKind::Method))
);
let refs = ana.unresolved_refs("demo/Parser.java", src);
assert!(refs.iter().any(|r| {
r.name == "compute" && r.qualifier.as_deref() == Some("Outer.Inner") && !r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "parseInt" && r.qualifier.as_deref() == Some("Integer") && !r.is_method
}));
assert!(
refs.iter()
.any(|r| r.name == "emptyList" && r.qualifier.is_none() && !r.is_method)
);
assert!(
refs.iter()
.any(|r| { r.name == "requireNonNull" && r.qualifier.is_none() && !r.is_method })
);
let imports = ana.imports_in_file("demo/Parser.java", src);
assert_eq!(
imports.get("emptyList").map(String::as_str),
Some("java::util::Collections::emptyList")
);
assert_eq!(
imports.get("requireNonNull").map(String::as_str),
Some("java::util::Objects::requireNonNull")
);
}
#[test]
fn java_hard_case_fixture_v041_overload_static_import_nested_type() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases_v041.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/Engine.java", src);
assert!(
syms.iter()
.any(|s| s.name == "Engine" && matches!(s.kind, SymbolKind::Struct))
);
assert!(
syms.iter()
.any(|s| s.name == "Nested" && matches!(s.kind, SymbolKind::Struct))
);
let parse_count = syms
.iter()
.filter(|s| s.name == "parse" && matches!(s.kind, SymbolKind::Method))
.count();
assert_eq!(parse_count, 2, "expected overloaded parse methods");
assert!(
syms.iter()
.any(|s| s.name == "run" && matches!(s.kind, SymbolKind::Method))
);
let refs = ana.unresolved_refs("demo/Engine.java", src);
assert!(refs.iter().any(|r| {
r.name == "eval" && r.qualifier.as_deref() == Some("Engine.Nested") && !r.is_method
}));
assert!(
refs.iter()
.any(|r| r.name == "emptyList" && r.qualifier.is_none() && !r.is_method)
);
assert!(
refs.iter()
.any(|r| { r.name == "requireNonNull" && r.qualifier.is_none() && !r.is_method })
);
let imports = ana.imports_in_file("demo/Engine.java", src);
assert_eq!(
imports.get("emptyList").map(String::as_str),
Some("java::util::Collections::emptyList")
);
assert_eq!(
imports.get("requireNonNull").map(String::as_str),
Some("java::util::Objects::requireNonNull")
);
}
#[test]
fn java_hard_case_fixture_lambda_method_ref_inner_class_call() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases_lambda_methodref_inner.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/Flow.java", src);
assert!(
syms.iter()
.any(|s| s.name == "Flow" && matches!(s.kind, SymbolKind::Struct))
);
assert!(
syms.iter()
.any(|s| s.name == "run" && matches!(s.kind, SymbolKind::Method))
);
let parse_count = syms
.iter()
.filter(|s| s.name == "parse" && matches!(s.kind, SymbolKind::Method))
.count();
assert!(
parse_count >= 2,
"expected parse methods in outer and inner classes"
);
let refs = ana.unresolved_refs("demo/Flow.java", src);
assert!(refs.iter().any(|r| {
r.name == "parse" && r.qualifier.as_deref() == Some("this") && r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "compute" && r.qualifier.as_deref() == Some("inner") && r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "parse" && r.qualifier.as_deref() == Some("Outer.Inner") && !r.is_method
}));
assert!(
refs.iter()
.any(|r| r.name == "parse" && r.qualifier.is_none() && !r.is_method)
);
let imports = ana.imports_in_file("demo/Flow.java", src);
assert_eq!(
imports.get("Function").map(String::as_str),
Some("java::util::function::Function")
);
}
#[test]
fn java_hard_case_fixture_lambda_method_ref_overload_v73() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases_lambda_methodref_overload.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/OverloadLab.java", src);
assert!(
syms.iter()
.any(|s| s.name == "OverloadLab" && matches!(s.kind, SymbolKind::Struct))
);
assert!(syms.iter().any(|s| {
s.name == "run"
&& matches!(s.kind, SymbolKind::Method)
&& s.id.0 == "java:demo/OverloadLab.java:method:run:23"
}));
let parse_count = syms
.iter()
.filter(|s| s.name == "parse" && matches!(s.kind, SymbolKind::Method))
.count();
assert!(parse_count >= 3, "expected overloaded parse methods");
let refs = ana.unresolved_refs("demo/OverloadLab.java", src);
assert!(refs.iter().any(|r| {
r.name == "parse" && r.qualifier.as_deref() == Some("this") && r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "parseStatic"
&& r.qualifier.as_deref() == Some("OverloadLab")
&& !r.is_method
}));
assert!(
refs.iter()
.any(|r| r.name == "parse" && r.qualifier.is_none() && !r.is_method)
);
let imports = ana.imports_in_file("demo/OverloadLab.java", src);
assert_eq!(
imports.get("Function").map(String::as_str),
Some("java::util::function::Function")
);
assert_eq!(
imports.get("BiFunction").map(String::as_str),
Some("java::util::function::BiFunction")
);
}
#[test]
fn java_hard_case_fixture_lambda_method_ref_overload_v2() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases_lambda_methodref_overload_v2.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/JavaOverloadLabV2.java", src);
assert!(syms.iter().any(|s| {
s.name == "JavaOverloadLabV2" && matches!(s.kind, SymbolKind::Struct)
}));
assert!(
syms.iter()
.any(|s| s.name == "run" && matches!(s.kind, SymbolKind::Method))
);
let decode_count = syms
.iter()
.filter(|s| s.name == "decode" && matches!(s.kind, SymbolKind::Method))
.count();
assert!(decode_count >= 3, "expected overloaded decode methods");
let refs = ana.unresolved_refs("demo/JavaOverloadLabV2.java", src);
let refs_dbg: Vec<_> = refs
.iter()
.map(|r| match &r.qualifier {
Some(q) => format!("{}@{}[{}]/{}", r.name, r.line, q, r.is_method),
None => format!("{}@{}/{}", r.name, r.line, r.is_method),
})
.collect();
assert!(
refs.iter().any(|r| {
r.name == "decode" && r.qualifier.as_deref() == Some("this") && r.is_method
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().any(|r| {
r.name == "decode"
&& r.qualifier.as_deref() == Some("this")
&& r.is_method
&& r.line == 31
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().any(|r| {
r.name == "decode"
&& r.qualifier.as_deref() == Some("this")
&& r.is_method
&& r.line == 32
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().any(|r| {
r.name == "decode"
&& r.qualifier.as_deref() == Some("this")
&& r.is_method
&& r.line == 33
}),
"refs={refs_dbg:?}"
);
assert!(refs.iter().any(|r| {
r.name == "decode" && r.qualifier.as_deref() == Some("codec") && r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "decodeStatic"
&& r.qualifier.as_deref() == Some("JavaOverloadLabV2")
&& !r.is_method
}));
assert!(refs.iter().any(|r| {
r.name == "decode" && r.qualifier.is_none() && !r.is_method
}));
assert!(
refs.iter().all(|r| {
let on_decl_line = matches!(r.line, 12 | 16 | 20 | 24);
if !on_decl_line {
return true;
}
if !matches!(r.name.as_str(), "decode" | "decodeStatic" | "run") {
return true;
}
!(r.qualifier.is_none() || r.qualifier.as_deref() == Some("this"))
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().all(|r| !(r.name == "JavaOverloadLabV2" && r.qualifier.is_none())),
"refs={refs_dbg:?}"
);
let imports = ana.imports_in_file("demo/JavaOverloadLabV2.java", src);
assert_eq!(
imports.get("Function").map(String::as_str),
Some("java::util::function::Function")
);
assert_eq!(
imports.get("BiFunction").map(String::as_str),
Some("java::util::function::BiFunction")
);
}
#[test]
fn java_hard_case_fixture_extraction_fp_points_v4() {
let src = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/java/analyzer_hard_cases_extraction_fp_points_v4.java"
));
let ana = SpecJavaAnalyzer::new();
let syms = ana.symbols_in_file("demo/ExtractionFpPointsV4.java", src);
assert!(syms.iter().any(|s| {
s.name == "ExtractionFpPointsV4" && matches!(s.kind, SymbolKind::Struct)
}));
assert!(
syms.iter()
.any(|s| s.name == "run" && matches!(s.kind, SymbolKind::Method))
);
let refs = ana.unresolved_refs("demo/ExtractionFpPointsV4.java", src);
let refs_dbg: Vec<_> = refs
.iter()
.map(|r| match &r.qualifier {
Some(q) => format!("{}@{}[{}]/{}", r.name, r.line, q, r.is_method),
None => format!("{}@{}/{}", r.name, r.line, r.is_method),
})
.collect();
assert!(
refs.iter().any(|r| {
r.name == "dispatch" && r.qualifier.as_deref() == Some("router") && r.is_method
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().any(|r| {
r.name == "invoke" && r.qualifier.is_none() && !r.is_method
}),
"refs={refs_dbg:?}"
);
assert!(
refs.iter().any(|r| {
r.name == "apply" && r.qualifier.as_deref() == Some("fn") && r.is_method
}),
"refs={refs_dbg:?}"
);
assert!(
!refs.iter().any(|r| r.name == "missing"),
"refs={refs_dbg:?}"
);
assert!(
!refs.iter().any(|r| r.name == "helper"),
"refs={refs_dbg:?}"
);
let imports = ana.imports_in_file("demo/ExtractionFpPointsV4.java", src);
assert_eq!(
imports.get("Function").map(String::as_str),
Some("java::util::function::Function")
);
}
}