All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.autonomousapps.services.GlobalDslService.kt Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
package com.autonomousapps.services

import com.autonomousapps.BuildHealthPlugin
import com.autonomousapps.extension.*
import com.autonomousapps.internal.utils.mapToMutableList
import com.autonomousapps.subplugin.DEPENDENCY_ANALYSIS_PLUGIN
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.property
import javax.inject.Inject

/**
 * This class is used alongside [DependencyAnalysisExtension][com.autonomousapps.DependencyAnalysisExtension] and
 * [DependencyAnalysisSubExtension][com.autonomousapps.DependencyAnalysisSubExtension] to safely (in isolated projects-
 * terms) configure the entire build, globally, without any subproject touching mutable properties of any other project.
 */
abstract class GlobalDslService @Inject constructor(
  objects: ObjectFactory,
) : BuildService {

  // Used for validity check. The plugin must be registered on the root project.
  private var registeredOnRoot = false

  // Used for error message when AGP or KGP missing from classpath.
  private var registeredOnSettings = false

  internal fun setRegisteredOnRoot() {
    registeredOnRoot = true
  }

  internal fun setRegisteredOnSettings() {
    registeredOnSettings = true
  }

  internal fun notifyAgpMissing() {
    val msg = if (registeredOnSettings) {
      """
        Android Gradle Plugin (AGP) not found on classpath. This might be a classloader issue. For the Dependency 
        Analysis Gradle Plugin (DAGP) to be able to analyze Android projects, AGP must be loaded in the same class
        loader as DAGP, or a parent. One solution is to ensure your settings script looks like this:
        
          // settings.gradle[.kts]
          buildscript {
            repositories { ... }
            dependencies {
              classpath("com.android.tools.build:gradle:<>")
            }
          }
          
          plugins {
            id("${BuildHealthPlugin.ID}") version "<>"
            
            // Optional
            id("org.jetbrains.kotlin.android") version "<>" apply false
          }
      """.trimIndent()
    } else {
      """
        Android Gradle Plugin (AGP) not found on classpath. This might be a classloader issue. For the Dependency 
        Analysis Gradle Plugin (DAGP) to be able to analyze Android projects, AGP must be loaded in the same class
        loader as DAGP, or a parent. One solution is to ensure your root build script looks like this:
        
          // root build.gradle[.kts]
          buildscript {
            repositories { ... }
            dependencies {
              classpath("com.android.tools.build:gradle:<>")
            }
          }
          
          plugins {
            id("$DEPENDENCY_ANALYSIS_PLUGIN") version "<>"
            
            // Optional
            id("org.jetbrains.kotlin.android") version "<>" apply false
          }
      """.trimIndent()
    }

    error(msg)
  }

  internal fun notifyKgpMissing() {
    val msg = if (registeredOnSettings) {
      """
        Kotlin Gradle Plugin (KGP) not found on classpath. This might be a classloader issue. For the Dependency 
        Analysis Gradle Plugin (DAGP) to be able to analyze Kotlin projects, KGP must be loaded in the same class
        loader as DAGP, or a parent. One solution is to ensure your settings script looks like this:
        
          // settings.gradle[.kts]
          plugins {
            id("${BuildHealthPlugin.ID}") version "<>"
            id("org.jetbrains.kotlin.)" version "<>" apply false
          }
      """.trimIndent()
    } else {
      """
        Kotlin Gradle Plugin (KGP) not found on classpath. This might be a classloader issue. For the Dependency 
        Analysis Gradle Plugin (DAGP) to be able to analyze Kotlin projects, KGP must be loaded in the same class
        loader as DAGP, or a parent. One solution is to ensure your root build script looks like this:
        
          // root build.gradle[.kts]
          plugins {
            id("$DEPENDENCY_ANALYSIS_PLUGIN") version "<>"
            id("org.jetbrains.kotlin.)" version "<>" apply false
          }
      """.trimIndent()
    }

    error(msg)
  }

  // Global handlers, one instance each for the whole build.
  internal val abiHandler: AbiHandler = objects.newInstance()
  internal val dependenciesHandler: DependenciesHandler = objects.newInstance()
  internal val usagesHandler: UsagesHandler = objects.newInstance()

  /**
   * Hydrate dependencies map with version catalog entries.
   */
  internal fun withVersionCatalogs(project: Project) {
    dependenciesHandler.withVersionCatalogs(project)
  }

  /*
   * Issues Handler, one instance per project.
   */

  private val undefined = objects.newInstance()
  private val defaultBehavior = objects.property().convention(Warn())

  private val all = objects.newInstance(ProjectIssueHandler::class.java, "__all")
  private val projects = objects.domainObjectContainer(ProjectIssueHandler::class.java)

  internal fun all(action: Action) {
    action.execute(all)
  }

  internal fun project(projectPath: String, action: Action) {
    projects.maybeCreate(projectPath).apply {
      action.execute(this)
    }
  }

  internal fun shouldAnalyzeSourceSet(sourceSetName: String, projectPath: String): Boolean {
    val a = sourceSetName !in all.ignoreSourceSets.get()
    val b = sourceSetName !in projects.findByName(projectPath)?.ignoreSourceSets?.get().orEmpty()

    return a && b
  }

  internal fun anyIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.anyIssue }
  }

  internal fun unusedDependenciesIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.unusedDependenciesIssue }
  }

  internal fun usedTransitiveDependenciesIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.usedTransitiveDependenciesIssue }
  }

  internal fun incorrectConfigurationIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.incorrectConfigurationIssue }
  }

  internal fun compileOnlyIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.compileOnlyIssue }
  }

  internal fun runtimeOnlyIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.runtimeOnlyIssue }
  }

  internal fun unusedAnnotationProcessorsIssueFor(projectPath: String): List> {
    return issuesFor(projectPath) { it.unusedAnnotationProcessorsIssue }
  }

  internal fun redundantPluginsIssueFor(projectPath: String): Provider {
    return overlay(all.redundantPluginsIssue, projects.findByName(projectPath)?.redundantPluginsIssue)
  }

  internal fun moduleStructureIssueFor(projectPath: String): Provider {
    return overlay(all.moduleStructureIssue, projects.findByName(projectPath)?.moduleStructureIssue)
  }

  private fun issuesFor(projectPath: String, mapper: (ProjectIssueHandler) -> Issue): List> {
    val projectHandler = projects.findByName(projectPath)

    val allIssuesBySourceSet = all.issuesBySourceSet(mapper)
    val projectIssuesBySourceSet = projectHandler.issuesBySourceSet(mapper)
    val globalProjectMatches = mutableListOf>()

    // Iterate through the global/all list first
    val iter = allIssuesBySourceSet.iterator()
    while (iter.hasNext()) {
      // Find matching (by sourceSetName) elements in both lists
      val a = iter.next()
      val b = projectIssuesBySourceSet.find { p -> p.sourceSet.get() == a.sourceSet.get() }

      // Drain both lists
      iter.remove()
      if (b != null) projectIssuesBySourceSet.remove(b)

      // Add to result list (it's ok if `b` is null)
      globalProjectMatches.add(a to b)
    }

    // Now iterate through the remaining elements of the proj list
    projectIssuesBySourceSet.forEach { b ->
      val a = allIssuesBySourceSet.find { a -> a.sourceSet.get() == b.sourceSet.get() }

      // In contrast to the above, it is NOT ok if `a` is null, so we use `undefined` instead.
      if (a != null) {
        globalProjectMatches.add(a to b)
      } else {
        globalProjectMatches.add(undefined to b)
      }
    }

    val primaryBehavior = overlay(mapper(all), projectHandler?.let(mapper))
    val result = mutableListOf(primaryBehavior)
    globalProjectMatches.mapTo(result) { (global, project) ->
      overlay(global, project, primaryBehavior)
    }

    return result
  }

  private fun ProjectIssueHandler?.issuesBySourceSet(mapper: (ProjectIssueHandler) -> Issue): MutableList {
    return this?.sourceSets.mapToMutableList { s ->
      s.issueOf(mapper)
    }
  }

  /** Project severity wins over global severity. Excludes are unioned. */
  private fun overlay(global: Issue, project: Issue?, coerceTo: Provider? = null): Provider {
    val c = coerceTo ?: defaultBehavior

    // If there's no project-specific handler, just return the global handler
    return if (project == null) {
      c.flatMap { coerce ->
        global.behavior().map { g ->
          if (g is Undefined) {
            when (coerce) {
              is Fail -> Fail(filter = g.filter, sourceSetName = g.sourceSetName)
              is Warn, is Undefined -> Warn(filter = g.filter, sourceSetName = g.sourceSetName)
              is Ignore -> Ignore(sourceSetName = g.sourceSetName)
            }
          } else {
            g
          }
        }
      }
    } else {
      global.behavior().flatMap { g ->
        val allFilter = g.filter
        project.behavior().map { p ->
          val projFilter = p.filter
          val union = allFilter + projFilter

          when (p) {
            is Fail -> Fail(filter = union, sourceSetName = p.sourceSetName)
            is Warn -> Warn(filter = union, sourceSetName = p.sourceSetName)
            is Ignore -> Ignore(sourceSetName = p.sourceSetName)
            is Undefined -> {
              when (g) {
                is Fail -> Fail(filter = union, sourceSetName = p.sourceSetName)
                is Warn -> Warn(filter = union, sourceSetName = p.sourceSetName)
                is Undefined -> Warn(filter = union, sourceSetName = p.sourceSetName)
                is Ignore -> Ignore(sourceSetName = p.sourceSetName)
              }
            }
          }
        }
      }
    }
  }

  internal companion object {
    fun of(project: Project): Provider = of(project.gradle)

    fun of(gradle: Gradle): Provider {
      return gradle
        .sharedServices
        .registerIfAbsent("dagpDslService", GlobalDslService::class.java) {}
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy