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