Skip to main content

code_moniker_core/lang/java/
mod.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use tree_sitter::{Language, Parser, Tree};
5
6use crate::core::code_graph::CodeGraph;
7use crate::core::moniker::Moniker;
8
9use crate::lang::canonical_walker::CanonicalWalker;
10
11pub mod build;
12mod canonicalize;
13mod kinds;
14mod strategy;
15
16use canonicalize::{compute_module_moniker, read_package_name};
17use strategy::{Strategy, collect_callable_table, collect_type_table};
18
19#[derive(Clone, Debug, Default)]
20pub struct Presets {
21	pub external_packages: Vec<String>,
22}
23
24pub fn parse(source: &str) -> Tree {
25	let mut parser = Parser::new();
26	let language: Language = tree_sitter_java::LANGUAGE.into();
27	parser
28		.set_language(&language)
29		.expect("failed to load tree-sitter Java grammar");
30	parser
31		.parse(source, None)
32		.expect("tree-sitter parse returned None on a non-cancelled call")
33}
34
35pub fn extract(
36	uri: &str,
37	source: &str,
38	anchor: &Moniker,
39	deep: bool,
40	presets: &Presets,
41) -> CodeGraph {
42	let tree = parse(source);
43	let pkg = read_package_name(tree.root_node(), source.as_bytes());
44	let pieces: Vec<&str> = pkg.split('.').filter(|s| !s.is_empty()).collect();
45	let module = compute_module_moniker(anchor, uri, &pieces);
46	let (def_cap, ref_cap) = CodeGraph::capacity_for_source(source.len());
47	let mut graph = CodeGraph::with_capacity(module.clone(), kinds::MODULE, def_cap, ref_cap);
48	let mut type_table: HashMap<&[u8], Moniker> = HashMap::new();
49	collect_type_table(
50		tree.root_node(),
51		source.as_bytes(),
52		&module,
53		&mut type_table,
54	);
55	let mut callable_table: HashMap<(Moniker, Vec<u8>), Vec<u8>> = HashMap::new();
56	collect_callable_table(
57		tree.root_node(),
58		source.as_bytes(),
59		&module,
60		&mut callable_table,
61	);
62	let strat = Strategy {
63		module: module.clone(),
64		source_bytes: source.as_bytes(),
65		deep,
66		presets,
67		imports: RefCell::new(HashMap::<Vec<u8>, &'static [u8]>::new()),
68		import_targets: RefCell::new(HashMap::<Vec<u8>, _>::new()),
69		local_scope: RefCell::new(Vec::new()),
70		type_table,
71		callable_table,
72	};
73	let walker = CanonicalWalker::new(&strat, source.as_bytes());
74	walker.walk(tree.root_node(), &module, &mut graph);
75	graph
76}
77
78pub struct Lang;
79
80impl crate::lang::LangExtractor for Lang {
81	type Presets = Presets;
82	const LANG_TAG: &'static str = "java";
83	const ALLOWED_KINDS: &'static [&'static str] = &[
84		"class",
85		"interface",
86		"enum",
87		"record",
88		"annotation_type",
89		"method",
90		"constructor",
91		"field",
92		"enum_constant",
93	];
94	const ALLOWED_VISIBILITIES: &'static [&'static str] =
95		&["public", "protected", "package", "private"];
96
97	fn extract(
98		uri: &str,
99		source: &str,
100		anchor: &Moniker,
101		deep: bool,
102		presets: &Self::Presets,
103	) -> CodeGraph {
104		extract(uri, source, anchor, deep, presets)
105	}
106}
107
108#[cfg(test)]
109mod tests {
110	use super::*;
111	use crate::core::moniker::MonikerBuilder;
112	use crate::lang::assert_conformance;
113
114	fn make_anchor() -> Moniker {
115		MonikerBuilder::new().project(b"app").build()
116	}
117
118	fn extract_default(uri: &str, source: &str, anchor: &Moniker, deep: bool) -> CodeGraph {
119		let g = extract(uri, source, anchor, deep, &Presets::default());
120		assert_conformance::<super::Lang>(&g, anchor);
121		g
122	}
123
124	#[test]
125	fn parse_empty_returns_program() {
126		let tree = parse("");
127		assert_eq!(tree.root_node().kind(), "program");
128	}
129
130	#[test]
131	fn extract_default_package_skips_package_segments() {
132		let g = extract_default("Foo.java", "class Foo {}", &make_anchor(), false);
133		let expected = MonikerBuilder::new()
134			.project(b"app")
135			.segment(b"lang", b"java")
136			.segment(b"module", b"Foo")
137			.build();
138		assert_eq!(g.root(), &expected);
139	}
140
141	#[test]
142	fn extract_class_emits_class_def_with_package_visibility_default() {
143		let g = extract_default("Foo.java", "class Foo {}", &make_anchor(), false);
144		let foo = g.defs().find(|d| d.kind == b"class").expect("class def");
145		assert_eq!(foo.visibility, b"package".to_vec());
146	}
147
148	#[test]
149	fn extract_field_one_def_per_declarator() {
150		let src = "class Foo { int a, b; private String name; }";
151		let g = extract_default("Foo.java", src, &make_anchor(), false);
152		let fields: Vec<_> = g.defs().filter(|d| d.kind == b"field").collect();
153		assert_eq!(
154			fields.len(),
155			3,
156			"got {:?}",
157			fields.iter().map(|d| &d.moniker).collect::<Vec<_>>()
158		);
159		let private_field = fields
160			.iter()
161			.find(|d| d.moniker.as_view().segments().last().unwrap().name == b"name")
162			.unwrap();
163		assert_eq!(private_field.visibility, b"private".to_vec());
164	}
165
166	#[test]
167	fn extract_enum_emits_enum_constants() {
168		let g = extract_default(
169			"Color.java",
170			"public enum Color { RED, GREEN }",
171			&make_anchor(),
172			false,
173		);
174		let red = MonikerBuilder::new()
175			.project(b"app")
176			.segment(b"lang", b"java")
177			.segment(b"module", b"Color")
178			.segment(b"enum", b"Color")
179			.segment(b"enum_constant", b"RED")
180			.build();
181		assert!(
182			g.contains(&red),
183			"missing RED, defs: {:?}",
184			g.def_monikers()
185		);
186	}
187
188	#[test]
189	fn extract_wildcard_import_emits_imports_module() {
190		let src = "import com.acme.*;\nclass Foo {}";
191		let g = extract_default("Foo.java", src, &make_anchor(), false);
192		let r = g
193			.refs()
194			.find(|r| r.kind == b"imports_module")
195			.expect("imports_module ref");
196		assert_eq!(r.confidence, b"imported".to_vec());
197	}
198
199	#[test]
200	fn extract_method_call_carries_receiver_hint() {
201		let src = r#"
202            class Foo {
203                void m() { this.bar(); }
204                void bar() {}
205            }
206        "#;
207		let g = extract_default("Foo.java", src, &make_anchor(), false);
208		let r = g
209			.refs()
210			.find(|r| r.kind == b"method_call")
211			.expect("method_call ref");
212		assert_eq!(r.receiver_hint, b"this".to_vec());
213	}
214
215	#[test]
216	fn method_call_on_imported_class_carries_imported_confidence() {
217		let src = r#"
218            import com.acme.Util;
219            class Foo {
220                void m() { Util.run(); }
221            }
222        "#;
223		let g = extract_default("src/Foo.java", src, &make_anchor(), false);
224		let r = g
225			.refs()
226			.find(|r| r.kind == b"method_call" && r.receiver_hint == b"Util")
227			.expect("method_call on Util");
228		assert_eq!(r.confidence, b"imported");
229	}
230
231	#[test]
232	fn method_call_on_non_imported_identifier_stays_name_match() {
233		let src = r#"
234            class Foo {
235                void m() { obj.bar(); }
236            }
237        "#;
238		let g = extract_default("src/Foo.java", src, &make_anchor(), false);
239		let r = g
240			.refs()
241			.find(|r| r.kind == b"method_call" && r.receiver_hint == b"obj")
242			.expect("method_call on obj");
243		assert_eq!(r.confidence, b"name_match");
244	}
245
246	#[test]
247	fn this_call_resolves_to_full_slot_signature() {
248		let src = r#"
249            class Foo {
250                void m() { this.bar(); }
251                void bar() {}
252            }
253        "#;
254		let g = extract_default("Foo.java", src, &make_anchor(), false);
255		let r = g
256			.refs()
257			.find(|r| r.kind == b"method_call")
258			.expect("method_call ref");
259		let last = r.target.as_view().segments().last().unwrap();
260		assert_eq!(last.kind, b"method");
261		assert_eq!(
262			last.name, b"bar()",
263			"this.bar() must resolve to the def's slot signature, not to a name-only fallback"
264		);
265	}
266
267	#[test]
268	fn method_call_on_unresolved_receiver_falls_back_to_name_only() {
269		let src = r#"
270            class Foo {
271                void m() { obj.bar(1); }
272            }
273        "#;
274		let g = extract_default("Foo.java", src, &make_anchor(), false);
275		let r = g
276			.refs()
277			.find(|r| r.kind == b"method_call")
278			.expect("method_call ref");
279		let last = r.target.as_view().segments().last().unwrap();
280		assert_eq!(
281			last.name, b"bar",
282			"unresolved receiver must produce a name-only target (no parens, no arity)"
283		);
284	}
285
286	#[test]
287	fn extract_imported_call_marks_confidence_imported() {
288		let src = r#"
289            import com.acme.Helpers;
290            class Foo { void m() { Helpers.go(); } }
291        "#;
292		let g = extract_default("Foo.java", src, &make_anchor(), false);
293		let reads_helpers = g.refs().find(|r| {
294			r.kind == b"reads" && r.target.as_view().segments().last().unwrap().name == b"Helpers"
295		});
296		if let Some(r) = reads_helpers {
297			assert_eq!(r.confidence, b"imported".to_vec());
298		}
299	}
300
301	#[test]
302	fn extract_deep_catch_param_emits_local_def() {
303		let src = r#"
304            class Foo {
305                void m() { try {} catch (IOException e) { e.toString(); } }
306            }
307        "#;
308		let g = extract_default("Foo.java", src, &make_anchor(), true);
309		let monikers = g.def_monikers();
310		let e = monikers.iter().find(|m| {
311			let last = m.as_view().segments().last().unwrap();
312			last.kind == b"param" && last.name == b"e"
313		});
314		assert!(
315			e.is_some(),
316			"catch param should be emitted as a param def in deep mode"
317		);
318	}
319
320	#[test]
321	fn extract_deep_enhanced_for_var_is_local() {
322		let src = r#"
323            class Foo {
324                void m(java.util.List<String> xs) { for (String x : xs) { x.length(); } }
325            }
326        "#;
327		let g = extract_default("Foo.java", src, &make_anchor(), true);
328		assert!(
329			g.defs().any(|d| d.kind == b"local"
330				&& d.moniker.as_view().segments().last().unwrap().name == b"x"),
331			"enhanced-for var should be a local def"
332		);
333	}
334}