Android Lint and Detekt warnings in GitHub pull requests
As Android developers we are used to using various static analysers like Android Lint that have been around for a long time: ErrorProne for Java in the past and Detekt for Kotlin now. Static analysis most of the time runs on a Continuous Integration server and fails the run if issues have appeared.
Unfortunately, identifying the problem involves checking CI logs, file outputs or even running the check locally. While there are different third-party tools that can automatically parse the result of the check and display it as comments in the corresponding pull request, some of these tools run code analysis in a separate environment with a separate config and use baselines that are not shared with Gradle.
But the good news is that GitHub has introduced its own tool to display static analysis issues in pull requests, and here I’m going to show you how easy it is to use and without any additional third-party tools.
We at Bumble Inc also faced the need for a proper indication of issues generated by static analysis tools in pull requests. To explore the issue and capabilities of GitHub we implemented a proper indication in our open-source project, Appyx — a model-driven navigation library for Jetpack Compose.
GitHub code scanning
GitHub’s code scanning feature allows you to scan and display warnings and errors directly in the GitHub interface without the need for bots.
It is important to say that the feature is only available for open-source projects and GitHub Enterprise Cloud, and not normal private repositories.
Code scanning works in 2 different modes:
- CodeQL analysis mode: the analysis will be run in a separate tool maintained by the GitHub team and it supports a limited number of languages (Kotlin excluded).
- Report file mod: In this case, we will need to generate specially-formatted report files manually and upload them using github/codeql-action/upload-sarif action.
Because CodeQL does not support Kotlin and has no direct integrations with widely used Kotlin/Android related static analysis tools, we need to use the second mode to display errors and warnings in pull requests. Specially formatted files used for it are SARIF files, which I will describe in the next section.
Code static analyser should support SARIF file output. Otherwise, SARIF should be manually generated from other types of output like XML. Lucky for us, both Android Lint and Detekt support it, albeit with some caveats.
After a SARIF file is generated, we can upload it to GitHub using the following snippet for GitHub Actions:
SARIF (Static Analysis Results Interchange Format) is a standard, JSON-based format for the output of static analysis tools.
This is an example of what this JSON file looks like:
A single file can contain multiple runs and tools. Whenever an error is produced, the file contains detailed information regarding the error, like a location in the file, an error message, and rules.
To discover more about the format and supported features check out microsoft/sarif-tutorials repository.
SARIF support for Android Lint was implemented in the 4.2.0 version of Android Gradle Plugin.
To enable SARIF report, use the following code snippet:
Now if you run Android Lint, you will see this in the log:
After you run Lint in your project you will also have separate build/reports/lint-results-debug.sarif files for each of your Gradle modules. What you need to do next depends on the number of modules you have and their structure.
If you are using a single-module project structure, then everything is easy for you. After you run lintDebug you can use output directly to upload it to GitHub.
Multi-module app project
In the case of a multi-module app project, we need to automatically merge all reports into a single one. This can be done in 2 different ways:
- Use Android Lint to generate a merged report.
- Manually merge multiple SARIF files into a single one.
In this section, I will describe the first variant. The second variant is described in the next section as it is the only option for multi-module library projects.
To make Android Lint merge reports we can use checkDependencies option:
By doing so you can launch app:lintDebug and the report produced will contain not only errors from app module, but also errors from other dependent modules.
If you have modules that are not connected to app module, or you do not want to enable checkDependencies flag then proceed to the next section.
Multi-module library project
Multi-module library projects are a little different because they might not have app module that has all modules as direct and indirect dependencies.
If your project structure does not change over time, you can manually specify files to upload in a GitHub Actions file. For example:
This approach has a couple of disadvantages. Firstly, GitHub has a limitation of 27 runs including all uploaded SARIF files, and each separate file is a separate run. Secondly, it is quite easy to forget to change the list of files when the project structure is changed. That is why we might want to automatically collect all SARIF files from modules and merge them. To do so we can use a simple Gradle plugin. I won’t describe the process of creating a new Gradle plugin here but you can find that in the official documentation or in my other article — How to use Composite builds as a replacement of buildSrc in Gradle.
Let’s start with a collector plugin. The plugin will register a collector task. Other modules will register their lint tasks as inputs for the collector task. This plugin will also use appyx-collect-sarif identifier.
Here I am using ReportMergeTask. This is a custom task that exists inside Detekt plugin to merge SARIF files produced by Detekt. The good thing is that the task is actually tool-agnostic, the only limitation being that it supports only one tool in a SARIF file. Read more about the task here.
If a single SARIF file contains multiple tools, merge them by using @microsoft/sarif-multitool by calling npx @microsoft/sarif-multitool merge — output-file merged.sarif — merge-runs — force file1.sarif file1.sarif … but this is not our particular case. Check out a Gradle task that uses sarif-multitool here.
The second plugin that we will use will be a reporter plugin. The reporter plugin will set up Android Lint and add a dependency to the collector task. This plugin will use appyx-lint identifier.
The only thing that is left to do is to register the new plugins.
Now we can launch mergeLintSarif task from GitHub actions and upload lint-merged.sarif file to GitHub. It is important to use --сontinue flag, so the build is not canceled after the first failure of Lint to produce reports for all modules.
SARIF support in Detekt appeared almost 18 months ago in version 1.15.0.
To enable SARIF report, use the following code snippet:
Once you have run detekt in your project you will have a separate build/reports/detekt/detekt.sarif file for each of your Gradle modules.
You should apply the same strategies for your project structure as for Android Lint.
Absolute path issue
By default, Detekt does not specify a base path for SARIF reports and the generated report has absolute file paths rather than relative ones. It prevents GitHub code scanning from understanding where exactly the problem is. This is because producing the report and analysing the report happen on different machines with different work paths.
To overcome this we have to specify the root folder of the repository as a base path. Most of the time the root folder will be the root Gradle project, but be careful as this might not be so in your particular case.
For multi-module projects we can create DetektPlugin with appyx-detekt identifier which is similar to LintPlugin and update CollectSarifPlugin.
Now we can add appyx-detekt plugin to the corresponding Gradle modules and update our GitHub Action to launch detekt mergeDetektSarif --continue and upload build/detekt-merged.sarif file.
As a result, you will have the following comments in your GitHub pull request once the code analysis is done:
So, I trust that you can see how relatively easy it is to set up Detekt and Android Lint warnings so that they are displayed directly in your GitHub pull requests without the need for third-party tools. By uploading SARIF files directly, we maintain consistency between baselines managed by Gradle and the code scanning tool. Unfortunately, the feature is only available for open-source projects or GitHub Enterprise Cloud.
Do check out the source code of the solution in our Appyx repository:
- build.yml GitHub workflow that invokes merge Gradle task and uploads the results.
- verification-plugin module with the Gradle plugins that I used.