├── .gitignore ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── plugin-best-practices-plugin ├── src │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── autonomousapps │ │ │ ├── internal │ │ │ ├── utils │ │ │ │ ├── strings.kt │ │ │ │ ├── files.kt │ │ │ │ ├── collections.kt │ │ │ │ └── Json.kt │ │ │ ├── logging │ │ │ │ └── ConfigurableLogger.kt │ │ │ ├── graphs │ │ │ │ └── graphs.kt │ │ │ └── analysis │ │ │ │ ├── ClassAnalyzer.kt │ │ │ │ └── IssueListener.kt │ │ │ ├── logging │ │ │ └── LogLevel.kt │ │ │ ├── issue │ │ │ ├── Trace.kt │ │ │ ├── IssueRenderer.kt │ │ │ └── Issue.kt │ │ │ ├── task │ │ │ ├── CreateBaselineTask.kt │ │ │ ├── CheckBestPracticesTask.kt │ │ │ └── DetectBestPracticesViolationsTask.kt │ │ │ ├── GradleBestPracticesExtension.kt │ │ │ └── GradleBestPracticesPlugin.kt │ ├── test │ │ └── kotlin │ │ │ └── com │ │ │ └── autonomousapps │ │ │ └── GradleBestPracticesPluginTest.kt │ └── functionalTest │ │ └── groovy │ │ └── com │ │ └── autonomousapps │ │ ├── fixture │ │ ├── Runner.groovy │ │ └── SimplePluginProject.groovy │ │ └── FunctionalSpec.groovy └── build.gradle ├── gradle.properties ├── .gitattributes ├── CONTRIBUTING.md ├── RELEASING.md ├── .github └── workflows │ └── pr.yml ├── CHANGELOG.md ├── settings.gradle ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autonomousapps/gradle-best-practices-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/utils/strings.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.utils 2 | 3 | internal fun String.dotty(): String = replace('/', '.') 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError 2 | org.gradle.caching=true 3 | org.gradle.parallel=true 4 | 5 | dependency.analysis.autoapply=false 6 | dependency.analysis.print.build.health=true 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | ## Run the tests 4 | 5 | ```shell 6 | ./gradlew plugin-best-practices-plugin:check 7 | ``` 8 | 9 | ## Install to maven local 10 | 11 | ```shell 12 | ./gradlew plugin-best-practices-plugin:publishToMavenLocal 13 | ``` 14 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/logging/LogLevel.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.logging 2 | 3 | /** Used by [com.autonomousapps.GradleBestPracticesExtension] and tasks. */ 4 | @Suppress("EnumEntryName") // improved Gradle DSL support 5 | enum class LogLevel { 6 | default, 7 | reporting, 8 | debug 9 | } 10 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/utils/files.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.utils 2 | 3 | import org.gradle.api.file.RegularFileProperty 4 | import java.io.File 5 | 6 | /** Resolves the file from the property and deletes its contents, then returns the file. */ 7 | internal fun RegularFileProperty.getAndDelete(): File { 8 | val file = get().asFile 9 | file.delete() 10 | return file 11 | } 12 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/utils/collections.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.utils 2 | 3 | import org.gradle.api.file.FileCollection 4 | 5 | /** Filters a [FileCollection] to contain only class files (and not the module-info.class file). */ 6 | internal fun FileCollection.filterToClassFiles(): FileCollection { 7 | return filter { 8 | it.isFile && it.name.endsWith(".class") && it.name != "module-info.class" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/issue/Trace.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.issue 2 | 3 | import com.autonomousapps.internal.graphs.MethodNode 4 | 5 | /** 6 | * Code path to problematic method call. Never empty. See also [Issue]. 7 | */ 8 | data class Trace( 9 | /** Code path to problematic method call. Never empty. See also [Issue]. */ 10 | val trace: List 11 | ) { 12 | 13 | init { 14 | check(trace.size > 1) { "Trace must have at least two elements. Was $trace" } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/test/kotlin/com/autonomousapps/GradleBestPracticesPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.gradle.testfixtures.ProjectBuilder 5 | import org.junit.jupiter.api.Test 6 | 7 | class GradleBestPracticesPluginTest { 8 | 9 | @Test fun `plugin registers task`() { 10 | // Create a test project and apply the plugin 11 | val project = ProjectBuilder.builder().build() 12 | project.plugins.apply("java-gradle-plugin") 13 | project.plugins.apply("com.autonomousapps.plugin-best-practices-plugin") 14 | 15 | // Verify the result 16 | assertThat(project.tasks.findByName("checkBestPractices")).isNotNull() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/logging/ConfigurableLogger.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.logging 2 | 3 | import com.autonomousapps.logging.LogLevel 4 | import org.gradle.api.logging.Logger 5 | 6 | internal class ConfigurableLogger( 7 | private val delegate: Logger, 8 | private val level: LogLevel = LogLevel.default 9 | ) : Logger by delegate { 10 | 11 | fun report(msg: String) { 12 | if (level == LogLevel.reporting || level == LogLevel.debug) { 13 | delegate.quiet(msg) 14 | } else { 15 | delegate.debug(msg) 16 | } 17 | } 18 | 19 | override fun debug(msg: String) { 20 | if (level == LogLevel.debug) { 21 | delegate.quiet(msg) 22 | } else { 23 | delegate.debug(msg) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Release procedure 2 | 3 | 1. Update CHANGELOG. 4 | 2. Update README if needed. 5 | 3. Bump version number in `plugin-best-practices-plugin/build.gradle` to next stable version. 6 | 4. `git commit -am "Prepare for release x.y.z."`. 7 | 5. Publish: `./gradlew :plugin-best-practices-plugin:publishPlugins --no-configuration-cache` 8 | (this will automatically run all the tests, and won't publish if any fail). 9 | 6. `git tag -a vx.y.z -m "Version x.y.z".` 10 | 7. Update version number `build.gradle` to next snapshot version (x.y.z-SNAPSHOT). 11 | 8. `git commit -am "Prepare next development version."` 12 | 9. `git push && git push --tags` 13 | 14 | nb: if there are ever any issues with publishing to the Gradle Plugin Portal, open an issue on 15 | https://github.com/gradle/plugin-portal-requests/issues and email plugin-portal-support@gradle.com. -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '*.md' 7 | - '*.asciidoc' 8 | workflow_dispatch: 9 | inputs: 10 | reason: 11 | description: 'Reason for manual run' 12 | required: false 13 | 14 | concurrency: 15 | group: build-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | gradle: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Validate Gradle Wrapper 26 | uses: gradle/wrapper-validation-action@v1 27 | 28 | - name: Set up JDK 11 29 | uses: actions/setup-java@v3 30 | with: 31 | distribution: 'zulu' 32 | java-version: 11 33 | 34 | - name: Run tests 35 | uses: gradle/gradle-build-action@v2 36 | with: 37 | arguments: check -s 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Gradle Plugin Best Practices Plugin changelog 2 | 3 | # Version 0.10 4 | * [Fixed] Baseline was not respected. 5 | 6 | # Version 0.9 7 | * [New] Warn about using Eager APIs on TaskContainer. 8 | * [Fixed] Use moshi instead of kotlinx-serialization. 9 | 10 | # Version 0.8 11 | * Use Kotlin 1.7.20. 12 | 13 | # Version 0.7 14 | * [Fixed] Use kotlinx.serialization 1.3.3 instead of 1.4.0 (to sync on Kotlin 1.6.21). 15 | 16 | # Version 0.4, 0.5, 0.6 17 | * [New] New `gradleBestPractices` DSL with two configuration options: `logging` and `baseline`. See KDoc for more info. 18 | * Now using externally-published `com.autonomousapps:graph-support:0.1` library. 19 | 20 | # Version 0.3 21 | * Changed package `com.autonomousapps.internal.graph` to `com.autonomousapps.internal.graphs` to workaround classpath 22 | issue with dependency-analysis plugin. I should really publish that package as a standalone artifact... 23 | 24 | # Version 0.2 25 | * [New] Fail build if issues are discovered. 26 | 27 | # Version 0.1 28 | Initial release. 29 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.api.initialization.resolve.RepositoriesMode 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | } 8 | 9 | plugins { 10 | id 'com.gradle.enterprise' version '3.10.2' 11 | id 'com.gradle.plugin-publish' version '1.0.0' 12 | id 'com.autonomousapps.dependency-analysis' version '1.22.0' 13 | id 'com.autonomousapps.plugin-best-practices-plugin' version '0.9' 14 | id 'org.jetbrains.kotlin.jvm' version '1.9.0' 15 | } 16 | } 17 | 18 | plugins { 19 | id 'com.gradle.enterprise' 20 | } 21 | 22 | rootProject.name = 'gradle-best-practices' 23 | 24 | include 'plugin-best-practices-plugin' 25 | 26 | dependencyResolutionManagement { 27 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 28 | repositories { 29 | mavenCentral() 30 | } 31 | } 32 | 33 | gradleEnterprise { 34 | buildScan { 35 | publishAlways() 36 | termsOfServiceUrl = 'https://gradle.com/terms-of-service' 37 | termsOfServiceAgree = 'yes' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/task/CreateBaselineTask.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.task 2 | 3 | import com.autonomousapps.internal.utils.getAndDelete 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.file.RegularFileProperty 6 | import org.gradle.api.plugins.JavaBasePlugin 7 | import org.gradle.api.tasks.CacheableTask 8 | import org.gradle.api.tasks.InputFile 9 | import org.gradle.api.tasks.OutputFile 10 | import org.gradle.api.tasks.PathSensitive 11 | import org.gradle.api.tasks.PathSensitivity 12 | import org.gradle.api.tasks.TaskAction 13 | 14 | @CacheableTask 15 | abstract class CreateBaselineTask : DefaultTask() { 16 | 17 | init { 18 | group = JavaBasePlugin.VERIFICATION_GROUP 19 | description = "Generates baseline of best practices violations" 20 | } 21 | 22 | @get:PathSensitive(PathSensitivity.NONE) 23 | @get:InputFile 24 | abstract val report: RegularFileProperty 25 | 26 | @get:OutputFile 27 | abstract val baseline: RegularFileProperty 28 | 29 | @TaskAction 30 | fun action() { 31 | val baseline = baseline.getAndDelete() 32 | report.get().asFile.copyTo(baseline, overwrite = true) 33 | } 34 | } -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/issue/IssueRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.issue 2 | 3 | import com.autonomousapps.internal.graphs.MethodNode 4 | import com.autonomousapps.internal.utils.dotty 5 | 6 | internal object IssueRenderer { 7 | 8 | fun renderIssue(issue: Issue, pretty: Boolean = false): String { 9 | return if (pretty) { 10 | renderTracePretty(issue.trace) 11 | } else { 12 | renderTrace(issue.trace) 13 | } 14 | } 15 | 16 | private fun renderTracePretty(trace: Trace) = buildString { 17 | val nodes = trace.trace 18 | append(renderMethodNode(nodes.first())) 19 | appendLine(" ->") 20 | 21 | for (i in 1 until nodes.size) { 22 | append(" ") 23 | append(renderMethodNode(nodes[i])) 24 | if (i < nodes.size - 1) { 25 | appendLine(" ->") 26 | } 27 | } 28 | } 29 | 30 | private fun renderTrace(trace: Trace): String { 31 | return trace.trace.joinToString(separator = " -> ") { renderMethodNode(it) } 32 | } 33 | 34 | private fun renderMethodNode(node: MethodNode) = buildString { 35 | with(node) { 36 | append(owner) 37 | append("#") 38 | append(name) 39 | append(descriptor) 40 | } 41 | }.dotty() 42 | } -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/utils/Json.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.utils 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.Types 6 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 7 | import dev.zacsweers.moshix.sealed.reflect.MetadataMoshiSealedJsonAdapterFactory 8 | 9 | internal object Json { 10 | private val MOSHI: Moshi by lazy { 11 | Moshi.Builder() 12 | .add(MetadataMoshiSealedJsonAdapterFactory()) 13 | .addLast(KotlinJsonAdapterFactory()) 14 | .build() 15 | } 16 | 17 | private inline fun getJsonAdapter(): JsonAdapter { 18 | return MOSHI.adapter(T::class.java) 19 | } 20 | 21 | private inline fun getJsonListAdapter(): JsonAdapter> { 22 | val type = Types.newParameterizedType(List::class.java, T::class.java) 23 | return MOSHI.adapter(type) 24 | } 25 | 26 | inline fun T.toJson(): String { 27 | return getJsonAdapter().toJson(this) 28 | } 29 | 30 | private inline fun T.toPrettyString(): String { 31 | return getJsonAdapter().indent(" ").toJson(this) 32 | } 33 | 34 | inline fun String.fromJson(): T { 35 | return getJsonAdapter().fromJson(this)!! 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/functionalTest/groovy/com/autonomousapps/fixture/Runner.groovy: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.fixture 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.GradleRunner 5 | 6 | import javax.naming.OperationNotSupportedException 7 | import java.lang.management.ManagementFactory 8 | import java.nio.file.Path 9 | 10 | final class Runner { 11 | 12 | private Runner() { 13 | throw new OperationNotSupportedException() 14 | } 15 | 16 | static BuildResult build( 17 | Path projectDir, 18 | String... args 19 | ) { 20 | return GradleRunner.create() 21 | .withPluginClasspath() 22 | .forwardOutput() 23 | .withProjectDir(projectDir.toFile()) 24 | .withArguments(*args, '-s') 25 | // Ensure this value is true when `--debug-jvm` is passed to Gradle, and false otherwise 26 | .withDebug(ManagementFactory.getRuntimeMXBean().inputArguments.toString().indexOf('-agentlib:jdwp') > 0) 27 | .build() 28 | } 29 | 30 | static BuildResult buildAndFail( 31 | Path projectDir, 32 | String... args 33 | ) { 34 | return GradleRunner.create() 35 | .withPluginClasspath() 36 | .forwardOutput() 37 | .withProjectDir(projectDir.toFile()) 38 | .withArguments(*args, '-s') 39 | // Ensure this value is true when `--debug-jvm` is passed to Gradle, and false otherwise 40 | .withDebug(ManagementFactory.getRuntimeMXBean().inputArguments.toString().indexOf('-agentlib:jdwp') > 0) 41 | .buildAndFail() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/issue/Issue.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.issue 2 | 3 | import com.squareup.moshi.JsonClass 4 | import dev.zacsweers.moshix.sealed.annotations.TypeLabel 5 | 6 | class IssuesReport(val issues: List) 7 | 8 | @JsonClass(generateAdapter = false, generator = "sealed:type") 9 | sealed class Issue { 10 | abstract val name: String 11 | abstract val trace: Trace 12 | } 13 | 14 | @TypeLabel("subprojects") 15 | @JsonClass(generateAdapter = false) 16 | data class SubprojectsIssue( 17 | override val name: String, 18 | override val trace: Trace 19 | ) : Issue() 20 | 21 | @TypeLabel("get_subprojects") 22 | @JsonClass(generateAdapter = false) 23 | data class GetSubprojectsIssue( 24 | override val name: String, 25 | override val trace: Trace 26 | ) : Issue() 27 | 28 | @TypeLabel("allprojects") 29 | @JsonClass(generateAdapter = false) 30 | data class AllprojectsIssue( 31 | override val name: String, 32 | override val trace: Trace 33 | ) : Issue() 34 | 35 | @TypeLabel("get_allprojects") 36 | @JsonClass(generateAdapter = false) 37 | data class GetAllprojectsIssue( 38 | override val name: String, 39 | override val trace: Trace 40 | ) : Issue() 41 | 42 | @TypeLabel("get_project") 43 | @JsonClass(generateAdapter = false) 44 | data class GetProjectInTaskActionIssue( 45 | override val name: String, 46 | override val trace: Trace 47 | ) : Issue() 48 | 49 | @TypeLabel("eager_api") 50 | @JsonClass(generateAdapter = false) 51 | data class EagerApiIssue( 52 | override val name: String, 53 | override val trace: Trace 54 | ) : Issue() 55 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/graphs/graphs.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.graphs 2 | 3 | internal class Class( 4 | val name: String, 5 | val superName: String? 6 | ) 7 | 8 | internal class Method( 9 | val name: String, 10 | val descriptor: String 11 | ) 12 | 13 | data class MethodNode( 14 | val owner: String, 15 | val name: String, 16 | val descriptor: String, 17 | val metadata: Metadata = Metadata.EMPTY 18 | ) { 19 | 20 | data class Metadata( 21 | /** The associated [MethodNode] is annotated with `@TaskAction`. */ 22 | val isTaskAction: Boolean = false, 23 | 24 | /** The associated [MethodNode] does not really exist. It exists for algorithmic purposes only. */ 25 | val isVirtual: Boolean = false, 26 | ) { 27 | companion object { 28 | val EMPTY = Metadata() 29 | } 30 | } 31 | 32 | fun withVirtualOwner(owner: String) = copy( 33 | owner = owner, 34 | metadata = metadata.copy(isVirtual = true) 35 | ) 36 | 37 | fun signatureMatches(other: MethodNode): Boolean { 38 | return name == other.name && descriptor == other.descriptor 39 | } 40 | 41 | /* 42 | * Custom equals and hashCode because we don't want to include Metadata in the calculation. 43 | */ 44 | 45 | override fun equals(other: Any?): Boolean { 46 | if (this === other) return true 47 | if (javaClass != other?.javaClass) return false 48 | 49 | other as MethodNode 50 | 51 | if (owner != other.owner) return false 52 | if (name != other.name) return false 53 | if (descriptor != other.descriptor) return false 54 | 55 | return true 56 | } 57 | 58 | override fun hashCode(): Int { 59 | var result = owner.hashCode() 60 | result = 31 * result + name.hashCode() 61 | result = 31 * result + descriptor.hashCode() 62 | return result 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/GradleBestPracticesExtension.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps 2 | 3 | import com.autonomousapps.logging.LogLevel 4 | import org.gradle.api.Project 5 | import org.gradle.api.file.ProjectLayout 6 | import org.gradle.api.file.RegularFile 7 | import org.gradle.api.model.ObjectFactory 8 | import org.gradle.api.provider.Provider 9 | import java.io.File 10 | import javax.inject.Inject 11 | 12 | /** 13 | * ``` 14 | * gradleBestPractices { 15 | * // Use this to make it easier to incrementally resolve issues. 16 | * // This method accepts a `String`, a `File`, a `RegularFile`, or a `Provider`. 17 | * // default is "$projectDir/best-practices-baseline.json" 18 | * baseline '' 19 | * 20 | * // default is 'default' 21 | * logging '' 22 | * } 23 | * ``` 24 | */ 25 | @Suppress("unused") // intentional API 26 | open class GradleBestPracticesExtension @Inject constructor( 27 | private val layout: ProjectLayout, 28 | objects: ObjectFactory 29 | ) { 30 | 31 | internal val baseline = objects.fileProperty().convention(layout.projectDirectory.file("best-practices-baseline.json")) 32 | internal val level = objects.property(LogLevel::class.java).convention(LogLevel.default) 33 | 34 | fun baseline(baseline: Provider) { 35 | this.baseline.set(baseline) 36 | } 37 | 38 | fun baseline(baseline: RegularFile) { 39 | this.baseline.set(baseline) 40 | } 41 | 42 | fun baseline(baseline: File) { 43 | this.baseline.set(baseline) 44 | } 45 | 46 | fun baseline(baseline: String) { 47 | this.baseline.set(layout.projectDirectory.file(baseline)) 48 | } 49 | 50 | /** 51 | * 1. 'reporting' will emit the report to console (if there are issues). 52 | * 1. 'debug' will print debug information from the bytecode analysis. 53 | */ 54 | fun logging(level: LogLevel) { 55 | this.level.set(level) 56 | } 57 | 58 | internal companion object { 59 | 60 | internal fun create(project: Project) = project.extensions.create( 61 | "gradleBestPractices", 62 | GradleBestPracticesExtension::class.java, 63 | project.layout, 64 | project.objects 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/functionalTest/groovy/com/autonomousapps/FunctionalSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.autonomousapps 2 | 3 | import com.autonomousapps.fixture.SimplePluginProject 4 | import spock.lang.Specification 5 | import spock.lang.TempDir 6 | 7 | import java.nio.file.Path 8 | 9 | import static com.autonomousapps.fixture.Runner.build 10 | import static com.autonomousapps.fixture.Runner.buildAndFail 11 | 12 | final class FunctionalSpec extends Specification { 13 | 14 | @TempDir 15 | Path tempDir 16 | 17 | def "can check best practices with 'checkBestPractices' task"() { 18 | given: 19 | def project = new SimplePluginProject(tempDir, 'reporting') 20 | 21 | when: 22 | def result = buildAndFail(project.root, 'checkBestPractices') 23 | 24 | then: 'Console output matches expected value' 25 | result.output.contains project.expectedConsoleOutput 26 | 27 | and: 'File version of console output matches expected value' 28 | project.consoleReport.text.trim() == project.expectedConsoleReport.trim() 29 | 30 | and: 'Json report matches expected value' 31 | project.jsonReport.text.trim() == project.expectedJsonReport.trim() 32 | } 33 | 34 | // Same as above, but using `check` lifecycle task instead. 35 | def "can check best practices with 'check' task"() { 36 | given: 37 | def project = new SimplePluginProject(tempDir) 38 | 39 | when: 40 | buildAndFail(project.root, 'check') 41 | 42 | then: 43 | project.consoleReport.text.trim() == project.expectedConsoleReport.trim() 44 | } 45 | 46 | def "can create best practices baseline"() { 47 | given: 48 | def project = new SimplePluginProject(tempDir) 49 | 50 | when: 51 | build(project.root, 'bestPracticesBaseline') 52 | 53 | then: 54 | project.baselineReport.text.trim() == project.expectedBaseline.trim() 55 | } 56 | 57 | 58 | def "respects baseline"() { 59 | given: 60 | def project = new SimplePluginProject(tempDir) 61 | 62 | when: 'Generate the baseline and then check best practices' 63 | build(project.root, 'bestPracticesBaseline') 64 | build(project.root, 'checkBestPractices') 65 | 66 | then: "Build doesn't fail" 67 | project.baselineReport.text.trim() == project.expectedBaseline.trim() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/analysis/ClassAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.internal.analysis 2 | 3 | import com.autonomousapps.internal.asm.AnnotationVisitor 4 | import com.autonomousapps.internal.asm.ClassVisitor 5 | import com.autonomousapps.internal.asm.MethodVisitor 6 | import com.autonomousapps.internal.asm.Opcodes 7 | import org.gradle.api.logging.Logger 8 | 9 | private const val ASM_VERSION = Opcodes.ASM9 10 | 11 | internal class ClassAnalyzer( 12 | private val listener: IssueListener, 13 | private val logger: Logger, 14 | ) : ClassVisitor(ASM_VERSION) { 15 | 16 | override fun visit( 17 | version: Int, 18 | access: Int, 19 | name: String, 20 | signature: String?, 21 | superName: String?, 22 | interfaces: Array? 23 | ) { 24 | logger.debug("ClassAnalyzer#visit: $name super=$superName") 25 | listener.visitClass(name, superName, interfaces?.toList() ?: emptyList()) 26 | } 27 | 28 | override fun visitMethod( 29 | access: Int, 30 | name: String, 31 | descriptor: String, 32 | signature: String?, 33 | exceptions: Array? 34 | ): MethodVisitor { 35 | logger.debug("- visitMethod: name=$name descriptor=$descriptor signature=$signature access=$access") 36 | 37 | listener.visitMethod(name, descriptor) 38 | return MethodAnalyzer(logger, listener) 39 | } 40 | 41 | internal class MethodAnalyzer( 42 | private val logger: Logger, 43 | private val listener: IssueListener, 44 | ) : MethodVisitor(ASM_VERSION) { 45 | 46 | override fun visitEnd() { 47 | listener.visitMethodEnd() 48 | } 49 | 50 | override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor? { 51 | logger.debug(" - visitAnnotation: descriptor=$descriptor visible=$visible") 52 | listener.visitMethodAnnotation(descriptor) 53 | return null 54 | } 55 | 56 | override fun visitMethodInsn( 57 | opcode: Int, 58 | owner: String, 59 | name: String, 60 | descriptor: String, 61 | isInterface: Boolean 62 | ) { 63 | logger.debug(" - visitMethodInsn: owner=$owner name=$name descriptor=$descriptor opcode=$opcode") 64 | listener.visitMethodInstruction(owner, name, descriptor) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | java = "11" 3 | junit = "5.8.2" 4 | kotlin = "1.6.10" 5 | moshi = "1.14.0" 6 | moshix = "0.23.0" 7 | retrofit = "2.9.0" 8 | 9 | [libraries] 10 | guava = "com.google.guava:guava:31.1-jre" 11 | 12 | javax-inject = "javax.inject:javax.inject:1" 13 | 14 | kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } 15 | kotlin-dokka = { module = "org.jetbrains.dokka:kotlin-as-java-plugin", version.ref = "kotlin" } 16 | kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 17 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 18 | kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } 19 | kotlinx-metadata-jvm = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.4.2" 20 | 21 | moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } 22 | moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } 23 | moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } 24 | 25 | moshix-ksp = { module = "dev.zacsweers.moshix:moshi-ksp", version.ref = "moshix" } 26 | moshix-sealed-codegen = { module = "dev.zacsweers.moshix:moshi-sealed-codegen", version.ref = "moshix" } 27 | moshix-sealed-reflect = { module = "dev.zacsweers.moshix:moshi-sealed-metadata-reflect", version.ref = "moshix" } 28 | moshix-sealed-runtime = { module = "dev.zacsweers.moshix:moshi-sealed-runtime", version.ref = "moshix" } 29 | 30 | okhttp3 = "com.squareup.okhttp3:okhttp:4.9.0" 31 | 32 | relocated-antlr = "com.autonomousapps:antlr:4.10.1.2" 33 | relocated-asm = "com.autonomousapps:asm-relocated:9.4.0.1" 34 | 35 | retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 36 | retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } 37 | 38 | # tests 39 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } 40 | junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } 41 | junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } 42 | junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } 43 | mockito-kotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" 44 | spock = "org.spockframework:spock-core:2.1-groovy-3.0" 45 | truth = "com.google.truth:truth:1.1.3" 46 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/GradleBestPracticesPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps 2 | 3 | import com.autonomousapps.task.CheckBestPracticesTask 4 | import com.autonomousapps.task.CreateBaselineTask 5 | import com.autonomousapps.task.DetectBestPracticesViolationsTask 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.tasks.SourceSet 9 | import org.gradle.api.tasks.SourceSetContainer 10 | 11 | @Suppress("unused") 12 | class GradleBestPracticesPlugin : Plugin { 13 | 14 | override fun apply(project: Project): Unit = project.run { 15 | pluginManager.withPlugin("java-gradle-plugin") { 16 | val extension = GradleBestPracticesExtension.create(this) 17 | 18 | val mainOutput = extensions.getByType(SourceSetContainer::class.java) 19 | .findByName(SourceSet.MAIN_SOURCE_SET_NAME) 20 | ?.output 21 | ?.classesDirs 22 | ?: files() 23 | 24 | val baselineTask = tasks.register("bestPracticesBaseline", CreateBaselineTask::class.java) 25 | 26 | // A RegularFileProperty is allowed to wrap a nullable RegularFile 27 | @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") 28 | val detectViolationsTask = tasks.register("detectViolations", DetectBestPracticesViolationsTask::class.java) { 29 | with(it) { 30 | classesDirs.setFrom(mainOutput) 31 | baseline.set(extension.baseline.map { f -> 32 | if (f.asFile.exists()) f else null 33 | }) 34 | logLevel.set(extension.level) 35 | outputJson.set(layout.buildDirectory.file("reports/best-practices/report.json")) 36 | outputText.set(layout.buildDirectory.file("reports/best-practices/report.txt")) 37 | } 38 | } 39 | 40 | // A RegularFileProperty is allowed to wrap a nullable RegularFile 41 | @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") 42 | val checkBestPracticesTask = tasks.register("checkBestPractices", CheckBestPracticesTask::class.java) { t -> 43 | with(t) { 44 | reportJson.set(detectViolationsTask.flatMap { it.outputJson }) 45 | reportText.set(detectViolationsTask.flatMap { it.outputText }) 46 | baseline.set(extension.baseline.map { f -> 47 | if (f.asFile.exists()) f else null 48 | }) 49 | logLevel.set(extension.level) 50 | projectPath.set(project.path) 51 | } 52 | } 53 | 54 | baselineTask.configure { t -> 55 | with(t) { 56 | baseline.set(extension.baseline) 57 | report.set(detectViolationsTask.flatMap { it.outputJson }) 58 | } 59 | } 60 | 61 | tasks.named("check").configure { 62 | it.dependsOn(checkBestPracticesTask) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-gradle-plugin' 3 | id 'org.jetbrains.kotlin.jvm' 4 | id 'groovy' 5 | id 'maven-publish' 6 | id 'com.gradle.plugin-publish' 7 | id 'com.autonomousapps.plugin-best-practices-plugin' 8 | id 'com.autonomousapps.dependency-analysis' 9 | } 10 | 11 | group = 'com.autonomousapps' 12 | version = '0.11-SNAPSHOT' 13 | 14 | java { 15 | toolchain { 16 | languageVersion.set(JavaLanguageVersion.of(11)) 17 | } 18 | withJavadocJar() 19 | withSourcesJar() 20 | } 21 | 22 | dependencies { 23 | api(libs.moshi.core) 24 | api(libs.moshix.sealed.runtime) 25 | 26 | implementation platform(libs.kotlin.bom) 27 | implementation(libs.kotlin.stdlib.jdk8) 28 | implementation(libs.moshi.kotlin) 29 | implementation(libs.moshix.sealed.reflect) 30 | implementation(libs.relocated.asm) { 31 | because 'Bytecode analysis' 32 | } 33 | implementation('com.autonomousapps:graph-support:0.1') { 34 | because 'Graphs' 35 | } 36 | implementation(libs.guava) { 37 | because 'Graphs' 38 | } 39 | } 40 | 41 | dependencyAnalysis { 42 | issues { 43 | onAny { 44 | severity 'fail' 45 | } 46 | onIncorrectConfiguration { 47 | exclude( 48 | // DAGP thinks this should be on `functionalTestApi`, which might be technically correct but also makes no sense 49 | 'org.spockframework:spock-core', 50 | ) 51 | } 52 | onUsedTransitiveDependencies { 53 | exclude( 54 | // DAGP thinks this should be on `functionalTestApi`, which might be technically correct but also makes no sense 55 | 'org.codehaus.groovy:groovy', 56 | 'org.junit.jupiter:junit-jupiter-api', 57 | ) 58 | } 59 | } 60 | } 61 | 62 | testing { 63 | suites { 64 | // Configure the built-in test suite 65 | test { 66 | useJUnitJupiter() 67 | dependencies { 68 | implementation 'com.google.truth:truth:1.1.3' 69 | } 70 | } 71 | 72 | // Create a new test suite 73 | functionalTest(JvmTestSuite) { 74 | // Use Kotlin Test test framework 75 | useSpock() 76 | 77 | targets { 78 | all { 79 | // This test suite should run after the built-in test suite has run its tests 80 | testTask.configure { shouldRunAfter(test) } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | gradlePlugin { 88 | plugins { 89 | plugin { 90 | id = 'com.autonomousapps.plugin-best-practices-plugin' 91 | implementationClass = 'com.autonomousapps.GradleBestPracticesPlugin' 92 | displayName = 'Gradle Best Practices Plugin' 93 | description = 'Gradle Plugin that detects violations of Gradle best practices in Gradle Plugins' 94 | } 95 | } 96 | } 97 | 98 | gradlePlugin.testSourceSets(sourceSets.functionalTest) 99 | 100 | def check = tasks.named('check') { 101 | dependsOn(testing.suites.functionalTest) 102 | } 103 | 104 | tasks.named('publishPlugins') { 105 | dependsOn(check) 106 | } 107 | 108 | pluginBundle { 109 | website = 'https://github.com/autonomousapps/gradle-best-practices-plugin' 110 | vcsUrl = 'https://github.com/autonomousapps/gradle-best-practices-plugin' 111 | tags = ['best practices'] 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle Best Practices Plugin 2 | 3 | ## Add to your project 4 | 5 | ```gradle 6 | // build.gradle[.kts] 7 | plugins { 8 | id("com.autonomousapps.plugin-best-practices-plugin") version "<>" 9 | } 10 | ``` 11 | 12 | ## Use it 13 | 14 | ```shell 15 | ./gradlew :plugin:checkBestPractices 16 | ``` 17 | 18 | Where `:plugin` is the name of your plugin project. 19 | 20 | Or, since the `checkBestPractices` task is automatically added as a dependency of the `check` task: 21 | 22 | ```shell 23 | ./gradlew :plugin:check 24 | ``` 25 | 26 | ## Example results 27 | 28 | The `checkBestPractices` task may print a report such as the following: 29 | 30 | ```groovy 31 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 32 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 33 | org.gradle.api.Project#allprojects(Lorg.gradle.api.Action;)V 34 | 35 | com.test.FancyTask#action()V -> 36 | com.test.FancyTask#doAction()V -> 37 | com.test.FancyTask$ReallyFancyTask#doAction()V -> 38 | com.test.FancyTask$ReallyFancyTask#getProject()Lorg.gradle.api.Project; 39 | ``` 40 | 41 | This indicates that your plugin is calling `Project#allprojects()`, which violates best practices no matter the context; 42 | and also that it calls `Task#getProject()`, which violates best practices when called from the context of a method 43 | annotated with `@TaskAction`. 44 | 45 | ## Baselines 46 | 47 | In case there are many best practice violations in a project, it's worth generating a baseline to temporarily accept issues, and to prevent new ones from getting onto the main branch. 48 | 49 | To generate a baseline run the `bestPracticesBaseline` task: 50 | ```shell 51 | ./gradlew :plugin:bestPracticesBaseline 52 | ``` 53 | 54 | This will generate a file called: `best-practices-baseline.json` in the project directory. 55 | Version control this file, so gets propagated to CI and every developer. 56 | Future executions of `checkBestPractices` task will take this baseline into account and won't fail on recorded violations. 57 | 58 | ## Summary of issues currently detected 59 | 60 | ### Instances of cross-project configuration 61 | 62 | This is dangerous for a variety of reasons. It defeats configuration on demand and will be impermissible in the future 63 | when Gradle implements [project isolation](https://gradle.github.io/configuration-cache/#project_isolation). In the 64 | present, these APIs permit mutation of other projects, and this kind of cross-project configuration can easily lead to 65 | unmaintainable builds. 66 | 67 | 1. Any usage of `Project#allprojects()`. 68 | 2. Any usage of `Project#getAllprojects()`. 69 | 3. Any usage of `Project#subprojects()`. 70 | 4. Any usage of `Project#getSubprojects()`. 71 | 72 | ### Usages of a `Project` instance from a task action 73 | 74 | This will break the [configuration cache](https://docs.gradle.org/nightly/userguide/configuration_cache.html), since 75 | `Project`s cannot be serialized. 76 | 77 | 1. Usages of `getProject()` in the context of a method annotated with `@TaskAction`. 78 | 79 | ### Usages of eager APIs instead of lazy ones on the TaskContainer interface 80 | 81 | Lazy APIs delay the realization of tasks until they're actually required, which avoids doing intensive work 82 | during the configuration phase since it can have a large impact on build performance. 83 | Read more [here](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html#sec:old_vs_new_configuration_api_overview) 84 | 85 | 1. Any usage of `TaskContainer#all`. Use `configureEach` instead. 86 | 2. Any usage of `TaskContainer#create`. Use `register` instead. 87 | 3. Any usage of `TaskContainer#getByName`. Use `named` instead. 88 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/task/CheckBestPracticesTask.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.task 2 | 3 | import com.autonomousapps.internal.logging.ConfigurableLogger 4 | import com.autonomousapps.internal.utils.Json.fromJson 5 | import com.autonomousapps.issue.IssuesReport 6 | import com.autonomousapps.logging.LogLevel 7 | import org.gradle.api.DefaultTask 8 | import org.gradle.api.GradleException 9 | import org.gradle.api.file.RegularFileProperty 10 | import org.gradle.api.logging.Logging 11 | import org.gradle.api.plugins.JavaBasePlugin 12 | import org.gradle.api.provider.Property 13 | import org.gradle.api.tasks.Input 14 | import org.gradle.api.tasks.InputFile 15 | import org.gradle.api.tasks.Optional 16 | import org.gradle.api.tasks.PathSensitive 17 | import org.gradle.api.tasks.PathSensitivity 18 | import org.gradle.api.tasks.TaskAction 19 | import org.gradle.workers.WorkAction 20 | import org.gradle.workers.WorkParameters 21 | import org.gradle.workers.WorkerExecutor 22 | import javax.inject.Inject 23 | 24 | abstract class CheckBestPracticesTask @Inject constructor( 25 | private val workerExecutor: WorkerExecutor 26 | ) : DefaultTask() { 27 | 28 | init { 29 | group = JavaBasePlugin.VERIFICATION_GROUP 30 | description = "Checks for violations of Gradle plugin best practices" 31 | } 32 | 33 | @get:PathSensitive(PathSensitivity.NONE) 34 | @get:InputFile 35 | abstract val reportJson: RegularFileProperty 36 | 37 | @get:PathSensitive(PathSensitivity.NONE) 38 | @get:InputFile 39 | abstract val reportText: RegularFileProperty 40 | 41 | @get:Optional 42 | @get:PathSensitive(PathSensitivity.NONE) 43 | @get:InputFile 44 | abstract val baseline: RegularFileProperty 45 | 46 | @get:Input 47 | abstract val projectPath: Property 48 | 49 | @get:Input 50 | abstract val logLevel: Property 51 | 52 | @TaskAction 53 | fun action() { 54 | workerExecutor.noIsolation().submit(Action::class.java) { 55 | it.reportJson.set(this@CheckBestPracticesTask.reportJson) 56 | it.reportText.set(this@CheckBestPracticesTask.reportText) 57 | it.baseline.set(this@CheckBestPracticesTask.baseline) 58 | it.projectPath.set(this@CheckBestPracticesTask.projectPath) 59 | it.logLevel.set(this@CheckBestPracticesTask.logLevel) 60 | } 61 | } 62 | 63 | interface Parameters : WorkParameters { 64 | val reportJson: RegularFileProperty 65 | val reportText: RegularFileProperty 66 | val baseline: RegularFileProperty 67 | val projectPath: Property 68 | val logLevel: Property 69 | } 70 | 71 | abstract class Action : WorkAction { 72 | 73 | private val logger = Logging.getLogger(CheckBestPracticesTask::class.java.simpleName).run { 74 | ConfigurableLogger(this, parameters.logLevel.get()) 75 | } 76 | 77 | override fun execute() { 78 | // The project path, unless it's ":", in which case use an empty string 79 | val projectPath = parameters.projectPath.get().takeUnless { it == ":" } ?: "" 80 | val baselineFixText = "`./gradlew $projectPath:bestPracticesBaseline`" 81 | 82 | val report = parameters.reportJson.get().asFile 83 | val issues = report.readText().fromJson().issues 84 | val text = parameters.reportText.get().asFile.readText() 85 | // Get baseline, if it exists. 86 | val baseline = parameters.baseline.orNull?.asFile?.readText()?.fromJson()?.issues.orEmpty() 87 | 88 | // If we have a baseline, the behavior changes. 89 | // If we find an issue that isn't in the baseline, it's a new issue. 90 | val newIssues = issues.filter { it !in baseline } 91 | // any issue in the baseline that ISN'T also in the list of current issues has been fixed. 92 | val fixedIssues = baseline.filter { it !in issues } 93 | val hasNewIssues = newIssues.isNotEmpty() 94 | val hasFixedIssues = fixedIssues.isNotEmpty() 95 | 96 | // Optionally print to console and throw exception. 97 | if (issues.isNotEmpty()) { 98 | logger.report(text) 99 | 100 | if (baseline.isEmpty() || hasNewIssues) { 101 | val errorText = buildString { 102 | appendLine("Violations of best practices detected. See the report at ${report.absolutePath} ") 103 | appendLine() 104 | appendLine("To create or update the baseline, run $baselineFixText") 105 | } 106 | throw GradleException(errorText) 107 | } 108 | } 109 | 110 | // Users should maintain their baselines. 111 | if (hasFixedIssues) { 112 | throw GradleException("Your baseline contains resolved issues. Update with $baselineFixText") 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/task/DetectBestPracticesViolationsTask.kt: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.task 2 | 3 | import com.autonomousapps.internal.analysis.AllProjectsListener 4 | import com.autonomousapps.internal.analysis.ClassAnalyzer 5 | import com.autonomousapps.internal.analysis.CompositeIssueListener 6 | import com.autonomousapps.internal.analysis.EagerApisListener 7 | import com.autonomousapps.internal.analysis.GetAllprojectsListener 8 | import com.autonomousapps.internal.analysis.GetProjectListener 9 | import com.autonomousapps.internal.analysis.GetSubprojectsListener 10 | import com.autonomousapps.internal.analysis.IssueListener 11 | import com.autonomousapps.internal.analysis.SubprojectsListener 12 | import com.autonomousapps.internal.asm.ClassReader 13 | import com.autonomousapps.internal.logging.ConfigurableLogger 14 | import com.autonomousapps.internal.utils.Json.fromJson 15 | import com.autonomousapps.internal.utils.Json.toJson 16 | import com.autonomousapps.internal.utils.filterToClassFiles 17 | import com.autonomousapps.internal.utils.getAndDelete 18 | import com.autonomousapps.issue.IssueRenderer 19 | import com.autonomousapps.issue.IssuesReport 20 | import com.autonomousapps.logging.LogLevel 21 | import org.gradle.api.DefaultTask 22 | import org.gradle.api.file.ConfigurableFileCollection 23 | import org.gradle.api.file.RegularFileProperty 24 | import org.gradle.api.logging.Logging 25 | import org.gradle.api.provider.Property 26 | import org.gradle.api.tasks.CacheableTask 27 | import org.gradle.api.tasks.Input 28 | import org.gradle.api.tasks.InputFile 29 | import org.gradle.api.tasks.InputFiles 30 | import org.gradle.api.tasks.Optional 31 | import org.gradle.api.tasks.OutputFile 32 | import org.gradle.api.tasks.PathSensitive 33 | import org.gradle.api.tasks.PathSensitivity 34 | import org.gradle.api.tasks.TaskAction 35 | import org.gradle.workers.WorkAction 36 | import org.gradle.workers.WorkParameters 37 | import org.gradle.workers.WorkerExecutor 38 | import javax.inject.Inject 39 | 40 | @CacheableTask 41 | abstract class DetectBestPracticesViolationsTask @Inject constructor( 42 | private val workerExecutor: WorkerExecutor 43 | ) : DefaultTask() { 44 | 45 | @get:PathSensitive(PathSensitivity.NONE) 46 | @get:InputFiles 47 | abstract val classesDirs: ConfigurableFileCollection 48 | 49 | @get:Optional 50 | @get:PathSensitive(PathSensitivity.RELATIVE) 51 | @get:InputFile 52 | abstract val baseline: RegularFileProperty 53 | 54 | @get:Input 55 | abstract val logLevel: Property 56 | 57 | @get:OutputFile 58 | abstract val outputJson: RegularFileProperty 59 | 60 | @get:OutputFile 61 | abstract val outputText: RegularFileProperty 62 | 63 | @TaskAction 64 | fun action() { 65 | workerExecutor.noIsolation().submit(Action::class.java) { 66 | it.classesDirs.setFrom(classesDirs) 67 | it.baseline.set(baseline) 68 | it.logLevel.set(logLevel) 69 | it.outputJson.set(outputJson) 70 | it.outputText.set(outputText) 71 | } 72 | } 73 | 74 | interface Parameters : WorkParameters { 75 | val classesDirs: ConfigurableFileCollection 76 | val baseline: RegularFileProperty 77 | val logLevel: Property 78 | val outputJson: RegularFileProperty 79 | val outputText: RegularFileProperty 80 | } 81 | 82 | abstract class Action : WorkAction { 83 | 84 | private val logger = Logging.getLogger(DetectBestPracticesViolationsTask::class.java.simpleName).run { 85 | ConfigurableLogger(this, parameters.logLevel.get()) 86 | } 87 | 88 | override fun execute() { 89 | val outputJson = parameters.outputJson.getAndDelete() 90 | val outputText = parameters.outputText.getAndDelete() 91 | 92 | val classFiles = parameters.classesDirs.asFileTree.filterToClassFiles().files 93 | logger.debug("classFiles=${classFiles.joinToString(prefix = "[", postfix = "]")}") 94 | 95 | val issueListener = compositeListener() 96 | 97 | // Visit every class file. Extract information into `issueListener`. 98 | classFiles.forEach { classFile -> 99 | classFile.inputStream().use { fis -> 100 | ClassReader(fis.readBytes()).let { classReader -> 101 | ClassAnalyzer(issueListener, logger).apply { 102 | classReader.accept(this, 0) 103 | } 104 | } 105 | } 106 | } 107 | 108 | // This does a global analysis, so must come after the forEach. 109 | val issues = issueListener.computeIssues().sortedBy { 110 | it.javaClass.canonicalName ?: it.javaClass.simpleName 111 | } 112 | 113 | // Get baseline, if it exists. 114 | val baseline = parameters.baseline.orNull?.asFile?.readText()?.fromJson()?.issues.orEmpty() 115 | 116 | // Build console text. 117 | val text = if (baseline.isEmpty()) { 118 | // no baseline 119 | issues.joinToString(separator = "\n\n") { IssueRenderer.renderIssue(it, pretty = true) } 120 | } else { 121 | // If we have a baseline, the behavior changes 122 | // If we find an issue that isn't in the baseline, it's a new issue. 123 | val newIssues = issues.filter { it !in baseline } 124 | // any issue in the baseline that ISN'T also in the list of current issues has been fixed. 125 | val fixedIssues = baseline.filter { it !in issues } 126 | // any issue in the baseline that IS in the list of current issues is unfixed. 127 | val unfixedIssues = baseline.filter { it in issues } 128 | 129 | buildString { 130 | if (newIssues.isNotEmpty()) { 131 | appendLine("There are new issues:") 132 | appendLine(newIssues.joinToString(separator = "\n\n") { IssueRenderer.renderIssue(it, pretty = true) }) 133 | appendLine() 134 | } else { 135 | appendLine("No new issues.") 136 | appendLine() 137 | } 138 | 139 | if (fixedIssues.isNotEmpty()) { 140 | appendLine("These issues have been resolved and should be removed from your baseline:") 141 | appendLine(fixedIssues.joinToString(separator = "\n\n") { IssueRenderer.renderIssue(it, pretty = true) }) 142 | appendLine() 143 | } 144 | 145 | if (unfixedIssues.isNotEmpty()) { 146 | appendLine("These issues have been ignored as part of your baseline:") 147 | appendLine(unfixedIssues.joinToString(separator = "\n\n") { IssueRenderer.renderIssue(it, pretty = true) }) 148 | appendLine() 149 | } 150 | } 151 | } 152 | 153 | // Write output to disk. 154 | outputText.writeText(text) 155 | outputJson.writeText(IssuesReport(issues).toJson()) 156 | } 157 | 158 | private fun compositeListener(): IssueListener { 159 | val listeners = listOf( 160 | AllProjectsListener(), 161 | GetAllprojectsListener(), 162 | SubprojectsListener(), 163 | GetSubprojectsListener(), 164 | GetProjectListener(), 165 | EagerApisListener(), 166 | ) 167 | return CompositeIssueListener(listeners) 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2019 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 Anthony Robalik 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/main/kotlin/com/autonomousapps/internal/analysis/IssueListener.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage", "HasPlatformType") // Guava Graphs 2 | 3 | package com.autonomousapps.internal.analysis 4 | 5 | import com.autonomousapps.graph.ShortestPath 6 | import com.autonomousapps.internal.graphs.Class 7 | import com.autonomousapps.internal.graphs.Method 8 | import com.autonomousapps.internal.graphs.MethodNode 9 | import com.autonomousapps.issue.AllprojectsIssue 10 | import com.autonomousapps.issue.EagerApiIssue 11 | import com.autonomousapps.issue.GetAllprojectsIssue 12 | import com.autonomousapps.issue.GetProjectInTaskActionIssue 13 | import com.autonomousapps.issue.GetSubprojectsIssue 14 | import com.autonomousapps.issue.Issue 15 | import com.autonomousapps.issue.SubprojectsIssue 16 | import com.autonomousapps.issue.Trace 17 | import com.google.common.collect.MultimapBuilder 18 | import com.google.common.graph.ElementOrder 19 | import com.google.common.graph.EndpointPair 20 | import com.google.common.graph.Graph 21 | import com.google.common.graph.GraphBuilder 22 | 23 | internal interface IssueListener { 24 | 25 | fun computeIssues(): Set 26 | 27 | fun visitClass(name: String, superName: String?, interfaces: List) = Unit 28 | fun visitMethod(name: String, descriptor: String) = Unit 29 | fun visitMethodEnd() = Unit 30 | fun visitMethodInstruction(owner: String, name: String, descriptor: String) = Unit 31 | fun visitMethodAnnotation(descriptor: String) = Unit 32 | } 33 | 34 | internal abstract class AbstractIssueListener : IssueListener { 35 | 36 | private lateinit var currentClass: Class 37 | private var currentMethod: Method? = null 38 | 39 | protected val parentPointers = MultimapBuilder.hashKeys().hashSetValues().build() 40 | protected val graph = GraphBuilder 41 | .directed() 42 | .incidentEdgeOrder(ElementOrder.stable()) 43 | .allowsSelfLoops(true) 44 | .build() 45 | 46 | final override fun visitClass(name: String, superName: String?, interfaces: List) { 47 | currentClass = Class(name, superName) 48 | superName?.let { parent -> 49 | parentPointers.put(name, parent) 50 | } 51 | interfaces.forEach { parent -> 52 | parentPointers.put(name, parent) 53 | } 54 | onVisitClass(name, superName, interfaces) 55 | } 56 | 57 | protected open fun onVisitClass(name: String, superName: String?, interfaces: List) { 58 | // do nothing by default 59 | } 60 | 61 | final override fun visitMethod(name: String, descriptor: String) { 62 | currentMethod = Method(name, descriptor) 63 | } 64 | 65 | final override fun visitMethodEnd() { 66 | currentMethod = null 67 | onVisitMethodEnd() 68 | } 69 | 70 | protected open fun onVisitMethodEnd() { 71 | // do nothing by default 72 | } 73 | 74 | final override fun visitMethodAnnotation(descriptor: String) { 75 | onVisitMethodAnnotation(descriptor) 76 | } 77 | 78 | protected open fun onVisitMethodAnnotation(descriptor: String) { 79 | // do nothing by default 80 | } 81 | 82 | final override fun visitMethodInstruction(owner: String, name: String, descriptor: String) { 83 | // put an edge in the graph 84 | val source = methodNode() 85 | val target = methodInstructionNode(owner, name, descriptor) 86 | graph.putEdge(source, target) 87 | 88 | onVisitMethodInstruction(owner, name, descriptor) 89 | } 90 | 91 | protected open fun onVisitMethodInstruction(owner: String, name: String, descriptor: String) { 92 | // do nothing by default 93 | } 94 | 95 | protected fun computeTraces(): Set { 96 | preComputeTraces() 97 | 98 | val suspectNodes = graph.nodes().filter { isSuspectNode(graph, it) } 99 | if (suspectNodes.isEmpty()) return emptySet() 100 | 101 | val entryPoints = graph.nodes().filter { isEntryPointNode(graph, it) } 102 | if (entryPoints.isEmpty()) return emptySet() 103 | 104 | // We have valid entry points and suspect exit points. Is there a path between any of them? 105 | 106 | return entryPoints.flatMapTo(HashSet()) { entryNode -> 107 | val paths = ShortestPath(graph, entryNode) 108 | suspectNodes.asSequence() 109 | .map { paths.pathTo(it) } 110 | .map { it.toList() } 111 | .filter { it.isNotEmpty() } 112 | .map { Trace(it) } 113 | } 114 | } 115 | 116 | protected open fun preComputeTraces() { 117 | // do nothing by default 118 | } 119 | 120 | protected open fun isEntryPointNode(graph: Graph, methodNode: MethodNode): Boolean { 121 | return graph.inDegree(methodNode) == 0 122 | } 123 | 124 | abstract fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean 125 | 126 | private fun methodNode(): MethodNode { 127 | val currentMethod = checkNotNull(currentMethod) 128 | return MethodNode( 129 | owner = currentClass.name, 130 | name = currentMethod.name, 131 | descriptor = currentMethod.descriptor, 132 | metadata = methodMetadata(), 133 | ) 134 | } 135 | 136 | private fun methodInstructionNode( 137 | owner: String, 138 | name: String, 139 | descriptor: String 140 | ) = MethodNode( 141 | owner = owner, 142 | name = name, 143 | descriptor = descriptor 144 | ) 145 | 146 | protected open fun methodMetadata(): MethodNode.Metadata = MethodNode.Metadata.EMPTY 147 | } 148 | 149 | internal class CompositeIssueListener( 150 | private val listeners: List, 151 | ) : IssueListener { 152 | 153 | override fun computeIssues(): Set { 154 | return listeners.flatMapTo(HashSet()) { it.computeIssues() } 155 | } 156 | 157 | override fun visitClass(name: String, superName: String?, interfaces: List) { 158 | listeners.forEach { it.visitClass(name, superName, interfaces) } 159 | } 160 | 161 | override fun visitMethod(name: String, descriptor: String) { 162 | listeners.forEach { it.visitMethod(name, descriptor) } 163 | } 164 | 165 | override fun visitMethodEnd() { 166 | listeners.forEach { it.visitMethodEnd() } 167 | } 168 | 169 | override fun visitMethodAnnotation(descriptor: String) { 170 | listeners.forEach { it.visitMethodAnnotation(descriptor) } 171 | } 172 | 173 | override fun visitMethodInstruction(owner: String, name: String, descriptor: String) { 174 | listeners.forEach { it.visitMethodInstruction(owner, name, descriptor) } 175 | } 176 | } 177 | 178 | /** Calls `Project.allprojects()`. */ 179 | internal class AllProjectsListener : AbstractIssueListener() { 180 | 181 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 182 | AllprojectsIssue("allprojects", trace) 183 | } 184 | 185 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 186 | return methodNode.owner == "org/gradle/api/Project" && methodNode.name == "allprojects" 187 | } 188 | } 189 | 190 | /** Calls `Project.getAllprojects()`. */ 191 | internal class GetAllprojectsListener : AbstractIssueListener() { 192 | 193 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 194 | GetAllprojectsIssue("getAllprojects", trace) 195 | } 196 | 197 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 198 | return methodNode.owner == "org/gradle/api/Project" && methodNode.name == "getAllprojects" 199 | } 200 | } 201 | 202 | /** Calls `Project.subprojects()`. */ 203 | internal class SubprojectsListener : AbstractIssueListener() { 204 | 205 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 206 | SubprojectsIssue("subprojects", trace) 207 | } 208 | 209 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 210 | return methodNode.owner == "org/gradle/api/Project" && methodNode.name == "subprojects" 211 | } 212 | } 213 | 214 | 215 | /** Calls `Project.getSubprojects()`. */ 216 | internal class GetSubprojectsListener : AbstractIssueListener() { 217 | 218 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 219 | GetSubprojectsIssue("getSubprojects", trace) 220 | } 221 | 222 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 223 | return methodNode.owner == "org/gradle/api/Project" && methodNode.name == "getSubprojects" 224 | } 225 | } 226 | 227 | /** Calls `Project.getProject()`. */ 228 | internal class GetProjectListener : AbstractIssueListener() { 229 | 230 | private var isTaskAction = false 231 | 232 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 233 | GetProjectInTaskActionIssue("getProject", trace) 234 | } 235 | 236 | override fun onVisitMethodEnd() { 237 | isTaskAction = false 238 | } 239 | 240 | override fun onVisitMethodAnnotation(descriptor: String) { 241 | isTaskAction = descriptor == "Lorg/gradle/api/tasks/TaskAction;" 242 | } 243 | 244 | override fun isEntryPointNode(graph: Graph, methodNode: MethodNode): Boolean = isTaskAction(methodNode) 245 | 246 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 247 | return callsGetProject(methodNode) 248 | } 249 | 250 | private fun isTaskAction(methodNode: MethodNode): Boolean { 251 | return methodNode.metadata.isTaskAction 252 | } 253 | 254 | private fun callsGetProject(methodNode: MethodNode): Boolean { 255 | return methodNode.name == "getProject" && methodNode.descriptor == "()Lorg/gradle/api/Project;" 256 | } 257 | 258 | override fun preComputeTraces() { 259 | hydrateGraph() 260 | } 261 | 262 | /** 263 | * Hydrate the graph with artificial nodes and edges to account for class hierarchies and the many paths code may take 264 | * to reach suspect method calls. Example: 265 | * ``` 266 | * // Real method traces 267 | * Parent#action -> Parent#doAction // an abstract method 268 | * Child#doAction -> Child#getProject // Child is a Task, and so getProject is suspect _only if_ called from an action 269 | * 270 | * // What we want 271 | * Parent#action -> Parent#doAction // a real method call 272 | * Parent#doAction -> Child#doAction // a "virtual" method call 273 | * Child#doAction -> Child#getProject // a real method call 274 | * ``` 275 | */ 276 | private fun hydrateGraph() { 277 | val edges = graph.edges() 278 | parentPointers.forEach { child, parent -> 279 | // Get all edges in the graph that start at a parent (of the current child) node 280 | val parentEdges = edges.filter { it.source().owner == parent } 281 | 282 | // Find edges in the parent that don't have a matching source in the child. E.g., Child has no `action` method. 283 | val missingInChild = parentEdges.filter { edge -> 284 | edges.any { it.source().signatureMatches(edge.source()) } 285 | } 286 | 287 | // For every "missing method" in the child, we create a virtual edge in the graph. 288 | missingInChild.forEach { parentEdge -> 289 | maybeCreateVirtualEdge(child = child, parent = parent, parentEdge = parentEdge) 290 | } 291 | } 292 | } 293 | 294 | private fun maybeCreateVirtualEdge( 295 | child: String, 296 | parent: String, 297 | parentEdge: EndpointPair 298 | ) { 299 | val oldTarget = parentEdge.target() 300 | var newTarget = if (oldTarget.owner == parent) { 301 | oldTarget.withVirtualOwner(child) 302 | } else { 303 | oldTarget 304 | } 305 | 306 | // If there's already a matching node in the graph, just use that one. 307 | graph.nodes().find { it == newTarget }?.let { 308 | newTarget = it 309 | } 310 | 311 | if (oldTarget != newTarget) { 312 | graph.putEdge(oldTarget, newTarget) 313 | } 314 | } 315 | 316 | override fun methodMetadata(): MethodNode.Metadata { 317 | return MethodNode.Metadata(isTaskAction) 318 | } 319 | } 320 | 321 | /** Invokes Eager APIs instead of Lazy ones. 322 | * @see Lazy Configuration 323 | * @see Old vs New API Overview 324 | */ 325 | internal class EagerApisListener : AbstractIssueListener() { 326 | 327 | private val eagerApis = mapOf( 328 | "org/gradle/api/tasks/TaskContainer" to setOf("all", "create", "getByName") 329 | ) 330 | 331 | override fun computeIssues(): Set = computeTraces().mapTo(HashSet()) { trace -> 332 | EagerApiIssue("eagerApis", trace) 333 | } 334 | 335 | override fun isSuspectNode(graph: Graph, methodNode: MethodNode): Boolean { 336 | val methodOwner = methodNode.owner 337 | val methodName = methodNode.name 338 | return eagerApis[methodOwner]?.contains(methodName) ?: false 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /plugin-best-practices-plugin/src/functionalTest/groovy/com/autonomousapps/fixture/SimplePluginProject.groovy: -------------------------------------------------------------------------------- 1 | package com.autonomousapps.fixture 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | 6 | final class SimplePluginProject { 7 | 8 | private final Path tempDir 9 | private final String logLevel 10 | 11 | SimplePluginProject(Path tempDir, String logLevel = 'default') { 12 | this.tempDir = tempDir 13 | this.logLevel = logLevel 14 | build() 15 | } 16 | 17 | Path root = tempDir 18 | Path consoleReport = root.resolve('build/reports/best-practices/report.txt') 19 | Path jsonReport = root.resolve('build/reports/best-practices/report.json') 20 | Path baselineReport = root.resolve('best-practices-baseline.json') 21 | 22 | private void build() { 23 | newFile('build.gradle').write("""\ 24 | plugins { 25 | id 'java-gradle-plugin' 26 | id 'com.autonomousapps.plugin-best-practices-plugin' 27 | } 28 | 29 | gradlePlugin { 30 | plugins { 31 | greeting { 32 | id = 'com.test.greeting' 33 | implementationClass = 'com.test.GreetingPlugin' 34 | } 35 | } 36 | } 37 | 38 | gradleBestPractices { 39 | logging '$logLevel' 40 | } 41 | """.stripIndent()) 42 | 43 | newFile('src/main/java/com/test/GreetingPlugin.java').write('''\ 44 | package com.test; 45 | 46 | import org.gradle.api.tasks.TaskContainer; 47 | import org.gradle.api.Plugin; 48 | import org.gradle.api.Project; 49 | import java.util.*; 50 | 51 | public class GreetingPlugin implements Plugin { 52 | 53 | private Project project; 54 | 55 | public void apply(Project project) { 56 | this.project = project; 57 | 58 | project.subprojects(p -> { 59 | // a comment 60 | }); 61 | Set s = project.getSubprojects(); 62 | 63 | project.allprojects(p -> { 64 | // a comment 65 | }); 66 | Set a = project.getAllprojects(); 67 | 68 | foo(); 69 | } 70 | 71 | private void foo() { 72 | bar(); 73 | } 74 | 75 | private void bar() { 76 | project.getLogger().quiet("Foobar!"); 77 | } 78 | 79 | private void baz() { 80 | TaskContainer tasks = project.getTasks(); 81 | tasks.create("eagerTask"); 82 | tasks.getByName("eagerTask"); 83 | tasks.all(task -> {}); 84 | } 85 | } 86 | '''.stripIndent()) 87 | 88 | newFile('src/main/java/com/test/GreetingTask.java').write('''\ 89 | package com.test; 90 | 91 | import org.gradle.api.DefaultTask; 92 | import org.gradle.api.Project; 93 | import org.gradle.api.tasks.TaskAction; 94 | 95 | public abstract class GreetingTask extends DefaultTask { 96 | 97 | @TaskAction 98 | public void action() { 99 | 100 | } 101 | } 102 | '''.stripIndent()) 103 | 104 | newFile('src/main/java/com/test/FancyTask.java').write('''\ 105 | package com.test; 106 | 107 | import org.gradle.api.DefaultTask; 108 | import org.gradle.api.Project; 109 | import org.gradle.api.tasks.TaskAction; 110 | 111 | public abstract class FancyTask extends DefaultTask { 112 | 113 | protected abstract void doAction(); 114 | 115 | @TaskAction 116 | public void action() { 117 | doAction(); 118 | } 119 | 120 | public static abstract class ReallyFancyTask extends FancyTask { 121 | @Override 122 | protected void doAction() { 123 | getProject().getLogger().quiet("Hello from ReallyFancyTask"); 124 | } 125 | } 126 | } 127 | '''.stripIndent()) 128 | 129 | newFile('src/main/java/com/test/ParentTask.java').write('''\ 130 | package com.test; 131 | 132 | import org.gradle.api.DefaultTask; 133 | import org.gradle.api.Project; 134 | import org.gradle.api.tasks.TaskAction; 135 | 136 | public abstract class ParentTask extends DefaultTask { 137 | 138 | protected abstract void doAction(); 139 | 140 | @TaskAction 141 | public void action() { 142 | foo(); 143 | } 144 | 145 | private void foo() { 146 | bar(); 147 | } 148 | 149 | private void bar() { 150 | doAction(); 151 | } 152 | 153 | public static abstract class ChildTask extends ParentTask { 154 | @Override 155 | protected void doAction() { 156 | getProject().getLogger().quiet("Hello from ChildTask"); 157 | } 158 | } 159 | } 160 | '''.stripIndent()) 161 | 162 | newFile('src/main/java/com/test/ParentTask2.java').write('''\ 163 | package com.test; 164 | 165 | import org.gradle.api.DefaultTask; 166 | import org.gradle.api.Project; 167 | import org.gradle.api.tasks.TaskAction; 168 | 169 | public abstract class ParentTask2 extends DefaultTask { 170 | 171 | protected abstract void doAction(); 172 | 173 | @TaskAction 174 | public void action() { 175 | doAction(); 176 | } 177 | 178 | public static abstract class ChildTask2 extends ParentTask2 { 179 | @Override 180 | protected void doAction() { 181 | foo(); 182 | } 183 | 184 | private void foo() { 185 | bar(new int[1], "hello!"); 186 | } 187 | 188 | private void bar(int[] ints, String s) { 189 | getProject().getLogger().quiet("Hello from ChildTask2"); 190 | } 191 | } 192 | } 193 | '''.stripIndent()) 194 | } 195 | 196 | private Path newFile(String path) { 197 | def file = tempDir.resolve(path) 198 | Files.createDirectories(file.parent) 199 | return Files.createFile(file) 200 | } 201 | 202 | String expectedConsoleReport = '''\ 203 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 204 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 205 | org.gradle.api.Project#allprojects(Lorg.gradle.api.Action;)V 206 | 207 | com.test.GreetingPlugin#baz()V -> 208 | org.gradle.api.tasks.TaskContainer#getByName(Ljava.lang.String;)Lorg.gradle.api.Task; 209 | 210 | com.test.GreetingPlugin#baz()V -> 211 | org.gradle.api.tasks.TaskContainer#create(Ljava.lang.String;)Lorg.gradle.api.Task; 212 | 213 | com.test.GreetingPlugin#baz()V -> 214 | org.gradle.api.tasks.TaskContainer#all(Lorg.gradle.api.Action;)V 215 | 216 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 217 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 218 | org.gradle.api.Project#getAllprojects()Ljava.util.Set; 219 | 220 | com.test.FancyTask#action()V -> 221 | com.test.FancyTask#doAction()V -> 222 | com.test.FancyTask$ReallyFancyTask#doAction()V -> 223 | com.test.FancyTask$ReallyFancyTask#getProject()Lorg.gradle.api.Project; 224 | 225 | com.test.ParentTask#action()V -> 226 | com.test.ParentTask#foo()V -> 227 | com.test.ParentTask#bar()V -> 228 | com.test.ParentTask#doAction()V -> 229 | com.test.ParentTask$ChildTask#doAction()V -> 230 | com.test.ParentTask$ChildTask#getProject()Lorg.gradle.api.Project; 231 | 232 | com.test.ParentTask2#action()V -> 233 | com.test.ParentTask2#doAction()V -> 234 | com.test.ParentTask2$ChildTask2#doAction()V -> 235 | com.test.ParentTask2$ChildTask2#foo()V -> 236 | com.test.ParentTask2$ChildTask2#bar([ILjava.lang.String;)V -> 237 | com.test.ParentTask2$ChildTask2#getProject()Lorg.gradle.api.Project; 238 | 239 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 240 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 241 | org.gradle.api.Project#getSubprojects()Ljava.util.Set; 242 | 243 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 244 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 245 | org.gradle.api.Project#subprojects(Lorg.gradle.api.Action;)V 246 | '''.stripIndent() 247 | 248 | String expectedConsoleOutput = '''\ 249 | > Task :checkBestPractices FAILED 250 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 251 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 252 | org.gradle.api.Project#allprojects(Lorg.gradle.api.Action;)V 253 | 254 | com.test.GreetingPlugin#baz()V -> 255 | org.gradle.api.tasks.TaskContainer#getByName(Ljava.lang.String;)Lorg.gradle.api.Task; 256 | 257 | com.test.GreetingPlugin#baz()V -> 258 | org.gradle.api.tasks.TaskContainer#create(Ljava.lang.String;)Lorg.gradle.api.Task; 259 | 260 | com.test.GreetingPlugin#baz()V -> 261 | org.gradle.api.tasks.TaskContainer#all(Lorg.gradle.api.Action;)V 262 | 263 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 264 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 265 | org.gradle.api.Project#getAllprojects()Ljava.util.Set; 266 | 267 | com.test.FancyTask#action()V -> 268 | com.test.FancyTask#doAction()V -> 269 | com.test.FancyTask$ReallyFancyTask#doAction()V -> 270 | com.test.FancyTask$ReallyFancyTask#getProject()Lorg.gradle.api.Project; 271 | 272 | com.test.ParentTask#action()V -> 273 | com.test.ParentTask#foo()V -> 274 | com.test.ParentTask#bar()V -> 275 | com.test.ParentTask#doAction()V -> 276 | com.test.ParentTask$ChildTask#doAction()V -> 277 | com.test.ParentTask$ChildTask#getProject()Lorg.gradle.api.Project; 278 | 279 | com.test.ParentTask2#action()V -> 280 | com.test.ParentTask2#doAction()V -> 281 | com.test.ParentTask2$ChildTask2#doAction()V -> 282 | com.test.ParentTask2$ChildTask2#foo()V -> 283 | com.test.ParentTask2$ChildTask2#bar([ILjava.lang.String;)V -> 284 | com.test.ParentTask2$ChildTask2#getProject()Lorg.gradle.api.Project; 285 | 286 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 287 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 288 | org.gradle.api.Project#getSubprojects()Ljava.util.Set; 289 | 290 | com.test.GreetingPlugin#apply(Ljava.lang.Object;)V -> 291 | com.test.GreetingPlugin#apply(Lorg.gradle.api.Project;)V -> 292 | org.gradle.api.Project#subprojects(Lorg.gradle.api.Action;)V 293 | 294 | FAILURE: Build failed with an exception. 295 | '''.stripIndent() 296 | 297 | String expectedJsonReport = '''\ 298 | {"issues":[{"type":"allprojects","name":"allprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"allprojects","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"getByName","descriptor":"(Ljava/lang/String;)Lorg/gradle/api/Task;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"create","descriptor":"(Ljava/lang/String;)Lorg/gradle/api/Task;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"all","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_allprojects","name":"getAllprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"getAllprojects","descriptor":"()Ljava/util/Set;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/FancyTask","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/FancyTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/FancyTask$ReallyFancyTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/FancyTask$ReallyFancyTask","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/ParentTask","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"foo","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"bar","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask$ChildTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask$ChildTask","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/ParentTask2","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/ParentTask2","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"foo","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"bar","descriptor":"([ILjava/lang/String;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_subprojects","name":"getSubprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"getSubprojects","descriptor":"()Ljava/util/Set;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"subprojects","name":"subprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"subprojects","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}}]} 299 | '''.stripIndent() 300 | 301 | String expectedBaseline = '''\ 302 | {"issues":[{"type":"allprojects","name":"allprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"allprojects","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"getByName","descriptor":"(Ljava/lang/String;)Lorg/gradle/api/Task;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"create","descriptor":"(Ljava/lang/String;)Lorg/gradle/api/Task;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"eager_api","name":"eagerApis","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"baz","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/tasks/TaskContainer","name":"all","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_allprojects","name":"getAllprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"getAllprojects","descriptor":"()Ljava/util/Set;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/FancyTask","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/FancyTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/FancyTask$ReallyFancyTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/FancyTask$ReallyFancyTask","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/ParentTask","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"foo","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"bar","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask$ChildTask","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask$ChildTask","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_project","name":"getProject","trace":{"trace":[{"owner":"com/test/ParentTask2","name":"action","descriptor":"()V","metadata":{"isTaskAction":true,"isVirtual":false}},{"owner":"com/test/ParentTask2","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"doAction","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"foo","descriptor":"()V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"bar","descriptor":"([ILjava/lang/String;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/ParentTask2$ChildTask2","name":"getProject","descriptor":"()Lorg/gradle/api/Project;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"get_subprojects","name":"getSubprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"getSubprojects","descriptor":"()Ljava/util/Set;","metadata":{"isTaskAction":false,"isVirtual":false}}]}},{"type":"subprojects","name":"subprojects","trace":{"trace":[{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Ljava/lang/Object;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"com/test/GreetingPlugin","name":"apply","descriptor":"(Lorg/gradle/api/Project;)V","metadata":{"isTaskAction":false,"isVirtual":false}},{"owner":"org/gradle/api/Project","name":"subprojects","descriptor":"(Lorg/gradle/api/Action;)V","metadata":{"isTaskAction":false,"isVirtual":false}}]}}]} 303 | '''.stripIndent() 304 | } 305 | --------------------------------------------------------------------------------