Skip to content

Commit c895aa0

Browse files
committed
feat: support aggregate projects and format result
1 parent 609dfd2 commit c895aa0

File tree

1 file changed

+229
-5
lines changed

1 file changed

+229
-5
lines changed

jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java

Lines changed: 229 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import java.util.ArrayList;
44
import java.util.List;
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
57

68
import org.eclipse.core.resources.IProject;
79
import org.eclipse.core.resources.IWorkspaceRoot;
810
import org.eclipse.core.resources.ResourcesPlugin;
11+
import org.eclipse.core.runtime.CoreException;
912
import org.eclipse.core.runtime.IPath;
1013
import org.eclipse.core.runtime.IProgressMonitor;
1114
import org.eclipse.jdt.core.IClasspathEntry;
@@ -44,6 +47,7 @@ public DependencyInfo(String key, String value) {
4447

4548
/**
4649
* Resolve project dependencies information including JDK version.
50+
* Supports both single projects and multi-module aggregator projects.
4751
*
4852
* @param projectUri The project URI
4953
* @param monitor Progress monitor for cancellation support
@@ -64,8 +68,13 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
6468
}
6569

6670
IJavaProject javaProject = JavaCore.create(project);
71+
// Check if this is a Java project
6772
if (javaProject == null || !javaProject.exists()) {
68-
return result;
73+
// Not a Java project - might be an aggregator/parent project
74+
// Try to find Java sub-projects under this path
75+
JdtlsExtActivator.logInfo("Not a Java project: " + project.getName() +
76+
", checking for sub-projects");
77+
return resolveAggregatorProjectDependencies(root, projectPath, monitor);
6978
}
7079

7180
// Add basic project information
@@ -83,6 +92,210 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
8392

8493
return result;
8594
}
95+
96+
/**
97+
* Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects.
98+
* This handles multi-module Maven/Gradle projects where the parent is not a Java project itself.
99+
* Returns aggregated information useful for AI context (Java version, common dependencies, build tool).
100+
*
101+
* @param root The workspace root
102+
* @param parentPath The path of the parent/aggregator project
103+
* @param monitor Progress monitor
104+
* @return Aggregated dependency information from all sub-projects
105+
*/
106+
private static List<DependencyInfo> resolveAggregatorProjectDependencies(
107+
IWorkspaceRoot root, IPath parentPath, IProgressMonitor monitor) {
108+
109+
List<DependencyInfo> result = new ArrayList<>();
110+
List<IJavaProject> javaProjects = new ArrayList<>();
111+
112+
// Find all Java projects under the parent path
113+
IProject[] allProjects = root.getProjects();
114+
for (IProject p : allProjects) {
115+
if (p.getLocation() != null && parentPath.isPrefixOf(p.getLocation())) {
116+
try {
117+
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
118+
IJavaProject jp = JavaCore.create(p);
119+
if (jp != null && jp.exists()) {
120+
javaProjects.add(jp);
121+
}
122+
}
123+
} catch (CoreException e) {
124+
// Skip this project
125+
}
126+
}
127+
}
128+
129+
if (javaProjects.isEmpty()) {
130+
JdtlsExtActivator.logInfo("No Java sub-projects found under: " + parentPath.toOSString());
131+
return result;
132+
}
133+
134+
JdtlsExtActivator.logInfo("Found " + javaProjects.size() +
135+
" Java sub-project(s) under: " + parentPath.toOSString());
136+
137+
// Mark as aggregator project
138+
result.add(new DependencyInfo("aggregatorProject", "true"));
139+
result.add(new DependencyInfo("totalSubProjects", String.valueOf(javaProjects.size())));
140+
141+
// Collect sub-project names for reference
142+
StringBuilder projectNames = new StringBuilder();
143+
for (int i = 0; i < javaProjects.size(); i++) {
144+
if (i > 0) projectNames.append(", ");
145+
projectNames.append(javaProjects.get(i).getProject().getName());
146+
}
147+
result.add(new DependencyInfo("subProjectNames", projectNames.toString()));
148+
149+
// Determine the primary/representative Java version (most common or highest)
150+
String primaryJavaVersion = determinePrimaryJavaVersion(javaProjects);
151+
if (primaryJavaVersion != null) {
152+
result.add(new DependencyInfo(KEY_JAVA_VERSION, primaryJavaVersion));
153+
}
154+
155+
// Collect all unique libraries across sub-projects (top 10 most common)
156+
Map<String, Integer> libraryFrequency = collectLibraryFrequency(javaProjects, monitor);
157+
addTopLibraries(result, libraryFrequency, 10);
158+
159+
// Detect build tool from parent directory
160+
IProject parentProject = findProjectByPath(root, parentPath);
161+
if (parentProject != null) {
162+
detectBuildTool(result, parentProject);
163+
}
164+
165+
// Get JRE container info from first sub-project (usually consistent across modules)
166+
if (!javaProjects.isEmpty()) {
167+
extractJreInfo(result, javaProjects.get(0));
168+
}
169+
170+
return result;
171+
}
172+
173+
/**
174+
* Determine the primary Java version from all sub-projects.
175+
* Returns the most common version, or the highest if there's a tie.
176+
*/
177+
private static String determinePrimaryJavaVersion(List<IJavaProject> javaProjects) {
178+
Map<String, Integer> versionCount = new ConcurrentHashMap<>();
179+
180+
for (IJavaProject jp : javaProjects) {
181+
String version = jp.getOption(JavaCore.COMPILER_COMPLIANCE, true);
182+
if (version != null) {
183+
versionCount.put(version, versionCount.getOrDefault(version, 0) + 1);
184+
}
185+
}
186+
187+
if (versionCount.isEmpty()) {
188+
return null;
189+
}
190+
191+
// Find most common version (or highest if tie)
192+
return versionCount.entrySet().stream()
193+
.max((e1, e2) -> {
194+
int countCompare = Integer.compare(e1.getValue(), e2.getValue());
195+
if (countCompare != 0) return countCompare;
196+
// If same count, prefer higher version
197+
return e1.getKey().compareTo(e2.getKey());
198+
})
199+
.map(Map.Entry::getKey)
200+
.orElse(null);
201+
}
202+
203+
/**
204+
* Collect frequency of all libraries across sub-projects.
205+
* Returns a map of library name to frequency count.
206+
*/
207+
private static Map<String, Integer> collectLibraryFrequency(
208+
List<IJavaProject> javaProjects, IProgressMonitor monitor) {
209+
210+
Map<String, Integer> libraryFrequency = new ConcurrentHashMap<>();
211+
212+
for (IJavaProject jp : javaProjects) {
213+
if (monitor.isCanceled()) {
214+
break;
215+
}
216+
217+
try {
218+
IClasspathEntry[] entries = jp.getResolvedClasspath(true);
219+
for (IClasspathEntry entry : entries) {
220+
if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
221+
IPath libPath = entry.getPath();
222+
if (libPath != null) {
223+
String libName = libPath.lastSegment();
224+
libraryFrequency.put(libName,
225+
libraryFrequency.getOrDefault(libName, 0) + 1);
226+
}
227+
}
228+
}
229+
} catch (JavaModelException e) {
230+
// Skip this project
231+
}
232+
}
233+
234+
return libraryFrequency;
235+
}
236+
237+
/**
238+
* Add top N most common libraries to result.
239+
*/
240+
private static void addTopLibraries(List<DependencyInfo> result,
241+
Map<String, Integer> libraryFrequency, int topN) {
242+
243+
if (libraryFrequency.isEmpty()) {
244+
result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, "0"));
245+
return;
246+
}
247+
248+
// Sort by frequency (descending) and take top N
249+
List<Map.Entry<String, Integer>> topLibs = libraryFrequency.entrySet().stream()
250+
.sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
251+
.limit(topN)
252+
.collect(java.util.stream.Collectors.toList());
253+
254+
result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES,
255+
String.valueOf(libraryFrequency.size())));
256+
257+
// Add top common libraries
258+
int index = 1;
259+
for (Map.Entry<String, Integer> entry : topLibs) {
260+
result.add(new DependencyInfo("commonLibrary_" + index,
261+
entry.getKey() + " (used in " + entry.getValue() + " modules)"));
262+
index++;
263+
}
264+
}
265+
266+
/**
267+
* Extract JRE container information from a Java project.
268+
*/
269+
private static void extractJreInfo(List<DependencyInfo> result, IJavaProject javaProject) {
270+
try {
271+
IClasspathEntry[] entries = javaProject.getResolvedClasspath(true);
272+
for (IClasspathEntry entry : entries) {
273+
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
274+
String containerPath = entry.getPath().toString();
275+
if (containerPath.contains("JRE_CONTAINER")) {
276+
try {
277+
String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath());
278+
addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName);
279+
return;
280+
} catch (Exception e) {
281+
// Fallback: extract from path
282+
if (containerPath.contains("JavaSE-")) {
283+
int startIdx = containerPath.lastIndexOf("JavaSE-");
284+
String version = containerPath.substring(startIdx);
285+
if (version.contains("/")) {
286+
version = version.substring(0, version.indexOf("/"));
287+
}
288+
result.add(new DependencyInfo(KEY_JRE_CONTAINER, version));
289+
return;
290+
}
291+
}
292+
}
293+
}
294+
}
295+
} catch (JavaModelException e) {
296+
// Ignore
297+
}
298+
}
86299

