Skip to content

Commit 6d2a8ff

Browse files
committed
feat: have class URIs use their original source name
Signed-off-by: Fred Bricon <fbricon@gmail.com>
1 parent 3a55135 commit 6d2a8ff

6 files changed

Lines changed: 136 additions & 28 deletions

File tree

org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
import org.eclipse.core.runtime.jobs.ISchedulingRule;
5555
import org.eclipse.jdt.core.CompletionProposal;
5656
import org.eclipse.jdt.core.Flags;
57-
5857
import org.eclipse.jdt.core.IAnnotatable;
5958
import org.eclipse.jdt.core.IAnnotation;
6059
import org.eclipse.jdt.core.IBuffer;
@@ -106,7 +105,6 @@
106105
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
107106
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
108107
import org.eclipse.jdt.core.dom.Type;
109-
110108
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
111109
import org.eclipse.jdt.core.manipulation.CoreASTProvider;
112110
import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
@@ -147,6 +145,7 @@
147145

148146
/**
149147
* General utilities for working with JDT APIs
148+
*
150149
* @author Gorkem Ercan
151150
*
152151
*/
@@ -298,10 +297,10 @@ static ICompilationUnit getFakeCompilationUnit(URI uri, IProgressMonitor monitor
298297
public IBuffer createBuffer(ICompilationUnit workingCopy) {
299298
return new DocumentAdapter(workingCopy, path);
300299
}
301-
};
302-
try {
303-
return owner.newWorkingCopy(fileName, new IClasspathEntry[] { JavaRuntime.getDefaultJREContainerEntry() }, monitor);
304-
} catch (JavaModelException e) {
300+
};
301+
try {
302+
return owner.newWorkingCopy(fileName, new IClasspathEntry[] { JavaRuntime.getDefaultJREContainerEntry() }, monitor);
303+
} catch (JavaModelException e) {
305304
return null;
306305
}
307306
}
@@ -432,9 +431,13 @@ public static IClassFile resolveClassFile(String uriString){
432431
* @param uri with 'jdt' scheme
433432
* @return class file
434433
*/
435-
public static IClassFile resolveClassFile(URI uri){
434+
public static IClassFile resolveClassFile(URI uri) {
436435
if (uri != null && JDT_SCHEME.equals(uri.getScheme()) && "contents".equals(uri.getAuthority())) {
437436
String handleId = uri.getQuery();
437+
int idx = handleId.indexOf("&element=");
438+
if (idx != -1) {
439+
handleId = handleId.substring(0, idx);
440+
}
438441
IJavaElement element = JavaCore.create(handleId);
439442
IClassFile cf = (IClassFile) element.getAncestor(IJavaElement.CLASS_FILE);
440443
return cf;
@@ -891,7 +894,25 @@ public static String toUri(IClassFile classFile) {
891894
String jarName = classFile.getParent().getParent().getElementName();
892895
String uriString = null;
893896
try {
894-
uriString = new URI(JDT_SCHEME, "contents", PATH_SEPARATOR + jarName + PATH_SEPARATOR + packageName + PATH_SEPARATOR + classFile.getElementName(), classFile.getHandleIdentifier(), null).toASCIIString();
897+
String elementName = classFile.getElementName();
898+
// Use the original source file name if available
899+
String sourceFileName = SourceFileAttributeReader.getSourceFileName(classFile);
900+
String fileName = sourceFileName == null ? elementName : sourceFileName;
901+
StringBuilder pathBuilder = new StringBuilder();
902+
pathBuilder.append(PATH_SEPARATOR).append(jarName);
903+
if (packageName != null && !packageName.isBlank()) {
904+
pathBuilder.append(PATH_SEPARATOR).append(packageName);
905+
}
906+
pathBuilder.append(PATH_SEPARATOR).append(fileName);
907+
908+
String handleIdentifier = classFile.getHandleIdentifier();
909+
StringBuilder query = new StringBuilder(handleIdentifier);
910+
if (!handleIdentifier.contains(elementName)) {
911+
//Add the element name to the query so decompilers can detect it (looking at you module-info.class!)
912+
query.append("&element=").append(elementName);
913+
}
914+
uriString = new URI(JDT_SCHEME, "contents", pathBuilder.toString(), query.toString(), null).toASCIIString();
915+
895916
} catch (URISyntaxException e) {
896917
JavaLanguageServerPlugin.logException("Error generating URI for class ", e);
897918
}
@@ -1168,26 +1189,26 @@ public static IResource findResource(URI uri, Function<URI, IResource[]> resourc
11681189
}
11691190
}
11701191
switch(resources.length) {
1171-
case 0:
1172-
return null;
1173-
case 1:
1174-
return resources[0];
1175-
default://several candidates if a linked resource was created before the real project was configured
1192+
case 0:
1193+
return null;
1194+
case 1:
1195+
return resources[0];
1196+
default://several candidates if a linked resource was created before the real project was configured
11761197
IResource resource = null;
11771198
for (IResource f : resources) {
1178-
//delete linked resource
1179-
if (ProjectsManager.getDefaultProject().equals(f.getProject())) {
1180-
try {
1181-
f.delete(true, null);
1182-
} catch (CoreException e) {
1199+
//delete linked resource
1200+
if (ProjectsManager.getDefaultProject().equals(f.getProject())) {
1201+
try {
1202+
f.delete(true, null);
1203+
} catch (CoreException e) {
11831204
JavaLanguageServerPlugin.logException(e.getMessage(), e);
1205+
}
11841206
}
1185-
}
1186-
//find closest project containing that file, in case of nested projects
1207+
//find closest project containing that file, in case of nested projects
11871208
if (resource == null || f.getProjectRelativePath().segmentCount() < resource.getProjectRelativePath().segmentCount()) {
11881209
resource = f;
1210+
}
11891211
}
1190-
}
11911212
return resource;
11921213
}
11931214
}
@@ -1970,3 +1991,4 @@ public static CompilationUnit getAst(ITypeRoot typeRoot, IProgressMonitor monito
19701991
}
19711992

19721993
}
1994+
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Red Hat Inc. and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Red Hat Inc. - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.jdt.ls.core.internal;
14+
15+
import org.eclipse.core.runtime.CoreException;
16+
import org.eclipse.jdt.core.IClassFile;
17+
import org.eclipse.jdt.core.util.IClassFileReader;
18+
import org.eclipse.jdt.core.util.ISourceAttribute;
19+
import org.eclipse.jdt.internal.core.util.ClassFileReader;
20+
21+
/**
22+
* Utility class to read the SourceFile attribute from class files.
23+
* The SourceFile attribute contains the name of the source file from which
24+
* the class was compiled (e.g., "OkHttpClient.kt" for Kotlin classes).
25+
*/
26+
public class SourceFileAttributeReader {
27+
28+
private SourceFileAttributeReader() {
29+
// Utility class - no instantiation
30+
}
31+
32+
/**
33+
* Gets the source file name from the SourceFile attribute of the given class file.
34+
*
35+
* @param classFile the class file to read
36+
* @return the source file name (e.g., "OkHttpClient.kt", "MyClass.java"),
37+
* or null if the attribute is not present or cannot be read
38+
*/
39+
public static String getSourceFileName(IClassFile classFile) {
40+
if (classFile == null) {
41+
return null;
42+
}
43+
try {
44+
return getSourceFileName(classFile.getBytes());
45+
} catch (CoreException e) {
46+
JavaLanguageServerPlugin.logException("Error reading class file bytes", e);
47+
return null;
48+
}
49+
}
50+
51+
/**
52+
* Gets the source file name from the SourceFile attribute of the given class file bytes.
53+
*
54+
* @param classFileBytes the raw bytes of the class file
55+
* @return the source file name (e.g., "OkHttpClient.kt", "MyClass.java"),
56+
* or null if the attribute is not present or cannot be read
57+
*/
58+
public static String getSourceFileName(byte[] classFileBytes) {
59+
if (classFileBytes == null || classFileBytes.length == 0) {
60+
return null;
61+
}
62+
63+
try {
64+
// Use Eclipse JDT's class file reader to parse the class file
65+
IClassFileReader reader = new ClassFileReader(classFileBytes, IClassFileReader.CLASSFILE_ATTRIBUTES);
66+
67+
// Get the SourceFile attribute from the class file
68+
ISourceAttribute sourceFileAttribute = reader.getSourceFileAttribute();
69+
if (sourceFileAttribute == null) {
70+
return null;
71+
}
72+
73+
// Get the source file name from the constant pool
74+
char[] sourceFileName = sourceFileAttribute.getSourceFileName();
75+
if (sourceFileName == null || sourceFileName.length == 0) {
76+
return null;
77+
}
78+
79+
return new String(sourceFileName);
80+
} catch (Exception e) {
81+
JavaLanguageServerPlugin.logException("Error parsing class file format", e);
82+
}
83+
return null;
84+
}
85+
}

org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/managers/ContentProviderManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.net.URI;
1616
import java.util.Arrays;
17+
import java.util.Collections;
1718
import java.util.List;
1819
import java.util.Set;
1920
import java.util.regex.Pattern;
@@ -153,7 +154,7 @@ private List<ContentProviderDescriptor> findMatchingProviders(URI uri) {
153154
Set<ContentProviderDescriptor> descriptors = getDescriptors(preferredProviderIds);
154155
if (descriptors.isEmpty()) {
155156
JavaLanguageServerPlugin.logError("No content providers found");
156-
return null;
157+
return Collections.emptyList();
157158
}
158159

159160
String uriString = uri != null ? uri.toString() : null;
@@ -166,7 +167,7 @@ private List<ContentProviderDescriptor> findMatchingProviders(URI uri) {
166167

167168
if (matches.isEmpty()) {
168169
JavaLanguageServerPlugin.logError("Unable to find content provider for URI " + uri);
169-
return null;
170+
return Collections.emptyList();
170171
}
171172

172173
return matches;

org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/commands/SourceAttachmentCommandTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
import com.google.gson.Gson;
4646

4747
public class SourceAttachmentCommandTest extends AbstractProjectsManagerBasedTest {
48-
private static final String classFileUri = "jdt://contents/foo.jar/foo/bar.class?%3Dsource-attachment%2Ffoo.jar%3Cfoo%28bar.class";
48+
private static final String classFileUri = "jdt://contents/foo.jar/foo/bar.java?%3Dsource-attachment%2Ffoo.jar%3Cfoo%28bar.class";
4949
private IProject project;
5050

5151
@BeforeEach

org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ public void outgoing_jar() throws Exception {
189189

190190
String jarUri = call0Calls.get(0).getTo().getUri();
191191
assertTrue(jarUri.startsWith("jdt://"));
192-
assertTrue(jarUri.contains("org.apache.commons.lang3.text"));
193-
assertTrue(jarUri.contains("WordUtils.class"));
192+
assertTrue(jarUri.contains("org.apache.commons.lang3.text/WordUtils.java?"));
193+
assertTrue(jarUri.contains("org.apache.commons.lang3.text(WordUtils.class"));
194194
}
195195

196196
@Test

org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/ResolveSourceMappingHandlerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ public void testResolveSourceUri() {
3636
@Test
3737
public void testResolveDependencyUri() {
3838
String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", Arrays.asList("quickstart2"));
39-
assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.class"));
39+
assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.java"));
4040
assertTrue(uri.contains(
4141
"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"));
4242
}
4343

4444
@Test
4545
public void testResolveDependencyUriWithoutGivingProjectNames() {
4646
String uri = ResolveSourceMappingHandler.resolveStackTraceLocation("at org.junit.Assert.assertEquals(Assert.java:117)", null);
47-
assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.class"));
47+
assertTrue(uri.startsWith("jdt://contents/junit-4.13.jar/org.junit/Assert.java"));
4848
assertTrue(uri.contains(
4949
"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"));
5050
}

0 commit comments

Comments
 (0)