From cfeb68a114df784d2fc89ae23f675f2d23be24ab Mon Sep 17 00:00:00 2001 From: Jeff Johnston Date: Mon, 1 Jun 2026 19:21:31 -0400 Subject: [PATCH 1/2] Add additional logic to CallHierarchyCore.getCompilationUnitNode() - add check for class file to avoid checking for a java file name which won't find the .java extension and will return false - fixes #3015 --- .../jdt/internal/corext/callhierarchy/CallHierarchyCore.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/callhierarchy/CallHierarchyCore.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/callhierarchy/CallHierarchyCore.java index 5d2427527fd..8d8283bb2fe 100644 --- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/callhierarchy/CallHierarchyCore.java +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/callhierarchy/CallHierarchyCore.java @@ -28,6 +28,7 @@ import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; @@ -308,7 +309,7 @@ static CompilationUnit getCompilationUnitNode(IMember member, boolean resolveBin ITypeRoot typeRoot= member.getTypeRoot(); try { if (typeRoot != null && typeRoot.exists() && typeRoot.getBuffer() != null - && JavaCore.isJavaLikeFileName(typeRoot.getElementName())) { + && (typeRoot instanceof IClassFile || JavaCore.isJavaLikeFileName(typeRoot.getElementName()))) { ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL); parser.setSource(typeRoot); parser.setResolveBindings(resolveBindings); From a027d46f638a4a7ee898fa02e999a9cca7812e3a Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 2 Jun 2026 15:56:26 -0400 Subject: [PATCH 2/2] Add regression test --- .../core/CallHierarchyParticipantTest.java | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/core/CallHierarchyParticipantTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/core/CallHierarchyParticipantTest.java index 9cabb27ba96..cca38a74ee0 100644 --- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/core/CallHierarchyParticipantTest.java +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/core/CallHierarchyParticipantTest.java @@ -24,6 +24,8 @@ import org.junit.Before; import org.junit.Test; +import org.eclipse.jdt.testplugin.JavaProjectHelper; + import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; @@ -48,8 +50,6 @@ import org.eclipse.jdt.ui.tests.callhierarchy.TestCallHierarchyParticipant; -import org.eclipse.jdt.testplugin.JavaProjectHelper; - /** * Tests for call hierarchy integration with contributed search participants. * @@ -398,4 +398,68 @@ public void targetMethod() {} assertTrue("Should find firstCaller", foundFirst); assertTrue("Should find secondCaller", foundSecond); } + + /** + * Regression test for https://github.com/eclipse-jdt/eclipse.jdt.ui/issues/3015 + * + * Verifies that outgoing call hierarchy works for methods in class files + * from a JAR/class folder with attached source. The {@code isJavaLikeFileName} + * guard must not reject {@code IClassFile} members whose element name ends + * in {@code .class}. + */ + @Test + public void outgoingCallsWorkForClassFileWithAttachedSource() throws Exception { + JavaProjectHelper.addRTJar9(fSourceProject); + IPackageFragmentRoot src = JavaProjectHelper.addSourceContainer(fSourceProject, "src"); + IPackageFragment pkg = src.createPackageFragment("testpkg", true, null); + pkg.createCompilationUnit("Lib.java", + """ + package testpkg; + public class Lib { + public void targetMethod() {} + public void anotherTarget() {} + public void callerMethod() { targetMethod(); anotherTarget(); } + } + """, + true, null); + fSourceProject.getProject().build(IncrementalProjectBuilder.FULL_BUILD, null); + + // Add source project's output as a class folder WITH source attachment + JavaProjectHelper.addRTJar9(fBinaryProject); + IPath outputPath = fSourceProject.getOutputLocation(); + IFolder outputFolder = fSourceProject.getProject() + .getFolder(outputPath.removeFirstSegments(1)); + JavaProjectHelper.addLibrary(fBinaryProject, outputFolder.getFullPath(), + src.getPath(), null); + + IType binaryType = fBinaryProject.findType("testpkg.Lib"); + assertNotNull("Binary type should be found", binaryType); + IMethod binaryCallerMethod = binaryType.getMethod("callerMethod", new String[0]); + assertTrue("Binary callerMethod should exist", binaryCallerMethod.exists()); + + // Verify preconditions: this is a class file with source attached + ITypeRoot typeRoot = binaryCallerMethod.getTypeRoot(); + assertTrue("Should be a class file", typeRoot instanceof IClassFile); + assertNotNull("Class file with source attachment should have a buffer", + typeRoot.getBuffer()); + + MethodWrapper[] roots = CallHierarchyCore.getDefault().getCalleeRoots( + new IMember[] { binaryCallerMethod }); + assertEquals(1, roots.length); + MethodWrapper[] callees = roots[0].getCalls(new NullProgressMonitor()); + + assertEquals("Should find 2 callees via Java AST from attached source", + 2, callees.length); + + boolean foundTarget = false; + boolean foundAnother = false; + for (MethodWrapper callee : callees) { + String name = callee.getMember().getElementName(); + if ("targetMethod".equals(name)) foundTarget = true; + if ("anotherTarget".equals(name)) foundAnother = true; + } + assertTrue("Should find targetMethod as callee", foundTarget); + assertTrue("Should find anotherTarget as callee", foundAnother); + } + }