87300
/**
88301
* Find project by path from all projects in workspace.
@@ -158,17 +371,19 @@ private static void processClasspathEntries(List<DependencyInfo> result, IJavaPr
158371

159372
/**
160373
* Process a library classpath entry.
374+
* Only returns the library file name without full path to reduce data size.
161375
*/
162376
private static void processLibraryEntry(List<DependencyInfo> result, IClasspathEntry entry, int libCount) {
163377
IPath libPath = entry.getPath();
164378
if (libPath != null) {
165-
result.add(new DependencyInfo("library_" + libCount,
166-
libPath.lastSegment() + " (" + libPath.toOSString() + ")"));
379+
// Only keep the file name, remove the full path
380+
result.add(new DependencyInfo("library_" + libCount, libPath.lastSegment()));
167381
}
168382
}
169383

170384
/**
171385
* Process a project reference classpath entry.
386+
* Simplified to only extract essential information.
172387
*/
173388
private static void processProjectEntry(List<DependencyInfo> result, IClasspathEntry entry, int projectRefCount) {
174389
IPath projectRefPath = entry.getPath();
@@ -185,12 +400,21 @@ private static void processContainerEntry(List<DependencyInfo> result, IClasspat
185400
String containerPath = entry.getPath().toString();
186401

187402
if (containerPath.contains("JRE_CONTAINER")) {
188-
result.add(new DependencyInfo(KEY_JRE_CONTAINER_PATH, containerPath));
403+
// Only extract the JRE version, not the full container path
189404
try {
190405
String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath());
191406
addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName);
192407
} catch (Exception e) {
193-
// Ignore if unable to get VM install name
408+
// Fallback: try to extract version from path
409+
if (containerPath.contains("JavaSE-")) {
410+
int startIdx = containerPath.lastIndexOf("JavaSE-");
411+
String version = containerPath.substring(startIdx);
412+
// Clean up any trailing characters
413+
if (version.contains("/")) {
414+
version = version.substring(0, version.indexOf("/"));
415+
}
416+
result.add(new DependencyInfo(KEY_JRE_CONTAINER, version));
417+
}
194418
}
195419
} else if (containerPath.contains("MAVEN")) {
196420
result.add(new DependencyInfo(KEY_BUILD_TOOL, "Maven"));

0 commit comments

Comments
 (0)