From 6ca070cea971759f28dd46b7a0d227c5b88ccfe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:12:39 +0000 Subject: [PATCH 1/2] Initial plan From 972aa28d81e500a1f30d2568a08b862d574eaae4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:17:13 +0000 Subject: [PATCH 2/2] Fix stale buffer reuse race condition in ClassFile and ModularClassFile Add validation to detect and remove stale cached buffers when jar files are recreated with the same path. This prevents reusing buffers with null contents that can cause source=null errors in tests. Co-authored-by: carstenartur <3164220+carstenartur@users.noreply.github.com> --- .../jdt/core/tests/model/ClassFileTests.java | 57 +++++++++++++++++++ .../eclipse/jdt/internal/core/ClassFile.java | 15 +++++ .../jdt/internal/core/ModularClassFile.java | 25 ++++++-- 3 files changed, 93 insertions(+), 4 deletions(-) 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 */