blob: b7e3bf93176ecee0fb7b7a4b1458ce979e12d97d [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.tasks
import com.android.SdkConstants
import com.android.utils.FileUtils
import com.google.gson.GsonBuilder
import org.gradle.api.artifacts.ArtifactCollection
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.RenderableDependency
import org.gradle.api.tasks.diagnostics.internal.graph.nodes.RenderableModuleResult
import java.io.File
import java.util.LinkedList
import java.util.zip.ZipFile
/** Finds used/unused dependencies in our variant. */
class DependencyUsageFinder(
private val classFinder: ClassFinder,
private val variantClasses: AnalyzeDependenciesTask.VariantClassesHolder,
private val variantDependencies: AnalyzeDependenciesTask.VariantDependenciesHolder) {
/** All the dependencies required across our code base. */
val requiredDependencies: Set<String> =
variantClasses.getUsedClasses().mapNotNull { classFinder.find(it) }.toSet()
/** Dependencies we direct declare and are being used. */
val usedDirectDependencies: Set<String> =
variantDependencies.all.intersect(requiredDependencies)
/** Dependencies we direct declare and are not being used. */
val unusedDirectDependencies: Set<String> =
variantDependencies.all.minus(requiredDependencies)
}
/** Find required dependencies that are being included indirectly and would be unreachable if
* we remove unused direct dependencies. */
class DependencyGraphAnalyzer(
private val configuration: Configuration,
private val depsUsageFinder: DependencyUsageFinder) {
// Keep a map from dependencyId to the correspondent RenderableDependency
val renderableDependencies = mapIdsToRenderableDependencies()
// TODO: Handle 'project' dependencies
fun findIndirectRequiredDependencies(): Set<String> {
/* Get the ids of all required dependencies that are:
- valid (they map to a valid RenderableDependency in the renderableDependencies hashmap)
- not 'project' dependencies */
val requiredRenderableDependencies = depsUsageFinder.requiredDependencies
.asSequence()
.mapNotNull { renderableDependencies[it] }
.filterNot { it.id is ProjectComponentIdentifier }
.map { (it.id as ComponentIdentifier).displayName }.toSet()
/* From the remaining ones, find those that are still available to the module
(those that can still be reached in the dependency graph) */
val accessibleDependencies = findAccessibleDependencies()
return requiredRenderableDependencies.minus(accessibleDependencies)
}
private fun findAccessibleDependencies (): Set<String> {
// Traverse the dependency tree to find the ones that are still accessible
val visited = mutableSetOf<String>()
val queue = LinkedList<String>()
// Initially, Add all direct dependencies in the Queue
depsUsageFinder.usedDirectDependencies.forEach {
if (renderableDependencies.containsKey(it)) {
queue.add(it)
}
}
// Do a BFS to find the reachable (visited) dependencies
while (!queue.isEmpty()) {
val componentIdentifier = queue.pop()
val dependency = renderableDependencies[componentIdentifier]
visited.add(componentIdentifier)
dependency?.children?.forEach {
val childComponentIdentifier = (it.id as ComponentIdentifier).displayName
if (!visited.contains(childComponentIdentifier)) {
queue.push(childComponentIdentifier)
}
}
}
return visited
}
private fun mapIdsToRenderableDependencies(): Map<String, RenderableDependency> {
val dependencyGraph = configuration.incoming.resolutionResult.root
val renderableGraph = RenderableModuleResult(dependencyGraph)
val renderableDependencies = mutableMapOf<String, RenderableDependency>()
// Map id to the correspondent RenderableDependency
renderableGraph.children.forEach {
val componentIdentifier = it.id as ComponentIdentifier
renderableDependencies[componentIdentifier.displayName] = it
}
return renderableDependencies
}
}
/** Finds where a class is coming from. */
class ClassFinder(private val externalArtifactCollection: ArtifactCollection) {
private val classToDependency: Map<String, String> by lazy {
val map = mutableMapOf<String, String>()
externalArtifactCollection
.asSequence()
.filter { it.file.name.endsWith(SdkConstants.DOT_JAR) }
.forEach { artifact ->
val classNamesInJar = getClassFilesInJar(artifact.file)
classNamesInJar.forEach { artifactClass ->
map[artifactClass] = artifact.id.componentIdentifier.displayName
}
}
map
}
/** Returns the dependency that contains {@code className} or null if we can't find it. */
fun find(className: String) = classToDependency[className]
fun findClassesInDependency(dependencyId: String) =
classToDependency.filterValues { it == dependencyId }.keys
private fun getClassFilesInJar(jarFile: File): List<String> {
val classes = mutableListOf<String>()
val zipFile = ZipFile(jarFile)
val entries = zipFile.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.name.endsWith(SdkConstants.DOT_CLASS)) {
classes.add(entry.name)
}
}
return classes
}
}
class DependencyUsageReporter(
private val variantClasses: AnalyzeDependenciesTask.VariantClassesHolder,
private val variantDependencies: AnalyzeDependenciesTask.VariantDependenciesHolder,
private val classFinder: ClassFinder,
private val depsUsageFinder: DependencyUsageFinder,
private val graphAnalyzer: DependencyGraphAnalyzer) {
// TODO: update the analyzer to detect dependencies in resources
// Temporary workaround: A list of common prefixes from libraries that are not yet detected
// by the analyzer. such as the Kotlin libraries, or popular libraries that are often
// used in resources but not referenced in classes.
private val dontReportPrefixSet = setOf(
// Kotlin libraries are added by default and should not be removed
"org.jetbrains.kotlin:",
// Some libraries that are often used exclusively in resource files
"androidx.gridlayout:gridlayout:",
"androidx.appcompat:appcompat:",
"de.hdodenhof:circleimageview:")
fun writeUnusedDependencies(destinationFile: File) {
val toRemove = depsUsageFinder.unusedDirectDependencies
.filter { graphAnalyzer.renderableDependencies.containsKey(it) }
val toAdd = graphAnalyzer.findIndirectRequiredDependencies()
val dontReportTemp = mutableListOf<String>()
toRemove.forEach { dependency ->
dontReportPrefixSet.forEach {
if (dependency.startsWith(it)) {
dontReportTemp.add(dependency)
}
}
}
val report = mapOf("remove" to toRemove.minus(dontReportTemp), "add" to toAdd)
writeToFile(report, destinationFile)
}
fun writeMisconfiguredDependencies(destinationFile: File) {
val apiDependencies = variantClasses.getPublicClasses()
.mapNotNull { classFinder.find(it) }
.filter { variantDependencies.api.contains(it) }
val misconfiguredDependencies = variantDependencies.api.minus(apiDependencies)
writeToFile(misconfiguredDependencies, destinationFile)
}
private fun writeToFile(output: Any, destinationFile: File) {
val gson = GsonBuilder().setPrettyPrinting().create()
FileUtils.writeToFile(destinationFile, gson.toJson(output))
}
}