Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ junit = "4.13.2"

# plugins
changelog = "2.2.1"
intelliJPlatform = "2.9.0"
intelliJPlatform = "2.10.2"
kotlin = "2.2.20"
kover = "0.9.1"
qodana = "2025.1.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.github.mr3zee.kotlinPlugins

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
Expand All @@ -13,7 +12,7 @@ import javax.swing.JComponent
open class KotlinPluginsClearCachesAction : AnAction(AllIcons.Actions.ClearCash) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val treeState = TreeState.getInstance(project)
val treeState = KotlinPluginsTreeState.getInstance(project)

val clear = !treeState.showClearCachesDialog || run {
val (clear, dontShowAgain) = ClearCachesDialog.show()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
Expand Down Expand Up @@ -51,7 +52,7 @@ private class LocalState {

fun isModified(
analyzer: KotlinPluginsExceptionAnalyzerState,
tree: TreeState,
tree: KotlinPluginsTreeState,
settings: KotlinPluginsSettings.State,
): Boolean {
return repositories != settings.repositories ||
Expand All @@ -64,7 +65,7 @@ private class LocalState {

fun reset(
analyzer: KotlinPluginsExceptionAnalyzerState,
tree: TreeState,
tree: KotlinPluginsTreeState,
settings: KotlinPluginsSettings.State,
) {
repositories.clear()
Expand All @@ -83,7 +84,7 @@ private class LocalState {

fun applyTo(
analyzer: KotlinPluginsExceptionAnalyzerService,
tree: TreeState,
tree: KotlinPluginsTreeState,
settings: KotlinPluginsSettings,
) {
val enabledPlugins = plugins.map {
Expand All @@ -101,6 +102,21 @@ private class LocalState {
}

class KotlinPluginsConfigurable(private val project: Project) : Configurable {
companion object {
@Volatile
private var selectArtifactsInitially: Boolean = false

fun showArtifacts(project: Project) {
selectArtifactsInitially = true
ShowSettingsUtil.getInstance().showSettingsDialog(project, KotlinPluginsConfigurable::class.java)
}

fun showGeneral(project: Project) {
selectArtifactsInitially = false
ShowSettingsUtil.getInstance().showSettingsDialog(project, KotlinPluginsConfigurable::class.java)
}
}

private val local: LocalState = LocalState()

private lateinit var repoTable: JBTable
Expand Down Expand Up @@ -303,6 +319,10 @@ class KotlinPluginsConfigurable(private val project: Project) : Configurable {
val tabs = JBTabbedPane()
tabs.addTab("General", generalContent)
tabs.addTab("Artifacts", artifactsContent)
if (selectArtifactsInitially) {
tabs.selectedIndex = 1
selectArtifactsInitially = false
}
rootPanel = JPanel(BorderLayout()).apply { add(tabs, BorderLayout.NORTH) }

reset() // initialise from a persisted state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,14 @@ class KotlinPluginsEditorNotificationProvider : EditorNotificationProvider {
}

panel.createActionLabel("Open diagnostics") {
// todo update state to show error panel
val pluginOrNull = grouped.keys.singleOrNull()

val jarIdOrNull = pluginOrNull?.let { grouped[it]?.singleOrNull() }
val mavenIdOrNull = jarIdOrNull?.mavenId
val versionOrNull = jarIdOrNull?.version

KotlinPluginsTreeState.getInstance(project)
.select(pluginOrNull, mavenIdOrNull, versionOrNull)

KotlinPluginsToolWindowFactory.show(project)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ class KotlinPluginsExceptionAnalyzerService(
Disposable {
private val logger by lazy { thisLogger() }
private val handler: AtomicReference<Handler?> = AtomicReference(null)
val supervisorJob = SupervisorJob(scope.coroutineContext.job)
private val supervisorJob = scope.coroutineContext + SupervisorJob(scope.coroutineContext.job)
private val publisherSync by lazy { project.messageBus.syncPublisher(KotlinPluginStatusUpdater.TOPIC) }

override fun dispose() {
supervisorJob.cancel()
supervisorJob.job.cancel()
val handler = handler.getAndSet(null)
rootLogger().removeHandler(handler)
handler?.close()
Expand All @@ -59,6 +60,7 @@ class KotlinPluginsExceptionAnalyzerService(
}

project.service<KotlinPluginsExceptionReporter>().start()
publisherSync.redraw()
exceptionHandler.start()
logger.debug("Exception analyzer started")
rootLogger().addHandler(exceptionHandler)
Expand Down Expand Up @@ -87,7 +89,13 @@ class KotlinPluginsExceptionAnalyzerService(
?: return@launch

logger.debug("Exception detected for $ids: ${exception.message}")
reporter.matched(ids.toList(), exception, autoDisable = state.autoDisable)

reporter.matched(
ids = ids.toList(),
exception = exception,
autoDisable = state.autoDisable,
isProbablyIncompatible = exception.isProbablyIncompatible(),
)
}
}

Expand Down Expand Up @@ -151,5 +159,14 @@ class KotlinPluginsExceptionAnalyzerService(
.filter { it.isNotEmpty() }
.reduce { acc, set -> acc.intersect(set) }
}

private fun Throwable.isProbablyIncompatible(): Boolean {
return this is ClassNotFoundException ||
this is NoClassDefFoundError ||
this is IncompatibleClassChangeError ||
this is LinkageError ||
this is ClassCastException ||
this.cause?.isProbablyIncompatible() == true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,25 @@ interface KotlinPluginsExceptionReporter {

suspend fun lookFor(): Map<JarId, Set<String>>

fun matched(ids: List<JarId>, exception: Throwable, autoDisable: Boolean)
fun matched(ids: List<JarId>, exception: Throwable, autoDisable: Boolean, isProbablyIncompatible: Boolean)

fun hasExceptions(pluginName: String, mavenId: String, version: String): Boolean

fun getExceptionsReport(pluginName: String, mavenId: String, version: String): ExceptionsReport?
}

class ExceptionsReport(
// The jar is from a local repo
val isLocal: Boolean,
// The jar reloaded after to the same one exception occurred
val reloadedSame: Boolean,
// Exceptions that occurred point to the binary incompatibility
val isProbablyIncompatible: Boolean,
// The Kotlin version of the jar is different from the Kotlin version in the IDE
val kotlinVersionMismatch: KotlinVersionMismatch?,
val exceptions: List<Throwable>,
)

class KotlinPluginsExceptionReporterImpl(
val project: Project,
val scope: CoroutineScope,
Expand Down Expand Up @@ -82,10 +96,27 @@ class KotlinPluginsExceptionReporterImpl(
private val state = AtomicReference<State?>(null)

private val stackTraceMap = ConcurrentHashMap<JarId, Set<String>>()
private val metadata = ConcurrentHashMap<JarId, JarMetadata>()

private val caughtExceptions = ConcurrentHashMap<String, List<CaughtException>>()

private class CaughtException(
val jarId: JarId,
val exception: Throwable,
)

private data class JarMetadata(
val checksum: String,
val isLocal: Boolean,
val kotlinVersionMismatch: KotlinVersionMismatch?,
val reloadedSame: Boolean,
val isProbablyIncompatible: Boolean,
)

override fun dispose() {
state.getAndSet(null)?.close()
stackTraceMap.clear()
metadata.clear()
}

override fun start() {
Expand Down Expand Up @@ -155,18 +186,11 @@ class KotlinPluginsExceptionReporterImpl(
return stackTraceMap.toMap()
}

private val caughtExceptions = mutableMapOf<String, List<CaughtException>>()

private class CaughtException(
val jarId: JarId,
val exception: Throwable,
)

override fun matched(ids: List<JarId>, exception: Throwable, autoDisable: Boolean) {
override fun matched(ids: List<JarId>, exception: Throwable, autoDisable: Boolean, isProbablyIncompatible: Boolean) {
val settings = project.service<KotlinPluginsSettings>()

ids.groupBy { it.pluginName }.forEach { (pluginName, ids) ->
matched(settings, pluginName, ids, exception, autoDisable)
matched(settings, pluginName, ids, exception, autoDisable, isProbablyIncompatible)
}
}

Expand All @@ -176,6 +200,7 @@ class KotlinPluginsExceptionReporterImpl(
ids: List<JarId>,
exception: Throwable,
autoDisable: Boolean,
isProbablyIncompatible: Boolean,
) {
val plugin = settings.pluginByName(pluginName) ?: return

Expand All @@ -188,6 +213,10 @@ class KotlinPluginsExceptionReporterImpl(
}

ids.forEach { id ->
metadata.compute(id) { _, old ->
old?.copy(isProbablyIncompatible = isProbablyIncompatible)
}

statusPublisher.updateVersion(id.pluginName, id.mavenId, id.version, ArtifactStatus.ExceptionInRuntime)
}

Expand All @@ -198,10 +227,15 @@ class KotlinPluginsExceptionReporterImpl(
if (autoDisable) {
settings.disablePlugins(plugin.name)

KotlinPluginsNotificationBallon.notify(project, disabledPlugin = plugin.name)
KotlinPluginsNotificationBallon.notify(
project = project,
disabledPlugin = plugin.name,
mavenId = ids.singleOrNull()?.mavenId,
version = ids.singleOrNull()?.version,
)
} else {
// trigger editor notification across all Kotlin files
project.service<KotlinPluginsNotifications>().activate(plugin.name, ids.map { it.version })
project.service<KotlinPluginsNotifications>().activate(ids)
refreshNotifications()
}
}
Expand All @@ -210,15 +244,69 @@ class KotlinPluginsExceptionReporterImpl(
return caughtExceptions[pluginName].orEmpty().any { it.jarId.mavenId == mavenId && it.jarId.version == version }
}

override fun getExceptionsReport(pluginName: String, mavenId: String, version: String): ExceptionsReport? {
val list = caughtExceptions[pluginName].orEmpty()
.filter { it.jarId.mavenId == mavenId && it.jarId.version == version }
.map { it.exception }
// Deduplicate by exception "signature": class + message + full stacktrace
val exceptions = list.distinctBy { ex ->
buildString {
append(ex::class.java.name)
append('|')
append(ex.message ?: "")
append('|')
append(ex.stackTraceToString())
}
}

return if (exceptions.isEmpty()) {
null
} else {
val metadata = metadata[JarId(pluginName, mavenId, version)] ?: return null

ExceptionsReport(
exceptions = exceptions,
isLocal = metadata.isLocal,
reloadedSame = metadata.reloadedSame,
isProbablyIncompatible = metadata.isProbablyIncompatible,
kotlinVersionMismatch = metadata.kotlinVersionMismatch,
)
}
}

private inner class DiscoveryHandler : KotlinPluginDiscoveryUpdater {
override fun discoveredSync(discovery: KotlinPluginDiscoveryUpdater.Discovery) {
caughtExceptions.compute(discovery.pluginName) { _, old ->
old.orEmpty().filterNot {
it.jarId.mavenId == discovery.mavenId && it.jarId.version == discovery.version
val jarId = JarId(discovery.pluginName, discovery.mavenId, discovery.version)

metadata.compute(jarId) { _, old ->
// the same checksum -> the same exception will be thrown eventually
if (old == null || old.checksum != discovery.checksum) {
caughtExceptions.compute(discovery.pluginName) { _, old ->
old.orEmpty().filterNot {
it.jarId == jarId
}
}
JarMetadata(
checksum = discovery.checksum,
isLocal = discovery.isLocal,
kotlinVersionMismatch = discovery.kotlinVersionMismatch,
reloadedSame = false,
isProbablyIncompatible = false,
)
} else {
JarMetadata(
checksum = discovery.checksum,
isLocal = discovery.isLocal,
kotlinVersionMismatch = discovery.kotlinVersionMismatch,
reloadedSame = true,
isProbablyIncompatible = old.isProbablyIncompatible,
)
}
}

project.service<KotlinPluginsNotifications>().deactivate(discovery.pluginName, discovery.version)
project.service<KotlinPluginsNotifications>()
.deactivate(discovery.pluginName, discovery.mavenId, discovery.version)

refreshNotifications()

processDiscovery(discovery)
Expand All @@ -227,10 +315,14 @@ class KotlinPluginsExceptionReporterImpl(
override fun reset() {
state.getAndSet(null)?.close()
stackTraceMap.clear()
caughtExceptions.clear()
}
}

fun clearExceptions() {
caughtExceptions.clear()
metadata.clear()
}

private fun refreshNotifications() {
ApplicationManager.getApplication().invokeLater {
EditorNotifications.getInstance(project).updateAllNotifications()
Expand Down
Loading
Loading