Using Type Resolution
This page describes how to use detekt's type resolution feature.
caution
Please note that type resolution is still an experimental feature of Detekt. We expect it to be stable with the upcoming release of Detekt (2.x)
What is type resolution
Type resolution is a feature that allows Detekt to perform more advanced static analysis on your Kotlin source code.
Normally, Detekt doesn't have access to the types and symbols that are available to the compiler during the compilation. This restricts the inspection capability. By enabling type resolution, you provide to Detekt all the information to understand types and symbols in your code needed to perform more accurate analysis. This extends Detekt's inspection capability to ones of the Kotlin compiler.
An example
Detekt has a rule called MagicNumber to detect usages of magic numbers in your code.
In the following code:
val user = getUserById(42)?.toString()
Detekt is able to report the usage of the number 42
as a magic number, without type resolution. All the information needed to run this inspection is already available in the source code.
Similarly, Detekt has another rule called UnnecessarySafeCall to detect unnecessary usages of safe call operators (?.
).
In the previous example, Detekt is able to determine if the safe call in getUserById(42)?.toString()
is required only with type resolution.
This is because Detekt needs to know what is the return type of getUserById()
in order to correctly perform the inspection. If the return type is a nullable type, then the code is valid. If the return type is a non-nullable type, Detekt will report an UnnecessarySafeCall
as the ?.
is actually not needed.
With type resolution, Detekt has access to all the symbols and types of your codebase. Type resolution can be enabled by providing the classpath that is used during compilation. This will give Detekt access to all the code used to compile your project (both first and third party code) and will allow more advanced analysis.
Is my rule using type resolution?
If you're running Detekt without type resolution, all the rules that require type resolution will not run.
All the rules that require type resolution are annotated with @RequiresTypeResolution
.
Moreover, their official documentation in the Detekt website will mention Requires Type Resolution (like here).
caution
Please note that we do have some rules that have mixed behavior whether type resolution is enabled or not. Those rules are listed here: #2994
Before opening an issue that you're rule is not working, please verify, whether your rule requires type resolution and check if you have type resolution enabled.
Issues and proposals for rules that require type resolution are labelled with needs type and symbol solving on the Issue tracker.
Enabling on a JVM project
The easiest way to use type resolution is to use the Detekt Gradle plugin. On a JVM project, the following tasks will be created:
detekt
- Runs detekt WITHOUT type resolutiondetektMain
- Runs detekt with type resolution on themain
source setdetektTest
- Runs detekt with type resolution on thetest
source set
Moreover, you can use detektBaselineMain
and detektBaselineTest
to create baselines starting from runs of Detekt with type resolution enabled.
Alternatively, you can create a custom detekt task, making sure to specify the classpath
and jvmTarget
properties correctly. See the Run detekt using the Detekt Gradle Plugin and the Run detekt using Gradle Task for further readings on this.
Enabling on an Android project
Other than the aforementioned tasks for JVM projects, you can use the following Android-specific gradle tasks:
detekt<Variant>
- Runs detekt with type resolution on the specific build variantdetektBaseline<Variant>
- Creates a detekt baselines starting from a run of Detekt with type resolution enabled on the specific build variant.
Alternatively, you can create a custom detekt task, making sure to specify the classpath
and jvmTarget
properties correctly.
Doing this on Android is more complicated due to build types/flavors (see #2259 for further context).
Therefore, we recommend using the detekt<Variant>
tasks offered by the Gradle plugins.
In case of build related issues, you may try detekt.android.disabled=true
in gradle.properties
to prevent detekt
Gradle plugins from configuring Android-specific gradle tasks.
Enabling on Detekt CLI
If you're using Detekt via CLI, type resolution will be enabled only if you provide the --classpath
and
--jvm-target
flags. See the list of CLI options for details.
Writing a rule that uses type resolution
If you're writing a custom rule or if you're willing to write a rule to contribute to Detekt, you might want to leverage type resolution.
Rules that are using type resolution, access the bindingContext from the BaseRule
class (source).
By default, the bindingContext
is initialized as BindingContext.EMPTY
. This is the default value that the rule receives if type resolution is not enabled.
Therefore, is generally advised to wrap your rules with a check for an empty binding context (source):
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
if (bindingContext == BindingContext.EMPTY) return
// Rest of the rule that will run only with type resolution enabled.
}
If the bindingContext
is not EMPTY
, you are free to use it to resolve types and get access to all the information needed for your rules. As a rule of thumb, we recommend to get inspiration from other rules on how they're using the bindingContext
.
Testing a rule that uses type resolution
To test a rule that uses type resolution, you can use the lintWithContext
and compileAndLintWithContext
extension functions.
If you're using JUnit 5 for testing, you can use the @KotlinCoreEnvironmentTest
annotation on your test class, and
accept a parameter of type KotlinCoreEnvironment
in the class constructor. You can then access the environment by
referencing the parameter specified in the constructor:
@KotlinCoreEnvironmentTest
class MyRuleSpec(private val env: KotlinCoreEnvironment) {
@Test
fun `reports cast that cannot succeed`() {
val code = """/* The code you want to test */"""
assertThat(MyRuleSpec().compileAndLintWithContext(env, code)).hasSize(1)
}
}
If you're using Spek for testing, you can create a setupKotlinEnvironment()
util function, and get access to the
KotlinCoreEnvironment
by simply calling val env: KotlinCoreEnvironment by memoized()
:
fun org.spekframework.spek2.dsl.Root.setupKotlinEnvironment(additionalJavaSourceRootPath: Path? = null) {
val wrapper by memoized(
CachingMode.SCOPE,
{ createEnvironment(additionalJavaSourceRootPaths = listOfNotNull(additionalJavaSourceRootPath?.toFile())) },
{ it.dispose() }
)
// `env` name is used for delegation
@Suppress("UNUSED_VARIABLE")
val env: KotlinCoreEnvironment by memoized(CachingMode.EACH_GROUP) { wrapper.env }
}
class MyRuleTest : Spek({
setupKotlinEnvironment()
val env: KotlinCoreEnvironment by memoized()
it("reports cast that cannot succeed") {
val code = """/* The code you want to test */"""
assertThat(MyRuleSpec().compileAndLintWithContext(env, code)).hasSize(1)
}
})
If you're using another testing framework (e.g. JUnit 4), you can use the createEnvironment()
method from detekt-test-utils
.