[java] IDEA-341317 Navigation into multi-release JARs

GitOrigin-RevId: 43612b6950fca72ab58af9b1eb5dc4940f90c501
This commit is contained in:
Tagir Valeev
2023-12-19 16:48:57 +00:00
committed by intellij-monorepo-bot
parent de78307927
commit c74bfa2e57
16 changed files with 540 additions and 96 deletions
@@ -33,9 +33,11 @@ import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiCompiledElement;
import com.intellij.psi.PsiElementFinder;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.search.JavaVersionBasedScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.reference.SoftReference;
import com.intellij.unscramble.ThreadState;
@@ -79,7 +81,8 @@ public final class DebuggerSession implements AbstractDebuggerSession {
private final String mySessionName;
private final DebugProcessImpl myDebugProcess;
private final DebugEnvironment myDebugEnvironment;
private final GlobalSearchScope mySearchScope;
private final GlobalSearchScope myBaseScope;
private GlobalSearchScope mySearchScope;
private Sdk myAlternativeJre;
private final Sdk myRunJre;
@@ -115,6 +118,7 @@ public final class DebuggerSession implements AbstractDebuggerSession {
public void setAlternativeJre(Sdk sdk) {
myAlternativeJre = sdk;
updateScope();
PsiElementFinder.EP.findExtension(AlternativeJreClassFinder.class, getProject()).clearCache();
}
@@ -215,9 +219,23 @@ public final class DebuggerSession implements AbstractDebuggerSession {
myDebugProcess.addDebugProcessListener(new MyDebugProcessListener(debugProcess));
ValueLookupManager.getInstance(getProject()).startListening();
myDebugEnvironment = environment;
mySearchScope = environment.getSearchScope();
myBaseScope = environment.getSearchScope();
myAlternativeJre = environment.getAlternativeJre();
myRunJre = environment.getRunJre();
updateScope();
}
private void updateScope() {
Sdk jre = myAlternativeJre;
if (jre == null) jre = myRunJre;
GlobalSearchScope scope = myBaseScope;
if (jre != null) {
LanguageLevel level = LanguageLevel.parse(jre.getVersionString());
if (level != null) {
scope = new JavaVersionBasedScope(getProject(), scope, level);
}
}
mySearchScope = scope;
}
@NotNull
@@ -278,6 +278,7 @@ dialog.title.0.library.copy=Copy Library
dialog.title.0.library.move=Move Library
dialog.text.enter.common.prefix=Specify prefix for names of {0} selected modules:
class.file.decompiled.text=Decompiled .class file
class.file.multi.release.decompiled.text=Decompiled .class file from JDK-{0}-specific root
class.file.decompiled.bytecode.version.text=bytecode version: {0}.{1}
class.file.decompiled.sdk.version.text=(Java {0})
settings.remote.repo.Maven.Repository.URL=maven Repository URL
@@ -316,6 +317,7 @@ message.text.creating.deployment.descriptor=Creating Deployment Descriptor
modules.order.export.scope.column=Scope
choose.modules.dialog.title=Choose Modules
class.file.open.source.action=Open source file
class.file.open.source.version.specific.action=Open source file from the library root
library.java.attach.files.description=Select files or directories in which library classes, sources, documentation or native libraries are located
project.roots.project.banner.text=General Settings for Project ''{0}''
library.attach.sources.action=Attach Sources
@@ -41,9 +41,11 @@ import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.compiled.ClsParsingUtil;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.EditorNotificationProvider;
import com.intellij.ui.EditorNotifications;
@@ -96,13 +98,7 @@ final class AttachSourcesNotificationProvider implements EditorNotificationProvi
VirtualFile sourceFile = JavaEditorFileSwapper.findSourceFile(project, file);
if (sourceFile != null) {
return notificationPanelCreator.andThen(panel -> {
panel.createActionLabel(JavaUiBundle.message("class.file.open.source.action"), () -> {
if (sourceFile.isValid()) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, sourceFile);
FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
}
});
appendOpenFileAction(project, panel, sourceFile, JavaUiBundle.message("class.file.open.source.action"));
return panel;
});
}
@@ -113,6 +109,20 @@ final class AttachSourcesNotificationProvider implements EditorNotificationProvi
}
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile != null) {
PsiFile baseFile = JavaMultiReleaseUtil.findBaseFile(psiFile);
if (baseFile != null) {
VirtualFile baseSource = JavaEditorFileSwapper.findSourceFile(project, baseFile.getVirtualFile());
if (baseSource != null) {
return notificationPanelCreator.andThen(panel -> {
appendOpenFileAction(project, panel, baseSource, JavaUiBundle.message("class.file.open.source.version.specific.action"));
return panel;
});
}
}
}
List<? extends AttachSourcesProvider.AttachSourcesAction> actionsByFile = psiFile != null ?
collectActions(libraries, psiFile) :
List.of();
@@ -148,6 +158,16 @@ final class AttachSourcesNotificationProvider implements EditorNotificationProvi
});
}
private static void appendOpenFileAction(@NotNull Project project, EditorNotificationPanel panel, VirtualFile sourceFile,
@NotNull @Nls String title) {
panel.createActionLabel(title, () -> {
if (sourceFile.isValid()) {
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, sourceFile);
FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
}
});
}
@SuppressWarnings("IncorrectParentDisposable")
@RequiresEdt
private static void findLibraryEntriesForFile(@NotNull Project project,
@@ -208,7 +228,13 @@ final class AttachSourcesNotificationProvider implements EditorNotificationProvi
@RequiresBackgroundThread
private static @NotNull @NlsContexts.Label String getTextWithClassFileInfo(@NotNull VirtualFile file) {
@Nls StringBuilder info = new StringBuilder(JavaUiBundle.message("class.file.decompiled.text"));
LanguageLevel level = JavaMultiReleaseUtil.getVersion(file);
@Nls StringBuilder info = new StringBuilder();
if (level != null) {
info.append(JavaUiBundle.message("class.file.multi.release.decompiled.text", level.toJavaVersion().feature));
} else {
info.append(JavaUiBundle.message("class.file.decompiled.text"));
}
try {
byte[] data = file.contentsToByteArray(false);
@@ -42,7 +42,7 @@
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<resolveScopeProvider implementation="com.intellij.psi.impl.file.impl.JavaOutOfSourcesResolveScopeProvider"/>
<resolveScopeProvider implementation="com.intellij.psi.impl.file.impl.JavaResolveScopeProvider"/>
<highlightingPassFactory implementation="com.intellij.codeInsight.daemon.impl.JavaTextBlockIndentPassFactory"/>
<deadCode implementation="com.intellij.codeInspection.java19modules.Java9ModuleEntryPoint"/>
<metaLanguage implementation="com.intellij.uast.UastMetaLanguage"/>
@@ -28,6 +28,7 @@ import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import com.intellij.util.Query;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
@@ -119,10 +120,13 @@ public final class JavaFileManagerImpl implements JavaFileManager, Disposable {
private boolean hasAcceptablePackage(@NotNull VirtualFile vFile) {
if (FileTypeRegistry.getInstance().isFileOfType(vFile, JavaClassFileType.INSTANCE)) {
// See IDEADEV-5626
VirtualFile root = ProjectRootManager.getInstance(myManager.getProject()).getFileIndex().getClassRootForFile(vFile);
ProjectFileIndex index = ProjectRootManager.getInstance(myManager.getProject()).getFileIndex();
boolean checkMultiRelease = index.isInLibrary(vFile);
VirtualFile root = index.getClassRootForFile(vFile);
VirtualFile parent = vFile.getParent();
PsiNameHelper nameHelper = PsiNameHelper.getInstance(myManager.getProject());
while (parent != null && !Comparing.equal(parent, root)) {
while (parent != null && !Comparing.equal(parent, root) &&
(!checkMultiRelease || JavaMultiReleaseUtil.getVersionForVersionRoot(root, parent) == null)) {
if (!nameHelper.isIdentifier(parent.getName())) return false;
parent = parent.getParent();
}
@@ -211,8 +215,11 @@ public final class JavaFileManagerImpl implements JavaFileManager, Disposable {
}
private static Collection<PsiJavaModule> upgradeModules(Collection<PsiJavaModule> modules, String moduleName, GlobalSearchScope scope) {
if (modules.size() > 1 && PsiJavaModule.UPGRADEABLE.contains(moduleName) && scope instanceof ModuleWithDependenciesScope) {
Module module = ((ModuleWithDependenciesScope)scope).getModule();
if (scope instanceof DelegatingGlobalSearchScope delegatingScope) {
scope = delegatingScope.unwrap();
}
if (modules.size() > 1 && PsiJavaModule.UPGRADEABLE.contains(moduleName) && scope instanceof ModuleWithDependenciesScope moduleScope) {
Module module = moduleScope.getModule();
boolean isModular = Stream.of(ModuleRootManager.getInstance(module).getSourceRoots(true))
.filter(scope::contains)
.anyMatch(root -> root.findChild(PsiJavaModule.MODULE_INFO_FILE) != null);
@@ -1,45 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.impl.file.impl;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.ResolveScopeProvider;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Limited resolve scope for all java files in module content, but not under source roots.
* For example, java files from test data.
* There is still a possibility to modify this scope choice with the ResolveScopeEnlarger.
*/
public final class JavaOutOfSourcesResolveScopeProvider extends ResolveScopeProvider {
@Nullable
@Override
public GlobalSearchScope getResolveScope(@NotNull VirtualFile file, @NotNull Project project) {
// For java only! For other languages resolve may be implemented with different rules, requiring larger scope.
FileType type = file.getFileType();
if (type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage() == JavaLanguage.INSTANCE) {
ProjectFileIndex index = project.isDefault() ? null : ProjectRootManager.getInstance(project).getFileIndex();
if (index == null) {
return GlobalSearchScope.fileScope(project, file);
}
if (index.isInContent(file) && !index.isInSource(file)) {
PsiFile psi = PsiManager.getInstance(project).findFile(file);
if (psi == null || !JavaHighlightUtil.isJavaHashBangScript(psi)) {
return GlobalSearchScope.fileScope(project, file);
}
}
}
return null;
}
}
@@ -0,0 +1,82 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.impl.file.impl;
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil;
import com.intellij.ide.highlighter.JavaClassFileType;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.module.LanguageLevelUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.roots.TestSourcesFilter;
import com.intellij.openapi.roots.impl.LibraryScopeCache;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.ResolveScopeProvider;
import com.intellij.psi.impl.search.JavaVersionBasedScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import com.intellij.testFramework.LightVirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class JavaResolveScopeProvider extends ResolveScopeProvider {
@Nullable
@Override
public GlobalSearchScope getResolveScope(@NotNull VirtualFile file, @NotNull Project project) {
// For java only! For other languages resolve may be implemented with different rules, requiring larger scope.
FileType type = file.getFileType();
if (type instanceof LanguageFileType langType && langType.getLanguage() == JavaLanguage.INSTANCE) {
ProjectFileIndex index = project.isDefault() ? null : ProjectRootManager.getInstance(project).getFileIndex();
if (index == null) {
return GlobalSearchScope.fileScope(project, file);
}
if (index.isInContent(file) && !index.isInSource(file)) {
// Limited resolve scope for all java files in module content, but not under source roots.
// For example, java files from test data.
// There is still a possibility to modify this scope choice with the ResolveScopeEnlarger.
PsiFile psi = PsiManager.getInstance(project).findFile(file);
if (psi == null || !JavaHighlightUtil.isJavaHashBangScript(psi)) {
return GlobalSearchScope.fileScope(project, file);
}
}
Module module = index.getModuleForFile(file);
if (module != null) {
// Specify preferred language level to support multi-release Jars
LanguageLevel level = LanguageLevelUtil.getEffectiveLanguageLevel(module);
boolean includeTests = TestSourcesFilter.isTestSources(file, project);
GlobalSearchScope baseScope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, includeTests);
return new JavaVersionBasedScope(project, baseScope, level);
}
if (file instanceof LightVirtualFile) {
PsiFile psi = PsiManager.getInstance(project).findFile(file);
if (psi != null) {
PsiFile originalFile = psi.getOriginalFile();
if (originalFile != psi && originalFile.getFileType().equals(JavaClassFileType.INSTANCE)) {
return getClassFileScope(originalFile.getVirtualFile(), project);
}
}
}
}
else if (type.equals(JavaClassFileType.INSTANCE)) {
return getClassFileScope(file, project);
}
return null;
}
@Nullable
private static JavaVersionBasedScope getClassFileScope(@NotNull VirtualFile file, @NotNull Project project) {
ProjectFileIndex index = project.isDefault() ? null : ProjectRootManager.getInstance(project).getFileIndex();
LanguageLevel level = JavaMultiReleaseUtil.getVersion(file);
if (level != null && index != null) {
GlobalSearchScope baseScope = LibraryScopeCache.getInstance(project).getLibraryScope(index.getOrderEntriesForFile(file));
return new JavaVersionBasedScope(project, baseScope, level);
}
return null;
}
}
@@ -4,6 +4,7 @@ implicit.class.context.display=Implicit class
anonymous.class.context.display=Anonymous in {0}
anonymous.class.derived.display=Anonymous class derived from {0}
aux.context.display=of {0}
class.file.version=ver. {0}
bound.not.expected=Unexpected bound
catch.without.try='catch' without 'try'
class.context.display={0} in {1}
@@ -47,6 +47,7 @@ import com.intellij.psi.impl.source.javadoc.PsiSnippetAttributeValueImpl;
import com.intellij.psi.javadoc.PsiSnippetAttributeValue;
import com.intellij.psi.javadoc.PsiSnippetDocTagValue;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.testFramework.LightVirtualFile;
@@ -125,7 +126,24 @@ public final class JavaPsiImplementationHelperImpl extends JavaPsiImplementation
String sourceFileName = ((ClsClassImpl)classes[0]).getSourceFileName();
String packageName = clsFile.getPackageName();
String relativePath = packageName.isEmpty() ? sourceFileName : packageName.replace('.', '/') + '/' + sourceFileName;
finder = root -> root.findFileByRelativePath(relativePath);
LanguageLevel level = JavaMultiReleaseUtil.getVersion(clsFile);
if (level == null) {
finder = root -> root.findFileByRelativePath(relativePath);
}
else {
// Multi-release jar: assume that source file is placed in META-INF/versions/<ver>
// fallback to default location only if there's no the same file in the root
PsiFile baseFile = JavaMultiReleaseUtil.findBaseFile(clsFile);
String versionPath = "META-INF/versions/" + level.toJavaVersion().feature + "/" + relativePath;
if (baseFile != null) {
finder = root -> root.findFileByRelativePath(versionPath);
} else {
finder = root -> {
VirtualFile target = root.findFileByRelativePath(versionPath);
return target == null ? root.findFileByRelativePath(relativePath) : target;
};
}
}
filter = PsiClassOwner.class::isInstance;
}
else {
@@ -55,7 +55,7 @@ public final class JavaModuleNameIndex extends StringStubIndexExtension<PsiJavaM
}
public Collection<PsiJavaModule> getModules(@NotNull String name, @NotNull Project project, @NotNull GlobalSearchScope scope) {
Collection<PsiJavaModule> modules = StubIndex.getElements(getKey(), name, project, new JavaSourceFilterScope(scope, true), PsiJavaModule.class);
Collection<PsiJavaModule> modules = StubIndex.getElements(getKey(), name, project, new JavaSourceFilterScope(scope), PsiJavaModule.class);
if (modules.size() > 1) {
modules = filterVersions(project, modules);
}
@@ -124,7 +124,7 @@ public final class JavaFunctionalExpressionSearcher extends QueryExecutorBase<Ps
MultiMap<VirtualFile, FunExprOccurrence> result = MultiMap.createLinkedSet();
descriptors.get(0).dumbService.runReadActionInSmartMode(() -> {
for (SamDescriptor descriptor : descriptors) {
GlobalSearchScope scope = new JavaSourceFilterScope(descriptor.effectiveUseScope, false, true);
GlobalSearchScope scope = new JavaSourceFilterScope(descriptor.effectiveUseScope, true);
for (FunctionalExpressionKey key : descriptor.keys) {
FileBasedIndex.getInstance().processValues(JavaFunctionalExpressionIndex.INDEX_ID, key, null, (file, infos) -> {
result.putValues(file, ContainerUtil.map(infos, entry -> entry.occurrence));
@@ -6,7 +6,6 @@ import com.intellij.openapi.fileTypes.FileTypeRegistry;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
@@ -16,27 +15,21 @@ import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes;
public class JavaSourceFilterScope extends DelegatingGlobalSearchScope {
private final @Nullable ProjectFileIndex myIndex;
private final boolean myIncludeVersions;
private final boolean myIncludeLibrarySources;
public JavaSourceFilterScope(@NotNull GlobalSearchScope delegate) {
this(delegate, false);
}
public JavaSourceFilterScope(@NotNull GlobalSearchScope delegate, boolean includeVersions) {
this(delegate, includeVersions, false);
}
/**
* By default, the scope excludes version-specific classes of multi-release .jar files
* (i.e. *.class files located under META-INF/versions/ directory).
* Setting {@code includeVersions} parameter to {@code true} allows such files to pass the filter.
*/
public JavaSourceFilterScope(@NotNull GlobalSearchScope delegate, boolean includeVersions, boolean includeLibrarySources) {
public JavaSourceFilterScope(@NotNull GlobalSearchScope delegate, boolean includeLibrarySources) {
super(delegate);
Project project = getProject();
myIndex = project == null ? null : ProjectRootManager.getInstance(project).getFileIndex();
myIncludeVersions = includeVersions;
myIncludeLibrarySources = includeLibrarySources;
}
@@ -51,24 +44,10 @@ public class JavaSourceFilterScope extends DelegatingGlobalSearchScope {
}
if (FileTypeRegistry.getInstance().isFileOfType(file, JavaClassFileType.INSTANCE)) {
return myIndex.isInLibraryClasses(file) && (myIncludeVersions || !isVersioned(file, myIndex));
return myIndex.isInLibraryClasses(file);
}
return myIndex.isUnderSourceRootOfType(file, JavaModuleSourceRootTypes.SOURCES) ||
(myIncludeLibrarySources || myBaseScope.isForceSearchingInLibrarySources()) && myIndex.isInLibrarySource(file);
}
private static boolean isVersioned(VirtualFile file, ProjectFileIndex index) {
VirtualFile root = index.getClassRootForFile(file);
while ((file = file.getParent()) != null && !file.equals(root)) {
if (Comparing.equal(file.getNameSequence(), "versions")) {
VirtualFile parent = file.getParent();
if (parent != null && Comparing.equal(parent.getNameSequence(), "META-INF")) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,69 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.search;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* A scope decorator that allows to prefer a particular Java class version from multi-release Jar dependencies
*/
public final class JavaVersionBasedScope extends DelegatingGlobalSearchScope {
private final ProjectFileIndex myProjectFileIndex;
private final LanguageLevel myLevel;
public JavaVersionBasedScope(@NotNull Project project,
@NotNull GlobalSearchScope baseScope,
@NotNull LanguageLevel desiredLevel) {
super(project, baseScope);
myProjectFileIndex = ProjectFileIndex.getInstance(project);
// Set desired level to Java 8 if it's less than Java 8, to make all these scopes equal, so they could be deduplicated
myLevel = desiredLevel.isLessThan(JavaMultiReleaseUtil.MIN_MULTI_RELEASE_VERSION)
? JavaMultiReleaseUtil.MAX_NON_MULTI_RELEASE_VERSION
: desiredLevel;
}
@Override
public boolean contains(@NotNull VirtualFile file) {
if (!super.contains(file)) return false;
VirtualFile baseFile = JavaMultiReleaseUtil.findBaseFile(file);
if (myLevel.isLessThan(JavaMultiReleaseUtil.MIN_MULTI_RELEASE_VERSION)) {
// In pre-multi-release
return baseFile == null;
}
if (baseFile == null) {
baseFile = file;
}
VirtualFile specificFile = JavaMultiReleaseUtil.findVersionSpecificFile(baseFile, myLevel);
return file.equals(specificFile);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
JavaVersionBasedScope scope = (JavaVersionBasedScope)o;
return myLevel == scope.myLevel;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + Objects.hashCode(myLevel);
return result;
}
@Override
public String toString() {
return "Java " + myLevel.toJavaVersion().feature + " @ " + myBaseScope;
}
}
@@ -9,9 +9,11 @@ import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.util.Iconable;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassOwner;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.JavaMultiReleaseUtil;
import com.intellij.ui.NewUiValue;
import org.jetbrains.annotations.NotNull;
@@ -34,6 +36,10 @@ public final class ClassPresentationProvider implements ItemPresentationProvider
PsiClassOwner classOwner = (PsiClassOwner)file;
String packageName = classOwner.getPackageName();
if (packageName.isEmpty()) return null;
LanguageLevel version = JavaMultiReleaseUtil.getVersion(file);
if (version != null) {
packageName += "/" + JavaPsiBundle.message("class.file.version", version.toJavaVersion().feature);
}
return NewUiValue.isEnabled() ? JavaPsiBundle.message("aux.context.display", packageName) : "(" + packageName + ")";
}
return null;
@@ -0,0 +1,160 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.util;
import com.intellij.ide.highlighter.ArchiveFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Utility methods to support Multi-Release JARs (MR-JARs, <a href="https://openjdk.org/jeps/238">JEP 238</a>)
*/
public final class JavaMultiReleaseUtil {
/**
* Maximal JDK version which does not support multi-release Jars
*/
public static final LanguageLevel MAX_NON_MULTI_RELEASE_VERSION = LanguageLevel.JDK_1_8;
/**
* Minimal JDK version which supports multi-release Jars
*/
public static final LanguageLevel MIN_MULTI_RELEASE_VERSION = LanguageLevel.JDK_1_9;
private static class VersionRootInfo {
final @NotNull LanguageLevel level;
final @NotNull VirtualFile versionRoot;
final @NotNull VirtualFile classRoot;
private VersionRootInfo(@NotNull LanguageLevel level, @NotNull VirtualFile versionRoot, @NotNull VirtualFile classRoot) {
this.level = level;
this.versionRoot = versionRoot;
this.classRoot = classRoot;
}
}
private static @Nullable VersionRootInfo getVersionRootInfo(@NotNull VirtualFile file) {
VirtualFile root = VfsUtilCore.getRootFile(file);
if (!(root.getFileType() instanceof ArchiveFileType)) return null;
VirtualFile parent = file.getParent();
while (parent != null && !parent.equals(root)) {
LanguageLevel level = getVersionForVersionRoot(root, parent);
if (level != null) {
return new VersionRootInfo(level, parent, root);
}
parent = parent.getParent();
}
return null;
}
/**
* Return {@link LanguageLevel} that corresponds to the specified version root directory, relative to the specified root.
* Version root directory is something like {@code META_INF/versions/9}
* @param root root directory (the root of the JAR file)
* @param directory version root directory
* @return the {@link LanguageLevel} that corresponds to the specified version root directory. E.g., {@link LanguageLevel#JDK_1_9}
* for {@code META_INF/versions/9}. Returns null if either root or directory is null, or the specified directory is not a
* version root.
*/
@Contract("null, _ -> null; !null, null -> null")
public static @Nullable LanguageLevel getVersionForVersionRoot(@Nullable VirtualFile root, @Nullable VirtualFile directory) {
if (root == null || directory == null) return null;
VirtualFile parent = directory.getParent();
if (parent == null) return null;
if (parent.getName().equals("versions")) {
VirtualFile grandParent = parent.getParent();
if (grandParent != null && grandParent.getName().equals("META-INF") && root.equals(grandParent.getParent())) {
try {
LanguageLevel level = LanguageLevel.forFeature(Integer.parseInt(directory.getName()));
return level != null && level.isAtLeast(MIN_MULTI_RELEASE_VERSION) ? level : null;
}
catch (NumberFormatException ignored) {
}
}
}
return null;
}
/**
* @param psiFile a PSI file that represents a version-specific file from an MR-JAR
* @return a file representing the same class or resource from the JAR root; null if there's no corresponding resource in the JAR root,
* or if the supplied file is not a version-specific file from an MR-JAR
*/
public static @Nullable PsiFile findBaseFile(@NotNull PsiFile psiFile) {
VirtualFile file = psiFile.getVirtualFile();
if (file == null) return null;
Project project = psiFile.getProject();
if (project.isDefault()) return null;
VirtualFile target = findBaseFile(file);
if (target == null) return null;
return psiFile.getManager().findFile(target);
}
/**
* @param file virtual file that represents a version-specific file from an MR-JAR
* @return a file representing the same class or resource from the JAR root; null if there's no corresponding resource in the JAR root,
* or if the supplied file is not a version-specific file from an MR-JAR
*/
public static @Nullable VirtualFile findBaseFile(@NotNull VirtualFile file) {
VersionRootInfo info = getVersionRootInfo(file);
if (info == null) return null;
VirtualFile versionRoot = info.versionRoot;
VirtualFile classRoot = info.classRoot;
String relativePath = VfsUtilCore.getRelativePath(file, versionRoot);
if (relativePath == null) return null;
return classRoot.findFileByRelativePath(relativePath);
}
/**
* @param file file that represents a non-version-specific file from an MR-JAR
* @param level desired language level
* @return a version of the file, which should be loaded on the specified language level. Returns
* input file if there's no more-specific version of the file, or the input file is not located inside MR-JAR.
*/
public static @NotNull VirtualFile findVersionSpecificFile(@NotNull VirtualFile file, @NotNull LanguageLevel level) {
VirtualFile root = VfsUtilCore.getRootFile(file);
if (!(root.getFileType() instanceof ArchiveFileType)) return file;
String relativePath = VfsUtilCore.getRelativePath(file, root);
if (relativePath == null) return file;
VirtualFile versions = root.findFileByRelativePath("META-INF/versions");
if (versions == null) return file;
int feature = level.toJavaVersion().feature;
int minFeature = MIN_MULTI_RELEASE_VERSION.toJavaVersion().feature;
while (feature >= minFeature) {
VirtualFile versionRoot = versions.findChild(String.valueOf(feature));
if (versionRoot != null) {
VirtualFile target = versionRoot.findFileByRelativePath(relativePath);
if (target != null) {
return target;
}
}
feature--;
}
return file;
}
/**
* @param file PsiFile that represents a version-specific file from library
* @return a version that corresponds for a specified file (e.g. {@link LanguageLevel#JDK_1_9} for a file from {@code META-INF/versions/9});
* null if the given file is not a version-specific file from a library, or a corresponding version is not supported by current IDE.
*/
public static @Nullable LanguageLevel getVersion(@NotNull PsiFile file) {
return getVersion(file.getVirtualFile());
}
/**
* @param file VirtualFile that represents a version-specific file from MR-JAR
* @return a version that corresponds for a specified file (e.g. {@link LanguageLevel#JDK_1_9} for a file from {@code META-INF/versions/9});
* null if the given file is not a version-specific file from a library, or a corresponding version is not supported by current IDE.
*/
public static @Nullable LanguageLevel getVersion(@NotNull VirtualFile file) {
VersionRootInfo version = getVersionRootInfo(file);
return version == null ? null : version.level;
}
}
@@ -4,16 +4,21 @@ package com.intellij.java.codeInsight.daemon
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.java.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.pom.java.LanguageLevel
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiJavaReference
import com.intellij.psi.impl.search.JavaVersionBasedScope
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.ProjectScope
import com.intellij.psi.search.PsiShortNamesCache
import com.intellij.psi.stubs.StubUpdatingIndex
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.util.indexing.FileBasedIndex
import org.assertj.core.api.Assertions.assertThat
private const val CLASS_NAME = "com.example.MultiReleaseClass"
class MultiReleaseJarTest : LightJava9ModulesCodeInsightFixtureTestCase() {
private lateinit var scope: GlobalSearchScope
@@ -28,26 +33,132 @@ class MultiReleaseJarTest : LightJava9ModulesCodeInsightFixtureTestCase() {
}
fun testResolve() {
myFixture.configureByText("a.java", "import com.example.*;\nclass a { <caret>MultiReleaseClass f; }")
assertUnversioned((myFixture.getReferenceAtCaretPosition() as PsiJavaReference).multiResolve(false).map { it.element })
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_8) {
myFixture.configureByText("a.java", "import com.example.*;\nclass a { <caret>MultiReleaseClass f; }")
assertUnversioned((myFixture.getReferenceAtCaretPosition() as PsiJavaReference).multiResolve(false).map { it.element })
}
}
fun testResolve9() {
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_9) {
myFixture.configureByText("a.java", "import com.example.*;\nclass a { <caret>MultiReleaseClass f; }")
assertVersioned((myFixture.getReferenceAtCaretPosition() as PsiJavaReference).multiResolve(false).map { it.element })
}
}
fun testCompletion() {
myFixture.configureByText("a.java", "class a { MultiReleaseClass<caret> f; }")
assertUnversioned(myFixture.complete(CompletionType.BASIC).map { it.psiElement })
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_8) {
myFixture.configureByText("a.java", "class a { MultiReleaseClass<caret> f; }")
assertUnversioned(myFixture.complete(CompletionType.BASIC).map { it.psiElement })
}
}
fun testFindClassByFullName() =
assertUnversioned(myFixture.javaFacade.findClass("com.example.MultiReleaseClass", scope))
fun testCompletion9() {
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_9) {
myFixture.configureByText("a.java", "class a { MultiReleaseClass<caret> f; }")
assertVersioned(myFixture.complete(CompletionType.BASIC).map { it.psiElement })
}
}
fun testFindClassByShortName() =
assertUnversioned(PsiShortNamesCache.getInstance(project).getClassesByName("MultiReleaseClass", scope).toList())
fun testCompletionMethod() {
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_8) {
myFixture.configureByText("a.java", """
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("jav<caret>");
}
}
""".trimIndent())
myFixture.complete(CompletionType.BASIC)
myFixture.type('\n')
myFixture.checkResult("""
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("jav" +
"");
}
}
""".trimIndent())
}
}
fun testCompletionMethod9() {
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_1_9) {
myFixture.configureByText("a.java", """
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("jav<caret>");
}
}
""".trimIndent())
myFixture.complete(CompletionType.BASIC)
myFixture.type('\n')
myFixture.checkResult("""
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("multiReleaseJava9Impl");
}
}
""".trimIndent())
}
}
fun testCompletionMethod11() {
IdeaTestUtil.withLevel(module, LanguageLevel.JDK_11) {
myFixture.configureByText("a.java", """
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("jav<caret>");
}
}
""".trimIndent())
myFixture.complete(CompletionType.BASIC)
myFixture.type('\n')
myFixture.checkResult("""
import com.example.MultiReleaseClass;
class a {
Object test() {
return MultiReleaseClass.class.getDeclaredMethod("multiReleaseJava10Impl");
}
}
""".trimIndent())
}
}
fun testFindClassByFullName() {
assertUnversioned(myFixture.javaFacade.findClass(CLASS_NAME, scope8()))
assertVersioned(myFixture.javaFacade.findClass(CLASS_NAME, scope9()))
}
fun testClassPresentation() {
assertEquals("(com.example)", myFixture.javaFacade.findClass(CLASS_NAME, scope8())!!.presentation!!.locationString)
assertEquals("(com.example/ver. 9)", myFixture.javaFacade.findClass(CLASS_NAME, scope9())!!.presentation!!.locationString)
assertEquals("(com.example/ver. 10)", myFixture.javaFacade.findClass(CLASS_NAME, scope10())!!.presentation!!.locationString)
}
fun testFindClassByShortName() {
assertUnversioned(PsiShortNamesCache.getInstance(project).getClassesByName("MultiReleaseClass", scope8()).toList())
assertVersioned(PsiShortNamesCache.getInstance(project).getClassesByName("MultiReleaseClass", scope9()).toList())
assertVersioned(PsiShortNamesCache.getInstance(project).getClassesByName("MultiReleaseClass", scope10()).toList())
}
fun testFindMethod() {
assertUnversioned(PsiShortNamesCache.getInstance(project).getMethodsByName("multiReleaseDefaultImpl", scope).toList())
assertThat(PsiShortNamesCache.getInstance(project).getMethodsByName("multiReleaseJava9Impl", scope)).isEmpty()
assertUnversioned(PsiShortNamesCache.getInstance(project).getMethodsByName("multiReleaseDefaultImpl", scope8()).toList())
assertThat(PsiShortNamesCache.getInstance(project).getMethodsByName("multiReleaseJava9Impl", scope8())).isEmpty()
assertVersioned(PsiShortNamesCache.getInstance(project).getMethodsByName("multiReleaseJava9Impl", scope9()).toList())
}
private fun scope8() = JavaVersionBasedScope(project, scope, LanguageLevel.JDK_1_8)
private fun scope9() = JavaVersionBasedScope(project, scope, LanguageLevel.JDK_1_9)
private fun scope10() = JavaVersionBasedScope(project, scope, LanguageLevel.JDK_10)
fun testFindFile() {
assertThat(getAllAvailableVersionClasses()).hasSize(3)
}
@@ -72,4 +183,14 @@ class MultiReleaseJarTest : LightJava9ModulesCodeInsightFixtureTestCase() {
assertThat(element).isNotNull
assertThat(element!!.containingFile.virtualFile.path).doesNotContain("/META-INF/versions/")
}
private fun assertVersioned(elements: List<PsiElement?>) {
assertThat(elements).hasSize(1)
assertVersioned(elements[0])
}
private fun assertVersioned(element: PsiElement?) {
assertThat(element).isNotNull
assertThat(element!!.containingFile.virtualFile.path).contains("/META-INF/versions/")
}
}