use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, FileFacts, RefRole, Reference, Scope, ScopeId,
ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, import_bindings, innermost_scope, make_symbol, node_span,
node_text, one_line_signature, push_binding, push_ref, push_scope, push_type_ref,
simple_type_name,
};
const CALL_QUERY: &str = r#"
[
(method_invocation name: (identifier) @callee)
(object_creation_expression type: (type_identifier) @callee)
]
"#;
const JAVA_ROUTE_ANNOTATIONS: &[&str] = &[
"RestController",
"Controller",
"RequestMapping",
"GetMapping",
"PostMapping",
"PutMapping",
"DeleteMapping",
"PatchMapping",
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"OPTIONS",
"Path",
];
fn entry_points_for_java(method_name: Option<&str>, node: &Node, bytes: &[u8]) -> Vec<EntryPoint> {
let mut markers: Vec<EntryPoint> = Vec::new();
if method_name == Some("main") && has_modifier(node, bytes, "static") {
markers.push(EntryPoint::Main);
}
let Some(mods) = node
.children(&mut node.walk())
.find(|c| c.kind() == "modifiers")
else {
return markers;
};
for ann in mods.children(&mut mods.walk()) {
let kind = ann.kind();
if kind != "marker_annotation" && kind != "annotation" {
continue;
}
let Some(name_node) = ann.child_by_field_name("name") else {
continue;
};
let raw = node_text(&name_node, bytes);
let simple = simple_type_name(raw, ".");
if JAVA_ROUTE_ANNOTATIONS.contains(&simple) {
markers.push(EntryPoint::HttpRoute(simple.to_owned()));
}
}
markers
}
pub struct JavaExtractor;
impl Extractor for JavaExtractor {
fn lang(&self) -> Language {
Language::Java
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::java();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let namespaces = java_namespaces(&root, bytes, file);
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Java,
};
let defs = collect_symbols(&root, &ctx, &namespaces);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::Java, &namespaces, file, source.len());
let module_id = mod_sym.id.to_scip_string();
symbols.push(mod_sym);
let mut references =
collect_call_references(&root, &ts_language, CALL_QUERY, Language::Java, bytes, file)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references, &module_id);
collect_jni_natives(&root, bytes, file, &namespaces, &mut references);
collect_type_references(&root, bytes, file, &mut references);
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::Java.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn java_namespaces(root: &Node, bytes: &[u8], file: &str) -> Vec<String> {
for child in root.children(&mut root.walk()) {
if child.kind() != "package_declaration" {
continue;
}
for pkg_child in child.children(&mut child.walk()) {
match pkg_child.kind() {
"scoped_identifier" | "identifier" => {
let text = node_text(&pkg_child, bytes);
return text
.split('.')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
}
_ => {}
}
}
}
let p = file.strip_suffix(".java").unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn collect_symbols(root: &Node, ctx: &ExtractCtx, namespaces: &[String]) -> Vec<Symbol> {
let mut out = Vec::new();
for child in root.children(&mut root.walk()) {
let type_kind = match child.kind() {
k @ ("class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
| "annotation_type_declaration") => k,
_ => continue,
};
let Some(type_name) = field_text(&child, "name", ctx.bytes) else {
continue;
};
let type_sym_kind = match type_kind {
"class_declaration" | "record_declaration" => SymbolKind::Class,
"interface_declaration" | "annotation_type_declaration" => SymbolKind::Interface,
"enum_declaration" => SymbolKind::Enum,
_ => SymbolKind::Class,
};
let mut type_descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
type_descriptors.push(Descriptor::Type(type_name.clone()));
let mut type_sym = make_symbol(
ctx,
&child,
type_name.clone(),
type_sym_kind,
read_visibility(&child, ctx.bytes, false),
type_descriptors,
one_line_signature(node_text(&child, ctx.bytes), &['{', ';']),
);
type_sym.entry_points = entry_points_for_java(None, &child, ctx.bytes);
out.push(type_sym);
let implicit_public = matches!(
type_kind,
"interface_declaration" | "annotation_type_declaration"
);
let Some(body) = child.child_by_field_name("body") else {
continue;
};
collect_members(
&body,
ctx,
namespaces,
&type_name,
implicit_public,
&mut out,
);
}
out
}
fn collect_members(
body: &Node,
ctx: &ExtractCtx,
namespaces: &[String],
type_name: &str,
implicit_public: bool,
out: &mut Vec<Symbol>,
) {
for member in body.children(&mut body.walk()) {
match member.kind() {
"enum_body_declarations" => {
collect_members(&member, ctx, namespaces, type_name, implicit_public, out);
}
"method_declaration" | "constructor_declaration" => {
let Some(name) = field_text(&member, "name", ctx.bytes) else {
continue;
};
let mut descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
descriptors.push(Descriptor::Type(type_name.to_owned()));
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
let mut method_sym = make_symbol(
ctx,
&member,
name.clone(),
SymbolKind::Method,
read_visibility(&member, ctx.bytes, implicit_public),
descriptors,
one_line_signature(node_text(&member, ctx.bytes), &['{', ';']),
);
method_sym.entry_points = entry_points_for_java(Some(&name), &member, ctx.bytes);
out.push(method_sym);
}
"field_declaration" => {
let field_vis = read_visibility(&member, ctx.bytes, implicit_public);
let mut cursor = member.walk();
for declarator in member.children_by_field_name("declarator", &mut cursor) {
let Some(var_name) = field_text(&declarator, "name", ctx.bytes) else {
continue;
};
let mut descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
descriptors.push(Descriptor::Type(type_name.to_owned()));
descriptors.push(Descriptor::Term(var_name.clone()));
out.push(make_symbol(
ctx,
&member,
var_name,
SymbolKind::Static,
field_vis,
descriptors,
one_line_signature(node_text(&member, ctx.bytes), &['{', ';']),
));
}
}
_ => {}
}
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"class_declaration" => {
if let Some(superclass_node) = node.child_by_field_name("superclass") {
if let Some(type_node) = superclass_node
.children(&mut superclass_node.walk())
.find(|c| c.is_named())
{
super::push_ref(
out,
super::simple_type_name(node_text(&type_node, bytes), "."),
&type_node,
file,
RefRole::IsImplementation,
);
}
}
if let Some(ifaces_node) = node.child_by_field_name("interfaces") {
push_type_list_refs(&ifaces_node, bytes, file, out);
}
}
"interface_declaration" => {
if let Some(extends_node) = node
.children(&mut node.walk())
.find(|c| c.kind() == "extends_interfaces")
{
push_type_list_refs(&extends_node, bytes, file, out);
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn push_type_list_refs(container: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
let Some(type_list) = container
.children(&mut container.walk())
.find(|c| c.kind() == "type_list")
else {
return;
};
for type_node in type_list.children(&mut type_list.walk()) {
if type_node.is_named() {
super::push_ref(
out,
super::simple_type_name(node_text(&type_node, bytes), "."),
&type_node,
file,
RefRole::IsImplementation,
);
}
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "import_declaration" {
let mut scoped: Option<Node> = None;
let mut bare: Option<Node> = None;
let mut has_wildcard = false;
for child in node.children(&mut node.walk()) {
match child.kind() {
"asterisk" => has_wildcard = true,
"scoped_identifier" if scoped.is_none() => scoped = Some(child),
"identifier" if bare.is_none() => bare = Some(child),
_ => {}
}
}
if !has_wildcard {
if let Some(child) = scoped {
if let Some(name_node) = child.child_by_field_name("name") {
let name = super::node_text(&name_node, bytes);
let from_path = child
.child_by_field_name("scope")
.map_or("", |n| super::node_text(&n, bytes));
super::push_import_ref(out, name, &name_node, file, module_id, from_path);
}
} else if let Some(child) = bare {
let name = super::node_text(&child, bytes);
super::push_import_ref(out, name, &child, file, module_id, "");
}
}
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn has_modifier(node: &Node, bytes: &[u8], keyword: &str) -> bool {
node.children(&mut node.walk())
.find(|c| c.kind() == "modifiers")
.is_some_and(|mods| {
mods.children(&mut mods.walk())
.any(|m| node_text(&m, bytes) == keyword)
})
}
fn read_visibility(node: &Node, bytes: &[u8], implicit_public: bool) -> Visibility {
if has_modifier(node, bytes, "private") {
return Visibility::Private;
}
if has_modifier(node, bytes, "protected") {
return Visibility::Protected;
}
if has_modifier(node, bytes, "public") {
return Visibility::Public;
}
if implicit_public {
Visibility::Public
} else {
Visibility::Internal
}
}
fn collect_jni_natives(
root: &Node,
bytes: &[u8],
file: &str,
namespaces: &[String],
out: &mut Vec<Reference>,
) {
for ty in root.children(&mut root.walk()) {
if !matches!(
ty.kind(),
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
) {
continue;
}
let Some(class) = field_text(&ty, "name", bytes) else {
continue;
};
let Some(body) = ty.child_by_field_name("body") else {
continue;
};
for member in body.children(&mut body.walk()) {
if member.kind() != "method_declaration" || !has_modifier(&member, bytes, "native") {
continue;
}
let Some(name_node) = member.child_by_field_name("name") else {
continue;
};
let method = node_text(&name_node, bytes);
let mangled = jni_mangle(namespaces, &class, method);
push_ref(out, &mangled, &name_node, file, RefRole::Call);
}
}
}
fn jni_mangle(namespaces: &[String], class: &str, method: &str) -> String {
if namespaces.is_empty() {
format!("Java_{class}_{method}")
} else {
format!("Java_{}_{}_{}", namespaces.join("_"), class, method)
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
fn type_leaf(
node: &Node,
bytes: &[u8],
file: &str,
ctx: TypeRefContext,
out: &mut Vec<Reference>,
) {
match node.kind() {
"type_identifier" => {
let name = node_text(node, bytes);
push_type_ref(out, name, node, file, ctx);
}
"generic_type" => {
if let Some(base) = node.named_children(&mut node.walk()).next() {
type_leaf(&base, bytes, file, ctx, out);
}
if let Some(args) = node
.children(&mut node.walk())
.find(|c| c.kind() == "type_arguments")
{
for child in args.named_children(&mut args.walk()) {
if child.kind() == "wildcard" {
for wc_child in child.named_children(&mut child.walk()) {
type_leaf(&wc_child, bytes, file, TypeRefContext::GenericArg, out);
}
} else {
type_leaf(&child, bytes, file, TypeRefContext::GenericArg, out);
}
}
}
}
"scoped_type_identifier" => {
if let Some(name_node) = node.child_by_field_name("name") {
type_leaf(&name_node, bytes, file, ctx, out);
}
}
"array_type" => {
if let Some(element) = node.child_by_field_name("element") {
type_leaf(&element, bytes, file, ctx, out);
}
}
"integral_type"
| "floating_point_type"
| "boolean_type"
| "void_type"
| "annotated_type" => {}
_ => {}
}
}
match node.kind() {
"formal_parameter" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::ParameterType, out);
}
}
"spread_parameter" => {
if let Some(type_node) = node.named_children(&mut node.walk()).next() {
type_leaf(&type_node, bytes, file, TypeRefContext::ParameterType, out);
}
}
"method_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::ReturnType, out);
}
}
"field_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
type_leaf(&type_node, bytes, file, TypeRefContext::Field, out);
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
const DECL_KINDS_WITH_NAME: &[&str] = &[
"class_declaration",
"interface_declaration",
"enum_declaration",
"record_declaration",
"annotation_type_declaration",
"method_declaration",
"constructor_declaration",
"compact_constructor_declaration",
];
fn is_non_read_position(node: &Node) -> bool {
let mut ancestor = node.parent();
while let Some(p) = ancestor {
match p.kind() {
"package_declaration" | "import_declaration" => return true,
"scoped_identifier" | "scoped_type_identifier" => ancestor = p.parent(),
_ => break,
}
}
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"method_invocation" => parent.child_by_field_name("name").as_ref() == Some(node),
kind if DECL_KINDS_WITH_NAME.contains(&kind) => {
parent.child_by_field_name("name").as_ref() == Some(node)
}
"variable_declarator" => parent.child_by_field_name("name").as_ref() == Some(node),
"formal_parameter" => parent.child_by_field_name("name").as_ref() == Some(node),
"field_access" => parent.child_by_field_name("field").as_ref() == Some(node),
"assignment_expression" => parent.child_by_field_name("left").as_ref() == Some(node),
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if matches!(node.kind(), "assignment_expression" | "operator_assignment") {
if let Some(lhs) = node.child_by_field_name("left") {
if lhs.kind() == "identifier" {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
| "annotation_type_declaration" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
}
"method_declaration" | "constructor_declaration" | "compact_constructor_declaration" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"lambda_expression" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
if body.kind() == "block" {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
} else {
scope_dfs(&body, fn_id, scopes);
}
}
}
"block" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"method_declaration" | "constructor_declaration" | "compact_constructor_declaration" => {
if let Some(params) = node.child_by_field_name("parameters") {
collect_params(¶ms, bytes, scopes, out);
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"lambda_expression" => {
if let Some(params_node) = node.child_by_field_name("parameters") {
match params_node.kind() {
"formal_parameters" => {
collect_params(¶ms_node, bytes, scopes, out);
}
"inferred_parameters" => {
for child in params_node.named_children(&mut params_node.walk()) {
if child.kind() == "identifier" {
let name = node_text(&child, bytes);
let intro = child.start_byte();
push_binding(
out,
name.to_owned(),
intro,
BindingKind::Param,
scopes,
);
}
}
}
"identifier" => {
let name = node_text(¶ms_node, bytes);
let intro = params_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
_ => {}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"local_variable_declaration" => {
let mut cursor = node.walk();
for declarator in node.children_by_field_name("declarator", &mut cursor) {
if let Some(name_node) = declarator.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
if innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"enhanced_for_statement" => {
if let Some(name_node) = node.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
if innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"catch_formal_parameter" => {
if let Some(name_node) = node.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
_ => {
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
}
fn collect_params(params: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params.named_children(&mut params.walk()) {
match child.kind() {
"formal_parameter" => {
if let Some(name_node) = child.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
}
"spread_parameter" => {
for grandchild in child.named_children(&mut child.walk()) {
if grandchild.kind() == "variable_declarator" {
if let Some(name_node) = grandchild.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
}
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_all_types_and_members_with_visibility() {
let src = r#"package com.example.auth;
public class SessionManager {
public boolean validate(String token) { return true; }
private void secret() {}
int packagePrivate;
}
class Helper {}
"#;
let facts = JavaExtractor
.extract(src, "src/com/example/auth/SessionManager.java")
.unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let sm = by_name("SessionManager").unwrap();
assert_eq!(sm.kind, SymbolKind::Class);
assert_eq!(sm.visibility, Visibility::Public);
assert_eq!(
sm.id.to_scip_string(),
"codegraph . . . com/example/auth/SessionManager#"
);
let validate = by_name("validate").unwrap();
assert_eq!(validate.kind, SymbolKind::Method);
assert_eq!(validate.visibility, Visibility::Public);
assert_eq!(
validate.id.to_scip_string(),
"codegraph . . . com/example/auth/SessionManager#validate()."
);
let secret = by_name("secret").unwrap();
assert_eq!(secret.visibility, Visibility::Private);
let pkg_priv = by_name("packagePrivate").unwrap();
assert_eq!(pkg_priv.visibility, Visibility::Internal);
let helper = by_name("Helper").unwrap();
assert_eq!(helper.kind, SymbolKind::Class);
assert_eq!(helper.visibility, Visibility::Internal);
assert_eq!(facts.lang, "java");
}
#[test]
fn interface_members_are_public() {
let src = r#"package io.svc;
public interface Reader {
int read();
void close();
}
"#;
let facts = JavaExtractor
.extract(src, "src/io/svc/Reader.java")
.unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let reader = by_name("Reader").unwrap();
assert_eq!(reader.kind, SymbolKind::Interface);
assert_eq!(reader.id.to_scip_string(), "codegraph . . . io/svc/Reader#");
let read = by_name("read").unwrap();
assert_eq!(read.kind, SymbolKind::Method);
assert_eq!(read.visibility, Visibility::Public);
assert_eq!(
read.id.to_scip_string(),
"codegraph . . . io/svc/Reader#read()."
);
let close = by_name("close").unwrap();
assert_eq!(close.kind, SymbolKind::Method);
assert_eq!(close.visibility, Visibility::Public);
assert_eq!(
close.id.to_scip_string(),
"codegraph . . . io/svc/Reader#close()."
);
}
#[test]
fn extracts_call_references() {
let src = r#"package com.example;
public class Client {
public void run() {
validate("t");
new Server();
}
}
"#;
let facts = JavaExtractor
.extract(src, "src/com/example/Client.java")
.unwrap();
let names: Vec<&str> = facts.references.iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"validate"),
"expected 'validate' in {names:?}"
);
assert!(names.contains(&"Server"), "expected 'Server' in {names:?}");
}
#[test]
fn extracts_class_inheritance_references() {
let src = "package p; public class Foo extends Bar implements Baz {}";
let facts = JavaExtractor.extract(src, "src/p/Foo.java").unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Bar"),
"expected 'Bar' in {inherit_names:?}"
);
assert!(
inherit_names.contains(&"Baz"),
"expected 'Baz' in {inherit_names:?}"
);
}
#[test]
fn extracts_interface_extends_reference() {
let src = "package p; public interface I extends J {}";
let facts = JavaExtractor.extract(src, "src/p/I.java").unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"J"),
"expected 'J' in {inherit_names:?}"
);
}
#[test]
fn extracts_named_import_reference() {
let src = "import com.example.Service;\nclass A {}";
let facts = JavaExtractor.extract(src, "src/A.java").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_names.contains(&"Service"),
"expected 'Service' in {import_names:?}"
);
assert_eq!(
import_names.len(),
1,
"unexpected extra imports: {import_names:?}"
);
}
#[test]
fn extracts_static_import_reference() {
let src = "import static com.x.Util.helper;\nclass A {}";
let facts = JavaExtractor.extract(src, "src/A.java").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_names.contains(&"helper"),
"expected 'helper' in {import_names:?}"
);
assert_eq!(
import_names.len(),
1,
"unexpected extra imports: {import_names:?}"
);
}
#[test]
fn wildcard_import_emits_no_reference() {
let src = "import com.x.*;\nclass A {}";
let facts = JavaExtractor.extract(src, "src/A.java").unwrap();
let import_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_refs.is_empty(),
"expected no import refs but got: {import_refs:?}"
);
}
#[test]
fn import_refs_carry_source_module() {
let src = "import com.example.Service;\nclass A {}";
let file = "src/com/example/A.java";
let facts = JavaExtractor.extract(src, file).unwrap();
let p = file.strip_suffix(".java").unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
let namespaces: Vec<String> = p
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
let expected_module_id =
crate::extract::module_symbol(Language::Java, &namespaces, file, src.len())
.id
.to_scip_string();
let import_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.collect();
assert!(!import_refs.is_empty(), "expected at least one Import ref");
for r in &import_refs {
assert_eq!(
r.source_module,
Some(expected_module_id.clone()),
"Import ref '{}' should carry source_module = {:?}",
r.name,
expected_module_id
);
}
}
#[test]
fn package_declaration_emits_no_read_refs() {
let src = "package com.example;\n\
import com.example.alpha.Service;\n\
class Main { int run() { return Service.helper(); } }";
let facts = JavaExtractor
.extract(src, "src/com/example/Main.java")
.unwrap();
for seg in ["com", "example", "alpha"] {
assert!(
!facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == seg),
"package/import path segment '{seg}' must not be a Read ref; refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
assert!(
facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == "Service"),
"the `Service` receiver in `Service.helper()` should remain a Read ref"
);
}
#[test]
fn named_import_carries_from_path() {
let src = "import com.example.Service;\nclass A {}";
let facts = JavaExtractor.extract(src, "src/A.java").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Import && r.name == "Service")
.expect("expected Import ref for 'Service'");
assert_eq!(
r.from_path,
Some("com.example".to_owned()),
"from_path should be 'com.example', got {:?}",
r.from_path
);
}
#[test]
fn method_params_emit_param_bindings() {
let src = "package p;\npublic class C { public void f(int a, String b){} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let mut param_names: Vec<(&str, ScopeId)> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| (b.name.as_str(), b.scope))
.collect();
param_names.sort_by_key(|(n, _)| *n);
assert_eq!(
param_names,
vec![("a", fn_scope_id), ("b", fn_scope_id)],
"expected Param bindings for a and b, got {param_names:?}"
);
}
#[test]
fn constructor_param_emits_param_binding() {
let src = "package p;\npublic class C { public C(int x){} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "x")
.expect("expected Param binding for 'x'");
assert_eq!(
facts.scopes[x.scope].kind,
ScopeKind::Function,
"constructor param 'x' should be in a Function scope"
);
}
#[test]
fn local_var_inside_method_emits_local() {
let src = "package p;\npublic class C { public void f() { int x = 1; } }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected Local binding for 'x'");
assert_ne!(x.scope, 0, "local 'x' must not be in scope 0");
}
#[test]
fn multi_declarator_emits_two_locals() {
let src = "package p;\npublic class C { public void f() { int a = 1, b = 2; } }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let locals: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Local)
.map(|b| b.name.as_str())
.collect();
assert!(
locals.contains(&"a"),
"expected Local for 'a', got {locals:?}"
);
assert!(
locals.contains(&"b"),
"expected Local for 'b', got {locals:?}"
);
}
#[test]
fn enhanced_for_loop_var_emits_local() {
let src = "package p;\npublic class C { public void f(int[] xs) { for (int x : xs) {} } }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected Local binding for 'x'");
assert_ne!(x.scope, 0, "enhanced-for 'x' must not be in scope 0");
}
#[test]
fn class_field_is_definition_not_local() {
let src = "package p;\npublic class C { public int count; }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
assert!(
!facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "count"),
"class field 'count' must NOT be a Local binding"
);
let def = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Definition && b.name == "count")
.expect("expected a Definition binding for 'count'");
assert_eq!(
def.scope, 0,
"Definition binding for 'count' must be at scope 0"
);
}
#[test]
fn nesting_produces_correct_scope_hierarchy() {
let src = "package p;\npublic class C { public void f() {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
assert_eq!(
facts.scopes[0].kind,
ScopeKind::Module,
"scopes[0] must be Module"
);
let type_scopes: Vec<ScopeId> = facts
.scopes
.iter()
.enumerate()
.filter(|(_, s)| s.kind == ScopeKind::Type)
.map(|(i, _)| i)
.collect();
assert_eq!(type_scopes.len(), 1, "expected exactly one Type scope");
let type_scope_id = type_scopes[0];
assert_eq!(
facts.scopes[type_scope_id].parent,
Some(0),
"Type scope parent must be Module (0)"
);
let fn_scopes: Vec<ScopeId> = facts
.scopes
.iter()
.enumerate()
.filter(|(_, s)| s.kind == ScopeKind::Function)
.map(|(i, _)| i)
.collect();
assert_eq!(fn_scopes.len(), 1, "expected exactly one Function scope");
let fn_scope_id = fn_scopes[0];
assert_eq!(
facts.scopes[fn_scope_id].parent,
Some(type_scope_id),
"Function scope parent must be the Type scope"
);
}
#[test]
fn catch_param_emits_param_binding() {
let src = r#"package p;
public class C {
public void f() {
try {} catch (Exception e) {}
}
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let e = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "e")
.expect("expected Param binding for 'e'");
assert_ne!(e.scope, 0, "catch param 'e' must not be in scope 0");
}
#[test]
fn lambda_inferred_params_emit_param_bindings() {
let src = r#"package p;
public class C {
public void f() {
java.util.function.BiFunction<Integer,Integer,Integer> fn = (a, b) -> a + b;
}
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let params: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| b.name.as_str())
.collect();
assert!(params.contains(&"a"), "expected Param 'a', got {params:?}");
assert!(params.contains(&"b"), "expected Param 'b', got {params:?}");
for p in facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
{
assert_ne!(
p.scope, 0,
"lambda param '{}' must not be in scope 0",
p.name
);
}
}
#[test]
fn varargs_param_emits_param_binding() {
let src = "package p;\npublic class C { public void f(int... xs){} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let xs = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "xs")
.expect("expected Param binding for 'xs'");
assert_ne!(xs.scope, 0, "varargs param 'xs' must not be in scope 0");
}
#[test]
fn import_binding_emits_import_kind() {
let src = "import com.example.Service;\nclass A {}";
let facts = JavaExtractor.extract(src, "src/A.java").unwrap();
let svc = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Import && b.name == "Service")
.expect("expected Import binding for 'Service'");
assert_eq!(
svc.scope, 0,
"Import binding 'Service' should be in scope 0"
);
}
#[test]
fn same_file_call_ref_has_non_zero_scope() {
let src = r#"package com.example;
public class Calc {
public int add(int a, int b) {
return a + b;
}
public int doubleAdd(int x) {
return add(x, x);
}
}"#;
let facts = JavaExtractor
.extract(src, "src/com/example/Calc.java")
.unwrap();
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "add"),
"expected a Definition binding for 'add'"
);
let add_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "add")
.expect("expected a Call ref for 'add'");
let scope_id = add_ref
.scope
.expect("add() Call ref must have a scope attached");
assert_ne!(
scope_id, 0,
"add() Call ref scope must not be the module root"
);
}
#[test]
fn java_read_ref_emitted_for_use_not_decl() {
let src = r#"package p;
public class C {
public void f() {
int base = 1;
use(base);
}
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "base")
.collect();
assert!(
!read_refs.is_empty(),
"expected at least one Read ref for 'base', got none; all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let decl_byte = src.find("int base").expect("int base not found");
let use_byte = src.find("use(base)").expect("use(base) not found");
let has_use_read = read_refs.iter().any(|r| r.occ.byte > decl_byte + 10);
assert!(
has_use_read,
"Read ref for 'base' must be at the use site (byte > {}), got: {:?}",
decl_byte + 10,
read_refs.iter().map(|r| r.occ.byte).collect::<Vec<_>>()
);
let _ = use_byte; }
#[test]
fn java_write_ref_emitted_for_assignment() {
let src = r#"package p;
public class C {
public void f() {
int cnt = 0;
cnt = 5;
}
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "cnt")
.collect();
assert!(
!write_refs.is_empty(),
"expected at least one Write ref for 'cnt', got none; all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn java_call_not_also_read() {
let src = r#"package p;
public class C {
public void f() { helper(); }
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "helper")
.collect();
assert!(!call_refs.is_empty(), "expected a Call ref for 'helper'");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "helper")
.collect();
assert!(
read_refs.is_empty(),
"helper() must NOT produce a Read ref; got: {read_refs:?}"
);
}
#[test]
fn java_field_access_object_is_read_member_is_not() {
let src = r#"package p;
public class C {
public void f(C obj) { use(obj.field); }
}"#;
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let obj_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "obj")
.collect();
assert!(
!obj_reads.is_empty(),
"expected a Read ref for receiver 'obj'; all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let field_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "field")
.collect();
assert!(
field_reads.is_empty(),
"member name 'field' in field_access must NOT be a Read ref; got: {field_reads:?}"
);
}
#[test]
fn java_param_type_ref_emitted() {
let src = "package p; class C { void f(Config c) {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::ParameterType),
"expected ParameterType ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn java_return_type_ref_emitted() {
let src = "package p; class C { Config get() { return null; } }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::ReturnType),
"expected ReturnType ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn java_field_type_ref_emitted() {
let src = "package p; class C { Config conf; }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::Field),
"expected Field ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn java_generic_arg_type_ref_emitted() {
let src = "package p; class C { void f(List<Config> xs) {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let list_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "List")
.expect("expected TypeRef ref for 'List'");
assert_eq!(
list_ref.type_ref_ctx,
Some(TypeRefContext::ParameterType),
"expected ParameterType ctx for 'List', got {:?}",
list_ref.type_ref_ctx
);
let config_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.expect("expected TypeRef ref for 'Config'");
assert_eq!(
config_ref.type_ref_ctx,
Some(TypeRefContext::GenericArg),
"expected GenericArg ctx for 'Config', got {:?}",
config_ref.type_ref_ctx
);
}
#[test]
fn visibility_public_method() {
let src = "package p;\npublic class C { public void pub_m() {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "pub_m")
.expect("expected symbol 'pub_m'");
assert_eq!(
sym.visibility,
Visibility::Public,
"public method must have Visibility::Public"
);
}
#[test]
fn visibility_private_method_emitted_with_private() {
let src = "package p;\npublic class C { private void priv_m() {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "priv_m")
.expect("private method must now be emitted");
assert_eq!(
sym.visibility,
Visibility::Private,
"private method must have Visibility::Private"
);
}
#[test]
fn visibility_protected_method_emitted_with_protected() {
let src = "package p;\npublic class C { protected void prot_m() {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "prot_m")
.expect("protected method must be emitted");
assert_eq!(
sym.visibility,
Visibility::Protected,
"protected method must have Visibility::Protected"
);
}
#[test]
fn visibility_package_private_method_emitted_with_internal() {
let src = "package p;\npublic class C { void pkg_m() {} }";
let facts = JavaExtractor.extract(src, "src/p/C.java").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "pkg_m")
.expect("package-private method must be emitted");
assert_eq!(
sym.visibility,
Visibility::Internal,
"package-private method must have Visibility::Internal"
);
}
#[test]
fn visibility_interface_method_implicit_public() {
let src = "package p;\npublic interface I { void iface_m(); }";
let facts = JavaExtractor.extract(src, "src/p/I.java").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "iface_m")
.expect("interface method must be emitted");
assert_eq!(
sym.visibility,
Visibility::Public,
"interface method without modifier must be Visibility::Public (implicitly public)"
);
}
fn sym_by_name(facts: &FileFacts, name: &str) -> Symbol {
facts
.symbols
.iter()
.find(|s| s.name == name)
.unwrap_or_else(|| {
panic!("symbol '{name}' not found; symbols: {:?}", {
let names: Vec<&str> = facts.symbols.iter().map(|s| s.name.as_str()).collect();
names
})
})
.clone()
}
fn ep_str(eps: &[EntryPoint]) -> String {
eps.iter()
.map(|ep| match ep {
EntryPoint::Main => "Main".to_owned(),
EntryPoint::HttpRoute(m) => format!("HttpRoute({m})"),
})
.collect::<Vec<_>>()
.join(", ")
}
#[test]
fn java_entry_point_rest_controller_class() {
let src = "@RestController\npublic class UserController {}";
let facts = JavaExtractor
.extract(src, "src/UserController.java")
.unwrap();
let sym = sym_by_name(&facts, "UserController");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point, got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "RestController"),
"expected HttpRoute(\"RestController\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn java_entry_point_get_mapping_method() {
let src = r#"public class Api {
@GetMapping("/users")
public java.util.List<Object> list() { return null; }
}"#;
let facts = JavaExtractor.extract(src, "src/Api.java").unwrap();
let sym = sym_by_name(&facts, "list");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point on 'list', got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "GetMapping"),
"expected HttpRoute(\"GetMapping\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn java_entry_point_non_route_annotation_ignored() {
let src = r#"public class Foo {
@Override
public String toString() { return null; }
}"#;
let facts = JavaExtractor.extract(src, "src/Foo.java").unwrap();
let sym = sym_by_name(&facts, "toString");
assert!(
sym.entry_points.is_empty(),
"non-route annotation must not produce entry points; got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn java_entry_point_static_main() {
let src = r#"public class App {
public static void main(String[] args) {}
}"#;
let facts = JavaExtractor.extract(src, "src/App.java").unwrap();
let sym = sym_by_name(&facts, "main");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point on 'main', got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::Main),
"expected Main, got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn java_entry_point_plain_class_and_method_empty() {
let src = r#"public class Plain {
public void doWork() {}
}"#;
let facts = JavaExtractor.extract(src, "src/Plain.java").unwrap();
let cls = sym_by_name(&facts, "Plain");
assert!(
cls.entry_points.is_empty(),
"plain class must have no entry points; got [{}]",
ep_str(&cls.entry_points)
);
let method = sym_by_name(&facts, "doWork");
assert!(
method.entry_points.is_empty(),
"plain method must have no entry points; got [{}]",
ep_str(&method.entry_points)
);
}
#[test]
fn java_entry_point_post_mapping_method() {
let src = r#"public class Api {
@PostMapping("/x")
public void create() {}
}"#;
let facts = JavaExtractor.extract(src, "src/Api.java").unwrap();
let sym = sym_by_name(&facts, "create");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point on 'create', got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "PostMapping"),
"expected HttpRoute(\"PostMapping\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn java_entry_point_non_static_main_ignored() {
let src = r#"public class Foo {
public void main() {}
}"#;
let facts = JavaExtractor.extract(src, "src/Foo.java").unwrap();
let sym = sym_by_name(&facts, "main");
assert!(
sym.entry_points.is_empty(),
"non-static 'main' must not produce EntryPoint::Main; got [{}]",
ep_str(&sym.entry_points)
);
}
}