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 bf7d3db8eb5..047ff36c850 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
@@ -276,6 +276,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/model/org/eclipse/jdt/core/IJavaProject.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/core/IJavaProject.java
index 7efc551c598..7e61233d5d3 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,24 @@ IPackageFragmentRoot findPackageFragmentRoot(IPath path)
*/
IModuleDescription getModuleDescription() throws JavaModelException;
+ /**
+ * Returns the {@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.
+ *
+ * @param release
+ * specify the upper bound for the target multi-release, source folders that specify a release
+ * attribute are searched in descending order, starting with the value given by this parameter
+ * @return the {@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.44
+ */
+ IModuleDescription getModuleDescription(int release) throws JavaModelException;
+
/**
* Returns the IModuleDescription owned by this project or
* null if the Java project doesn't own a valid Java module descriptor.
@@ -640,6 +658,23 @@ IPackageFragmentRoot findPackageFragmentRoot(IPath path)
*/
IModuleDescription getOwnModuleDescription() throws JavaModelException;
+ /**
+ * Returns the multi-release specific IModuleDescription owned by this project or null if
+ * the Java project doesn't own a valid 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.
+ *
+ * @param release
+ * specify the upper bound for the target multi-release, source folders that specify a release
+ * attribute are searched in descending order, starting with the value given by this parameter
+ * @return the {@link IModuleDescription} this project owns.
+ * @exception JavaModelException
+ * if this element does not exist or if an exception occurs while accessing its
+ * corresponding resource
+ * @since 3.44
+ */
+ IModuleDescription getOwnModuleDescription(int release) throws JavaModelException;
+
/**
* Returns the raw classpath for the project, as a list of classpath
* entries. This corresponds to the exact set of entries which were assigned
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaProject.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaProject.java
index c8b47786185..56da13ee23b 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaProject.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/JavaProject.java
@@ -1743,7 +1743,7 @@ public IModuleDescription findModule(String moduleName, WorkingCopyOwner owner)
* Internal findModule with instantiated name lookup
*/
IModuleDescription findModule(String moduleName, NameLookup lookup) throws JavaModelException {
- NameLookup.Answer answer = lookup.findModule(moduleName.toCharArray());
+ NameLookup.Answer answer = lookup.findModule(moduleName.toCharArray(), NO_RELEASE);
if (answer != null)
return answer.module;
return null;
@@ -3753,10 +3753,15 @@ protected IStatus validateExistence(IResource underlyingResource) {
@Override
public IModuleDescription getModuleDescription() throws JavaModelException {
- JavaProjectElementInfo info = (JavaProjectElementInfo) getElementInfo();
- IModuleDescription module = info.getModule();
- if (module != null)
+ return getModuleDescription(NO_RELEASE);
+ }
+
+ @Override
+ public IModuleDescription getModuleDescription(int release) throws JavaModelException {
+ IModuleDescription module = getOwnModuleDescription(release);
+ if (module != null) {
return module;
+ }
for(IClasspathEntry entry : getRawClasspath()) {
List patchedModules = getPatchedModules(entry);
if (patchedModules.size() == 1) { // > 1 is malformed, 0 means not affecting this project
@@ -3764,7 +3769,7 @@ public IModuleDescription getModuleDescription() throws JavaModelException {
switch (entry.getEntryKind()) {
case IClasspathEntry.CPE_PROJECT:
IJavaProject referencedProject = getJavaModel().getJavaProject(entry.getPath().toString());
- module = referencedProject.getModuleDescription();
+ module = referencedProject.getModuleDescription(release);
if (module != null && module.getElementName().equals(mainModule))
return module;
break;
@@ -3783,6 +3788,36 @@ public IModuleDescription getModuleDescription() throws JavaModelException {
@Override
public IModuleDescription getOwnModuleDescription() throws JavaModelException {
+ return getOwnModuleDescription(NO_RELEASE);
+ }
+
+ @Override
+ public IModuleDescription getOwnModuleDescription(int release) throws JavaModelException {
+ if (release >= FIRST_MULTI_RELEASE) {
+ IModuleDescription releaseSpecificDescriptor = Arrays.stream(getRawClasspath()).map(e -> {
+ String attribute = ClasspathEntry.getExtraAttribute(e, IClasspathAttribute.RELEASE);
+ if (attribute != null) {
+ try {
+ return new ReleaseClasspathEntry(e, Integer.parseInt(attribute));
+ } catch (NumberFormatException nfe) {
+ // can't use then
+ }
+ }
+ return null;
+ }).filter(Objects::nonNull).filter(entry -> entry.release() <= release)
+ .sorted(Comparator.comparingInt(ReleaseClasspathEntry::release).reversed()).map(entry -> {
+ for (IPackageFragmentRoot root : findPackageFragmentRoots(entry.entry())) {
+ IModuleDescription module = root.getModuleDescription();
+ if (module != null) {
+ return module;
+ }
+ }
+ return null;
+ }).filter(Objects::nonNull).findFirst().orElse(null);
+ if (releaseSpecificDescriptor != null) {
+ return releaseSpecificDescriptor;
+ }
+ }
JavaProjectElementInfo info = (JavaProjectElementInfo) getElementInfo();
return info.getModule();
}
@@ -3824,11 +3859,16 @@ public IModuleDescription getAutomaticModuleDescription() throws JavaModelExcept
}
public void setModuleDescription(IModuleDescription module) throws JavaModelException {
+ IPackageFragmentRoot newRoot = (IPackageFragmentRoot) module.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
+ IClasspathEntry classpathEntry = newRoot.getRawClasspathEntry();
+ if (ClasspathEntry.getExtraAttribute(classpathEntry, IClasspathAttribute.RELEASE) != null) {
+ // Do not update the projects module descriptor with something from a release folder!
+ return;
+ }
JavaProjectElementInfo info = (JavaProjectElementInfo) getElementInfo();
IModuleDescription current = info.getModule();
if (current != null) {
IPackageFragmentRoot root = (IPackageFragmentRoot) current.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
- IPackageFragmentRoot newRoot = (IPackageFragmentRoot) module.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
if (!root.equals(newRoot))
throw new JavaModelException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID,
Messages.bind(Messages.classpath_duplicateEntryPath, TypeConstants.MODULE_INFO_FILE_NAME_STRING, getElementName())));
@@ -3864,4 +3904,8 @@ public Manifest getManifest() {
public Set determineModulesOfProjectsWithNonEmptyClasspath() throws JavaModelException {
return ModuleUpdater.determineModulesOfProjectsWithNonEmptyClasspath(this, getExpandedClasspath());
}
+
+ private static final record ReleaseClasspathEntry(IClasspathEntry entry, int release) {
+
+ }
}
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/NameLookup.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/NameLookup.java
index e5442ba77d0..eda795012f6 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/NameLookup.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/NameLookup.java
@@ -845,7 +845,7 @@ public Answer findType(
}
}
Answer answer = new Answer(type, accessRestriction, entry,
- getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get));
+ getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get, release));
if (!answer.ignoreIfBetter()) {
if (answer.isBetter(suggestedAnswer))
return answer;
@@ -938,7 +938,7 @@ public static IModule getModuleDescriptionInfo(IModuleDescription moduleDesc) {
}
/** Internal utility, which is able to answer explicit and automatic modules. */
- static IModuleDescription getModuleDescription(JavaProject project, IPackageFragmentRoot root, Map cache, Function rootToEntry) {
+ static IModuleDescription getModuleDescription(JavaProject project, IPackageFragmentRoot root, Map cache, Function rootToEntry, int release) {
IModuleDescription module = cache.get(root);
if (module != null)
return module != NO_MODULE ? module : null;
@@ -954,7 +954,7 @@ static IModuleDescription getModuleDescription(JavaProject project, IPackageFrag
}
try {
if (root.getKind() == IPackageFragmentRoot.K_SOURCE)
- module = root.getJavaProject().getModuleDescription(); // from any root in this project
+ module = root.getJavaProject().getModuleDescription(release); // from any root in this project
} catch (JavaModelException e) {
cache.put(root, NO_MODULE);
return null;
@@ -979,7 +979,7 @@ static IModuleDescription getModuleDescription(JavaProject project, IPackageFrag
}
public IModule getModuleDescriptionInfo(PackageFragmentRoot root) {
- IModuleDescription desc = getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get);
+ IModuleDescription desc = getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get, JavaProject.NO_RELEASE);
if (desc != null) {
return getModuleDescriptionInfo(desc);
}
@@ -1103,9 +1103,10 @@ public Answer findType(String name, boolean partialMatch, int acceptFlags, boole
}
return findType(className, packageName, partialMatch, acceptFlags, considerSecondaryTypes, waitForIndexes, checkRestrictions, monitor);
}
- public Answer findModule(char[] moduleName) {
+
+ public Answer findModule(char[] moduleName, int release) {
JavaElementRequestor requestor = new JavaElementRequestor();
- seekModule(moduleName, false, requestor);
+ seekModule(moduleName, false, requestor, release);
IModuleDescription[] modules = requestor.getModules();
if (modules.length == 0) {
try {
@@ -1387,9 +1388,9 @@ public void seekTypes(String name, IPackageFragment pkg, boolean partialMatch, i
}
public void seekModuleReferences(String name, IJavaElementRequestor requestor, IJavaProject javaProject) {
- seekModule(name.toCharArray(), true /* prefix */, requestor);
+ seekModule(name.toCharArray(), true /* prefix */, requestor, JavaProject.NO_RELEASE);
}
- public void seekModule(char[] name, boolean prefixMatch, IJavaElementRequestor requestor) {
+ public void seekModule(char[] name, boolean prefixMatch, IJavaElementRequestor requestor, int release) {
long start = -1;
if (VERBOSE)
start = System.currentTimeMillis();
@@ -1412,7 +1413,7 @@ public void seekModule(char[] name, boolean prefixMatch, IJavaElementRequestor r
continue;
}
}
- module = getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get);
+ module = getModuleDescription(this.rootProject, root, this.rootToModule, this.rootToResolvedEntries::get, release);
if (module != null && prefixMatcher.matches(name, module.getElementName().toCharArray(), false)) {
requestor.acceptModule(module);
}
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/SearchableEnvironment.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/SearchableEnvironment.java
index 60e9f4d3cd4..4f32ebf7b17 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/SearchableEnvironment.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/SearchableEnvironment.java
@@ -308,7 +308,7 @@ private NameEnvironmentAnswer createAnswer(Answer lookupAnswer, String packageNa
* ISearchRequestor.acceptModule(char[][] moduleName)
*/
public void findModules(char[] prefix, ISearchRequestor requestor, IJavaProject javaProject) {
- this.nameLookup.seekModule(prefix, true, new SearchableEnvironmentRequestor(requestor));
+ this.nameLookup.seekModule(prefix, true, new SearchableEnvironmentRequestor(requestor), this.release);
}
@Override
@@ -1137,7 +1137,7 @@ private IModuleDescription getModuleDescription(IPackageFragmentRoot[] roots) {
this.rootToModule = new HashMap<>();
}
for (IPackageFragmentRoot root : roots) {
- IModuleDescription moduleDescription = NameLookup.getModuleDescription(this.project, root, this.rootToModule, this.nameLookup.rootToResolvedEntries::get);
+ IModuleDescription moduleDescription = NameLookup.getModuleDescription(this.project, root, this.rootToModule, this.nameLookup.rootToResolvedEntries::get, this.release);
if (moduleDescription != null)
return moduleDescription;
}
@@ -1149,7 +1149,7 @@ private IPackageFragmentRoot[] findModuleContext(char[] moduleName) {
if (this.knownModuleLocations != null && moduleName != null && moduleName.length > 0) {
moduleContext = this.knownModuleLocations.get(String.valueOf(moduleName));
if (moduleContext == null) {
- Answer moduleAnswer = this.nameLookup.findModule(moduleName);
+ Answer moduleAnswer = this.nameLookup.findModule(moduleName, this.release);
if (moduleAnswer != null) {
IProject currentProject = moduleAnswer.module.getJavaProject().getProject();
IJavaElement current = moduleAnswer.module.getParent();
@@ -1223,7 +1223,7 @@ public void cleanup() {
@Override
public org.eclipse.jdt.internal.compiler.env.IModule getModule(char[] name) {
- NameLookup.Answer answer = this.nameLookup.findModule(name);
+ NameLookup.Answer answer = this.nameLookup.findModule(name, this.release);
IModule module = null;
if (answer != null) {
module = NameLookup.getModuleDescriptionInfo(answer.module);
diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/NameEnvironment.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/NameEnvironment.java
index 93fb9ac0f74..c255caf008f 100644
--- a/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/NameEnvironment.java
+++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/internal/core/builder/NameEnvironment.java
@@ -154,7 +154,7 @@ private void computeClasspathLocations(
this.moduleUpdater.addReadUnnamedForNonEmptyClasspath(javaProject, classpathEntries);
}
}
- IModuleDescription projectModule = javaProject.getModuleDescription();
+ IModuleDescription projectModule = javaProject.getModuleDescription(releaseTarget);
String patchedModuleName = ModuleEntryProcessor.pushPatchToFront(classpathEntries, javaProject);
IModule patchedModule = null;
@@ -402,6 +402,7 @@ private void computeClasspathLocations(
try {
AbstractModule sourceModule = (AbstractModule)projectModule;
IModule info = (IModule) sourceModule.getElementInfo();
+ // Add all source locations to the module path entry
final ClasspathLocation[] sourceLocations2;
if(sLocationsForTest.size() == 0) {
sourceLocations2 = this.sourceLocations;