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")); - } + } }