Add a new script to generate a csv for filing bugs for dependency analysis advice using culvert.

Bug: 295874502

Test: n/a
Change-Id: Ifcf10d7733df38d3fce816e17394532751bb0a3d
diff --git a/development/buildHealthAdviceToCsv.main.kts b/development/buildHealthAdviceToCsv.main.kts
new file mode 100755
index 0000000..40dc9c4
--- /dev/null
+++ b/development/buildHealthAdviceToCsv.main.kts
@@ -0,0 +1,222 @@
+#!/usr/bin/env kotlin
+
+/*
+ * Copyright 2024 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.
+ */
+
+/**
+ * To run .kts files, follow these steps:
+ *
+ * 1. Download and install the Kotlin compiler (kotlinc). There are several ways to do this; see
+ *    https://kotlinlang.org/docs/command-line.html
+ * 2. Run the script from the command line:
+ *    <path_to>/kotlinc -script <script_file>.kts <arguments>
+ */
+
+@file:Repository("https://repo1.maven.org/maven2")
+@file:DependsOn("com.google.code.gson:gson:2.11.0")
+
+import java.io.File
+import java.util.Locale
+import kotlin.system.exitProcess
+import com.google.gson.Gson
+
+if (args.isEmpty() || !args.contains("-input")) {
+    println("Expected path to buildHealth Advice json file using -input.")
+    println("Provide output file path using -output. By default, output.csv will be generated " +
+        "in the current directory")
+    println("Usage ex: kotlinc -script buildHealthAdviceToCsv.main.kts -- -input " +
+        "path/to/build-health-report.json -output /path/to/output.csv")
+    exitProcess(1)
+}
+
+val input = args[1]
+if (!File(input).exists()) {
+    println("Could not find input files: $input")
+    exitProcess(1)
+}
+val inputJsonFile = File(input)
+println("Parsing ${inputJsonFile.path}...")
+
+val outputFile = if(args.contains("-output")) {
+    args[3]
+} else {
+    "output.csv"
+}
+
+val csvOutputFile = File(outputFile)
+if(csvOutputFile.exists()) {
+    csvOutputFile.delete()
+}
+
+val csvData = StringBuilder()
+val columnHeaders = listOf(
+    "ID", // leave blank
+    "Priority",
+    "Summary",
+    "HotlistIds",
+    "Assignee", // leave blank
+    "Status",
+    "ComponentPath",
+    "Reporter",
+    "FirstNote"
+)
+csvData.append(columnHeaders.joinToString(","))
+csvData.append("\n")
+
+val gson = Gson()
+val advice: Advice = gson.fromJson(inputJsonFile.readLines().first(), Advice::class.java)
+
+//list of projects we want to file issues for
+//val androidxProjects = listOf("lint", "lint-checks", "buildSrc-tests", "androidx-demos", "stableaidl", "test")
+//val flanProjects = listOf("activity", "fragment", "lifecycle", "navigation")
+
+var numProjects = 0
+advice.projectAdvice.forEach projectAdvice@{ projectAdvice ->
+    val projectPath = projectAdvice.projectPath
+    val title = "Please fix misconfigured dependencies for $projectPath"
+
+    val project = projectPath.split(":")[1]
+
+//    Uncomment the following section if you want to create bugs for specific projects
+//    if(project !in flanProjects) {
+//        return@projectAdvice
+//    }
+
+    val description = StringBuilder()
+    description.appendLine(
+        "The dependency analysis gradle plugin found some dependencies that may have been " +
+            "misconfigured. Please fix the following dependencies: \n"
+    )
+
+    val unused = mutableSetOf<String>()
+    val transitive = mutableSetOf<String>()
+    val modified = mutableSetOf<String>()
+    projectAdvice.dependencyAdvice.forEach { dependencyAdvice ->
+        val fromConfiguration = dependencyAdvice.fromConfiguration
+        val toConfiguration = dependencyAdvice.toConfiguration
+        val coordinates = dependencyAdvice.coordinates
+        val resolvedVersion = coordinates.resolvedVersion
+
+        val isCompileOnly = toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true
+        val isModifyDependencyAdvice = fromConfiguration != null && toConfiguration != null
+        val isTransitiveDependencyAdvice = fromConfiguration == null && toConfiguration != null && !isCompileOnly
+        val isUnusedDependencyAdvice = fromConfiguration != null && toConfiguration == null
+
+        var identifier = if(resolvedVersion == null) {
+            "'${coordinates.identifier}'"
+        } else {
+            "'${coordinates.identifier}:${coordinates.resolvedVersion}'"
+        }
+        if (coordinates.type == "project") {
+            identifier = "project($identifier)"
+        }
+        if (isModifyDependencyAdvice) {
+            modified.add("$toConfiguration($identifier) (was $fromConfiguration)")
+        }
+        if(isTransitiveDependencyAdvice) {
+            transitive.add("$toConfiguration($identifier)")
+        }
+        if(isUnusedDependencyAdvice) {
+            unused.add("$fromConfiguration($identifier)")
+        }
+    }
+
+    if(unused.isNotEmpty()) {
+        description.appendLine("Unused dependencies which should be removed:")
+        description.appendLine("```")
+        description.appendLine(unused.sorted().joinToString(separator = "\n"))
+        description.appendLine("```")
+
+    }
+    if (transitive.isNotEmpty()) {
+        description.appendLine("These transitive dependencies can be declared directly:")
+        description.appendLine("```")
+        description.appendLine(transitive.sorted().joinToString(separator = "\n"))
+        description.appendLine("```")
+    }
+    if (modified.isNotEmpty()) {
+        description.appendLine(
+            "These dependencies can be modified to be as indicated. Please be careful " +
+                "while changing the type of dependencies since it can affect the consumers of " +
+                "this library. To learn more about the various dependency configurations, " +
+                "please visit: [dac]" +
+                "(https://developer.android.com/build/dependencies#dependency_configurations). " +
+                "Also check [Gradle's guide for dependency management]" +
+                "(https://docs.gradle.org/current/userguide/dependency_management.html).\n"
+        )
+        description.appendLine("```")
+        description.appendLine(modified.sorted().joinToString(separator = "\n"))
+        description.appendLine("```")
+    }
+
+    description.appendLine("To get more up-to-date project advice, please run:")
+    description.appendLine("```")
+    description.appendLine("./gradlew $projectPath:projectHealth")
+    description.appendLine("```")
+    val newColumns = listOf(
+        "NEW00000", // ID
+        "P2", // priority
+        title,
+        "=HYPERLINK(\"https://issuetracker.google.com/issues?q=status%3Aopen%20hotlistid%3A(5997499)\", \"Androidx misconfigured dependencies\")",
+        "", // Assignee: leave blank
+        "Assigned", // status
+        "Android > Android OS & Apps > Jetpack (androidx) > ${project.replaceFirstChar {
+            if (it.isLowerCase()) it.titlecase(
+                Locale.getDefault()
+            ) else it.toString()
+        }}",
+        "", // reporter: add your ldap here
+        description.toString()
+    )
+
+    if (projectAdvice.dependencyAdvice.isNotEmpty()) {
+        numProjects++
+        csvData.append(newColumns.joinToString(",") { data ->
+            "\"${data.replace("\"", "\"\"")}\""
+        })
+        csvData.append("\n")
+    }
+}
+
+csvOutputFile.appendText(csvData.toString())
+println("Wrote CSV output to ${csvOutputFile.path} for $numProjects projects")
+
+data class Advice(
+    val projectAdvice: List<ProjectAdvice>,
+)
+
+data class ProjectAdvice(
+    val projectPath: String,
+    val dependencyAdvice: List<DependencyAdvice>,
+    val pluginAdvice: List<PluginAdvice>,
+)
+
+data class DependencyAdvice(
+    val coordinates: Coordinates,
+    val fromConfiguration: String?,
+    val toConfiguration: String?
+)
+
+data class Coordinates(
+    val type: String,
+    val identifier: String,
+    val resolvedVersion: String?
+)
+
+data class PluginAdvice(
+    val redundantPlugin: String,
+    val reason: String
+)