Current state
The current state of the dependencyAnalysis
extension leaves a bit to be desired. Consider:
// root build.gradle
dependencyAnalysis {
issues {
onAny {
fail("some:dep", "some-other:dep")
}
}
}
What does that mean? In fact, what it means is that, on detection of "any" issue (and there are three types that are distinguished; see below), the plugin will fail the build. Oh, and if it encounters either of "some:dep" or "some-other:dep", it will pretend those don't exist; they simply won't show up in any report, because they're being ignored (if you look at the source code for the fail()
method, you'll see that its optional argument is named "ignore").
This is not very intuitive.
Three types of issue:
- Unused dependencies (which should be removed!) ->
onUnusedDependencies {}
- Used transitive dependencies (which should be declared directly!) ->
onUsedTransitiveDependencies {}
- Incorrect configuration for a dependency (is
api
but should be implementation
, and vice-versa). -> onIncorrectConfiguration {}
And in addition, as one can see above, there is an onAny {}
block which configures all three types simultaneously and equally.
There are currently three options for each of these three types:
warn()
. This is the default behavior. The plugin will print its advice to console and the build will not fail due to any dependency issue detected.
fail()
. This will tell the plugin to fail the build upon detection of the configured issue type.
ignore()
. This tells the plugin to ignore any dependencies issues of the associated type. Perhaps you don't care if you're using transitive dependencies, for example.
Consider this more complex example:
// root build.gradle
dependencyAnalysis {
issues {
onUnusedDependencies {
fail("some:dep")
}
onUsedTransitiveDependencies {
ignore()
}
onIncorrectConfiguration {
warn("some-other:dep")
}
}
}
and the result of this is that:
- The plugin will fail if it discovers any unused (but declared) dependencies, unless that dependency is "some:dep", which it ignores.
- The plugin will ignore all issues relating to used transitive dependencies.
- The plugin will emit a warning and not fail the build if it detects a plugin on the wrong configuration, unless that dependency is "some-other:dep", which it ignores.
So, that's the current state.
Goals for an improved API
Readable
The first goal is readability. A DSL with a block like onUnusedDependencies { fail("some:dep") }
isn't very readable, as it is natural to think we're telling the plugin to fail if "some:dep" is unused, which is roughly the opposite of the actual behavior.
Flexible
There is already some degree of flexibility, but more is desirable. For example, it might be useful to be able to ignore certain dependencies only for a particular project (or set of projects). Currently we can only ignore a dependency for all projects.
What about configuring this behavior via a file, instead of or addition to the DSL? Consider lint for example, which can be configured with an XML file. We could do the same, with XML or JSON or whatever.
Unambiguous
Currently there's a degree of ambiguity. If one configures both the onAny {}
block and one of the others, what is the result? Does onAny supersede the more specific rule? Does the specific rule supersede the general? Are the rules combined in some way? In point of fact it's the last option (the rules are unioned together), but this is not obvious.
Proposed API
I'm currently considering something like the following:
issues {
assumeIsolated = false // one of [true, false]
// configure ':proj-a'
project(':proj-a') {
behavior = 'warn' // one of ['warn', 'fail', 'ignore']
// The following are assumed to be correct. I.e., the plugin will not
// suggest removing these if theyโre found on the given configuration,
// or variant thereof.
convention {
implementation = ['androidx.core:core-ktx', '*-ktx']
api = ['androidx.multidex:multidex']
// note that, by default, the plugin will not suggest changing a
// compileOnly dependency. It assumes users know what theyโre
// doing when they do that.
}
}
// configure all projects
all {
...
}
}
Overlapping rules
When rulesets overlap (for example, you've configured all {}
and specific projects, the system will layer these, with the more general at bottom and the more specific at top. So you might set all { behavior = 'warn' }
, but project(':proj-a') { behavior = 'fail' }
. Any issue detected in proj-a will lead to build failure, but issues in other projects would merely result in a warning.
Non-holistic / isolated changes
assumeIsolated
: when unset or set to false (the default), the plugin follows default (current) behavior. When set to true, it will assume changes to the project will be made piecemeal (not holistically). As such, it will not suggest removing an "unused" dependency that is part of the ABI of a parent project unless that ABI dependency is on the api
configuration; nor will it suggest reducing the visibility of any dependencies that are currently on api
(even if they "should" be implementation
). The idea is to minimize the potential to break a given subproject that is undergoing incremental (non-holistic) change. Correct implementation of such an algorithm would likely be very complex, and so would likely come last, if at all. Such a need may be highly specialized, and of uncertain general benefit.
Config file
One additional change I would like to make to the DSL is to provide an option to write it out to disk as a json config file. The dependencyAnalysis {}
extension could then take a single config parameter that points to such a file, and then the file is the source of truth. Sort of like how lint can be configured in the Android world.