1 /*
2  * Copyright (C) 2023 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 com.android.tools.metalava
18 
19 import java.io.ByteArrayOutputStream
20 import javax.inject.Inject
21 import org.gradle.api.DefaultTask
22 import org.gradle.api.Project
23 import org.gradle.api.Task
24 import org.gradle.api.artifacts.Configuration
25 import org.gradle.api.file.ConfigurableFileCollection
26 import org.gradle.api.file.FileCollection
27 import org.gradle.api.file.RegularFile
28 import org.gradle.api.model.ObjectFactory
29 import org.gradle.api.provider.Provider
30 import org.gradle.api.tasks.Classpath
31 import org.gradle.api.tasks.InputFiles
32 import org.gradle.api.tasks.OutputFiles
33 import org.gradle.api.tasks.PathSensitive
34 import org.gradle.api.tasks.PathSensitivity
35 import org.gradle.api.tasks.TaskAction
36 import org.gradle.process.ExecOperations
37 
38 /** Create a configuration that includes all the dependencies required to run ktfmt. */
Projectnull39 private fun Project.getKtfmtConfiguration(): Configuration {
40     return configurations.findByName("ktfmt")
41         ?: configurations.create("ktfmt") {
42             val dependency = project.dependencies.create("com.facebook:ktfmt:0.44")
43             it.dependencies.add(dependency)
44         }
45 }
46 
47 /** Creates two tasks for checking and formatting kotlin sources. */
Projectnull48 fun Project.configureKtfmt() {
49     tasks.register("ktCheck", KtfmtCheckTask::class.java) {
50         it.description = "Check Kotlin code style."
51         it.group = "Verification"
52         it.ktfmtClasspath.from(getKtfmtConfiguration())
53         it.cacheEvenIfNoOutputs()
54     }
55     tasks.register("ktFormat", KtfmtFormatTask::class.java) {
56         it.description = "Fix Kotlin code style deviations."
57         it.group = "formatting"
58         it.ktfmtClasspath.from(getKtfmtConfiguration())
59     }
60 }
61 
62 abstract class KtfmtBaseTask : DefaultTask() {
63     @get:Inject abstract val execOperations: ExecOperations
64 
65     @get:Classpath abstract val ktfmtClasspath: ConfigurableFileCollection
66 
67     @get:Inject abstract val objects: ObjectFactory
68 
69     private val shouldIncludeBuildSrc: Boolean = project.rootProject == project
70 
71     @[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
getInputFilesnull72     fun getInputFiles(): FileCollection {
73         var files =
74             objects.fileTree().setDir("src").apply { include("**/*.kt") } +
75                 objects.fileCollection().apply { from("build.gradle.kts") }
76         if (shouldIncludeBuildSrc) {
77             files += objects.fileTree().setDir("buildSrc/src").apply { include("**/*.kt") }
78         }
79         return files
80     }
81 
getArgsnull82     fun getArgs(dryRun: Boolean): List<String> {
83         return if (dryRun) {
84             listOf("--kotlinlang-style", "--dry-run") +
85                 getInputFiles().files.map { it.absolutePath }
86         } else {
87             listOf("--kotlinlang-style") + getInputFiles().files.map { it.absolutePath }
88         }
89     }
90 }
91 
92 /** A task that formats the Kotlin code. */
93 abstract class KtfmtFormatTask : KtfmtBaseTask() {
94     // Output needs to be defined for this task as it rewrites these files
95     @OutputFiles
getOutputFilesnull96     fun getOutputFiles(): FileCollection {
97         return getInputFiles()
98     }
99 
100     @TaskAction
doCheckingnull101     fun doChecking() {
102         execOperations.javaexec {
103             it.mainClass.set("com.facebook.ktfmt.cli.Main")
104             it.classpath = ktfmtClasspath
105             it.args = getArgs(dryRun = false)
106         }
107     }
108 }
109 
110 /** A task that checks of the Kotlin code passes formatting checks. */
111 abstract class KtfmtCheckTask : KtfmtBaseTask() {
112     @TaskAction
doCheckingnull113     fun doChecking() {
114         val outputStream = ByteArrayOutputStream()
115         execOperations.javaexec {
116             it.standardOutput = outputStream
117             it.mainClass.set("com.facebook.ktfmt.cli.Main")
118             it.classpath = ktfmtClasspath
119             it.args = getArgs(dryRun = true)
120         }
121         val output = outputStream.toString()
122         if (output.isNotEmpty()) {
123             throw Exception(
124                 """Failed check for the following files:
125                 |$output
126                 |
127                 |Run ./gradlew ktFormat to fix it."""
128                     .trimMargin()
129             )
130         }
131     }
132 }
133 
134 // Tells Gradle to skip running this task, even if this task declares no output files
Tasknull135 fun Task.cacheEvenIfNoOutputs() {
136     this.outputs.file(this.getPlaceholderOutput())
137 }
138 
139 // Returns a placeholder/unused output path that we can pass to Gradle to prevent Gradle from
140 // thinking that we forgot to declare outputs of this task, and instead to skip this task if its
141 // inputs are unchanged
Tasknull142 private fun Task.getPlaceholderOutput(): Provider<RegularFile> =
143     project.layout.buildDirectory.file("placeholderOutput/${name.replace(':', '-')}")
144