diff --git a/org.eclipse.jdt.ls.core/plugin.xml b/org.eclipse.jdt.ls.core/plugin.xml
index 02e80bb17b..ac9cac2097 100644
--- a/org.eclipse.jdt.ls.core/plugin.xml
+++ b/org.eclipse.jdt.ls.core/plugin.xml
@@ -225,4 +225,10 @@
id="org.eclipse.jdt.core.javanature">
+
+
+
+
diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java
index 264cc503d3..d5c384fdb5 100644
--- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java
+++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java
@@ -54,7 +54,6 @@
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.Flags;
-
import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IBuffer;
@@ -106,7 +105,6 @@
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.Type;
-
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.manipulation.CoreASTProvider;
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
@@ -147,6 +145,7 @@
/**
* General utilities for working with JDT APIs
+ *
* @author Gorkem Ercan
*
*/
@@ -298,10 +297,10 @@ static ICompilationUnit getFakeCompilationUnit(URI uri, IProgressMonitor monitor
public IBuffer createBuffer(ICompilationUnit workingCopy) {
return new DocumentAdapter(workingCopy, path);
}
- };
- try {
- return owner.newWorkingCopy(fileName, new IClasspathEntry[] { JavaRuntime.getDefaultJREContainerEntry() }, monitor);
- } catch (JavaModelException e) {
+ };
+ try {
+ return owner.newWorkingCopy(fileName, new IClasspathEntry[] { JavaRuntime.getDefaultJREContainerEntry() }, monitor);
+ } catch (JavaModelException e) {
return null;
}
}
@@ -432,9 +431,13 @@ public static IClassFile resolveClassFile(String uriString){
* @param uri with 'jdt' scheme
* @return class file
*/
- public static IClassFile resolveClassFile(URI uri){
+ public static IClassFile resolveClassFile(URI uri) {
if (uri != null && JDT_SCHEME.equals(uri.getScheme()) && "contents".equals(uri.getAuthority())) {
String handleId = uri.getQuery();
+ int idx = handleId.indexOf("&element=");
+ if (idx != -1) {
+ handleId = handleId.substring(0, idx);
+ }
IJavaElement element = JavaCore.create(handleId);
IClassFile cf = (IClassFile) element.getAncestor(IJavaElement.CLASS_FILE);
return cf;
@@ -891,7 +894,25 @@ public static String toUri(IClassFile classFile) {
String jarName = classFile.getParent().getParent().getElementName();
String uriString = null;
try {
- uriString = new URI(JDT_SCHEME, "contents", PATH_SEPARATOR + jarName + PATH_SEPARATOR + packageName + PATH_SEPARATOR + classFile.getElementName(), classFile.getHandleIdentifier(), null).toASCIIString();
+ String elementName = classFile.getElementName();
+ // Use the original source file name if available
+ String sourceFileName = SourceFileAttributeReader.getSourceFileName(classFile);
+ String fileName = sourceFileName == null ? elementName : sourceFileName;
+ StringBuilder pathBuilder = new StringBuilder();
+ pathBuilder.append(PATH_SEPARATOR).append(jarName);
+ if (packageName != null && !packageName.isBlank()) {
+ pathBuilder.append(PATH_SEPARATOR).append(packageName);
+ }
+ pathBuilder.append(PATH_SEPARATOR).append(fileName);
+
+ String handleIdentifier = classFile.getHandleIdentifier();
+ StringBuilder query = new StringBuilder(handleIdentifier);
+ if (!handleIdentifier.contains(elementName)) {
+ //Add the element name to the query so decompilers can detect it (looking at you module-info.class!)
+ query.append("&element=").append(elementName);
+ }
+ uriString = new URI(JDT_SCHEME, "contents", pathBuilder.toString(), query.toString(), null).toASCIIString();
+
} catch (URISyntaxException e) {
JavaLanguageServerPlugin.logException("Error generating URI for class ", e);
}
@@ -1168,26 +1189,26 @@ public static IResource findResource(URI uri, Function resourc
}
}
switch(resources.length) {
- case 0:
- return null;
- case 1:
- return resources[0];
- default://several candidates if a linked resource was created before the real project was configured
+ case 0:
+ return null;
+ case 1:
+ return resources[0];
+ default://several candidates if a linked resource was created before the real project was configured
IResource resource = null;
for (IResource f : resources) {
- //delete linked resource
- if (ProjectsManager.getDefaultProject().equals(f.getProject())) {
- try {
- f.delete(true, null);
- } catch (CoreException e) {
+ //delete linked resource
+ if (ProjectsManager.getDefaultProject().equals(f.getProject())) {
+ try {
+ f.delete(true, null);
+ } catch (CoreException e) {
JavaLanguageServerPlugin.logException(e.getMessage(), e);
+ }
}
- }
- //find closest project containing that file, in case of nested projects
+ //find closest project containing that file, in case of nested projects
if (resource == null || f.getProjectRelativePath().segmentCount() < resource.getProjectRelativePath().segmentCount()) {
resource = f;
+ }
}
- }
return resource;
}
}
@@ -1970,3 +1991,4 @@ public static CompilationUnit getAst(ITypeRoot typeRoot, IProgressMonitor monito
}
}
+
diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SourceFileAttributeReader.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SourceFileAttributeReader.java
new file mode 100644
index 0000000000..11ee997386
--- /dev/null
+++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/SourceFileAttributeReader.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Red Hat Inc. and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.ls.core.internal;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.util.IClassFileReader;
+import org.eclipse.jdt.core.util.ISourceAttribute;
+import org.eclipse.jdt.internal.core.util.ClassFileReader;
+
+/**
+ * Utility class to read the SourceFile attribute from class files.
+ * The SourceFile attribute contains the name of the source file from which
+ * the class was compiled (e.g., "OkHttpClient.kt" for Kotlin classes).
+ */
+public class SourceFileAttributeReader {
+
+ private SourceFileAttributeReader() {
+ // Utility class - no instantiation
+ }
+
+ /**
+ * Gets the source file name from the SourceFile attribute of the given class file.
+ *
+ * @param classFile the class file to read
+ * @return the source file name (e.g., "OkHttpClient.kt", "MyClass.java"),
+ * or null if the attribute is not present or cannot be read
+ */
+ public static String getSourceFileName(IClassFile classFile) {
+ if (classFile == null) {
+ return null;
+ }
+ try {
+ return getSourceFileName(classFile.getBytes());
+ } catch (CoreException e) {
+ JavaLanguageServerPlugin.logException("Error reading class file bytes", e);
+ return null;
+ }
+ }
+
+ /**
+ * Gets the source file name from the SourceFile attribute of the given class file bytes.
+ *
+ * @param classFileBytes the raw bytes of the class file
+ * @return the source file name (e.g., "OkHttpClient.kt", "MyClass.java"),
+ * or null if the attribute is not present or cannot be read
+ */
+ public static String getSourceFileName(byte[] classFileBytes) {
+ if (classFileBytes == null || classFileBytes.length == 0) {
+ return null;
+ }
+
+ try {
+ // Use Eclipse JDT's class file reader to parse the class file
+ IClassFileReader reader = new ClassFileReader(classFileBytes, IClassFileReader.CLASSFILE_ATTRIBUTES);
+
+ // Get the SourceFile attribute from the class file
+ ISourceAttribute sourceFileAttribute = reader.getSourceFileAttribute();
+ if (sourceFileAttribute == null) {
+ return null;
+ }
+
+ // Get the source file name from the constant pool
+ char[] sourceFileName = sourceFileAttribute.getSourceFileName();
+ if (sourceFileName == null || sourceFileName.length == 0) {
+ return null;
+ }
+
+ return new String(sourceFileName);
+ } catch (Exception e) {
+ JavaLanguageServerPlugin.logException("Error parsing class file format", e);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandler.java
index 05691e18cc..57685414c3 100644
--- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandler.java
+++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandler.java
@@ -19,18 +19,20 @@
import org.apache.commons.lang3.StringUtils;
public class ResolveSourceMappingHandler {
- private static final Pattern SOURCE_PATTERN = Pattern.compile("([\\w$\\.]+\\/)?(([\\w$]+\\.)+[<\\w$>]+)\\(([\\w-$]+\\.java:\\d+)\\)");
+ private static final Pattern SOURCE_PATTERN = Pattern.compile(
+ "([\\w$\\.]+\\/)?(([\\w$]+\\.)+[<\\w$>]+)\\(([\\w-$]+\\.(?:java|kt|groovy|clj|scala)(?::\\d+)?)+\\)"
+ );
private static final JdtSourceLookUpProvider sourceProvider = new JdtSourceLookUpProvider();
/**
* Given a line of stacktrace, resolve the uri of the source file or class file.
- *
+ *
* @param lineText
* the line of the stacktrace.
* @param projectNames
* A list of the project names that needs to search in. If the given list is empty,
* All the projects in the workspace will be searched.
- *
+ *
* @return the uri of the associated source file or class file.
*/
public static String resolveStackTraceLocation(String lineText, List projectNames) {
diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ContentProviderManager.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ContentProviderManager.java
index 437c61ad07..0be31eb820 100644
--- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ContentProviderManager.java
+++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ContentProviderManager.java
@@ -14,6 +14,7 @@
import java.net.URI;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
@@ -153,7 +154,7 @@ private List findMatchingProviders(URI uri) {
Set descriptors = getDescriptors(preferredProviderIds);
if (descriptors.isEmpty()) {
JavaLanguageServerPlugin.logError("No content providers found");
- return null;
+ return Collections.emptyList();
}
String uriString = uri != null ? uri.toString() : null;
@@ -166,7 +167,7 @@ private List findMatchingProviders(URI uri) {
if (matches.isEmpty()) {
JavaLanguageServerPlugin.logError("Unable to find content provider for URI " + uri);
- return null;
+ return Collections.emptyList();
}
return matches;
diff --git a/org.eclipse.jdt.ls.tests/projects/maven/quickstart2/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/quickstart2/pom.xml
index ecda6aadff..40b38abcf5 100644
--- a/org.eclipse.jdt.ls.tests/projects/maven/quickstart2/pom.xml
+++ b/org.eclipse.jdt.ls.tests/projects/maven/quickstart2/pom.xml
@@ -25,6 +25,16 @@
4.13
test
+
+ com.squareup.okhttp3
+ okhttp-jvm
+ 5.3.2
+
+
+ com.typesafe.akka
+ akka-actor_2.13
+ 2.8.8
+
diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/SourceAttachmentCommandTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/SourceAttachmentCommandTest.java
index fc2bcf976f..c600469ae9 100644
--- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/SourceAttachmentCommandTest.java
+++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/SourceAttachmentCommandTest.java
@@ -45,7 +45,7 @@
import com.google.gson.Gson;
public class SourceAttachmentCommandTest extends AbstractProjectsManagerBasedTest {
- private static final String classFileUri = "jdt://contents/foo.jar/foo/bar.class?%3Dsource-attachment%2Ffoo.jar%3Cfoo%28bar.class";
+ private static final String classFileUri = "jdt://contents/foo.jar/foo/bar.java?%3Dsource-attachment%2Ffoo.jar%3Cfoo%28bar.class";
private IProject project;
@BeforeEach
diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java
index 4ef2d55542..61227374ef 100644
--- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java
+++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java
@@ -189,8 +189,8 @@ public void outgoing_jar() throws Exception {
String jarUri = call0Calls.get(0).getTo().getUri();
assertTrue(jarUri.startsWith("jdt://"));
- assertTrue(jarUri.contains("org.apache.commons.lang3.text"));
- assertTrue(jarUri.contains("WordUtils.class"));
+ assertTrue(jarUri.contains("org.apache.commons.lang3.text/WordUtils.java?"));
+ assertTrue(jarUri.contains("org.apache.commons.lang3.text(WordUtils.class"));
}
@Test
diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandlerTest.java
index 97a4e4365d..936524c69f 100644
--- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandlerTest.java
+++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandlerTest.java
@@ -21,31 +21,45 @@
public class ResolveSourceMappingHandlerTest extends AbstractProjectsManagerBasedTest {
- @BeforeEach
+ @BeforeEach
public void setup() throws Exception {
importProjects("maven/quickstart2");
- }
-
- @Test
- public void testResolveSourceUri() {
- String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at quickstart.AppTest.shouldAnswerWithTrue(AppTest.java:10)", Arrays.asList("quickstart2"));
- assertTrue(uri.startsWith("file://"));
- assertTrue(uri.contains("quickstart2/src/test/java/quickstart/AppTest.java"));
- }
-
- @Test
- public void testResolveDependencyUri() {
- String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", Arrays.asList("quickstart2"));
- assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.class"));
+ }
+
+ @Test
+ public void testResolveSourceUri() {
+ String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at quickstart.AppTest.shouldAnswerWithTrue(AppTest.java:10)", Arrays.asList("quickstart2"));
+ assertTrue(uri.startsWith("file://"));
+ assertTrue(uri.contains("quickstart2/src/test/java/quickstart/AppTest.java"));
+ }
+
+ @Test
+ public void testResolveKotlinDerivedSources() {
+ String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at okhttp3.OkHttpClient.(OkHttpClient.kt)", Arrays.asList("quickstart2"));
+ assertTrue(uri.startsWith("jdt://contents/okhttp-jvm-5.3.2.jar/okhttp3/OkHttpClient.kt"), "Unexpected URI: " + uri);
+ assertTrue(uri.contains("com%5C/squareup%5C/okhttp3%5C/okhttp-jvm%5C/5.3.2%5C/okhttp-jvm-5.3.2.jar"));
+ }
+
+ @Test
+ public void testResolveScalaDerivedSources() {
+ String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at akka.actor.Actor.$init$(Actor.scala:492)", Arrays.asList("quickstart2"));
+ assertTrue(uri.startsWith("jdt://contents/akka-actor_2.13-2.8.8.jar/akka.actor/Actor.scala"), "Unexpected URI: " + uri);
+ assertTrue(uri.contains("com%5C/typesafe%5C/akka%5C/akka-actor_2.13%5C/2.8.8%5C/akka-actor_2.13-2.8.8.jar"));
+ }
+
+ @Test
+ public void testResolveDependencyUri() {
+ String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", Arrays.asList("quickstart2"));
+ assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.java"));
assertTrue(uri.contains(
"junit%5C/junit%5C/4.13%5C/junit-4.13.jar=/maven.pomderived=/true=/=/test=/true=/=/maven.groupId=/junit=/=/maven.artifactId=/junit=/=/maven.version=/4.13=/=/maven.scope=/test=/=/maven.pomderived=/true=/%3Corg.junit(Assert.class"));
- }
+ }
- @Test
- public void testResolveDependencyUriWithoutGivingProjectNames() {
- String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", null);
- assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.class"));
+ @Test
+ public void testResolveDependencyUriWithoutGivingProjectNames() {
+ String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", null);
+ assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.java"));
assertTrue(uri.contains(
"junit%5C/4.13%5C/junit-4.13.jar=/maven.pomderived=/true=/=/test=/true=/=/maven.groupId=/junit=/=/maven.artifactId=/junit=/=/maven.version=/4.13=/=/maven.scope=/test=/=/maven.pomderived=/true=/%3Corg.junit(Assert.class"));
- }
+ }
}