diff --git a/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/MultiReleaseTests.java b/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/MultiReleaseTests.java index bb9b96e1dd9..ade860f5f88 100644 --- a/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/MultiReleaseTests.java +++ b/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/MultiReleaseTests.java @@ -35,6 +35,10 @@ public class MultiReleaseTests extends BuilderTests { private static final String JAVA9_SRC_FOLDER = "src9"; private static final String DEFAULT_SRC_FOLDER = "src"; +// static { +// TESTS_NAMES = new String[] { "testMultiReleaseModuleInfoPerRelease" }; +// } + public MultiReleaseTests(String name) { super(name); } @@ -319,6 +323,92 @@ public String print() { expectingNoProblems(); } + /** + * Test multi-release compilation with different module-info.java per release. + * This verifies that each source folder with a different release uses its own + * module-info.java for compilation, not a shared module from the project. + * See issue https://github.com/eclipse-jdt/eclipse.jdt.core/issues/4268 + */ + public void testMultiReleaseModuleInfoPerRelease() throws JavaModelException, IOException { + // Create modular project with Java 11 as base + IPath projectPath = createMRProject(CompilerOptions.VERSION_11); + IPath defaultSrc = env.getPackageFragmentRootPath(projectPath, DEFAULT_SRC_FOLDER); + + // Base module-info requires no extra modules + env.addClass(defaultSrc, "", "module-info", + """ + module MRmodular { + } + """ + ); + + // Base Test.java - should have errors for both java.desktop and java.xml types + IPath classDefault = env.addClass(defaultSrc, "p", "Test", + """ + package p; + public class Test { + java.awt.Window w11; + org.w3c.dom.Element element11; + } + """ + ); + + // Java 17 source with module-info requiring java.desktop + IClasspathAttribute[] attributes17 = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17") }; + IPath src17 = env.addPackageFragmentRoot(projectPath, "src17", attributes17); + env.addClass(src17, "", "module-info", + """ + module MRmodular { + requires java.desktop; + } + """ + ); + env.addClass(src17, "p", "Test", + """ + package p; + public class Test { + java.awt.Window w17; + org.w3c.dom.Element element17; + } + """ + ); + + // Java 21 source with module-info requiring java.xml + IClasspathAttribute[] attributes21 = new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21") }; + IPath src21 = env.addPackageFragmentRoot(projectPath, "src21", attributes21); + env.addClass(src21, "", "module-info", + """ + module MRmodular { + requires java.xml; + } + """ + ); + IPath class21 = env.addClass(src21, "p", "Test", + """ + package p; + public class Test { + java.awt.Window w21; + org.w3c.dom.Element element21; + } + """ + ); + + fullBuild(); + //As our default module descriptor does not import anything both should give an error + expectingSpecificProblemsFor(defaultSrc, new Problem[] { // + new Problem("", "The type java.awt.Window is not accessible", classDefault, 32, 47, 40, IMarker.SEVERITY_ERROR), + new Problem("", "The type org.w3c.dom.Element is not accessible", classDefault, 54, 73, 40, IMarker.SEVERITY_ERROR) + }); + //java.desktop includes java.xml so no errors to expect here + expectingNoProblemsFor(src17); + //we only import java.xml so desktop should give an error! + expectingSpecificProblemsFor(src21, new Problem[] { // + new Problem("", "The type java.awt.Window is not accessible", class21, 32, 47, 40, IMarker.SEVERITY_ERROR), + }); + } + private IPath whenSetupMRRpoject() throws JavaModelException { return whenSetupMRRpoject(CompilerOptions.VERSION_1_8); } diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ASTParserMultiReleaseTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ASTParserMultiReleaseTests.java new file mode 100644 index 00000000000..2f616c2ccee --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ASTParserMultiReleaseTests.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich and others. + * + * 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 + *******************************************************************************/ +package org.eclipse.jdt.core.tests.model; + +import junit.framework.Test; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTRequestor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; + +/** + * Tests that {@link ASTParser#createASTs(ICompilationUnit[], String[], ASTRequestor, org.eclipse.core.runtime.IProgressMonitor)} + * resolves bindings as seen from the release specific source folder the given compilation + * units live in, honoring a release specific {@code module-info.java}. + * + * See https://github.com/eclipse-jdt/eclipse.jdt.core/pull/4534#discussion_r3660872811 + */ +public class ASTParserMultiReleaseTests extends AbstractJavaModelTests { + + static { +// TESTS_NAMES = new String[] { "testResolveWindowInRelease17" }; + } + + public ASTParserMultiReleaseTests(String name) { + super(name); + } + + public static Test suite() { + return buildModelTestSuite(ASTParserMultiReleaseTests.class); + } + + @Override + public void setUpSuite() throws Exception { + super.setUpSuite(); + IJavaProject project = createJava9ProjectWithJREAttributes("ASTParserMR", + new String[] { "src", "src17", "src21" }, null, "21"); + IClasspathEntry[] classpath = project.getRawClasspath(); + for (int i = 0; i < classpath.length; i++) { + IClasspathEntry entry = classpath[i]; + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + if (entry.getPath().toString().endsWith("src17")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17") }); + } else if (entry.getPath().toString().endsWith("src21")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21") }); + } + } + } + project.setRawClasspath(classpath, new NullProgressMonitor()); + project.setOption(JavaCore.COMPILER_RELEASE, JavaCore.ENABLED); + + createFolder("/ASTParserMR/src/p"); + createFolder("/ASTParserMR/src17/p"); + createFolder("/ASTParserMR/src21/p"); + + // base module requires nothing + createFile("/ASTParserMR/src/module-info.java", """ + module MRastparser { + } + """); + createFile("/ASTParserMR/src/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + + // release 17 requires java.desktop (which transitively reads java.xml) + createFile("/ASTParserMR/src17/module-info.java", """ + module MRastparser { + requires java.desktop; + } + """); + createFile("/ASTParserMR/src17/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + + // release 21 requires java.xml + createFile("/ASTParserMR/src21/module-info.java", """ + module MRastparser { + requires java.xml; + } + """); + createFile("/ASTParserMR/src21/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + } + + @Override + public void tearDownSuite() throws Exception { + deleteProject("ASTParserMR"); + super.tearDownSuite(); + } + + private ITypeBinding resolveFieldType(String unitPath, String fieldName) throws Exception { + ICompilationUnit unit = getCompilationUnit(unitPath); + ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); + parser.setResolveBindings(true); + parser.setProject(getJavaProject("ASTParserMR")); + final ITypeBinding[] result = new ITypeBinding[1]; + parser.createASTs(new ICompilationUnit[] { unit }, new String[0], new ASTRequestor() { + @Override + public void acceptAST(ICompilationUnit source, CompilationUnit ast) { + TypeDeclaration type = (TypeDeclaration) ast.types().get(0); + for (Object bodyDecl : type.bodyDeclarations()) { + if (bodyDecl instanceof FieldDeclaration field) { + VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0); + if (fragment.getName().getIdentifier().equals(fieldName)) { + result[0] = field.getType().resolveBinding(); + } + } + } + } + }, null); + return result[0]; + } + + // org.w3c.dom.Element is reachable in src21 via 'requires java.xml': resolving its + // field type must succeed with a non-recovered binding for exactly that type. + public void testResolveElementInRelease21() throws Exception { + ITypeBinding binding = resolveFieldType("/ASTParserMR/src21/p/Test.java", "element"); + assertNotNull("Field 'element' not resolved", binding); + assertFalse("Binding should not be recovered/unresolved", binding.isRecovered()); + assertEquals("org.w3c.dom.Element", binding.getQualifiedName()); + } + + // java.awt.Window is reachable in src17 via 'requires java.desktop'. + public void testResolveWindowInRelease17() throws Exception { + ITypeBinding binding = resolveFieldType("/ASTParserMR/src17/p/Test.java", "w"); + assertNotNull("Field 'w' not resolved", binding); + assertFalse("Binding should not be recovered/unresolved", binding.isRecovered()); + assertEquals("java.awt.Window", binding.getQualifiedName()); + } + + // java.desktop reads java.xml transitively, so org.w3c.dom.Element is reachable in src17 too. + public void testResolveElementInRelease17() throws Exception { + ITypeBinding binding = resolveFieldType("/ASTParserMR/src17/p/Test.java", "element"); + assertNotNull("Field 'element' not resolved", binding); + assertFalse("Binding should not be recovered/unresolved", binding.isRecovered()); + assertEquals("org.w3c.dom.Element", binding.getQualifiedName()); + } + + // The base module-info requires neither java.desktop nor java.xml, so java.awt.Window + // is not accessible and its field type must fail to resolve to a proper binding. + public void testResolveWindowInBaseReleaseIsUnresolved() throws Exception { + ITypeBinding binding = resolveFieldType("/ASTParserMR/src/p/Test.java", "w"); + assertTrue("Binding for inaccessible java.awt.Window should not resolve properly", + binding == null || binding.isRecovered()); + } +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java index 102de7b4839..230985b058a 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/AllJavaModelTests.java @@ -152,6 +152,10 @@ private static Class[] getAllTestClasses() { ReconcilerTests21.class, ReconcilerStatementsRecoveryTests.class, ReconcilerMultiReleaseTests.class, + ReconcilerModuleMultiReleaseTests.class, + SelectionMultiReleaseTests.class, + ASTParserMultiReleaseTests.class, + HierarchyMultiReleaseTests.class, // Copy and move operation tests CopyMoveElementsTests.class, diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/HierarchyMultiReleaseTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/HierarchyMultiReleaseTests.java new file mode 100644 index 00000000000..efc949dd910 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/HierarchyMultiReleaseTests.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich and others. + * + * 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 + *******************************************************************************/ +package org.eclipse.jdt.core.tests.model; + +import junit.framework.Test; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; + +/** + * Tests that computing a type hierarchy (see + * {@code org.eclipse.jdt.internal.core.hierarchy.HierarchyBuilder}) resolves super types as + * seen from the release specific source folder the focus type lives in, honoring a release + * specific {@code module-info.java}. + * + * See https://github.com/eclipse-jdt/eclipse.jdt.core/pull/4534#discussion_r3660886418 + */ +public class HierarchyMultiReleaseTests extends AbstractJavaModelTests { + + static { +// TESTS_NAMES = new String[] { "testSupertypeWindowInRelease17" }; + } + + public HierarchyMultiReleaseTests(String name) { + super(name); + } + + public static Test suite() { + return buildModelTestSuite(HierarchyMultiReleaseTests.class); + } + + @Override + public void setUpSuite() throws Exception { + super.setUpSuite(); + IJavaProject project = createJava9ProjectWithJREAttributes("HierarchyMR", + new String[] { "src", "src17", "src21" }, null, "21"); + IClasspathEntry[] classpath = project.getRawClasspath(); + for (int i = 0; i < classpath.length; i++) { + IClasspathEntry entry = classpath[i]; + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + if (entry.getPath().toString().endsWith("src17")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17") }); + } else if (entry.getPath().toString().endsWith("src21")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21") }); + } + } + } + project.setRawClasspath(classpath, new NullProgressMonitor()); + project.setOption(JavaCore.COMPILER_RELEASE, JavaCore.ENABLED); + + createFolder("/HierarchyMR/src/p"); + createFolder("/HierarchyMR/src17/p"); + createFolder("/HierarchyMR/src21/p"); + + // base module requires nothing, so neither java.desktop nor java.xml types are + // accessible: Test extends/implements plain java.lang types only. + createFile("/HierarchyMR/src/module-info.java", """ + module MRhierarchy { + } + """); + createFile("/HierarchyMR/src/p/Test.java", """ + package p; + public class Test { + } + """); + + // release 17 requires java.desktop (which transitively reads java.xml): the focus + // type extends java.awt.Window, only reachable through the release 17 module-info. + createFile("/HierarchyMR/src17/module-info.java", """ + module MRhierarchy { + requires java.desktop; + } + """); + createFile("/HierarchyMR/src17/p/Test.java", """ + package p; + public class Test extends java.awt.Window { + } + """); + + // release 21 requires java.xml: the focus type implements org.w3c.dom.Element, + // only reachable through the release 21 module-info. + createFile("/HierarchyMR/src21/module-info.java", """ + module MRhierarchy { + requires java.xml; + } + """); + createFile("/HierarchyMR/src21/p/Test.java", """ + package p; + public class Test implements org.w3c.dom.Element { + } + """); + } + + @Override + public void tearDownSuite() throws Exception { + deleteProject("HierarchyMR"); + super.tearDownSuite(); + } + + private IType getFocusType(String unitPath) throws Exception { + ICompilationUnit unit = getCompilationUnit(unitPath); + IType type = unit.getType("Test"); + assertTrue("Test type does not exist in " + unitPath, type.exists()); + return type; + } + + // java.awt.Window is reachable in src17 via 'requires java.desktop': the supertype + // hierarchy of the release 17 focus type must resolve it as the direct superclass. + public void testSupertypeWindowInRelease17() throws Exception { + IType type = getFocusType("/HierarchyMR/src17/p/Test.java"); + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType superclass = hierarchy.getSuperclass(type); + assertNotNull("Superclass of release 17 Test not resolved", superclass); + assertEquals("java.awt.Window", superclass.getFullyQualifiedName()); + } + + // org.w3c.dom.Element is reachable in src21 via 'requires java.xml': the supertype + // hierarchy of the release 21 focus type must resolve it as a super interface. + public void testSuperinterfaceElementInRelease21() throws Exception { + IType type = getFocusType("/HierarchyMR/src21/p/Test.java"); + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType[] superInterfaces = hierarchy.getSuperInterfaces(type); + boolean found = false; + for (IType superInterface : superInterfaces) { + if ("org.w3c.dom.Element".equals(superInterface.getFullyQualifiedName())) { + found = true; + } + } + assertTrue("org.w3c.dom.Element not found among super interfaces of release 21 Test", found); + } +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ReconcilerModuleMultiReleaseTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ReconcilerModuleMultiReleaseTests.java new file mode 100644 index 00000000000..756c28f0948 --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ReconcilerModuleMultiReleaseTests.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich and others. + * + * 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 + *******************************************************************************/ +package org.eclipse.jdt.core.tests.model; + +import junit.framework.Test; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IProblemRequestor; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.WorkingCopyOwner; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.TypeDeclaration; + +/** + * Tests that reconciling (problem detection in working copies) of a multi-release + * modular project resolves modules and types as seen from the release specific source + * folder the reconciled unit lives in, honoring a per-release {@code module-info.java}. + * + * See https://github.com/eclipse-jdt/eclipse.jdt.core/issues/4268 + */ +public class ReconcilerModuleMultiReleaseTests extends ModifyingResourceTests { + + static { +// TESTS_NAMES = new String[] { "testReconcileUsesReleaseModuleInfo21" }; + } + + public ReconcilerModuleMultiReleaseTests(String name) { + super(name); + } + + public static Test suite() { + return buildModelTestSuite(ReconcilerModuleMultiReleaseTests.class); + } + + @Override + public void setUpSuite() throws Exception { + super.setUpSuite(); + IJavaProject project = createJava9ProjectWithJREAttributes("ReconcilerModuleMR", + new String[] { "src", "src17", "src21" }, null, "21"); + IClasspathEntry[] classpath = project.getRawClasspath(); + for (int i = 0; i < classpath.length; i++) { + IClasspathEntry entry = classpath[i]; + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + if (entry.getPath().toString().endsWith("src17")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17") }); + } else if (entry.getPath().toString().endsWith("src21")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21") }); + } + } + } + project.setRawClasspath(classpath, new NullProgressMonitor()); + project.setOption(JavaCore.COMPILER_RELEASE, JavaCore.ENABLED); + + createFolder("/ReconcilerModuleMR/src/p"); + createFolder("/ReconcilerModuleMR/src17/p"); + createFolder("/ReconcilerModuleMR/src21/p"); + + // base module requires nothing + createFile("/ReconcilerModuleMR/src/module-info.java", """ + module MRmodular { + } + """); + // release 17 requires java.desktop (which transitively reads java.xml) + createFile("/ReconcilerModuleMR/src17/module-info.java", """ + module MRmodular { + requires java.desktop; + } + """); + // release 21 requires java.xml + createFile("/ReconcilerModuleMR/src21/module-info.java", """ + module MRmodular { + requires java.xml; + } + """); + } + + @Override + public void tearDownSuite() throws Exception { + deleteProject("ReconcilerModuleMR"); + super.tearDownSuite(); + } + + private void assertReconcileProblems(String path, String source, String expectedProblems) throws Exception { + ProblemRequestor problemRequestor = new ProblemRequestor(); + WorkingCopyOwner owner = new WorkingCopyOwner() { + @Override + public IProblemRequestor getProblemRequestor(ICompilationUnit unit) { + return problemRequestor; + } + }; + ICompilationUnit wc = getWorkingCopy(path, source, owner); + try { + problemRequestor.initialize(source.toCharArray()); + wc.reconcile(ICompilationUnit.NO_AST, true/*force problem detection*/, owner, null); + assertProblems("Unexpected problems for " + path, expectedProblems, problemRequestor); + } finally { + wc.discardWorkingCopy(); + } + } + + // java.xml is required in src21, so org.w3c.dom.Element must be accessible (no problem). + public void testReconcileUsesReleaseModuleInfo21() throws Exception { + assertReconcileProblems("/ReconcilerModuleMR/src21/p/Use.java", """ + package p; + public class Use { + org.w3c.dom.Element element; + } + """, "----------\n----------\n"); + } + + // java.desktop is required in src17 and reads java.xml transitively, so both are accessible. + public void testReconcileUsesReleaseModuleInfo17() throws Exception { + assertReconcileProblems("/ReconcilerModuleMR/src17/p/Use.java", """ + package p; + public class Use { + java.awt.Window window; + org.w3c.dom.Element element; + } + """, "----------\n----------\n"); + } + + // the base module requires nothing, so org.w3c.dom.Element is not accessible here. + public void testReconcileUsesBaseModuleInfo() throws Exception { + assertReconcileProblems("/ReconcilerModuleMR/src/p/Use.java", """ + package p; + public class Use { + org.w3c.dom.Element element; + } + """, + "----------\n" + + "1. ERROR in /ReconcilerModuleMR/src/p/Use.java (at line 3)\n" + + " org.w3c.dom.Element element;\n" + + " ^^^^^^^^^^^^^^^^^^^\n" + + "The type org.w3c.dom.Element is not accessible\n" + + "----------\n"); + } + + private ITypeBinding resolveFieldType(String path, String source) throws Exception { + ICompilationUnit wc = getWorkingCopy(path, source); + try { + ASTParser parser = ASTParser.newParser(AST.getJLSLatest()); + parser.setResolveBindings(true); + parser.setSource(wc); + org.eclipse.jdt.core.dom.CompilationUnit ast = + (org.eclipse.jdt.core.dom.CompilationUnit) parser.createAST(null); + TypeDeclaration type = (TypeDeclaration) ast.types().get(0); + FieldDeclaration field = type.getFields()[0]; + return field.getType().resolveBinding(); + } finally { + wc.discardWorkingCopy(); + } + } + + // ASTParser.createAST (resolved DOM AST) must resolve org.w3c.dom.Element as seen from src21. + public void testCreateASTUsesReleaseModuleInfo21() throws Exception { + ITypeBinding binding = resolveFieldType("/ReconcilerModuleMR/src21/p/Use.java", """ + package p; + public class Use { + org.w3c.dom.Element element; + } + """); + assertNotNull("Type binding should be resolved", binding); + assertEquals("org.w3c.dom.Element", binding.getQualifiedName()); + } + + // java.desktop reads java.xml transitively, so the binding resolves in src17 as well. + public void testCreateASTUsesReleaseModuleInfo17() throws Exception { + ITypeBinding binding = resolveFieldType("/ReconcilerModuleMR/src17/p/Use.java", """ + package p; + public class Use { + org.w3c.dom.Element element; + } + """); + assertNotNull("Type binding should be resolved", binding); + assertEquals("org.w3c.dom.Element", binding.getQualifiedName()); + } +} diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/SelectionMultiReleaseTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/SelectionMultiReleaseTests.java new file mode 100644 index 00000000000..c450dd8534a --- /dev/null +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/SelectionMultiReleaseTests.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * Copyright (c) 2026 Christoph Läubrich and others. + * + * 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 + *******************************************************************************/ +package org.eclipse.jdt.core.tests.model; + +import junit.framework.Test; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; + +/** + * Tests that code selection (Hover / Open Declaration) in a multi-release modular + * project resolves types as seen from the release specific source folder the selected + * unit lives in. + * + * See https://github.com/eclipse-jdt/eclipse.jdt.core/pull/4534#issuecomment-4743290623 + * where selecting a type that is only accessible through a release specific + * {@code module-info.java} produced multiple (duplicate) results because selection + * always resolved against the base {@code module-info.java}. + */ +public class SelectionMultiReleaseTests extends AbstractJavaModelTests { + + static { +// TESTS_NAMES = new String[] { "testSelectElementInRelease21" }; + } + + public SelectionMultiReleaseTests(String name) { + super(name); + } + + public static Test suite() { + return buildModelTestSuite(SelectionMultiReleaseTests.class); + } + + @Override + public void setUpSuite() throws Exception { + super.setUpSuite(); + IJavaProject project = createJava9ProjectWithJREAttributes("SelectionMR", + new String[] { "src", "src17", "src21" }, null, "21"); + IClasspathEntry[] classpath = project.getRawClasspath(); + for (int i = 0; i < classpath.length; i++) { + IClasspathEntry entry = classpath[i]; + if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { + if (entry.getPath().toString().endsWith("src17")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17") }); + } else if (entry.getPath().toString().endsWith("src21")) { + classpath[i] = JavaCore.newSourceEntry(entry.getPath(), null, null, null, + new IClasspathAttribute[] { + JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21") }); + } + } + } + project.setRawClasspath(classpath, new NullProgressMonitor()); + project.setOption(JavaCore.COMPILER_RELEASE, JavaCore.ENABLED); + + createFolder("/SelectionMR/src/p"); + createFolder("/SelectionMR/src17/p"); + createFolder("/SelectionMR/src21/p"); + + // base module requires nothing + createFile("/SelectionMR/src/module-info.java", """ + module MRmodular { + } + """); + createFile("/SelectionMR/src/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + + // release 17 requires java.desktop (which transitively reads java.xml) + createFile("/SelectionMR/src17/module-info.java", """ + module MRmodular { + requires java.desktop; + } + """); + createFile("/SelectionMR/src17/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + + // release 21 requires java.xml + createFile("/SelectionMR/src21/module-info.java", """ + module MRmodular { + requires java.xml; + } + """); + createFile("/SelectionMR/src21/p/Test.java", """ + package p; + public class Test { + java.awt.Window w; + org.w3c.dom.Element element; + } + """); + } + + @Override + public void tearDownSuite() throws Exception { + deleteProject("SelectionMR"); + super.tearDownSuite(); + } + + private IType assertSingleType(String unitPath, String reference, String selection, String expectedFqn) + throws Exception { + ICompilationUnit unit = getCompilationUnit(unitPath); + String source = unit.getSource(); + int referenceStart = source.indexOf(reference); + assertTrue("reference '" + reference + "' not found in " + unitPath, referenceStart >= 0); + int start = source.indexOf(selection, referenceStart); + IJavaElement[] elements = unit.codeSelect(start, selection.length()); + StringBuilder details = new StringBuilder(); + for (IJavaElement element : elements) { + details.append('\n').append(element); + } + assertEquals("Expected exactly one selection result but got: " + details, 1, elements.length); + assertTrue("Selection result is not a type: " + elements[0], elements[0] instanceof IType); + IType type = (IType) elements[0]; + assertEquals("Unexpected resolved type", expectedFqn, type.getFullyQualifiedName()); + return type; + } + + // org.w3c.dom.Element is reachable in src21 via 'requires java.xml': selecting it must + // resolve to exactly that single type and not offer all the JDK types named 'Element'. + public void testSelectElementInRelease21() throws Exception { + assertSingleType("/SelectionMR/src21/p/Test.java", "org.w3c.dom.Element", "Element", + "org.w3c.dom.Element"); + } + + // java.awt.Window is reachable in src17 via 'requires java.desktop'. + public void testSelectWindowInRelease17() throws Exception { + assertSingleType("/SelectionMR/src17/p/Test.java", "java.awt.Window", "Window", + "java.awt.Window"); + } + + // java.desktop reads java.xml transitively, so org.w3c.dom.Element is reachable in src17 too. + public void testSelectElementInRelease17() throws Exception { + assertSingleType("/SelectionMR/src17/p/Test.java", "org.w3c.dom.Element", "Element", + "org.w3c.dom.Element"); + } +} diff --git a/org.eclipse.jdt.core/META-INF/MANIFEST.MF b/org.eclipse.jdt.core/META-INF/MANIFEST.MF index 171a059f602..642bb31c198 100644 --- a/org.eclipse.jdt.core/META-INF/MANIFEST.MF +++ b/org.eclipse.jdt.core/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.jdt.core; singleton:=true -Bundle-Version: 3.46.100.qualifier +Bundle-Version: 3.47.0.qualifier Bundle-Activator: org.eclipse.jdt.core.JavaCore Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/CompilationUnitResolver.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/CompilationUnitResolver.java index ce5dcbf7898..74ff1b9fc77 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/CompilationUnitResolver.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/CompilationUnitResolver.java @@ -716,7 +716,11 @@ public static void resolve( try { int amountOfWork = (compilationUnits.length + bindingKeys.length) * 2; // 1 for beginToCompile, 1 for resolve SubMonitor subMonitor = SubMonitor.convert(monitor, amountOfWork); - environment = new CancelableNameEnvironment(((JavaProject) javaProject), owner, subMonitor); + // resolve as seen from the source folder the units live in, honoring a release specific module-info.java + int release = compilationUnits.length > 0 + ? JavaProject.getRelease(compilationUnits[0]) + : JavaProject.NO_RELEASE; + environment = new CancelableNameEnvironment(((JavaProject) javaProject), owner, subMonitor, false, release); problemFactory = new CancelableProblemFactory(subMonitor); CompilerOptions compilerOptions = getCompilerOptions(options, (flags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0); compilerOptions.ignoreMethodBodies = (flags & ICompilationUnit.IGNORE_METHOD_BODIES) != 0; @@ -810,7 +814,12 @@ public static CompilationUnitDeclaration resolve( classpaths.toArray(allEntries); environment = new NameEnvironmentWithProgress(allEntries, null, monitor); } else { - environment = new CancelableNameEnvironment((JavaProject) javaProject, owner, monitor); + // resolve as seen from the source folder the unit lives in, honoring a release specific module-info.java + char[] fileName = sourceUnit.getFileName(); + int release = fileName != null && fileName.length > 0 + ? ((JavaProject) javaProject).getRelease(IPath.fromPortableString(new String(fileName))) + : JavaProject.NO_RELEASE; + environment = new CancelableNameEnvironment((JavaProject) javaProject, owner, monitor, false, release); } problemFactory = new CancelableProblemFactory(monitor); CompilerOptions compilerOptions = getCompilerOptions(options, (flags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) != 0); diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ModuleBinding.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ModuleBinding.java index c5c637ee198..7675bdd1cd0 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ModuleBinding.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/ModuleBinding.java @@ -19,6 +19,7 @@ import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; import org.eclipse.jdt.internal.compiler.util.Util; +import org.eclipse.jdt.internal.core.JavaProject; import org.eclipse.jdt.internal.core.NameLookup; import org.eclipse.jdt.internal.core.NameLookup.Answer; import org.eclipse.jdt.internal.core.SearchableEnvironment; @@ -124,7 +125,7 @@ public IJavaElement getJavaElement() { if (!(nameEnvironment instanceof SearchableEnvironment)) return null; NameLookup nameLookup = ((SearchableEnvironment) nameEnvironment).nameLookup; if (nameLookup == null) return null; - Answer answer = nameLookup.findModule(this.getName().toCharArray()); + Answer answer = nameLookup.findModule(this.getName().toCharArray(), JavaProject.NO_RELEASE); if (answer == null) return null; return answer.module; } diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/core/IJavaProject.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/core/IJavaProject.java index 7efc551c598..93ea3715a88 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/core/IJavaProject.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/core/IJavaProject.java @@ -625,6 +625,32 @@ IPackageFragmentRoot findPackageFragmentRoot(IPath path) */ IModuleDescription getModuleDescription() throws JavaModelException; + /** + * Returns an {@link IModuleDescription} this project represents or null if the Java project doesn't represent any + * named module. A Java project is said to represent a module if any of its source package fragment roots (see + * {@link IPackageFragmentRoot#K_SOURCE}) contains a valid Java module descriptor, or if one of its classpath + * entries has a valid {@link IClasspathAttribute#PATCH_MODULE} attribute affecting the current project. In the + * latter case the corresponding module description of the location referenced by that classpath entry is returned. + * + *
Furthermore, if the project is multi-release aware, then the specified release
+ * selects the most specific suitable module description. For this purpose the {@link IClasspathAttribute#RELEASE}
+ * attribute of each source folder is inspected, if present. Source folders are then search from the requested release down
+ * to the lowest release and finally to a source folder with no {@link IClasspathAttribute#RELEASE} attribute.
+ * The first valid module description found in a source folder visited during this search is then returned.
A module description contributed via {@link IClasspathAttribute#PATCH_MODULE} is considered only if the + * regular search finds no module description
+ * + * @param release + * Specify the upper bound for version specific source folders to be searched. + * @return a {@link IModuleDescription} this project represents. + * @exception JavaModelException + * if this element does not exist or if an exception occurs while accessing its + * corresponding resource + * @since 3.47 + */ + IModuleDescription getModuleDescription(int release) throws JavaModelException; + /** * Returns theIModuleDescription owned by this project or
* null if the Java project doesn't own a valid Java module descriptor.
@@ -640,6 +666,28 @@ IPackageFragmentRoot findPackageFragmentRoot(IPath path)
*/
IModuleDescription getOwnModuleDescription() throws JavaModelException;
+ /**
+ * Returns an {@link IModuleDescription} owned by this project or null if
+ * the Java project doesn't own a suitable Java module descriptor.
+ * This method considers only module descriptions contained in any of the project's source package fragment roots
+ * (see {@link IPackageFragmentRoot#K_SOURCE}). In
+ * particular any {@link IClasspathAttribute#PATCH_MODULE} attribute is not considered.
+ * Furthermore, if the project is multi-release aware, then the specified release
+ * selects the most specific suitable module description. For this purpose the {@link IClasspathAttribute#RELEASE}
+ * attribute of each source folder is inspected, if present. Source folders are then search from the requested release down
+ * to the lowest release and finally to a source folder with no {@link IClasspathAttribute#RELEASE} attribute.
+ * The first valid module description found in a source folder visited during this search is then returned.