diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ClassFileTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ClassFileTests.java index a65ca29a8c1..814d2268551 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ClassFileTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ClassFileTests.java @@ -1686,4 +1686,61 @@ public void run(){ } } + /* + * Ensures that stale buffers from deleted/recreated jars are properly detected and replaced. + * This test simulates the race condition where a jar file is deleted and recreated with the same path, + * and verifies that stale cached buffers are not reused. + * See https://github.com/eclipse-jdt/eclipse.jdt.ui/issues/736 + */ + public void testStaleBufferAfterJarRecreation() throws CoreException, IOException { + IJavaProject project = null; + try { + // Create a test project + project = createJavaProject("TestStaleBuffer", new String[0], new String[] {"JCL18_LIB"}, "", JavaCore.VERSION_1_8); + + // Create initial jar with source + String[] pathAndContents = new String[] { + "pack/age/X.java", + "package pack.age;\n" + + "public interface X {\n" + + " String test();\n" + + "}" + }; + addLibrary(project, "testlib.jar", "testlibsrc.zip", pathAndContents, JavaCore.VERSION_1_8); + + // Get the class file and trigger buffer creation + IPackageFragmentRoot root = project.getPackageFragmentRoot(project.getProject().getFile("testlib.jar")); + IOrdinaryClassFile classFile1 = root.getPackageFragment("pack.age").getOrdinaryClassFile("X.class"); + String source1 = classFile1.getSource(); + assertNotNull("Source should be available for first jar", source1); + assertTrue("Source should contain 'test()' method", source1.contains("test()")); + + // Delete and recreate the jar with different content + removeLibrary(project, "testlib.jar", "testlibsrc.zip"); + String[] newPathAndContents = new String[] { + "pack/age/X.java", + "package pack.age;\n" + + "public interface X {\n" + + " String newMethod();\n" + + "}" + }; + addLibrary(project, "testlib.jar", "testlibsrc.zip", newPathAndContents, JavaCore.VERSION_1_8); + + // Get the class file again (same path, but new jar) + root = project.getPackageFragmentRoot(project.getProject().getFile("testlib.jar")); + IOrdinaryClassFile classFile2 = root.getPackageFragment("pack.age").getOrdinaryClassFile("X.class"); + String source2 = classFile2.getSource(); + + // Verify that we get the new content, not stale buffer + assertNotNull("Source should be available for recreated jar", source2); + assertFalse("Source should not contain old 'test()' method", source2.contains("test()")); + assertTrue("Source should contain new 'newMethod()'", source2.contains("newMethod()")); + + } finally { + if (project != null) { + deleteProject(project); + } + } + } + } diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ClassFile.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ClassFile.java index 6e169ec752e..847e1ca17b7 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ClassFile.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ClassFile.java @@ -454,6 +454,21 @@ protected IBuffer openBuffer(IProgressMonitor pm, IElementInfo info) throws Java // Check the cache for the top-level type first IType outerMostEnclosingType = getOuterMostEnclosingType(); IBuffer buffer = getBufferManager().getBuffer(outerMostEnclosingType.getClassFile()); + + // Validate the cached buffer is still valid (not stale from a deleted/recreated jar) + if (buffer != null) { + if (buffer instanceof NullBuffer) { + // NullBuffer is valid - it represents a class file without source + return null; + } + // Check if buffer contents are still valid + if (buffer.getCharacters() == null) { + // Stale buffer - remove from cache and recreate + getBufferManager().removeBuffer(buffer); + buffer = null; + } + } + if (buffer == null) { SourceMapper mapper = getSourceMapper(); IBinaryType typeInfo = info instanceof IBinaryType ? (IBinaryType) info : null; diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ModularClassFile.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ModularClassFile.java index 17847f4f82b..a6a1ba86acb 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ModularClassFile.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/ModularClassFile.java @@ -254,11 +254,28 @@ public ICompilationUnit getWorkingCopy(WorkingCopyOwner owner, IProgressMonitor */ @Override protected IBuffer openBuffer(IProgressMonitor pm, IElementInfo info) throws JavaModelException { - SourceMapper mapper = getSourceMapper(); - if (mapper != null) { - return mapSource(mapper); + // First check if there's an existing buffer in the cache + IBuffer buffer = getBufferManager().getBuffer(this); + + // Validate the cached buffer is still valid + if (buffer != null) { + if (buffer instanceof NullBuffer) { + return null; + } + if (buffer.getCharacters() == null) { + // Stale buffer - remove from cache and recreate + getBufferManager().removeBuffer(buffer); + buffer = null; + } } - return null; + + if (buffer == null) { + SourceMapper mapper = getSourceMapper(); + if (mapper != null) { + buffer = mapSource(mapper); + } + } + return buffer; } /** Loads the buffer via SourceMapper, and maps it in SourceMapper */