diff --git a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinAbiVersionIndexBase.kt b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinAbiVersionIndexBase.kt index 2b778eed0be..b9bc3217306 100644 --- a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinAbiVersionIndexBase.kt +++ b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinAbiVersionIndexBase.kt @@ -29,24 +29,24 @@ import java.io.DataOutput /** * Important! This is not a stub-based index. And it has its own version */ -abstract class KotlinAbiVersionIndexBase( +abstract class KotlinAbiVersionIndexBase( private val classOfIndex: Class, - private val createBinaryVersion: (IntArray) -> BinaryVersion -) : ScalarIndexExtension() { + protected val createBinaryVersion: (IntArray) -> V +) : ScalarIndexExtension() { - override fun getName() = ID.create(classOfIndex.canonicalName) + override fun getName() = ID.create(classOfIndex.canonicalName) - override fun getKeyDescriptor(): KeyDescriptor = object : KeyDescriptor { - override fun isEqual(val1: BinaryVersion, val2: BinaryVersion): Boolean = val1 == val2 + override fun getKeyDescriptor(): KeyDescriptor = object : KeyDescriptor { + override fun isEqual(val1: V, val2: V): Boolean = val1 == val2 - override fun getHashCode(value: BinaryVersion): Int = value.hashCode() + override fun getHashCode(value: V): Int = value.hashCode() - override fun read(input: DataInput): BinaryVersion { + override fun read(input: DataInput): V { val size = DataInputOutputUtil.readINT(input) return createBinaryVersion((0..size - 1).map { DataInputOutputUtil.readINT(input) }.toIntArray()) } - override fun save(output: DataOutput, value: BinaryVersion) { + override fun save(output: DataOutput, value: V) { val array = value.toArray() DataInputOutputUtil.writeINT(output, array.size) for (number in array) { diff --git a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinJavaScriptAbiVersionIndex.kt b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinJavaScriptAbiVersionIndex.kt index 98ebdb5085d..ccbc6829c77 100644 --- a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinJavaScriptAbiVersionIndex.kt +++ b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinJavaScriptAbiVersionIndex.kt @@ -21,13 +21,12 @@ import com.intellij.util.indexing.DataIndexer import com.intellij.util.indexing.FileBasedIndex import com.intellij.util.indexing.FileContent import org.jetbrains.kotlin.js.JavaScript -import org.jetbrains.kotlin.serialization.deserialization.BinaryVersion import org.jetbrains.kotlin.utils.JsBinaryVersion import org.jetbrains.kotlin.utils.KotlinJavascriptMetadata import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils import java.util.* -object KotlinJavaScriptAbiVersionIndex : KotlinAbiVersionIndexBase( +object KotlinJavaScriptAbiVersionIndex : KotlinAbiVersionIndexBase( KotlinJavaScriptAbiVersionIndex::class.java, { JsBinaryVersion(*it) } ) { override fun getIndexer() = INDEXER @@ -39,7 +38,7 @@ object KotlinJavaScriptAbiVersionIndex : KotlinAbiVersionIndexBase - val result = HashMap() + val result = HashMap() tryBlock(inputData) { val text = VfsUtilCore.loadText(inputData.file) diff --git a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinMetadataVersionIndex.kt b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinMetadataVersionIndex.kt index 78c86367be1..0a3b0b07730 100644 --- a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinMetadataVersionIndex.kt +++ b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinMetadataVersionIndex.kt @@ -23,13 +23,12 @@ import com.intellij.util.indexing.FileContent import org.jetbrains.kotlin.load.java.JvmAnnotationNames.* import org.jetbrains.kotlin.load.kotlin.JvmMetadataVersion import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader -import org.jetbrains.kotlin.serialization.deserialization.BinaryVersion import org.jetbrains.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor import org.jetbrains.org.objectweb.asm.Opcodes -object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase( +object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase( KotlinMetadataVersionIndex::class.java, { JvmMetadataVersion(*it) } ) { override fun getIndexer() = INDEXER @@ -46,8 +45,8 @@ object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase() { inputData: FileContent -> - var version: BinaryVersion? = null + private val INDEXER = DataIndexer() { inputData: FileContent -> + var version: JvmMetadataVersion? = null var annotationPresent = false var kind: KotlinClassHeader.Kind? = null @@ -62,7 +61,7 @@ object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase if (value is IntArray) { - version = JvmMetadataVersion(*value) + version = createBinaryVersion(value) } KIND_FIELD_NAME -> if (value is Int) { kind = KotlinClassHeader.Kind.getById(value) diff --git a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinRuntimeLibraryUtil.kt b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinRuntimeLibraryUtil.kt index 711de3f500c..74dcd8cf5c9 100644 --- a/idea/src/org/jetbrains/kotlin/idea/versions/KotlinRuntimeLibraryUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/versions/KotlinRuntimeLibraryUtil.kt @@ -47,25 +47,30 @@ import org.jetbrains.kotlin.idea.configuration.getConfiguratorByName import org.jetbrains.kotlin.idea.framework.* import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.runWithAlternativeResolveEnabled +import org.jetbrains.kotlin.load.kotlin.JvmMetadataVersion import org.jetbrains.kotlin.serialization.deserialization.BinaryVersion +import org.jetbrains.kotlin.utils.JsBinaryVersion import org.jetbrains.kotlin.utils.KotlinJavascriptMetadataUtils import org.jetbrains.kotlin.utils.PathUtil import java.io.File import java.io.IOException -fun getLibraryRootsWithAbiIncompatibleKotlinClasses(module: Module): Collection { +fun getLibraryRootsWithAbiIncompatibleKotlinClasses(module: Module): Collection> { return getLibraryRootsWithAbiIncompatibleVersion( - module, KotlinMetadataVersionIndex, + module, + JvmMetadataVersion.INSTANCE, + KotlinMetadataVersionIndex, { version -> !version.isCompatible() }) } -fun getLibraryRootsWithAbiIncompatibleForKotlinJs(module: Module): Collection { +fun getLibraryRootsWithAbiIncompatibleForKotlinJs(module: Module): Collection> { return getLibraryRootsWithAbiIncompatibleVersion( - module, KotlinJavaScriptAbiVersionIndex, + module, + JsBinaryVersion.INSTANCE, + KotlinJavaScriptAbiVersionIndex, { version -> !KotlinJavascriptMetadataUtils.isAbiVersionCompatible(version.minor) }) // TODO: support major.minor.patch version in JS metadata } - fun updateLibraries(project: Project, libraries: Collection) { ApplicationManager.getApplication().invokeLater { val kJvmConfigurator = getConfiguratorByName(KotlinJavaModuleConfigurator.NAME) as KotlinJavaModuleConfigurator? ?: @@ -215,10 +220,13 @@ internal fun replaceFile(updatedFile: File, replacedJarFile: VirtualFile) { } } -private fun getLibraryRootsWithAbiIncompatibleVersion( +data class BinaryVersionedFile(val file: VirtualFile, val version: T, val supportedVersion: T) + +private fun getLibraryRootsWithAbiIncompatibleVersion( module: Module, - index: ScalarIndexExtension, - checkVersion: (BinaryVersion) -> Boolean): Collection { + supportedVersion: T, + index: ScalarIndexExtension, + checkVersion: (T) -> Boolean): Collection> { val id = index.name val moduleWithAllDependencies = setOf(module) + ModuleUtil.getAllDependentModules(module) @@ -227,7 +235,7 @@ private fun getLibraryRootsWithAbiIncompatibleVersion( val allVersions = FileBasedIndex.getInstance().getAllKeys(id, module.project) val badVersions = allVersions.filter(checkVersion).toHashSet() - val badRoots = Sets.newHashSet() + val badRoots = Sets.newHashSet>() val fileIndex = ProjectFileIndex.SERVICE.getInstance(module.project) for (version in badVersions) { @@ -236,7 +244,7 @@ private fun getLibraryRootsWithAbiIncompatibleVersion( val libraryRoot = fileIndex.getClassRootForFile(indexedFile) ?: error("Only library roots were requested, and only class files should be indexed with KotlinAbiVersionIndex key. " + "File: ${indexedFile.path}") - badRoots.add(getLocalFile(libraryRoot)) + badRoots.add(BinaryVersionedFile(getLocalFile(libraryRoot), version, supportedVersion)) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt b/idea/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt index 6aedab6a52e..48c437836f7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/versions/UnsupportedAbiVersionNotificationPanelProvider.kt @@ -31,6 +31,7 @@ import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootAdapter import com.intellij.openapi.roots.ModuleRootEvent +import com.intellij.openapi.roots.libraries.Library import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep @@ -40,12 +41,22 @@ import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotifications +import com.intellij.ui.HyperlinkAdapter import com.intellij.ui.HyperlinkLabel import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.KotlinPluginUpdater +import org.jetbrains.kotlin.idea.KotlinPluginUtil +import org.jetbrains.kotlin.idea.PluginUpdateStatus import org.jetbrains.kotlin.idea.framework.getJsStdLibJar +import org.jetbrains.kotlin.idea.framework.getReflectJar import org.jetbrains.kotlin.idea.framework.getRuntimeJar +import org.jetbrains.kotlin.idea.framework.getTestJar +import org.jetbrains.kotlin.serialization.deserialization.BinaryVersion import java.text.MessageFormat +import java.util.* import javax.swing.Icon +import javax.swing.JLabel +import javax.swing.event.HyperlinkEvent class UnsupportedAbiVersionNotificationPanelProvider(private val project: Project) : EditorNotifications.Provider() { @@ -66,43 +77,87 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec }) } - private fun doCreate(module: Module, badRoots: Collection): EditorNotificationPanel { + private fun doCreate(module: Module, badVersionedRoots: Collection>): EditorNotificationPanel { val answer = ErrorNotificationPanel() + val badRootFiles = badVersionedRoots.map { it.file } val kotlinLibraries = findAllUsedLibraries(project).keySet() val badRuntimeLibraries = kotlinLibraries.filter { library -> val runtimeJar = getLocalJar(getRuntimeJar(library)) val jsLibJar = getLocalJar(getJsStdLibJar(library)) - badRoots.contains(runtimeJar) || badRoots.contains(jsLibJar) + badRootFiles.contains(runtimeJar) || badRootFiles.contains(jsLibJar) } - if (!badRuntimeLibraries.isEmpty()) { - val otherBadRootsCount = badRoots.size - badRuntimeLibraries.size + val isPluginOldForAllRoots = badVersionedRoots.all { it.supportedVersion < it.version } + val isPluginNewForAllRoots = badVersionedRoots.all { it.supportedVersion > it.version } - val text = MessageFormat.format("{0,choice,0#|1#|1" + "{1,choice,0#|1# and one other jar|1< and {1} other jars} " + "{1,choice,0#has|0", + if (!badRuntimeLibraries.isEmpty()) { + val badRootsInRuntimeLibraries = findBadRootsInRuntimeLibraries(badRuntimeLibraries, badVersionedRoots) + val otherBadRootsCount = badVersionedRoots.size - badRootsInRuntimeLibraries.size + + val text = MessageFormat.format("{0,choice,0#|1#|1" + + "{1,choice,0#|1# and one other jar|1< and {1} other jars} " + + "{1,choice,0#has|0", badRuntimeLibraries.size, otherBadRootsCount) - val actionLabelText = MessageFormat.format("Update {0,choice,0#|1#|1 0) { - createShowPathsActionLabel(module, answer, "Show all") - } - } - else if (badRoots.size == 1) { - val root = badRoots.iterator().next() - val presentableName = root.presentableName - answer.setText("Kotlin library '$presentableName' has an unsupported format. Please update the library or the plugin") - answer.createActionLabel("Go to " + presentableName) { navigateToLibraryRoot(project, root) } + if (isPluginOldForAllRoots) { + createUpdatePluginLink(answer) + } + + val isPluginOldForAllRuntimeLibraries = badRootsInRuntimeLibraries.all { it.supportedVersion < it.version } + val isPluginNewForAllRuntimeLibraries = badRootsInRuntimeLibraries.all { it.supportedVersion > it.version } + + val updateAction = when { + isPluginNewForAllRuntimeLibraries -> "Update" + isPluginOldForAllRuntimeLibraries -> "Downgrade" + else -> "Replace" + } + + val actionLabelText = MessageFormat.format("$updateAction {0,choice,0#|1#|1 { + answer.setText("Kotlin library '$presentableName' was compiled with a newer Kotlin compiler and can't be read. Please update Kotlin plugin.") + createUpdatePluginLink(answer) + } + + isPluginNewForAllRoots -> + answer.setText("Kotlin library '$presentableName' has outdated binary format and can't be read by current plugin. Please update the library.") + + else -> { + throw IllegalStateException("Bad root with compatible version found: $badVersionedRoot") + } + } + + answer.createActionLabel("Go to " + presentableName) { navigateToLibraryRoot(project, badVersionedRoot.file) } } else { - answer.setText("Some Kotlin libraries attached to this project have unsupported format. Please update the libraries or the plugin") - createShowPathsActionLabel(module, answer, "Show paths") + when { + isPluginOldForAllRoots -> { + answer.setText("Some Kotlin libraries attached to this project were compiled with a newer Kotlin compiler and can't be read. " + + "Please update Kotlin plugin.") + createUpdatePluginLink(answer) + } + + isPluginNewForAllRoots -> + answer.setText("Some Kotlin libraries attached to this project have outdated binary format and can't be read by current plugin. " + + "Please update found libraries.") + + else -> + answer.setText("Some Kotlin libraries attached to this project have unsupported binary format. Please update the libraries or the plugin.") + } } + + createShowPathsActionLabel(module, answer, "Details") + return answer } @@ -112,7 +167,7 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec val badRoots = collectBadRoots(module) assert(!badRoots.isEmpty()) { "This action should only be called when bad roots are present" } - val listPopupModel = LibraryRootsPopupModel("Unsupported format", project, badRoots) + val listPopupModel = LibraryRootsPopupModel("Unsupported format, plugin version: " + KotlinPluginUtil.getPluginVersion(), project, badRoots) val popup = JBPopupFactory.getInstance().createListPopup(listPopupModel) popup.showUnderneathOf(label) @@ -121,6 +176,30 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec } } + private fun createUpdatePluginLink(answer: ErrorNotificationPanel) { + answer.createProgressAction(" Check...", "Update plugin") { link, updateLink -> + KotlinPluginUpdater.getInstance().runUpdateCheck { pluginUpdateStatus -> + when (pluginUpdateStatus) { + is PluginUpdateStatus.Update -> { + link.isVisible = false + updateLink.isVisible = true + + updateLink.addHyperlinkListener(object : HyperlinkAdapter() { + override fun hyperlinkActivated(e: HyperlinkEvent) { + KotlinPluginUpdater.getInstance().installPluginUpdate(pluginUpdateStatus) + } + }) + } + is PluginUpdateStatus.LatestVersionInstalled -> { + link.text = "No updates found" + } + } + + false // do not auto-retry update check + } + } + } + override fun getKey(): Key = KEY override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? { @@ -159,23 +238,50 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec return null } - private class LibraryRootsPopupModel(title: String, private val project: Project, roots: Collection) - : BaseListPopupStep(title, *roots.toTypedArray()) { + private fun findBadRootsInRuntimeLibraries( + badRuntimeLibraries: List, + badVersionedRoots: Collection>): ArrayList> { + val badRootsInLibraries = ArrayList>() - override fun getTextFor(root: VirtualFile): String { - val relativePath = VfsUtilCore.getRelativePath(root, project.baseDir, '/') - return relativePath ?: root.path + fun addToBadRoots(file: VirtualFile?) { + if (file != null) { + val runtimeJarBadRoot = badVersionedRoots.firstOrNull { it.file == file } + if (runtimeJarBadRoot != null) { + badRootsInLibraries.add(runtimeJarBadRoot) + } + } } - override fun getIconFor(aValue: VirtualFile): Icon? { - if (aValue.isDirectory) { + badRuntimeLibraries.forEach { library -> + addToBadRoots(getLocalJar(getRuntimeJar(library))) + addToBadRoots(getLocalJar(getJsStdLibJar(library))) + addToBadRoots(getLocalJar(getReflectJar(library))) + addToBadRoots(getLocalJar(getTestJar(library))) + } + + return badRootsInLibraries + } + + private class LibraryRootsPopupModel( + title: String, + private val project: Project, + roots: Collection> + ) : BaseListPopupStep>(title, *roots.toTypedArray()) { + + override fun getTextFor(root: BinaryVersionedFile): String { + val relativePath = VfsUtilCore.getRelativePath(root.file, project.baseDir, '/') + return "${relativePath ?: root.file.path} (${root.version}) - expected: ${root.supportedVersion}" + } + + override fun getIconFor(aValue: BinaryVersionedFile): Icon? { + if (aValue.file.isDirectory) { return AllIcons.Nodes.Folder } return AllIcons.FileTypes.Archive } - override fun onChosen(selectedValue: VirtualFile, finalChoice: Boolean): PopupStep? { - navigateToLibraryRoot(project, selectedValue) + override fun onChosen(selectedValue: BinaryVersionedFile, finalChoice: Boolean): PopupStep? { + navigateToLibraryRoot(project, selectedValue.file) return PopupStep.FINAL_CHOICE } @@ -186,6 +292,16 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec init { myLabel.icon = AllIcons.General.Error } + + fun createProgressAction(text: String, successLinkText: String, updater: (JLabel, HyperlinkLabel) -> Unit) { + val label = JLabel(text) + myLinksPanel.add(label) + + val successLink = createActionLabel(successLinkText, { }) + successLink.isVisible = false + + updater(label, successLink) + } } private val updateNotifications = Runnable { updateNotifications() } @@ -205,7 +321,7 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec OpenFileDescriptor(project, root).navigate(true) } - fun collectBadRoots(module: Module): Collection { + fun collectBadRoots(module: Module): Collection> { val badJVMRoots = getLibraryRootsWithAbiIncompatibleKotlinClasses(module) val badJSRoots = getLibraryRootsWithAbiIncompatibleForKotlinJs(module) @@ -223,3 +339,16 @@ fun EditorNotificationPanel.createComponentActionLabel(labelText: String, callba } label.set(createActionLabel(labelText, action)) } + +private operator fun BinaryVersion.compareTo(other: BinaryVersion): Int { + for (i in 0..Math.max(numbers.size, other.numbers.size) - 1) { + val thisPart = numbers.getOrNull(i) ?: -1 + val otherPart = other.numbers.getOrNull(i) ?: -1 + + if (thisPart != otherPart) { + return thisPart - otherPart + } + } + + return 0 +}