1 /*
<lambda>null2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.build
18 
19 import androidx.build.logging.TERMINAL_RED
20 import androidx.build.logging.TERMINAL_RESET
21 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
22 import com.autonomousapps.AbstractPostProcessingTask
23 import com.autonomousapps.model.ModuleCoordinates
24 import com.autonomousapps.model.ProjectAdvice
25 import com.autonomousapps.model.ProjectCoordinates
26 import com.google.gson.Gson
27 import com.google.gson.GsonBuilder
28 import java.io.File
29 import kotlin.text.appendLine
30 import org.gradle.api.Project
31 import org.gradle.api.file.RegularFileProperty
32 import org.gradle.api.tasks.CacheableTask
33 import org.gradle.api.tasks.InputFile
34 import org.gradle.api.tasks.Internal
35 import org.gradle.api.tasks.Optional
36 import org.gradle.api.tasks.OutputFile
37 import org.gradle.api.tasks.PathSensitive
38 import org.gradle.api.tasks.PathSensitivity
39 import org.gradle.api.tasks.TaskAction
40 
41 /**
42  * Task that reports dependency analysis advice for the project. It gets advice from the dependency
43  * analysis gradle plugin and checks the baselines for the advice already captured and only reports
44  * if additional violations are found.
45  */
46 @CacheableTask
47 abstract class ReportDependencyAnalysisAdviceTask : AbstractPostProcessingTask() {
48     init {
49         group = "Verification"
50         description = "Task for generating advice for dependency analysis"
51     }
52 
53     @get:Internal abstract val baseLineFile: RegularFileProperty
54 
55     @InputFile
56     @Optional
57     @PathSensitive(PathSensitivity.NONE)
58     fun getDependencyAnalysisBaseline(): File? = baseLineFile.get().asFile.takeIf { it.exists() }
59 
60     @get:Internal val projectPath: String = project.path
61     @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
62     @get:Internal
63     val isPublishedLibrary: Boolean =
64         project.extensions.getByType(AndroidXExtension::class.java).type ==
65             SoftwareType.PUBLISHED_LIBRARY
66 
67     @TaskAction
68     fun getAdvice() {
69         val projectAdvice =
70             this@ReportDependencyAnalysisAdviceTask.projectAdvice().toAndroidxProjectAdvice()
71 
72         val baselineAdvice =
73             Gson()
74                 .fromJson<AndroidxProjectAdvice>(
75                     getDependencyAnalysisBaseline()?.readText(),
76                     AndroidxProjectAdvice::class.java
77                 )
78 
79         val advice =
80             if (baselineAdvice != null) {
81                 getIncrementalAdvice(
82                     projectAdvice.dependencyAdvice.filter {
83                         !baselineAdvice.dependencyAdvice.contains(it)
84                     }
85                 )
86             } else {
87                 getIncrementalAdvice(projectAdvice.dependencyAdvice)
88             }
89 
90         if (advice.isNotBlank()) {
91             error(
92                 """
93                     There are some new dependencies added to this change that might be misconfigured:
94                     $advice
95                     ********************************************************************************
96                     $TERMINAL_RED
97                     To get a complete list of misconfigured dependencies, please run:
98                     ./gradlew $projectPath:projectHealth.
99                     To update the dependency analysis baseline file, please run:
100                     ./gradlew $projectPath:updateDependencyAnalysisBaseline
101                     $TERMINAL_RESET
102                     ********************************************************************************
103                 """
104                     .trimIndent()
105             )
106         }
107     }
108 
109     private fun getIncrementalAdvice(missingDependencyAdvice: List<DependencyAdvice>): String {
110         // Skip the reporting of modify dependencies for now, so that advice is easier to follow.
111         val unused = mutableSetOf<String>()
112         val transitive = mutableSetOf<String>()
113         val advice = StringBuilder()
114 
115         missingDependencyAdvice.forEach {
116             // Don't fail CI if test source set has misconfigured dependencies
117             if (it.fromConfiguration?.contains("test", ignoreCase = true) == true) {
118                 return@forEach
119             }
120             if (it.toConfiguration?.contains("test", ignoreCase = true) == true) {
121                 return@forEach
122             }
123 
124             val isCompileOnly =
125                 it.toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true
126             val isTransitiveDependencyAdvice =
127                 it.fromConfiguration == null && it.toConfiguration != null && !isCompileOnly
128             val isUnusedDependencyAdvice =
129                 it.fromConfiguration != null && it.toConfiguration == null
130 
131             val identifier =
132                 if (it.coordinates.type == "project") {
133                     "project(${it.coordinates.identifier})"
134                 } else {
135                     "'${it.coordinates.identifier}:${it.coordinates.resolvedVersion}'"
136                 }
137             if (isTransitiveDependencyAdvice) {
138                 transitive.add("${it.toConfiguration}($identifier)")
139             }
140             if (isUnusedDependencyAdvice) {
141                 unused.add("${it.fromConfiguration}($identifier)")
142             }
143         }
144         if (unused.isNotEmpty()) {
145             advice.appendLine("Unused dependencies which should be removed:")
146             advice.appendLine(unused.sorted().joinToString(separator = "\n"))
147         }
148         if (transitive.isNotEmpty()) {
149             advice.appendLine("These transitive dependencies can be declared directly:")
150             advice.appendLine(transitive.sorted().joinToString(separator = "\n"))
151         }
152         return advice.toString()
153     }
154 }
155 
156 /** Task to update dependency analysis baselines for the project. */
157 @CacheableTask
158 abstract class UpdateDependencyAnalysisBaseLineTask : AbstractPostProcessingTask() {
159     init {
160         group = "Verification"
161         description = "Task for updating dependency analysis baselines"
162     }
163 
164     @get:OutputFile abstract val outputFile: RegularFileProperty
165     @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
166     @get:Internal
167     val isPublishedLibrary: Boolean =
168         project.extensions.getByType(AndroidXExtension::class.java).type ==
169             SoftwareType.PUBLISHED_LIBRARY
170 
171     @TaskAction
updateBaseLineForDependencyAnalysisAdvicenull172     fun updateBaseLineForDependencyAnalysisAdvice() {
173         val projectAdvice =
174             this@UpdateDependencyAnalysisBaseLineTask.projectAdvice().toAndroidxProjectAdvice()
175         val outputFile = outputFile.get()
176         val gson = GsonBuilder().setPrettyPrinting().create()
177         outputFile.asFile.writeText(gson.toJson(projectAdvice))
178     }
179 }
180 
181 /**
182  * Configure the dependency analysis gradle plugin and register new post-processing tasks:
183  * 1. Updating the baselines for advice provided by the plugin.
184  * 2. Getting any incremental advice not captured in the baselines.
185  */
configureDependencyAnalysisPluginnull186 internal fun Project.configureDependencyAnalysisPlugin() {
187     plugins.apply("com.autonomousapps.dependency-analysis")
188 
189     val updateDependencyAnalysisBaselineTask =
190         tasks.register(
191             "updateDependencyAnalysisBaseline",
192             UpdateDependencyAnalysisBaseLineTask::class.java
193         ) { task ->
194             task.outputFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json"))
195             task.cacheEvenIfNoOutputs()
196             // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
197             task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
198         }
199 
200     val reportDependencyAnalysisAdviceTask =
201         tasks.register(
202             "reportDependencyAnalysisAdvice",
203             ReportDependencyAnalysisAdviceTask::class.java
204         ) { task ->
205             var baselineFile = layout.projectDirectory.file("dependencyAnalysis-baseline.json")
206             task.baseLineFile.set(baselineFile)
207             task.cacheEvenIfNoOutputs()
208             // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
209             task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
210         }
211 
212     val dependencyAnalysisSubExtension =
213         extensions.getByType(com.autonomousapps.DependencyAnalysisSubExtension::class.java)
214     dependencyAnalysisSubExtension.registerPostProcessingTask(reportDependencyAnalysisAdviceTask)
215     dependencyAnalysisSubExtension.registerPostProcessingTask(updateDependencyAnalysisBaselineTask)
216 
217     // Ignore advice for runTimeOnly, compileOnly or incorrect dependency configs
218     // since it affects downstream consumers
219     dependencyAnalysisSubExtension.issues { it.onIncorrectConfiguration { it.severity("ignore") } }
220     dependencyAnalysisSubExtension.issues { it.onRuntimeOnly { it.severity("ignore") } }
221     dependencyAnalysisSubExtension.issues { it.onCompileOnly { it.severity("ignore") } }
222 
223     // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
224     // Enable CI check for published libraries
225     if (
226         multiplatformExtension == null && androidXExtension.type == SoftwareType.PUBLISHED_LIBRARY
227     ) {
228         addToBuildOnServer(reportDependencyAnalysisAdviceTask)
229     }
230 }
231 
232 /**
233  * Helper data classes to store the advice provided Dependency Analysis Gradle plugin in baselines.
234  */
235 internal data class AndroidxProjectAdvice(
236     val projectPath: String,
237     val dependencyAdvice: List<DependencyAdvice>
238 )
239 
240 internal data class DependencyAdvice(
241     val coordinates: Coordinates,
242     val fromConfiguration: String?,
243     val toConfiguration: String?
244 )
245 
246 internal data class Coordinates(
247     val type: String,
248     val identifier: String,
249     val resolvedVersion: String?
250 )
251 
252 /** Convert advice reported by DAGP into format suitable for storing in baselines. */
toAndroidxProjectAdvicenull253 internal fun ProjectAdvice.toAndroidxProjectAdvice(): AndroidxProjectAdvice {
254     return AndroidxProjectAdvice(
255         projectPath = projectPath,
256         dependencyAdvice =
257             dependencyAdvice.map {
258                 val type =
259                     if (it.coordinates is ProjectCoordinates) {
260                         "project"
261                     } else {
262                         "module"
263                     }
264                 val resolvedVersion =
265                     if (it.coordinates is ModuleCoordinates) {
266                         (it.coordinates as ModuleCoordinates).resolvedVersion
267                     } else {
268                         null
269                     }
270                 DependencyAdvice(
271                     coordinates =
272                         Coordinates(
273                             identifier = it.coordinates.identifier,
274                             resolvedVersion = resolvedVersion,
275                             type = type
276                         ),
277                     fromConfiguration = it.fromConfiguration,
278                     toConfiguration = it.toConfiguration
279                 )
280             }
281     )
282 }
283