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;