From 497f31bbeaa0c297bb67e4df974305dcaf4aaaa5 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Mon, 22 Jun 2026 11:08:26 +0800 Subject: [PATCH 1/2] Fix Scala Gradle classpath pollution --- .../gradle/scala/javals.gradle | 93 +++++++------- .../internal/managers/GradleBuildSupport.java | 83 +++++++++---- .../internal/managers/ScalaGradleSupport.java | 113 +++++++++++------- .../managers/StandardProjectsManager.java | 9 +- .../projects/gradle/scala/app/build.gradle | 2 + .../src/main/resources/application.properties | 1 + .../projects/gradle/scala/common/build.gradle | 10 ++ .../src/main/java/org/example/Library.java | 8 ++ .../projects/gradle/scala/settings.gradle | 2 +- .../managers/GradleProjectImporterTest.java | 34 ++++++ 10 files changed, 234 insertions(+), 121 deletions(-) create mode 100644 org.eclipse.jdt.ls.tests/projects/gradle/scala/app/src/main/resources/application.properties create mode 100644 org.eclipse.jdt.ls.tests/projects/gradle/scala/common/build.gradle create mode 100644 org.eclipse.jdt.ls.tests/projects/gradle/scala/common/src/main/java/org/example/Library.java diff --git a/org.eclipse.jdt.ls.core/gradle/scala/javals.gradle b/org.eclipse.jdt.ls.core/gradle/scala/javals.gradle index df74f297f4..93496d3415 100644 --- a/org.eclipse.jdt.ls.core/gradle/scala/javals.gradle +++ b/org.eclipse.jdt.ls.core/gradle/scala/javals.gradle @@ -1,4 +1,6 @@ import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.tasks.scala.ScalaCompile import org.gradle.jvm.JvmLibrary import org.gradle.language.base.artifact.SourcesArtifact @@ -7,62 +9,61 @@ allprojects { if (project.tasks.findByName('javalsCheckProject')) return project.tasks.register('javalsCheckProject') { doLast { - println "JAVALS_START" - def targetConfNames = ['runtimeClasspath', 'testRuntimeClasspath', 'scalaRuntime'] - def componentIds = [] as Set - def processedFiles = [] as Set - targetConfNames.findAll { project.configurations.findByName(it) }.each { name -> - def conf = project.configurations[name] - if (conf.canBeResolved) { - conf.incoming.resolutionResult.allComponents.each { - if (it.id instanceof ModuleComponentIdentifier) componentIds.add(it.id) + if (project.plugins.hasPlugin('scala')) { + println "JAVALS_START" + def targetConfNames = ['runtimeClasspath', 'testRuntimeClasspath', 'scalaRuntime'] + def componentIds = [] as Set + def processedFiles = [] as Set + def addLib = { file, sourcePath -> + if (file == null) return + def binPath = file.absolutePath + if (!processedFiles.contains(binPath)) { + println "LIB|${binPath}|${sourcePath ?: 'NO_SOURCE'}" + processedFiles.add(binPath) } } - } - def sourceMap = [:] - if (!componentIds.isEmpty()) { - project.dependencies.createArtifactResolutionQuery() - .forComponents(componentIds) - .withArtifacts(JvmLibrary, SourcesArtifact) - .execute() - .resolvedComponents.each { res -> - def sources = res.getArtifacts(SourcesArtifact) - if (!sources.isEmpty()) { - sourceMap["${res.id.group}:${res.id.module}".toString()] = sources.iterator().next().file.absolutePath - } - } - } - targetConfNames.findAll { project.configurations.findByName(it) }.each { name -> - def conf = project.configurations[name] - if (conf.canBeResolved) { - conf.resolvedConfiguration.resolvedArtifacts.each { artifact -> - def binPath = artifact.file.absolutePath - if (!processedFiles.contains(binPath)) { - def id = artifact.moduleVersion.id - def key = "${id.group}:${id.name}".toString() - def srcPath = sourceMap[key] ?: "NO_SOURCE" - println "LIB|${binPath}|${srcPath}" - processedFiles.add(binPath) + project.tasks.withType(ScalaCompile).each { task -> + def classesDir = task.hasProperty('destinationDirectory') + ? task.destinationDirectory.get().asFile + : task.destinationDir + addLib(classesDir, "NO_SOURCE") + } + targetConfNames.findAll { project.configurations.findByName(it) }.each { name -> + def conf = project.configurations[name] + if (conf.canBeResolved) { + conf.incoming.resolutionResult.allComponents.each { + if (it.id instanceof ModuleComponentIdentifier) componentIds.add(it.id) } } } - } - if (project.hasProperty('sourceSets')) { - project.sourceSets.each { ss -> - def scope = ss.name // 'main', 'test', 'integrationTest' - ss.allSource.srcDirs.each { dir -> - if (dir.exists() && !ss.resources.srcDirs.contains(dir)) { - println "SRC|${scope}|${dir.absolutePath}" + def sourceMap = [:] + if (!componentIds.isEmpty()) { + project.dependencies.createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(JvmLibrary, SourcesArtifact) + .execute() + .resolvedComponents.each { res -> + def sources = res.getArtifacts(SourcesArtifact) + if (!sources.isEmpty()) { + sourceMap["${res.id.group}:${res.id.module}".toString()] = sources.iterator().next().file.absolutePath + } } - } - ss.resources.srcDirs.each { dir -> - if (dir.exists()) { - println "RES|${scope}|${dir.absolutePath}" + } + targetConfNames.findAll { project.configurations.findByName(it) }.each { name -> + def conf = project.configurations[name] + if (conf.canBeResolved) { + conf.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (!(artifact.id.componentIdentifier instanceof ProjectComponentIdentifier)) { + def id = artifact.moduleVersion?.id + def key = id == null ? null : "${id.group}:${id.name}".toString() + def srcPath = key == null ? "NO_SOURCE" : sourceMap[key] ?: "NO_SOURCE" + addLib(artifact.file, srcPath) + } } } } + println "JAVALS_END" } - println "JAVALS_END" } } } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleBuildSupport.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleBuildSupport.java index 4894903e59..27307abdf8 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleBuildSupport.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/GradleBuildSupport.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -519,7 +520,7 @@ public void compile(IResource resource, IProgressMonitor monitor) { return; } if (resource == null) { - Set projects = new HashSet<>(); + Map projectsByRoot = new HashMap<>(); for (IProject project : ProjectUtils.getGradleProjects()) { if (!project.isOpen()) { continue; @@ -529,28 +530,37 @@ public void compile(IResource resource, IProgressMonitor monitor) { GradleBuild gb = gradleBuild.get(); if (gb instanceof InternalGradleBuild igb) { File rootDir = igb.getBuildConfig().getProperties().getRootProjectDirectory(); - if (rootDir != null && rootDir.equals(project.getRawLocation().toFile())) { - projects.add(project); + if (rootDir == null) { + continue; + } + // Prefer the workspace project whose location IS the gradle root + // (single-project or root-imported case). Otherwise, fall back to + // the first workspace project belonging to that build so we still + // have a connection point for multi-project builds where the root + // itself is not imported. + File projectLocation = project.getRawLocation() == null ? null : project.getRawLocation().toFile(); + if (rootDir.equals(projectLocation)) { + projectsByRoot.put(rootDir, project); + } else { + projectsByRoot.putIfAbsent(rootDir, project); } } } } - Map roots = new HashMap<>(); - for (IProject project : projects) { - File projectDir = project.getLocation().toFile(); - try (ProjectConnection connection = GradleConnector.newConnector().forProjectDirectory(projectDir).connect()) { + Map roots = new HashMap<>(); + for (Map.Entry entry : projectsByRoot.entrySet()) { + File rootDir = entry.getKey(); + try (ProjectConnection connection = GradleConnector.newConnector().forProjectDirectory(rootDir).connect()) { GradleProject gradleProject = connection.getModel(GradleProject.class); - roots.put(gradleProject.getPath(), gradleProject); + roots.put(rootDir, gradleProject); } } for (GradleProject gradleProject : roots.values()) { - List taskNames = new ArrayList<>(); + Set taskNames = new LinkedHashSet<>(); for (String taskName : includeTasks) { - if (hasTask(gradleProject, taskName)) { - taskNames.add(taskName); - } + collectTaskPaths(gradleProject, taskName, taskNames); } - compile(gradleProject, taskNames, monitor); + compile(gradleProject, new ArrayList<>(taskNames), monitor); } } } @@ -596,10 +606,10 @@ private void compile(GradleProject gradleProject, List taskNames, IProgr String error = errorStream.toString(); if (!error.isBlank()) { publishDiagnostics(error, new ParseOptions( - taskNames.contains(COMPILE_KOTLIN) || taskNames.contains(COMPILE_TEST_KOTLIN), - taskNames.contains(COMPILE_GROOVY) || taskNames.contains(COMPILE_TEST_GROOVY), - taskNames.contains(COMPILE_ASPECTJ) || taskNames.contains(COMPILE_TEST_ASPECTJ), - taskNames.contains(COMPILE_SCALA) || taskNames.contains(COMPILE_TEST_SCALA) + containsTask(taskNames, COMPILE_KOTLIN) || containsTask(taskNames, COMPILE_TEST_KOTLIN), + containsTask(taskNames, COMPILE_GROOVY) || containsTask(taskNames, COMPILE_TEST_GROOVY), + containsTask(taskNames, COMPILE_ASPECTJ) || containsTask(taskNames, COMPILE_TEST_ASPECTJ), + containsTask(taskNames, COMPILE_SCALA) || containsTask(taskNames, COMPILE_TEST_SCALA) ) ); } @@ -612,13 +622,20 @@ private void compile(GradleProject gradleProject, List taskNames, IProgr .map(container -> (IProject) container) .toArray(IProject[]::new); List workspaceProjects = ProjectUtils.getGradleProjects(); + Set refreshed = new HashSet<>(); for (IProject project: projects) { - project.refreshLocal(IResource.DEPTH_INFINITE, monitor); - for (IProject wp: workspaceProjects) { - if (wp.isAccessible() && !wp.equals(project)) { - if (project.getLocation().isPrefixOf(wp.getLocation())) { - wp.refreshLocal(IResource.DEPTH_INFINITE, monitor); - } + if (refreshed.add(project)) { + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + } + } + // Also refresh any workspace gradle project located under the + // gradle root directory. This covers multi-project builds where + // the root directory itself is not imported as an Eclipse project + // (so the loop above would otherwise be a no-op). + for (IProject wp : workspaceProjects) { + if (wp.isAccessible() && wp.getLocation() != null && path.isPrefixOf(wp.getLocation())) { + if (refreshed.add(wp)) { + wp.refreshLocal(IResource.DEPTH_INFINITE, monitor); } } } @@ -650,6 +667,26 @@ private boolean hasTask(GradleProject project, String taskName) { return false; } + private void collectTaskPaths(GradleProject project, String taskName, Set taskPaths) { + for (GradleTask task : project.getTasks()) { + if (taskName.equals(task.getName())) { + String projectPath = project.getPath(); + if (projectPath == null || ":".equals(projectPath)) { + taskPaths.add(taskName); + } else { + taskPaths.add(projectPath + ":" + taskName); + } + } + } + for (GradleProject childProject : project.getChildren()) { + collectTaskPaths(childProject, taskName, taskPaths); + } + } + + private boolean containsTask(List taskNames, String taskName) { + return taskNames.stream().anyMatch(name -> taskName.equals(name) || name.endsWith(":" + taskName)); + } + private void publishDiagnostics(String error, ParseOptions parseOptions) { JavaLanguageServerPlugin.logError("Gradle Error log: " + error); Map> diagnosticMap = new HashMap<>(); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java index 52114ab9aa..ceb46e4781 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java @@ -62,7 +62,6 @@ public class ScalaGradleSupport { public static final Path CONTAINER_PATH = new Path("org.eclipse.buildship.core.gradleclasspathcontainer"); - public static String[] EXCLUSIONS_PATTERNS = { "**" }; // Gradle Tooling API removes several scala libraries and adds the Scala builder and container that aren't recognized by Java LS. // See https://github.com/gradle/gradle/blob/b3c5d40e82439da4627b38b4ced93121e551b0eb/platforms/ide/ide-plugins/src/main/java/org/gradle/plugins/ide/eclipse/EclipsePlugin.java#L375-L377 @@ -136,20 +135,21 @@ private void checkSourcePaths(IProject project, IProgressMonitor monitor) { } File projectDir = project.getLocation().toFile(); File initScript = null; - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ProjectConnection connection = GradleConnector.newConnector().forProjectDirectory(projectDir).connect()) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ProjectConnection connection = GradleConnector.newConnector().forProjectDirectory(projectDir).connect()) { initScript = getInitScript(); BuildLauncher launcher = connection.newBuild(); launcher.withArguments("--init-script", initScript.getAbsolutePath(), "--no-configuration-cache"); launcher.forTasks("javalsCheckProject"); launcher.setStandardOutput(outputStream); launcher.run(); - String output = outputStream.toString(); - process(project, output, monitor); } catch (Exception e) { if (Boolean.getBoolean("jdt.ls.debug")) { JavaLanguageServerPlugin.logException(e); } } finally { + process(project, outputStream.toString(), monitor); + addDefaultScalaOutputPaths(project, monitor); if (initScript != null) { try { Files.delete(initScript.toPath()); @@ -161,6 +161,32 @@ private void checkSourcePaths(IProject project, IProgressMonitor monitor) { return; } + private static void addDefaultScalaOutputPaths(IProject project, IProgressMonitor monitor) { + File projectDir = project.getLocation().toFile(); + List paths = new ArrayList<>(); + addDefaultScalaOutputPath(projectDir, "main", paths); + addDefaultScalaOutputPath(projectDir, "test", paths); + if (!paths.isEmpty()) { + IJavaProject javaProject = JavaCore.create(project); + List toAdd = getMissingRawClasspathPaths(javaProject, paths); + if (!toAdd.isEmpty()) { + try { + configureClasspath(javaProject, toAdd, new HashMap<>(), monitor); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException(e); + } + } + } + } + + private static void addDefaultScalaOutputPath(File projectDir, String sourceSet, List paths) { + File sourceDir = new File(projectDir, "src/" + sourceSet + "/scala"); + File outputDir = new File(projectDir, "build/classes/scala/" + sourceSet); + if (sourceDir.isDirectory() || outputDir.exists()) { + paths.add(outputDir.getAbsolutePath()); + } + } + private File getInitScript() throws IOException { return GradleUtils.getGradleInitScript("/gradle/scala/javals.gradle"); } @@ -169,13 +195,10 @@ private File getInitScript() throws IOException { * The process method checks if a library has been added and if not, adds it. * Gradle Tooling API excludes some scala libraries because it expects Scala IDE to add them. * Since Scala IDE for VS Code doesn't exist, we add those libraries. - * The method also checks resources folders and excludes them from the compilation. */ private static void process(IProject project, String output, IProgressMonitor monitor) { List taskClasspaths = new LinkedList<>(); Map taskClasspathSources = new HashMap<>(); - List sources = new LinkedList<>(); - List resources = new LinkedList<>(); boolean start = false; try (BufferedReader reader = new BufferedReader(new StringReader(output))) { String line; @@ -199,14 +222,6 @@ private static void process(IProject project, String output, IProgressMonitor mo } break; } - case "SRC": { - sources.add(elements[2]); - break; - } - case "RES": { - resources.add(elements[2]); - break; - } default: JavaLanguageServerPlugin.logInfo("Unexpected value: " + elements[0]); break; @@ -232,7 +247,7 @@ private static void process(IProject project, String output, IProgressMonitor mo List toAdd = getMissingPaths(javaProject, paths); if (!toAdd.isEmpty()) { try { - configureClasspath(javaProject, toAdd, taskClasspathSources, resources, monitor); + configureClasspath(javaProject, toAdd, taskClasspathSources, monitor); } catch (JavaModelException e) { JavaLanguageServerPlugin.logException(e); } @@ -240,7 +255,7 @@ private static void process(IProject project, String output, IProgressMonitor mo } } - private static void configureClasspath(IJavaProject javaProject, List toAdd, Map taskClasspathSources, List resources, IProgressMonitor monitor) throws JavaModelException { + private static void configureClasspath(IJavaProject javaProject, List toAdd, Map taskClasspathSources, IProgressMonitor monitor) throws JavaModelException { IClasspathEntry[] classpath = javaProject.getRawClasspath(); List entries = new LinkedList<>(); for (String path : toAdd) { @@ -263,42 +278,50 @@ private static void configureClasspath(IJavaProject javaProject, List to .distinct() .toArray(IClasspathEntry[]::new); // @formatter:on - for (int i = 0; i < newClasspath.length; i++) { - IClasspathEntry entry = newClasspath[i]; - if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { - Optional optional = resources.stream().filter(r -> Objects.equals(entry.getPath().removeFirstSegments(1), new Path(r).makeRelativeTo(javaProject.getProject().getLocation()))).findFirst(); - if (optional.isPresent()) { - IPath[] exclusions = entry.getExclusionPatterns(); - if (exclusions == null) { - exclusions = new IPath[0]; - } - List currentExclusions = new ArrayList<>(Arrays.asList(exclusions)); - for (String ext : EXCLUSIONS_PATTERNS) { - IPath newPath = new Path(ext); - boolean exists = currentExclusions.stream().anyMatch(existingPath -> existingPath.toString().equals(ext)); - if (!exists) { - currentExclusions.add(newPath); - } - } - IClasspathEntry newEntry = JavaCore.newSourceEntry(entry.getPath(), entry.getInclusionPatterns(), currentExclusions.toArray(new IPath[0]), entry.getOutputLocation(), entry.getExtraAttributes()); - newClasspath[i] = newEntry; + javaProject.setRawClasspath(newClasspath, monitor); + } + + private static List getMissingPaths(IJavaProject javaProject, List paths) { + List toAdd = new ArrayList<>(); + IClasspathContainer container; + try { + container = JavaCore.getClasspathContainer(CONTAINER_PATH, javaProject); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException(e); + return toAdd; + } + if (container == null) { + return toAdd; + } + for (String path : paths) { + try { + Path classpathPath = new Path(path); + boolean exists = Arrays.stream(javaProject.getRawClasspath()) + .filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) + .anyMatch(entry -> Objects.equals(entry.getPath(), classpathPath)); + IClasspathEntry[] entries = container.getClasspathEntries(); + Optional optional = Arrays.stream(entries).filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY).filter(entry -> Objects.equals(entry.getPath(), classpathPath)).findFirst(); + exists |= optional.isPresent(); + if (!exists) { + toAdd.add(path); } + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException(e); } } - javaProject.setRawClasspath(newClasspath, monitor); + return toAdd; } - private static List getMissingPaths(IJavaProject javaProject, List paths) { + private static List getMissingRawClasspathPaths(IJavaProject javaProject, List paths) { List toAdd = new ArrayList<>(); for (String path : paths) { try { - IClasspathContainer container = JavaCore.getClasspathContainer(CONTAINER_PATH, javaProject); - if (container != null) { - IClasspathEntry[] entries = container.getClasspathEntries(); - Optional optional = Arrays.stream(entries).filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY).filter(entry -> Objects.equals(entry.getPath(), new Path(path))).findFirst(); - if (!optional.isPresent()) { - toAdd.add(path); - } + Path classpathPath = new Path(path); + boolean exists = Arrays.stream(javaProject.getRawClasspath()) + .filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) + .anyMatch(entry -> Objects.equals(entry.getPath(), classpathPath)); + if (!exists) { + toAdd.add(path); } } catch (JavaModelException e) { JavaLanguageServerPlugin.logException(e); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/StandardProjectsManager.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/StandardProjectsManager.java index c15380076f..c4b5535ed5 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/StandardProjectsManager.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/StandardProjectsManager.java @@ -725,12 +725,9 @@ public void projectsBuildFinished(IProgressMonitor monitor) { } this.preferenceManager.getPreferences().updateAnnotationNullAnalysisOptions(); new ScalaGradleSupport().cleanScalaProjects(monitor); - ProjectsManager projectsManager = JavaLanguageServerPlugin.getProjectsManager(); - if (projectsManager != null) { - projectsManager.buildSupports().forEach(bs -> { - bs.compile(null, monitor); - }); - } + buildSupports().forEach(bs -> { + bs.compile(null, monitor); + }); } @Override diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/build.gradle b/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/build.gradle index f51ee8b896..44e018d44c 100644 --- a/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/build.gradle +++ b/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/build.gradle @@ -31,6 +31,8 @@ sourceSets { dependencies { + implementation project(':common') + // Use Scala 2.13 in our library project implementation libs.scala.library diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/src/main/resources/application.properties b/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/src/main/resources/application.properties new file mode 100644 index 0000000000..8baed2b84b --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/gradle/scala/app/src/main/resources/application.properties @@ -0,0 +1 @@ +sample.value=true diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/build.gradle b/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/build.gradle new file mode 100644 index 0000000000..cf59a34aa8 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = '21' +targetCompatibility = '21' diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/src/main/java/org/example/Library.java b/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/src/main/java/org/example/Library.java new file mode 100644 index 0000000000..8b95fa6005 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/gradle/scala/common/src/main/java/org/example/Library.java @@ -0,0 +1,8 @@ +package org.example; + +public class Library { + + public String name() { + return "library"; + } +} diff --git a/org.eclipse.jdt.ls.tests/projects/gradle/scala/settings.gradle b/org.eclipse.jdt.ls.tests/projects/gradle/scala/settings.gradle index 2c6e7a8b79..942019353b 100644 --- a/org.eclipse.jdt.ls.tests/projects/gradle/scala/settings.gradle +++ b/org.eclipse.jdt.ls.tests/projects/gradle/scala/settings.gradle @@ -4,4 +4,4 @@ plugins { } rootProject.name = 'scala' -include('app') +include('app', 'common') diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporterTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporterTest.java index 9357dea772..40b46e981a 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporterTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/managers/GradleProjectImporterTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.ByteArrayOutputStream; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; @@ -68,7 +69,11 @@ import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; import org.eclipse.jdt.ls.core.internal.preferences.Preferences; import org.eclipse.jdt.ls.core.internal.preferences.Preferences.FeatureStatus; +import org.gradle.tooling.BuildLauncher; +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ProjectConnection; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -868,11 +873,40 @@ public void testScalaSupportEnabled() throws Exception { IProject project = ProjectUtils.getProject("app"); assertTrue(ProjectUtils.isGradleProject(project)); assertNoErrors(project); + IClasspathEntry[] classpathEntries = JavaCore.create(project).getRawClasspath(); + boolean hasBuildLibEntry = Arrays.stream(classpathEntries).anyMatch(cpe -> cpe.getEntryKind() == IClasspathEntry.CPE_LIBRARY && cpe.getPath().toString().replace('\\', '/').contains("/build/libs/")); + boolean hasUnexcludedResources = Arrays.stream(classpathEntries).anyMatch(cpe -> cpe.getEntryKind() == IClasspathEntry.CPE_SOURCE && cpe.getPath().toString().endsWith("src/main/resources") + && Arrays.stream(cpe.getExclusionPatterns()).noneMatch(pattern -> "**".equals(pattern.toString()))); + assertFalse(hasBuildLibEntry); + assertTrue(hasUnexcludedResources); } finally { this.preferences.setScalaSupportEnabled(oldScalaSupported); } } + @Test + public void testScalaSupportTaskNoopsForNonScalaProject() throws Exception { + // Gradle 8.5 can fail to compile the init script with JDK 25 (ASM/Groovy classfile support). + Assumptions.assumeTrue(Runtime.version().feature() < 25, "Skip on JDK 25+"); + File projectDir = new File(getSourceProjectDirectory(), "gradle/scala"); + File initScript = null; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ProjectConnection connection = GradleConnector.newConnector().forProjectDirectory(projectDir).connect()) { + initScript = GradleUtils.getGradleInitScript("/gradle/scala/javals.gradle"); + BuildLauncher launcher = connection.newBuild(); + launcher.withArguments("--init-script", initScript.getAbsolutePath(), "--no-configuration-cache", "--quiet"); + launcher.forTasks(":common:javalsCheckProject"); + launcher.setStandardOutput(outputStream); + launcher.run(); + String output = outputStream.toString(); + assertFalse(output.contains("JAVALS_START")); + assertFalse(output.contains("LIB|")); + } finally { + if (initScript != null) { + Files.deleteIfExists(initScript.toPath()); + } + } + } + @Test public void testScalaSupportDisabled() throws Exception { boolean oldScalaSupported = this.preferences.isScalaSupportEnabled(); From f50d9905ab133d04c3532967741a39b6ce0a2846 Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Wed, 24 Jun 2026 15:46:26 +0800 Subject: [PATCH 2/2] Close Scala Gradle output stream --- .../jdt/ls/core/internal/managers/ScalaGradleSupport.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java index ceb46e4781..c390408eeb 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ScalaGradleSupport.java @@ -150,6 +150,11 @@ private void checkSourcePaths(IProject project, IProgressMonitor monitor) { } finally { process(project, outputStream.toString(), monitor); addDefaultScalaOutputPaths(project, monitor); + try { + outputStream.close(); + } catch (IOException e) { + JavaLanguageServerPlugin.logException(e); + } if (initScript != null) { try { Files.delete(initScript.toPath());