@@ -229,6 +229,7 @@ public DependencyInfo(String key, String value) {
229229 /**
230230 * Resolve project dependencies information including JDK version.
231231 * Uses cache with automatic invalidation on project changes.
232+ * Supports both single projects and multi-module aggregator projects.
232233 *
233234 * @param projectUri The project URI
234235 * @param monitor Progress monitor for cancellation support
@@ -252,8 +253,14 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
252253 }
253254
254255 IJavaProject javaProject = JavaCore .create (project );
256+
257+ // Check if this is a Java project
255258 if (javaProject == null || !javaProject .exists ()) {
256- return result ;
259+ // Not a Java project - might be an aggregator/parent project
260+ // Try to find Java sub-projects under this path
261+ JdtlsExtActivator .logInfo ("Not a Java project: " + project .getName () +
262+ ", checking for sub-projects" );
263+ return resolveAggregatorProjectDependencies (root , projectPath , monitor );
257264 }
258265
259266 // Generate cache key based on project location
@@ -291,6 +298,210 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
291298 return result ;
292299 }
293300
301+ /**
302+ * Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects.
303+ * This handles multi-module Maven/Gradle projects where the parent is not a Java project itself.
304+ * Returns aggregated information useful for AI context (Java version, common dependencies, build tool).
305+ *
306+ * @param root The workspace root
307+ * @param parentPath The path of the parent/aggregator project
308+ * @param monitor Progress monitor
309+ * @return Aggregated dependency information from all sub-projects
310+ */
311+ private static List <DependencyInfo > resolveAggregatorProjectDependencies (
312+ IWorkspaceRoot root , IPath parentPath , IProgressMonitor monitor ) {
313+
314+ List <DependencyInfo > result = new ArrayList <>();
315+ List <IJavaProject > javaProjects = new ArrayList <>();
316+
317+ // Find all Java projects under the parent path
318+ IProject [] allProjects = root .getProjects ();
319+ for (IProject p : allProjects ) {
320+ if (p .getLocation () != null && parentPath .isPrefixOf (p .getLocation ())) {
321+ try {
322+ if (p .isAccessible () && p .hasNature (JavaCore .NATURE_ID )) {
323+ IJavaProject jp = JavaCore .create (p );
324+ if (jp != null && jp .exists ()) {
325+ javaProjects .add (jp );
326+ }
327+ }
328+ } catch (CoreException e ) {
329+ // Skip this project
330+ }
331+ }
332+ }
333+
334+ if (javaProjects .isEmpty ()) {
335+ JdtlsExtActivator .logInfo ("No Java sub-projects found under: " + parentPath .toOSString ());
336+ return result ;
337+ }
338+
339+ JdtlsExtActivator .logInfo ("Found " + javaProjects .size () +
340+ " Java sub-project(s) under: " + parentPath .toOSString ());
341+
342+ // Mark as aggregator project
343+ result .add (new DependencyInfo ("aggregatorProject" , "true" ));
344+ result .add (new DependencyInfo ("totalSubProjects" , String .valueOf (javaProjects .size ())));
345+
346+ // Collect sub-project names for reference
347+ StringBuilder projectNames = new StringBuilder ();
348+ for (int i = 0 ; i < javaProjects .size (); i ++) {
349+ if (i > 0 ) projectNames .append (", " );
350+ projectNames .append (javaProjects .get (i ).getProject ().getName ());
351+ }
352+ result .add (new DependencyInfo ("subProjectNames" , projectNames .toString ()));
353+
354+ // Determine the primary/representative Java version (most common or highest)
355+ String primaryJavaVersion = determinePrimaryJavaVersion (javaProjects );
356+ if (primaryJavaVersion != null ) {
357+ result .add (new DependencyInfo (KEY_JAVA_VERSION , primaryJavaVersion ));
358+ }
359+
360+ // Collect all unique libraries across sub-projects (top 10 most common)
361+ Map <String , Integer > libraryFrequency = collectLibraryFrequency (javaProjects , monitor );
362+ addTopLibraries (result , libraryFrequency , 10 );
363+
364+ // Detect build tool from parent directory
365+ IProject parentProject = findProjectByPath (root , parentPath );
366+ if (parentProject != null ) {
367+ detectBuildTool (result , parentProject );
368+ }
369+
370+ // Get JRE container info from first sub-project (usually consistent across modules)
371+ if (!javaProjects .isEmpty ()) {
372+ extractJreInfo (result , javaProjects .get (0 ));
373+ }
374+
375+ return result ;
376+ }
377+
378+ /**
379+ * Determine the primary Java version from all sub-projects.
380+ * Returns the most common version, or the highest if there's a tie.
381+ */
382+ private static String determinePrimaryJavaVersion (List <IJavaProject > javaProjects ) {
383+ Map <String , Integer > versionCount = new ConcurrentHashMap <>();
384+
385+ for (IJavaProject jp : javaProjects ) {
386+ String version = jp .getOption (JavaCore .COMPILER_COMPLIANCE , true );
387+ if (version != null ) {
388+ versionCount .put (version , versionCount .getOrDefault (version , 0 ) + 1 );
389+ }
390+ }
391+
392+ if (versionCount .isEmpty ()) {
393+ return null ;
394+ }
395+
396+ // Find most common version (or highest if tie)
397+ return versionCount .entrySet ().stream ()
398+ .max ((e1 , e2 ) -> {
399+ int countCompare = Integer .compare (e1 .getValue (), e2 .getValue ());
400+ if (countCompare != 0 ) return countCompare ;
401+ // If same count, prefer higher version
402+ return e1 .getKey ().compareTo (e2 .getKey ());
403+ })
404+ .map (Map .Entry ::getKey )
405+ .orElse (null );
406+ }
407+
408+ /**
409+ * Collect frequency of all libraries across sub-projects.
410+ * Returns a map of library name to frequency count.
411+ */
412+ private static Map <String , Integer > collectLibraryFrequency (
413+ List <IJavaProject > javaProjects , IProgressMonitor monitor ) {
414+
415+ Map <String , Integer > libraryFrequency = new ConcurrentHashMap <>();
416+
417+ for (IJavaProject jp : javaProjects ) {
418+ if (monitor .isCanceled ()) {
419+ break ;
420+ }
421+
422+ try {
423+ IClasspathEntry [] entries = jp .getResolvedClasspath (true );
424+ for (IClasspathEntry entry : entries ) {
425+ if (entry .getEntryKind () == IClasspathEntry .CPE_LIBRARY ) {
426+ IPath libPath = entry .getPath ();
427+ if (libPath != null ) {
428+ String libName = libPath .lastSegment ();
429+ libraryFrequency .put (libName ,
430+ libraryFrequency .getOrDefault (libName , 0 ) + 1 );
431+ }
432+ }
433+ }
434+ } catch (JavaModelException e ) {
435+ // Skip this project
436+ }
437+ }
438+
439+ return libraryFrequency ;
440+ }
441+
442+ /**
443+ * Add top N most common libraries to result.
444+ */
445+ private static void addTopLibraries (List <DependencyInfo > result ,
446+ Map <String , Integer > libraryFrequency , int topN ) {
447+
448+ if (libraryFrequency .isEmpty ()) {
449+ result .add (new DependencyInfo (KEY_TOTAL_LIBRARIES , "0" ));
450+ return ;
451+ }
452+
453+ // Sort by frequency (descending) and take top N
454+ List <Map .Entry <String , Integer >> topLibs = libraryFrequency .entrySet ().stream ()
455+ .sorted ((e1 , e2 ) -> Integer .compare (e2 .getValue (), e1 .getValue ()))
456+ .limit (topN )
457+ .collect (java .util .stream .Collectors .toList ());
458+
459+ result .add (new DependencyInfo (KEY_TOTAL_LIBRARIES ,
460+ String .valueOf (libraryFrequency .size ())));
461+
462+ // Add top common libraries
463+ int index = 1 ;
464+ for (Map .Entry <String , Integer > entry : topLibs ) {
465+ result .add (new DependencyInfo ("commonLibrary_" + index ,
466+ entry .getKey () + " (used in " + entry .getValue () + " modules)" ));
467+ index ++;
468+ }
469+ }
470+
471+ /**
472+ * Extract JRE container information from a Java project.
473+ */
474+ private static void extractJreInfo (List <DependencyInfo > result , IJavaProject javaProject ) {
475+ try {
476+ IClasspathEntry [] entries = javaProject .getResolvedClasspath (true );
477+ for (IClasspathEntry entry : entries ) {
478+ if (entry .getEntryKind () == IClasspathEntry .CPE_CONTAINER ) {
479+ String containerPath = entry .getPath ().toString ();
480+ if (containerPath .contains ("JRE_CONTAINER" )) {
481+ try {
482+ String vmInstallName = JavaRuntime .getVMInstallName (entry .getPath ());
483+ addIfNotNull (result , KEY_JRE_CONTAINER , vmInstallName );
484+ return ;
485+ } catch (Exception e ) {
486+ // Fallback: extract from path
487+ if (containerPath .contains ("JavaSE-" )) {
488+ int startIdx = containerPath .lastIndexOf ("JavaSE-" );
489+ String version = containerPath .substring (startIdx );
490+ if (version .contains ("/" )) {
491+ version = version .substring (0 , version .indexOf ("/" ));
492+ }
493+ result .add (new DependencyInfo (KEY_JRE_CONTAINER , version ));
494+ return ;
495+ }
496+ }
497+ }
498+ }
499+ }
500+ } catch (JavaModelException e ) {
501+ // Ignore
502+ }
503+ }
504+
294505 /**
295506 * Find project by path from all projects in workspace.
296507 */
0 commit comments