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

dagger.hilt.processor.internal.root.ir.ComponentTreeDepsIrCreator.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2021 The Dagger Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dagger.hilt.processor.internal.root.ir

import com.squareup.javapoet.ClassName

// Produces ComponentTreeDepsIr for a set of aggregated deps and roots to process.
class ComponentTreeDepsIrCreator
private constructor(
  private val isSharedTestComponentsEnabled: Boolean,
  private val aggregatedRoots: Set,
  private val defineComponentDeps: Set,
  private val aliasOfDeps: Set,
  private val aggregatedDeps: Set,
  private val aggregatedUninstallModulesDeps: Set,
  private val aggregatedEarlyEntryPointDeps: Set,
) {
  private fun prodComponents(): Set {
    // There should only be one prod root in a given build.
    val aggregatedRoot = aggregatedRoots.single()
    return setOf(
      ComponentTreeDepsIr(
        name = ComponentTreeDepsNameGenerator().generate(aggregatedRoot.root),
        rootDeps = setOf(aggregatedRoot.fqName),
        defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(),
        aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(),
        aggregatedDeps =
          // @AggregatedDeps with non-empty replaces are from @TestInstallIn and should not be
          // installed in production components
          aggregatedDeps.filter { it.replaces.isEmpty() }.map { it.fqName }.toSet(),
        uninstallModulesDeps = emptySet(),
        earlyEntryPointDeps = emptySet(),
      )
    )
  }

  private fun testComponents(): Set {
    val rootsUsingSharedComponent = rootsUsingSharedComponent(aggregatedRoots)
    val aggregatedRootsByRoot = aggregatedRoots.associateBy { it.root }
    val aggregatedDepsByRoot =
      aggregatedDepsByRoot(
        aggregatedRoots = aggregatedRoots,
        rootsUsingSharedComponent = rootsUsingSharedComponent,
        hasEarlyEntryPoints = aggregatedEarlyEntryPointDeps.isNotEmpty()
      )
    val uninstallModuleDepsByRoot =
      aggregatedUninstallModulesDeps.associate { it.test to it.fqName }
    return mutableSetOf().apply {
      aggregatedDepsByRoot.keys.forEach { root ->
        val isDefaultRoot = root == DEFAULT_ROOT_CLASS_NAME
        val isEarlyEntryPointRoot = isDefaultRoot && aggregatedEarlyEntryPointDeps.isNotEmpty()
        // We want to base the generated name on the user written root rather than a generated root.
        val rootName =
          if (isDefaultRoot) {
            DEFAULT_ROOT_CLASS_NAME
          } else {
            aggregatedRootsByRoot.getValue(root).originatingRoot
          }
        val componentNameGenerator =
          if (isSharedTestComponentsEnabled) {
            ComponentTreeDepsNameGenerator(
              destinationPackage = "dagger.hilt.android.internal.testing.root",
              otherRootNames = aggregatedDepsByRoot.keys,
            )
          } else {
            ComponentTreeDepsNameGenerator()
          }
        add(
          ComponentTreeDepsIr(
            name = componentNameGenerator.generate(rootName),
            rootDeps =
              // Non-default component: the root
              // Shared component: all roots sharing the component
              // EarlyEntryPoint component: empty
              if (isDefaultRoot) {
                rootsUsingSharedComponent.map { aggregatedRootsByRoot.getValue(it).fqName }.toSet()
              } else {
                setOf(aggregatedRootsByRoot.getValue(root).fqName)
              },
            defineComponentDeps = defineComponentDeps.map { it.fqName }.toSet(),
            aliasOfDeps = aliasOfDeps.map { it.fqName }.toSet(),
            aggregatedDeps = aggregatedDepsByRoot.getOrElse(root) { emptySet() },
            uninstallModulesDeps =
              uninstallModuleDepsByRoot[root.canonicalName()]?.let { setOf(it) } ?: emptySet(),
            earlyEntryPointDeps =
              if (isEarlyEntryPointRoot) {
                aggregatedEarlyEntryPointDeps.map { it.fqName }.toSet()
              } else {
                emptySet()
              }
          )
        )
      }
    }
  }

  private fun rootsUsingSharedComponent(roots: Set): Set {
    if (!isSharedTestComponentsEnabled) {
      return emptySet()
    }
    val hasLocalModuleDependencies: Set =
      mutableSetOf().apply {
        addAll(aggregatedDeps.filter { it.module != null }.mapNotNull { it.test })
        addAll(aggregatedUninstallModulesDeps.map { it.test })
      }
    return roots
      .filter { it.isTestRoot && it.allowsSharingComponent }
      .map { it.root }
      .filter { !hasLocalModuleDependencies.contains(it.canonicalName()) }
      .toSet()
  }

  private fun aggregatedDepsByRoot(
    aggregatedRoots: Set,
    rootsUsingSharedComponent: Set,
    hasEarlyEntryPoints: Boolean
  ): Map> {
    val testDepsByRoot =
      aggregatedDeps
        .filter { it.test != null }
        .groupBy(keySelector = { it.test }, valueTransform = { it.fqName })
    val globalModules =
      aggregatedDeps.filter { it.test == null && it.module != null }.map { it.fqName }
    val globalEntryPointsByComponent =
      aggregatedDeps
        .filter { it.test == null && it.module == null }
        .groupBy(keySelector = { it.test }, valueTransform = { it.fqName })
    val result = mutableMapOf>()
    aggregatedRoots.forEach { aggregatedRoot ->
      if (!rootsUsingSharedComponent.contains(aggregatedRoot.root)) {
        result.getOrPut(aggregatedRoot.root) { linkedSetOf() }.apply {
          addAll(globalModules)
          addAll(globalEntryPointsByComponent.values.flatten())
          addAll(testDepsByRoot.getOrElse(aggregatedRoot.root.canonicalName()) { emptyList() })
        }
      }
    }
    // Add the Default/EarlyEntryPoint root if necessary.
    if (rootsUsingSharedComponent.isNotEmpty()) {
      result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply {
        addAll(globalModules)
        addAll(globalEntryPointsByComponent.values.flatten())
        addAll(
          rootsUsingSharedComponent.flatMap {
            testDepsByRoot.getOrElse(it.canonicalName()) { emptyList() }
          }
        )
      }
    } else if (hasEarlyEntryPoints) {
      result.getOrPut(DEFAULT_ROOT_CLASS_NAME) { linkedSetOf() }.apply {
        addAll(globalModules)
        addAll(
          globalEntryPointsByComponent.entries
            .filterNot { (component, _) ->
              component == SINGLETON_COMPONENT_CLASS_NAME.canonicalName()
            }
            .flatMap { (_, entryPoints) -> entryPoints }
        )
      }
    }
    return result
  }

  /**
   * Generates a component name for a tree that will be based off the given root after mapping it to
   * the [destinationPackage] and disambiguating from [otherRootNames].
   */
  private class ComponentTreeDepsNameGenerator(
    private val destinationPackage: String? = null,
    private val otherRootNames: Collection = emptySet()
  ) {
    private val simpleNameMap: Map by lazy {
      mutableMapOf().apply {
        otherRootNames.groupBy { it.enclosedName() }.values.forEach { conflictingRootNames ->
          if (conflictingRootNames.size == 1) {
            // If there's only 1 root there's nothing to disambiguate so return the simple name.
            put(conflictingRootNames.first(), conflictingRootNames.first().enclosedName())
          } else {
            // There are conflicting simple names, so disambiguate them with a unique prefix.
            // We keep them small to fix https://github.com/google/dagger/issues/421.
            // Sorted in order to guarantee determinism if this is invoked by different processors.
            val usedNames = mutableSetOf()
            conflictingRootNames.sorted().forEach { rootClassName ->
              val basePrefix =
                rootClassName.let { className ->
                  val containerName = className.enclosingClassName()?.enclosedName() ?: ""
                  if (containerName.isNotEmpty() && containerName[0].isUpperCase()) {
                    // If parent element looks like a class, use its initials as a prefix.
                    containerName.filterNot { it.isLowerCase() }
                  } else {
                    // Not in a normally named class. Prefix with the initials of the elements
                    // leading here.
                    className.toString().split('.').dropLast(1).joinToString(separator = "") {
                      "${it.first()}"
                    }
                  }
                }
              var uniqueName = basePrefix
              var differentiator = 2
              while (!usedNames.add(uniqueName)) {
                uniqueName = basePrefix + differentiator++
              }
              put(rootClassName, "${uniqueName}_${rootClassName.enclosedName()}")
            }
          }
        }
      }
    }

    fun generate(rootName: ClassName): ClassName =
      ClassName.get(
          destinationPackage ?: rootName.packageName(),
          if (otherRootNames.isEmpty()) {
            rootName.enclosedName()
          } else {
            simpleNameMap.getValue(rootName)
          }
        )
        .append("_ComponentTreeDeps")

    private fun ClassName.enclosedName() = simpleNames().joinToString(separator = "_")

    private fun ClassName.append(suffix: String) = peerClass(simpleName() + suffix)
  }

  companion object {

    @JvmStatic
    fun components(
      isTest: Boolean,
      isSharedTestComponentsEnabled: Boolean,
      aggregatedRoots: Set,
      defineComponentDeps: Set,
      aliasOfDeps: Set,
      aggregatedDeps: Set,
      aggregatedUninstallModulesDeps: Set,
      aggregatedEarlyEntryPointDeps: Set,
    ) =
      ComponentTreeDepsIrCreator(
          isSharedTestComponentsEnabled,
          // TODO(bcorso): Consider creating a common interface for fqName so that we can sort these
          // using a shared method rather than repeating the sorting logic.
          aggregatedRoots.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
          defineComponentDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
          aliasOfDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
          aggregatedDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
          aggregatedUninstallModulesDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet(),
          aggregatedEarlyEntryPointDeps.toList().sortedBy { it.fqName.canonicalName() }.toSet()
        )
        .let { producer ->
          if (isTest) {
            producer.testComponents()
          } else {
            producer.prodComponents()
          }
        }

    val DEFAULT_ROOT_CLASS_NAME: ClassName =
      ClassName.get("dagger.hilt.android.internal.testing.root", "Default")
    val SINGLETON_COMPONENT_CLASS_NAME: ClassName =
      ClassName.get("dagger.hilt.components", "SingletonComponent")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy