Adding Custom Messaging In Android Studio On Top of Confusing Gradle Errors Via IntelliJ Plugin

Adding Custom Messaging In Android Studio On Top of Confusing Gradle Errors Via IntelliJ Plugin

Inspecting Build Errors Via IntelliJ Plugins To Add Friendlier Messaging

ยท

4 min read

In our Android project at work, we have the separation of Android modules and JVM modules. There are a lot of benefits to this setup, but one side effect of this setup is that JVM modules cannot depend on Android modules. However, Android modules can depend on JVM modules, and JVM modules can depend on other JVM modules.

When you add an Android module as a dependency in a JVM module, you are left with a fairly confusing error message.

I wanted to intercept this and provide a more friendly error message to developers on the team who might run into this issue. We have an internally managed IntelliJ / Android Studio plugin that we use that I thought I could tie into to help with this situation.

There are many solutions to listen to build / sync / compilation status from within an IntelliJ plugin: GradleSyncListener, ExternalSystemExecutionAware, GradleBuildListener, CompilationStatusListener, ProjectTaskListener, and ExecutionListener . However, the issue I was running into was that when I added an Android library / module to a JVM module in my project, it was being reported as a successful build / sync.

(:coroutines is a sample Android module being added to a JVM module in my project)

My plan was to manually parse build files looking for our internal JVM + Android plugin identifiers and parsing through each module's dependencies if I couldn't find a way to accomplish this via IntelliJ / Gradle APIs.

Luckily, after some more digging, I came across the GradleIssueChecker extension point. Using the GradleIssueChecker, we can be alerted when a build fails with a useful error message and report a BuildIssue on top of the original Gradle issue.

To start using the GradleIssueChecker API, there were some dependencies I needed to add to the existing project.

In the build.gradle.kts file for the project I added:

plugins {
    ...
    id("idea") // added "idea" plugin
}

intellij {
    pluginName = properties("pluginName")
    version = properties("platformVersion")
    type = properties("platformType")

    plugins = "android" // added "android" plugin
}

In the resources/META_INF/plugin.xml I added:

<idea-plugin>
    <!-- Added -->
    <depends>com.intellij.gradle</depends>
    <depends>org.jetbrains.android</depends>

    <!-- Added -->
    <extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
        <!--suppress PluginXmlValidity -->
        <issueChecker implementation="path to class that implements GradleIssueChecker"/>
    </extensions>
</idea-plugin>

Finally, to implement our own GradleIssueChecker class:

import com.intellij.build.issue.BuildIssue
import com.intellij.build.issue.BuildIssueQuickFix
import com.intellij.openapi.project.Project
import com.intellij.pom.Navigatable
import org.gradle.internal.component.NoMatchingConfigurationSelectionException
import org.jetbrains.plugins.gradle.issue.GradleIssueChecker
import org.jetbrains.plugins.gradle.issue.GradleIssueData
import org.jetbrains.plugins.gradle.service.execution.GradleExecutionErrorHandler

/**
 * Checks for [NoMatchingConfigurationSelectionException] which is often a confusing error
 * and tries to offer a more straight forward error message to developers.
 */
@Suppress("UnstableApiUsage")
public class CustomGradleIssueChecker : GradleIssueChecker {

    override fun check(issueData: GradleIssueData): BuildIssue? {
        val throwable = GradleExecutionErrorHandler.getRootCauseAndLocation(issueData.error).first
        val throwableMessage = throwable.message
        val throwableName = throwable::class.qualifiedName
        val exceptionName = NoMatchingConfigurationSelectionException::class.qualifiedName
        // unsure why a type check of throwable is NoMatchingConfigurationSelectionException does not work
        if (throwableName == exceptionName && throwableMessage != null) {
            // matching some error messaging referring to (I think) the JVM module importing an Android module
            if (throwableMessage.contains("attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm'")) {
                val moduleRegex = Regex("""project\s*:\s*(\S+)""")
                val moduleMatches = moduleRegex.find(throwableMessage)
                val dependencyName = if (moduleMatches != null && moduleMatches.groupValues.isNotEmpty()) {
                    moduleMatches.groupValues[0]
                } else {
                    // If we cannot match a module, try matching the more generic artifact
                    val libraryRegex = Regex("""No matching variant of (\S+)""")
                    val libraryMatches = libraryRegex.find(throwableMessage)
                    if (libraryMatches != null && libraryMatches.groupValues.size >= 2) {
                        libraryMatches.groupValues[1]
                    } else {
                        "Unknown"
                    }
                }
                return object : BuildIssue {
                    override val description: String
                        get() = """
                            The dependency ('$dependencyName') looks to be an Android dependency added to a JVM only module.
                        """.trimIndent()
                    override val quickFixes: List<BuildIssueQuickFix>
                        get() = emptyList()
                    override val title: String
                        get() = "Android Dependency '$dependencyName' Detected In JVM Only Module"

                    override fun getNavigatable(project: Project): Navigatable? {
                        return null
                    }
                }
            }
        }

        // not an error we care about
        return null
    }
}

In our custom checker, we start at the main entry point of the GradleIssueChecker API - fun check(issueData: GradleIssueData): BuildIssue?. The main API gives us GradleIssueData and it is our job to return an optional BuildIssue to report.

The first thing I want to do is to try and match the error (NoMatchingConfigurationSelectionException) that occurs when an Android module is added to a JVM module. After we match that error, we can do some (probably brittle ๐Ÿ˜) string matching on the error message to make sure we are specifically in the scenario we want to match. The last part tries to match either the internal module, project :coroutines , or the Android artifact, No matching variant of androidx.activity:activity:1.8.2, that triggered the error so we can include it in our error message.

This pattern has more examples throughout the Android Studio plugin that can be found on Github here. For example, if you have ever seen the error message Disable Gradle 'offline mode' and sync project - here is that implementation.

I hope you found this interesting Intellij plugin API as useful as I did!

ย