Show the hint how to resolve incompatible ABI library problem (KT-11024)

This commit is contained in:
Nikolay Krasko
2016-03-01 20:05:29 +03:00
parent 04eea69a82
commit 7e39c2321a
5 changed files with 193 additions and 58 deletions
@@ -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<T>(
abstract class KotlinAbiVersionIndexBase<T, V : BinaryVersion>(
private val classOfIndex: Class<T>,
private val createBinaryVersion: (IntArray) -> BinaryVersion
) : ScalarIndexExtension<BinaryVersion>() {
protected val createBinaryVersion: (IntArray) -> V
) : ScalarIndexExtension<V>() {
override fun getName() = ID.create<BinaryVersion, Void>(classOfIndex.canonicalName)
override fun getName() = ID.create<V, Void>(classOfIndex.canonicalName)
override fun getKeyDescriptor(): KeyDescriptor<BinaryVersion> = object : KeyDescriptor<BinaryVersion> {
override fun isEqual(val1: BinaryVersion, val2: BinaryVersion): Boolean = val1 == val2
override fun getKeyDescriptor(): KeyDescriptor<V> = object : KeyDescriptor<V> {
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) {
@@ -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<KotlinJavaScriptAbiVersionIndex>(
object KotlinJavaScriptAbiVersionIndex : KotlinAbiVersionIndexBase<KotlinJavaScriptAbiVersionIndex, JsBinaryVersion>(
KotlinJavaScriptAbiVersionIndex::class.java, { JsBinaryVersion(*it) }
) {
override fun getIndexer() = INDEXER
@@ -39,7 +38,7 @@ object KotlinJavaScriptAbiVersionIndex : KotlinAbiVersionIndexBase<KotlinJavaScr
private val VERSION = 2
private val INDEXER = DataIndexer { inputData: FileContent ->
val result = HashMap<BinaryVersion, Void?>()
val result = HashMap<JsBinaryVersion, Void?>()
tryBlock(inputData) {
val text = VfsUtilCore.loadText(inputData.file)
@@ -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<KotlinMetadataVersionIndex>(
object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase<KotlinMetadataVersionIndex, JvmMetadataVersion>(
KotlinMetadataVersionIndex::class.java, { JvmMetadataVersion(*it) }
) {
override fun getIndexer() = INDEXER
@@ -46,8 +45,8 @@ object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase<KotlinMetadataVers
KotlinClassHeader.Kind.MULTIFILE_CLASS
)
private val INDEXER = DataIndexer<BinaryVersion, Void, FileContent>() { inputData: FileContent ->
var version: BinaryVersion? = null
private val INDEXER = DataIndexer<JvmMetadataVersion, Void, FileContent>() { inputData: FileContent ->
var version: JvmMetadataVersion? = null
var annotationPresent = false
var kind: KotlinClassHeader.Kind? = null
@@ -62,7 +61,7 @@ object KotlinMetadataVersionIndex : KotlinAbiVersionIndexBase<KotlinMetadataVers
override fun visit(name: String, value: Any) {
when (name) {
METADATA_VERSION_FIELD_NAME -> if (value is IntArray) {
version = JvmMetadataVersion(*value)
version = createBinaryVersion(value)
}
KIND_FIELD_NAME -> if (value is Int) {
kind = KotlinClassHeader.Kind.getById(value)
@@ -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<VirtualFile> {
fun getLibraryRootsWithAbiIncompatibleKotlinClasses(module: Module): Collection<BinaryVersionedFile<JvmMetadataVersion>> {
return getLibraryRootsWithAbiIncompatibleVersion(
module, KotlinMetadataVersionIndex,
module,
JvmMetadataVersion.INSTANCE,
KotlinMetadataVersionIndex,
{ version -> !version.isCompatible() })
}
fun getLibraryRootsWithAbiIncompatibleForKotlinJs(module: Module): Collection<VirtualFile> {
fun getLibraryRootsWithAbiIncompatibleForKotlinJs(module: Module): Collection<BinaryVersionedFile<JsBinaryVersion>> {
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<Library>) {
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<out T : BinaryVersion>(val file: VirtualFile, val version: T, val supportedVersion: T)
private fun <T : BinaryVersion> getLibraryRootsWithAbiIncompatibleVersion(
module: Module,
index: ScalarIndexExtension<BinaryVersion>,
checkVersion: (BinaryVersion) -> Boolean): Collection<VirtualFile> {
supportedVersion: T,
index: ScalarIndexExtension<T>,
checkVersion: (T) -> Boolean): Collection<BinaryVersionedFile<T>> {
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<VirtualFile>()
val badRoots = Sets.newHashSet<BinaryVersionedFile<T>>()
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))
}
}
@@ -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<EditorNotificationPanel>() {
@@ -66,43 +77,87 @@ class UnsupportedAbiVersionNotificationPanelProvider(private val project: Projec
})
}
private fun doCreate(module: Module, badRoots: Collection<VirtualFile>): EditorNotificationPanel {
private fun doCreate(module: Module, badVersionedRoots: Collection<BinaryVersionedFile<BinaryVersion>>): 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("<html><b>{0,choice,0#|1#|1<Some }Kotlin runtime librar{0,choice,0#|1#y|1<ies}</b>" + "{1,choice,0#|1# and one other jar|1< and {1} other jars} " + "{1,choice,0#has|0<have} an unsupported format</html>",
if (!badRuntimeLibraries.isEmpty()) {
val badRootsInRuntimeLibraries = findBadRootsInRuntimeLibraries(badRuntimeLibraries, badVersionedRoots)
val otherBadRootsCount = badVersionedRoots.size - badRootsInRuntimeLibraries.size
val text = MessageFormat.format("<html><b>{0,choice,0#|1#|1<Some }Kotlin runtime librar{0,choice,0#|1#y|1<ies}</b>" +
"{1,choice,0#|1# and one other jar|1< and {1} other jars} " +
"{1,choice,0#has|0<have} an unsupported binary format.</html>",
badRuntimeLibraries.size,
otherBadRootsCount)
val actionLabelText = MessageFormat.format("Update {0,choice,0#|1#|1<all }Kotlin runtime librar{0,choice,0#|1#y|1<ies} ",
badRuntimeLibraries.size)
answer.setText(text)
answer.createActionLabel(actionLabelText) { updateLibraries(project, badRuntimeLibraries) }
if (otherBadRootsCount > 0) {
createShowPathsActionLabel(module, answer, "Show all")
}
}
else if (badRoots.size == 1) {
val root = badRoots.iterator().next()
val presentableName = root.presentableName
answer.setText("<html>Kotlin library <b>'$presentableName'</b> has an unsupported format. Please update the library or the plugin</html>")
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<all }Kotlin runtime librar{0,choice,0#|1#y|1<ies} ", badRuntimeLibraries.size)
answer.createActionLabel(actionLabelText) { updateLibraries(project, badRuntimeLibraries) }
}
else if (badVersionedRoots.size == 1) {
val badVersionedRoot = badVersionedRoots.first()
val presentableName = badVersionedRoot.file.presentableName
when {
isPluginOldForAllRoots -> {
answer.setText("<html>Kotlin library <b>'$presentableName'</b> was compiled with a newer Kotlin compiler and can't be read. Please update Kotlin plugin.</html>")
createUpdatePluginLink(answer)
}
isPluginNewForAllRoots ->
answer.setText("<html>Kotlin library <b>'$presentableName'</b> has outdated binary format and can't be read by current plugin. Please update the library.</html>")
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<EditorNotificationPanel> = 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<VirtualFile>)
: BaseListPopupStep<VirtualFile>(title, *roots.toTypedArray()) {
private fun findBadRootsInRuntimeLibraries(
badRuntimeLibraries: List<Library>,
badVersionedRoots: Collection<BinaryVersionedFile<BinaryVersion>>): ArrayList<BinaryVersionedFile<BinaryVersion>> {
val badRootsInLibraries = ArrayList<BinaryVersionedFile<BinaryVersion>>()
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<BinaryVersionedFile<BinaryVersion>>
) : BaseListPopupStep<BinaryVersionedFile<BinaryVersion>>(title, *roots.toTypedArray()) {
override fun getTextFor(root: BinaryVersionedFile<BinaryVersion>): String {
val relativePath = VfsUtilCore.getRelativePath(root.file, project.baseDir, '/')
return "${relativePath ?: root.file.path} (${root.version}) - expected: ${root.supportedVersion}"
}
override fun getIconFor(aValue: BinaryVersionedFile<BinaryVersion>): Icon? {
if (aValue.file.isDirectory) {
return AllIcons.Nodes.Folder
}
return AllIcons.FileTypes.Archive
}
override fun onChosen(selectedValue: VirtualFile, finalChoice: Boolean): PopupStep<Any>? {
navigateToLibraryRoot(project, selectedValue)
override fun onChosen(selectedValue: BinaryVersionedFile<BinaryVersion>, finalChoice: Boolean): PopupStep<Any>? {
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<VirtualFile> {
fun collectBadRoots(module: Module): Collection<BinaryVersionedFile<BinaryVersion>> {
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
}