@@ -173,6 +173,98 @@ fn build_python_builtins_graph(sgl: &StackGraphLanguage) -> anyhow::Result<Stack
173173 Ok ( graph)
174174}
175175
176+ /// Stub source for a virtual, unpackaged Java compilation unit, standing in
177+ /// for `tree-sitter-stack-graphs-java` 0.5.0's own bundled `src/builtins.java`
178+ /// (which ships empty, like Python's did — see `build_python_builtins_graph`).
179+ /// Unlike Python, no `<builtins>`/`FILE_PATH` naming trick is needed: the
180+ /// Java TSG's own `(program (package_declaration)? @package) @prog` rule
181+ /// wires `ROOT_NODE -> @prog.defs` directly whenever `@package` is absent, so
182+ /// an unpackaged file's top-level declarations become reachable from any
183+ /// other file's plain-identifier reference (which always chains up through
184+ /// `lexical_scope` to `ROOT_NODE`, regardless of that other file's own
185+ /// package). Covers the bulk of `java.lang` (auto-imported in every `.java`
186+ /// file, no `import` needed) — not exhaustive, but the names most likely to
187+ /// show up in real code. Bodies are empty (`{}`) — only the *names* need to
188+ /// exist as top-level definitions for references to resolve to.
189+ const JAVA_BUILTINS_STUB : & str = r#"
190+ class Object {}
191+ class String {}
192+ class StringBuilder {}
193+ class StringBuffer {}
194+ class Boolean {}
195+ class Byte {}
196+ class Short {}
197+ class Character {}
198+ class Integer {}
199+ class Long {}
200+ class Float {}
201+ class Double {}
202+ class Number {}
203+ class Math {}
204+ class System {}
205+ class Class {}
206+ class Void {}
207+ class Thread {}
208+ class ThreadLocal {}
209+ class Runtime {}
210+ class Enum {}
211+ class Record {}
212+ class Throwable {}
213+ class Exception {}
214+ class RuntimeException {}
215+ class Error {}
216+ class NullPointerException {}
217+ class IllegalArgumentException {}
218+ class IllegalStateException {}
219+ class IndexOutOfBoundsException {}
220+ class ArrayIndexOutOfBoundsException {}
221+ class StringIndexOutOfBoundsException {}
222+ class ClassCastException {}
223+ class NumberFormatException {}
224+ class UnsupportedOperationException {}
225+ class ArithmeticException {}
226+ class CloneNotSupportedException {}
227+ class InterruptedException {}
228+ class SecurityException {}
229+ class OutOfMemoryError {}
230+ class StackOverflowError {}
231+ class AssertionError {}
232+ interface Runnable {}
233+ interface Comparable {}
234+ interface CharSequence {}
235+ interface Iterable {}
236+ interface AutoCloseable {}
237+ interface Cloneable {}
238+ interface Appendable {}
239+ @interface Override {}
240+ @interface Deprecated {}
241+ @interface SuppressWarnings {}
242+ @interface FunctionalInterface {}
243+ @interface SafeVarargs {}
244+ "# ;
245+
246+ /// Builds a `StackGraph` holding definitions for `JAVA_BUILTINS_STUB`, reusing
247+ /// the same compiled TSG rules (`sgl`) the upstream crate uses for ordinary
248+ /// files. The virtual file has no package declaration, so its top-level
249+ /// classes/interfaces/annotations attach directly to `ROOT_NODE` — see
250+ /// `JAVA_BUILTINS_STUB`'s doc comment for why that alone makes them visible
251+ /// from any other file's unqualified references.
252+ fn build_java_builtins_graph ( sgl : & StackGraphLanguage ) -> anyhow:: Result < StackGraph > {
253+ let mut graph = StackGraph :: new ( ) ;
254+ let file = graph. get_or_create_file ( "<builtins>.java" ) ;
255+
256+ let mut globals = Variables :: new ( ) ;
257+ globals
258+ . add ( "FILE_PATH" . into ( ) , "<builtins>.java" . into ( ) )
259+ . map_err ( |_| anyhow:: anyhow!( "Failed to set FILE_PATH global for builtins" ) ) ?;
260+
261+ let deadline = TsgCancelAfterDuration :: new ( RESOLVE_TIMEOUT ) ;
262+ sgl. build_stack_graph_into ( & mut graph, file, JAVA_BUILTINS_STUB , & globals, & deadline)
263+ . map_err ( |e| anyhow:: anyhow!( "Failed to build Java builtins stack graph: {e:?}" ) ) ?;
264+
265+ Ok ( graph)
266+ }
267+
176268/// Same as `ForwardPartialPathStitcher::find_minimal_partial_path_set_in_file`
177269/// (stack-graphs 0.14), but with `max_work_per_phase` bounded — see
178270/// `MAX_WORK_PER_PHASE` for why.
@@ -315,6 +407,30 @@ impl FormalResolver {
315407 Ok ( ( ) )
316408 }
317409
410+ /// Java formal resolution. Unlike Python (`load_python`), no dialect
411+ /// variant is needed — Java has no `.tsx`-style split, matching
412+ /// TypeScript's own primary (non-tsx) shape. Like Python, upstream's
413+ /// bundled builtins source (`tree-sitter-stack-graphs-java`'s
414+ /// `src/builtins.java`) ships empty, so `lc.builtins` is replaced with
415+ /// `build_java_builtins_graph`'s output — see that function's doc comment
416+ /// for why no `<builtins>` path/FILE_PATH trick is needed here, unlike
417+ /// Python.
418+ pub fn load_java ( & mut self ) -> anyhow:: Result < ( ) > {
419+ let lc = tree_sitter_stack_graphs_java:: try_language_configuration ( cancellation_flag ( ) )
420+ . map_err ( |e| anyhow:: anyhow!( "Failed to load Java stack-graphs config: {e}" ) ) ?;
421+ let builtins = build_java_builtins_graph ( & lc. sgl ) ?;
422+ self . configs . insert (
423+ "java" . to_string ( ) ,
424+ FormalLanguageConfig {
425+ sgl : lc. sgl ,
426+ builtins,
427+ no_similar_paths_in_file : lc. no_similar_paths_in_file ,
428+ tsx : None ,
429+ } ,
430+ ) ;
431+ Ok ( ( ) )
432+ }
433+
318434 pub fn resolve_file (
319435 & self ,
320436 language : & str ,
@@ -780,4 +896,115 @@ function Bar() {
780896 "a genuinely undefined name must not resolve. Edges: {edges:?}"
781897 ) ;
782898 }
899+
900+ #[ test]
901+ fn test_load_java ( ) {
902+ let mut resolver = FormalResolver :: new ( ) ;
903+ resolver. load_java ( ) . unwrap ( ) ;
904+ assert ! ( resolver. has_language( "java" ) ) ;
905+ }
906+
907+ #[ test]
908+ fn test_resolve_simple_java_def_ref ( ) {
909+ let mut resolver = FormalResolver :: new ( ) ;
910+ resolver. load_java ( ) . unwrap ( ) ;
911+ let source = r#"
912+ class Foo {
913+ static void greet() {}
914+
915+ static void run() {
916+ greet();
917+ }
918+ }
919+ "# ;
920+ let edges = resolver. resolve_file ( "java" , "Foo.java" , source) . unwrap ( ) ;
921+ let has_greet_edge = edges
922+ . iter ( )
923+ . any ( |e| e. definition_symbol == "greet" || e. reference_symbol == "greet" ) ;
924+ assert ! (
925+ has_greet_edge,
926+ "Should resolve greet() call to greet definition. Edges: {edges:?}"
927+ ) ;
928+ }
929+
930+ #[ test]
931+ fn test_resolve_java_class ( ) {
932+ let mut resolver = FormalResolver :: new ( ) ;
933+ resolver. load_java ( ) . unwrap ( ) ;
934+ let source = r#"
935+ class MyClass {
936+ void method() {}
937+ }
938+
939+ class UseClass {
940+ void useClass() {
941+ MyClass obj = new MyClass();
942+ }
943+ }
944+ "# ;
945+ let edges = resolver
946+ . resolve_file ( "java" , "MyClass.java" , source)
947+ . unwrap ( ) ;
948+ let has_class_edge = edges
949+ . iter ( )
950+ . any ( |e| e. definition_symbol == "MyClass" || e. reference_symbol == "MyClass" ) ;
951+ assert ! (
952+ has_class_edge,
953+ "Should resolve MyClass reference to class definition. Edges: {edges:?}"
954+ ) ;
955+ }
956+
957+ /// Mirrors `test_resolve_file_resolves_python_builtins`: `System` and
958+ /// `Object` (both `java.lang`, implicitly visible with no `import`) must
959+ /// resolve through the formal tier via `JAVA_BUILTINS_STUB`, not just
960+ /// merge into the graph without crashing.
961+ #[ test]
962+ fn test_resolve_file_resolves_java_builtins ( ) {
963+ let mut resolver = FormalResolver :: new ( ) ;
964+ resolver. load_java ( ) . unwrap ( ) ;
965+
966+ let edges = resolver
967+ . resolve_file (
968+ "java" ,
969+ "Foo.java" ,
970+ "class Foo {\n void bar() {\n Object o = new Object();\n System.out.println(o);\n }\n }\n " ,
971+ )
972+ . unwrap ( ) ;
973+
974+ assert ! (
975+ edges
976+ . iter( )
977+ . any( |e| e. reference_symbol == "Object" && e. definition_symbol == "Object" ) ,
978+ "Object must resolve through the formal tier. Edges: {edges:?}"
979+ ) ;
980+ assert ! (
981+ edges
982+ . iter( )
983+ . any( |e| e. reference_symbol == "System" && e. definition_symbol == "System" ) ,
984+ "System must resolve through the formal tier. Edges: {edges:?}"
985+ ) ;
986+ }
987+
988+ /// A genuinely undefined name must still fail to resolve — the builtins
989+ /// fix must not make FormalResolver resolve *everything*.
990+ #[ test]
991+ fn test_resolve_java_does_not_resolve_undefined_name ( ) {
992+ let mut resolver = FormalResolver :: new ( ) ;
993+ resolver. load_java ( ) . unwrap ( ) ;
994+
995+ let edges = resolver
996+ . resolve_file (
997+ "java" ,
998+ "Foo.java" ,
999+ "class Foo {\n void bar() {\n totallyUndefinedXyz();\n }\n }\n " ,
1000+ )
1001+ . unwrap ( ) ;
1002+
1003+ assert ! (
1004+ !edges
1005+ . iter( )
1006+ . any( |e| e. reference_symbol == "totallyUndefinedXyz" ) ,
1007+ "a genuinely undefined name must not resolve. Edges: {edges:?}"
1008+ ) ;
1009+ }
7831010}
0 commit comments