From c895aa06ce07bab1aa5e9e07b22f4ab13bfc60e1 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Thu, 30 Oct 2025 15:36:23 +0800 Subject: [PATCH 1/2] feat: support aggregate projects and format result --- .../ext/core/parser/ProjectResolver.java | 234 +++++++++++++++++- 1 file changed, 229 insertions(+), 5 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java index 2f317a41..3865568a 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java @@ -2,10 +2,13 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.IClasspathEntry; @@ -44,6 +47,7 @@ public DependencyInfo(String key, String value) { /** * Resolve project dependencies information including JDK version. + * Supports both single projects and multi-module aggregator projects. * * @param projectUri The project URI * @param monitor Progress monitor for cancellation support @@ -64,8 +68,13 @@ public static List resolveProjectDependencies(String projectUri, } IJavaProject javaProject = JavaCore.create(project); + // Check if this is a Java project if (javaProject == null || !javaProject.exists()) { - return result; + // Not a Java project - might be an aggregator/parent project + // Try to find Java sub-projects under this path + JdtlsExtActivator.logInfo("Not a Java project: " + project.getName() + + ", checking for sub-projects"); + return resolveAggregatorProjectDependencies(root, projectPath, monitor); } // Add basic project information @@ -83,6 +92,210 @@ public static List resolveProjectDependencies(String projectUri, return result; } + + /** + * Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects. + * This handles multi-module Maven/Gradle projects where the parent is not a Java project itself. + * Returns aggregated information useful for AI context (Java version, common dependencies, build tool). + * + * @param root The workspace root + * @param parentPath The path of the parent/aggregator project + * @param monitor Progress monitor + * @return Aggregated dependency information from all sub-projects + */ + private static List resolveAggregatorProjectDependencies( + IWorkspaceRoot root, IPath parentPath, IProgressMonitor monitor) { + + List result = new ArrayList<>(); + List javaProjects = new ArrayList<>(); + + // Find all Java projects under the parent path + IProject[] allProjects = root.getProjects(); + for (IProject p : allProjects) { + if (p.getLocation() != null && parentPath.isPrefixOf(p.getLocation())) { + try { + if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) { + IJavaProject jp = JavaCore.create(p); + if (jp != null && jp.exists()) { + javaProjects.add(jp); + } + } + } catch (CoreException e) { + // Skip this project + } + } + } + + if (javaProjects.isEmpty()) { + JdtlsExtActivator.logInfo("No Java sub-projects found under: " + parentPath.toOSString()); + return result; + } + + JdtlsExtActivator.logInfo("Found " + javaProjects.size() + + " Java sub-project(s) under: " + parentPath.toOSString()); + + // Mark as aggregator project + result.add(new DependencyInfo("aggregatorProject", "true")); + result.add(new DependencyInfo("totalSubProjects", String.valueOf(javaProjects.size()))); + + // Collect sub-project names for reference + StringBuilder projectNames = new StringBuilder(); + for (int i = 0; i < javaProjects.size(); i++) { + if (i > 0) projectNames.append(", "); + projectNames.append(javaProjects.get(i).getProject().getName()); + } + result.add(new DependencyInfo("subProjectNames", projectNames.toString())); + + // Determine the primary/representative Java version (most common or highest) + String primaryJavaVersion = determinePrimaryJavaVersion(javaProjects); + if (primaryJavaVersion != null) { + result.add(new DependencyInfo(KEY_JAVA_VERSION, primaryJavaVersion)); + } + + // Collect all unique libraries across sub-projects (top 10 most common) + Map libraryFrequency = collectLibraryFrequency(javaProjects, monitor); + addTopLibraries(result, libraryFrequency, 10); + + // Detect build tool from parent directory + IProject parentProject = findProjectByPath(root, parentPath); + if (parentProject != null) { + detectBuildTool(result, parentProject); + } + + // Get JRE container info from first sub-project (usually consistent across modules) + if (!javaProjects.isEmpty()) { + extractJreInfo(result, javaProjects.get(0)); + } + + return result; + } + + /** + * Determine the primary Java version from all sub-projects. + * Returns the most common version, or the highest if there's a tie. + */ + private static String determinePrimaryJavaVersion(List javaProjects) { + Map versionCount = new ConcurrentHashMap<>(); + + for (IJavaProject jp : javaProjects) { + String version = jp.getOption(JavaCore.COMPILER_COMPLIANCE, true); + if (version != null) { + versionCount.put(version, versionCount.getOrDefault(version, 0) + 1); + } + } + + if (versionCount.isEmpty()) { + return null; + } + + // Find most common version (or highest if tie) + return versionCount.entrySet().stream() + .max((e1, e2) -> { + int countCompare = Integer.compare(e1.getValue(), e2.getValue()); + if (countCompare != 0) return countCompare; + // If same count, prefer higher version + return e1.getKey().compareTo(e2.getKey()); + }) + .map(Map.Entry::getKey) + .orElse(null); + } + + /** + * Collect frequency of all libraries across sub-projects. + * Returns a map of library name to frequency count. + */ + private static Map collectLibraryFrequency( + List javaProjects, IProgressMonitor monitor) { + + Map libraryFrequency = new ConcurrentHashMap<>(); + + for (IJavaProject jp : javaProjects) { + if (monitor.isCanceled()) { + break; + } + + try { + IClasspathEntry[] entries = jp.getResolvedClasspath(true); + for (IClasspathEntry entry : entries) { + if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { + IPath libPath = entry.getPath(); + if (libPath != null) { + String libName = libPath.lastSegment(); + libraryFrequency.put(libName, + libraryFrequency.getOrDefault(libName, 0) + 1); + } + } + } + } catch (JavaModelException e) { + // Skip this project + } + } + + return libraryFrequency; + } + + /** + * Add top N most common libraries to result. + */ + private static void addTopLibraries(List result, + Map libraryFrequency, int topN) { + + if (libraryFrequency.isEmpty()) { + result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, "0")); + return; + } + + // Sort by frequency (descending) and take top N + List> topLibs = libraryFrequency.entrySet().stream() + .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())) + .limit(topN) + .collect(java.util.stream.Collectors.toList()); + + result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, + String.valueOf(libraryFrequency.size()))); + + // Add top common libraries + int index = 1; + for (Map.Entry entry : topLibs) { + result.add(new DependencyInfo("commonLibrary_" + index, + entry.getKey() + " (used in " + entry.getValue() + " modules)")); + index++; + } + } + + /** + * Extract JRE container information from a Java project. + */ + private static void extractJreInfo(List result, IJavaProject javaProject) { + try { + IClasspathEntry[] entries = javaProject.getResolvedClasspath(true); + for (IClasspathEntry entry : entries) { + if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { + String containerPath = entry.getPath().toString(); + if (containerPath.contains("JRE_CONTAINER")) { + try { + String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath()); + addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName); + return; + } catch (Exception e) { + // Fallback: extract from path + if (containerPath.contains("JavaSE-")) { + int startIdx = containerPath.lastIndexOf("JavaSE-"); + String version = containerPath.substring(startIdx); + if (version.contains("/")) { + version = version.substring(0, version.indexOf("/")); + } + result.add(new DependencyInfo(KEY_JRE_CONTAINER, version)); + return; + } + } + } + } + } + } catch (JavaModelException e) { + // Ignore + } + } /** * Find project by path from all projects in workspace. @@ -158,17 +371,19 @@ private static void processClasspathEntries(List result, IJavaPr /** * Process a library classpath entry. + * Only returns the library file name without full path to reduce data size. */ private static void processLibraryEntry(List result, IClasspathEntry entry, int libCount) { IPath libPath = entry.getPath(); if (libPath != null) { - result.add(new DependencyInfo("library_" + libCount, - libPath.lastSegment() + " (" + libPath.toOSString() + ")")); + // Only keep the file name, remove the full path + result.add(new DependencyInfo("library_" + libCount, libPath.lastSegment())); } } /** * Process a project reference classpath entry. + * Simplified to only extract essential information. */ private static void processProjectEntry(List result, IClasspathEntry entry, int projectRefCount) { IPath projectRefPath = entry.getPath(); @@ -185,12 +400,21 @@ private static void processContainerEntry(List result, IClasspat String containerPath = entry.getPath().toString(); if (containerPath.contains("JRE_CONTAINER")) { - result.add(new DependencyInfo(KEY_JRE_CONTAINER_PATH, containerPath)); + // Only extract the JRE version, not the full container path try { String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath()); addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName); } catch (Exception e) { - // Ignore if unable to get VM install name + // Fallback: try to extract version from path + if (containerPath.contains("JavaSE-")) { + int startIdx = containerPath.lastIndexOf("JavaSE-"); + String version = containerPath.substring(startIdx); + // Clean up any trailing characters + if (version.contains("/")) { + version = version.substring(0, version.indexOf("/")); + } + result.add(new DependencyInfo(KEY_JRE_CONTAINER, version)); + } } } else if (containerPath.contains("MAVEN")) { result.add(new DependencyInfo(KEY_BUILD_TOOL, "Maven")); From e5fdd8eed02e9a53b2f358d0477b3b8ab05eeaf4 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Fri, 31 Oct 2025 16:58:52 +0800 Subject: [PATCH 2/2] fix: update --- .../jdtls/ext/core/ProjectCommand.java | 6 +- .../ext/core/parser/ProjectResolver.java | 231 ++---------------- 2 files changed, 23 insertions(+), 214 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index 4aa6632b..002f8072 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -506,7 +506,7 @@ private static String getSeverityString(int severity) { /** * Get project dependencies information including JDK version. * - * @param arguments List containing the project URI as the first element + * @param arguments List containing the file URI as the first element * @param monitor Progress monitor for cancellation support * @return List of DependencyInfo containing key-value pairs of project information */ @@ -515,8 +515,8 @@ public static List getProjectDependencies(List arguments return new ArrayList<>(); } - String projectUri = (String) arguments.get(0); - List resolverResult = ProjectResolver.resolveProjectDependencies(projectUri, monitor); + String fileUri = (String) arguments.get(0); + List resolverResult = ProjectResolver.resolveProjectDependencies(fileUri, monitor); // Convert ProjectResolver.DependencyInfo to ProjectCommand.DependencyInfo List result = new ArrayList<>(); diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java index 3865568a..77d2d256 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java @@ -49,19 +49,19 @@ public DependencyInfo(String key, String value) { * Resolve project dependencies information including JDK version. * Supports both single projects and multi-module aggregator projects. * - * @param projectUri The project URI + * @param fileUri The file URI * @param monitor Progress monitor for cancellation support * @return List of DependencyInfo containing key-value pairs of project information */ - public static List resolveProjectDependencies(String projectUri, IProgressMonitor monitor) { + public static List resolveProjectDependencies(String fileUri, IProgressMonitor monitor) { List result = new ArrayList<>(); try { - IPath projectPath = ResourceUtils.canonicalFilePathFromURI(projectUri); + IPath fileIPath = ResourceUtils.canonicalFilePathFromURI(fileUri); // Find the project IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); - IProject project = findProjectByPath(root, projectPath); + IProject project = findProjectByPath(root, fileIPath); if (project == null || !project.isAccessible()) { return result; @@ -70,11 +70,7 @@ public static List resolveProjectDependencies(String projectUri, IJavaProject javaProject = JavaCore.create(project); // Check if this is a Java project if (javaProject == null || !javaProject.exists()) { - // Not a Java project - might be an aggregator/parent project - // Try to find Java sub-projects under this path - JdtlsExtActivator.logInfo("Not a Java project: " + project.getName() + - ", checking for sub-projects"); - return resolveAggregatorProjectDependencies(root, projectPath, monitor); + return result; } // Add basic project information @@ -92,221 +88,34 @@ public static List resolveProjectDependencies(String projectUri, return result; } - + /** - * Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects. - * This handles multi-module Maven/Gradle projects where the parent is not a Java project itself. - * Returns aggregated information useful for AI context (Java version, common dependencies, build tool). + * Find project by path from all projects in workspace. + * The path can be either a project root path or a file/folder path within a project. + * This method will find the project that contains the given path. * * @param root The workspace root - * @param parentPath The path of the parent/aggregator project - * @param monitor Progress monitor - * @return Aggregated dependency information from all sub-projects + * @param filePath The path to search for (can be project root or file within project) + * @return The project that contains the path, or null if not found */ - private static List resolveAggregatorProjectDependencies( - IWorkspaceRoot root, IPath parentPath, IProgressMonitor monitor) { - - List result = new ArrayList<>(); - List javaProjects = new ArrayList<>(); - - // Find all Java projects under the parent path + private static IProject findProjectByPath(IWorkspaceRoot root, IPath filePath) { IProject[] allProjects = root.getProjects(); - for (IProject p : allProjects) { - if (p.getLocation() != null && parentPath.isPrefixOf(p.getLocation())) { - try { - if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) { - IJavaProject jp = JavaCore.create(p); - if (jp != null && jp.exists()) { - javaProjects.add(jp); - } - } - } catch (CoreException e) { - // Skip this project - } - } - } - - if (javaProjects.isEmpty()) { - JdtlsExtActivator.logInfo("No Java sub-projects found under: " + parentPath.toOSString()); - return result; - } - - JdtlsExtActivator.logInfo("Found " + javaProjects.size() + - " Java sub-project(s) under: " + parentPath.toOSString()); - - // Mark as aggregator project - result.add(new DependencyInfo("aggregatorProject", "true")); - result.add(new DependencyInfo("totalSubProjects", String.valueOf(javaProjects.size()))); - - // Collect sub-project names for reference - StringBuilder projectNames = new StringBuilder(); - for (int i = 0; i < javaProjects.size(); i++) { - if (i > 0) projectNames.append(", "); - projectNames.append(javaProjects.get(i).getProject().getName()); - } - result.add(new DependencyInfo("subProjectNames", projectNames.toString())); - - // Determine the primary/representative Java version (most common or highest) - String primaryJavaVersion = determinePrimaryJavaVersion(javaProjects); - if (primaryJavaVersion != null) { - result.add(new DependencyInfo(KEY_JAVA_VERSION, primaryJavaVersion)); - } - - // Collect all unique libraries across sub-projects (top 10 most common) - Map libraryFrequency = collectLibraryFrequency(javaProjects, monitor); - addTopLibraries(result, libraryFrequency, 10); - - // Detect build tool from parent directory - IProject parentProject = findProjectByPath(root, parentPath); - if (parentProject != null) { - detectBuildTool(result, parentProject); - } - - // Get JRE container info from first sub-project (usually consistent across modules) - if (!javaProjects.isEmpty()) { - extractJreInfo(result, javaProjects.get(0)); - } - - return result; - } - - /** - * Determine the primary Java version from all sub-projects. - * Returns the most common version, or the highest if there's a tie. - */ - private static String determinePrimaryJavaVersion(List javaProjects) { - Map versionCount = new ConcurrentHashMap<>(); - - for (IJavaProject jp : javaProjects) { - String version = jp.getOption(JavaCore.COMPILER_COMPLIANCE, true); - if (version != null) { - versionCount.put(version, versionCount.getOrDefault(version, 0) + 1); - } - } - - if (versionCount.isEmpty()) { - return null; - } - - // Find most common version (or highest if tie) - return versionCount.entrySet().stream() - .max((e1, e2) -> { - int countCompare = Integer.compare(e1.getValue(), e2.getValue()); - if (countCompare != 0) return countCompare; - // If same count, prefer higher version - return e1.getKey().compareTo(e2.getKey()); - }) - .map(Map.Entry::getKey) - .orElse(null); - } - - /** - * Collect frequency of all libraries across sub-projects. - * Returns a map of library name to frequency count. - */ - private static Map collectLibraryFrequency( - List javaProjects, IProgressMonitor monitor) { - Map libraryFrequency = new ConcurrentHashMap<>(); - - for (IJavaProject jp : javaProjects) { - if (monitor.isCanceled()) { - break; - } - - try { - IClasspathEntry[] entries = jp.getResolvedClasspath(true); - for (IClasspathEntry entry : entries) { - if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { - IPath libPath = entry.getPath(); - if (libPath != null) { - String libName = libPath.lastSegment(); - libraryFrequency.put(libName, - libraryFrequency.getOrDefault(libName, 0) + 1); - } - } - } - } catch (JavaModelException e) { - // Skip this project + // First pass: check for exact project location match (most efficient) + for (IProject p : allProjects) { + if (p.getLocation() != null && p.getLocation().equals(filePath)) { + return p; } } - return libraryFrequency; - } - - /** - * Add top N most common libraries to result. - */ - private static void addTopLibraries(List result, - Map libraryFrequency, int topN) { - - if (libraryFrequency.isEmpty()) { - result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, "0")); - return; - } - - // Sort by frequency (descending) and take top N - List> topLibs = libraryFrequency.entrySet().stream() - .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())) - .limit(topN) - .collect(java.util.stream.Collectors.toList()); - - result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, - String.valueOf(libraryFrequency.size()))); - - // Add top common libraries - int index = 1; - for (Map.Entry entry : topLibs) { - result.add(new DependencyInfo("commonLibrary_" + index, - entry.getKey() + " (used in " + entry.getValue() + " modules)")); - index++; - } - } - - /** - * Extract JRE container information from a Java project. - */ - private static void extractJreInfo(List result, IJavaProject javaProject) { - try { - IClasspathEntry[] entries = javaProject.getResolvedClasspath(true); - for (IClasspathEntry entry : entries) { - if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { - String containerPath = entry.getPath().toString(); - if (containerPath.contains("JRE_CONTAINER")) { - try { - String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath()); - addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName); - return; - } catch (Exception e) { - // Fallback: extract from path - if (containerPath.contains("JavaSE-")) { - int startIdx = containerPath.lastIndexOf("JavaSE-"); - String version = containerPath.substring(startIdx); - if (version.contains("/")) { - version = version.substring(0, version.indexOf("/")); - } - result.add(new DependencyInfo(KEY_JRE_CONTAINER, version)); - return; - } - } - } - } - } - } catch (JavaModelException e) { - // Ignore - } - } - - /** - * Find project by path from all projects in workspace. - */ - private static IProject findProjectByPath(IWorkspaceRoot root, IPath projectPath) { - IProject[] allProjects = root.getProjects(); + // Second pass: check if the file path is within any project directory + // This handles cases where filePath points to a file or folder inside a project for (IProject p : allProjects) { - if (p.getLocation() != null && p.getLocation().equals(projectPath)) { + if (p.getLocation() != null && p.getLocation().isPrefixOf(filePath)) { return p; } } + return null; }