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

com.autonomousapps.tasks.InlineMemberExtractionTask.kt Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
@file:Suppress("UnstableApiUsage")

package com.autonomousapps.tasks

import com.autonomousapps.TASK_GROUP_DEP
import com.autonomousapps.advice.Dependency
import com.autonomousapps.internal.*
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.*
import com.autonomousapps.services.InMemoryCache
import kotlinx.metadata.Flag
import kotlinx.metadata.KmDeclarationContainer
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmProperty
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import java.util.zip.ZipFile
import javax.inject.Inject

/**
 * Algorithm:
 * 1. Parses the bytecode of all dependencies looking for inline members (functions or properties), and producing a
 *    report that associates these dependencies (see [Dependency]) with a set of imports
 *    ([ComponentWithInlineMembers.imports]) that would indicate use of an inline member. It is a best-guess heuristic.
 *    (So, `inline fun SpannableStringBuilder.bold()` gets associated with `androidx.core.text.bold` in the core-ktx
 *    module.)
 * 2. Parse all Kotlin source looking for imports that might be associated with an inline function
 * 3. Connect 1 and 2.
 */
@CacheableTask
abstract class InlineMemberExtractionTask @Inject constructor(
  private val workerExecutor: WorkerExecutor
) : DefaultTask() {

  init {
    group = TASK_GROUP_DEP
    description = "Produces a report of dependencies that contribute used inline members"
  }

  @get:PathSensitive(PathSensitivity.RELATIVE)
  @get:InputFile
  abstract val artifacts: RegularFileProperty

  /**
   * All the imports in the Kotlin source in this project.
   */
  @get:PathSensitive(PathSensitivity.RELATIVE)
  @get:InputFile
  abstract val imports: RegularFileProperty

  @get:OutputFile
  abstract val inlineMembersReport: RegularFileProperty

  @get:OutputFile
  abstract val inlineUsageReport: RegularFileProperty

  @get:Internal
  abstract val inMemoryCacheProvider: Property

  @TaskAction
  fun action() {
    workerExecutor.noIsolation().submit(InlineMemberExtractionWorkAction::class.java) {
      artifacts.set([email protected])
      imports.set([email protected])
      inlineMembersReport.set([email protected])
      inlineUsageReport.set([email protected])
      inMemoryCacheProvider.set([email protected])
    }
  }
}

interface InlineMemberExtractionParameters : WorkParameters {
  val artifacts: RegularFileProperty
  val imports: RegularFileProperty
  val inlineMembersReport: RegularFileProperty
  val inlineUsageReport: RegularFileProperty
  val inMemoryCacheProvider: Property
}

abstract class InlineMemberExtractionWorkAction : WorkAction {

  private val logger = getLogger()

  override fun execute() {
    // Outputs
    val inlineMembersReportFile = parameters.inlineMembersReport.getAndDelete() // TODO unused?
    val inlineUsageReportFile = parameters.inlineUsageReport.getAndDelete()

    // Inputs
    val artifacts = parameters.artifacts.fromJsonList()
    val imports = parameters.imports.fromJsonList().kotlinImports()

    // In principle, there may not be any Kotlin source, although a current implementation detail is
    // that "imports" will never be null, only empty. Making this null-safe seems harmless enough,
    // however.
    val usedComponents = imports?.let {
      InlineDependenciesFinder(parameters.inMemoryCacheProvider, logger, artifacts, it).find()
    } ?: emptySet()

    logger.debug("Inline usage:\n${usedComponents.toPrettyString()}")
    inlineUsageReportFile.writeText(usedComponents.toJson())
  }

  private fun List.kotlinImports(): Set? = find {
    it.sourceType == SourceType.KOTLIN
  }?.imports
}

internal class InlineDependenciesFinder(
  private val inMemoryCacheProvider: Property,
  private val logger: Logger,
  private val artifacts: List,
  private val actualImports: Set
) {

  /**
   * Returns a set of [Dependency]s that contribute used inline members in the current project (as indicated by
   * [actualImports]).
   */
  fun find(): Set {
    val inlineImportsCandidates: Set = findInlineImportCandidates()
    // This is not needed except as a manual diagnostic
    //inlineMembersReportFile.writeText(inlineImports.toPrettyString())
    return findUsedInlineImports(inlineImportsCandidates)
  }

  // from the upstream bytecode. Therefore "candidates" (not necessarily used)
  private fun findInlineImportCandidates(): Set {
    return artifacts
      .map { artifact ->
        artifact to InlineMemberFinder(inMemoryCacheProvider, logger, ZipFile(artifact.file)).find().toSortedSet()
      }.filterNot { (_, imports) ->
        imports.isEmpty()
      }.mapToOrderedSet { (artifact, imports) ->
        ComponentWithInlineMembers(artifact.dependency, imports)
      }
  }

  private fun findUsedInlineImports(
    inlineImportCandidates: Set
  ): Set {
    return actualImports.flatMapToOrderedSet { actualImport ->
      findUsedInlineImports(actualImport, inlineImportCandidates)
    }
  }

  /**
   * [actualImport] is, e.g.,
   * * `com.myapp.BuildConfig.DEBUG`
   * * `com.myapp.BuildConfig.*`
   */
  private fun findUsedInlineImports(
    actualImport: String, constantImportCandidates: Set
  ): List {
    // TODO it's a little disturbing there can be multiple matches. An issue with this naive algorithm.
    // TODO I need to be more intelligent in source parsing. Look at actual identifiers being used and associate those with their star-imports
    return constantImportCandidates.filter {
      it.imports.contains(actualImport)
    }.map {
      it.dependency
    }
  }
}

/**
 * Use to find inline members (functions or properties).
 */
internal class InlineMemberFinder(
  private val inMemoryCacheProvider: Property,
  private val logger: Logger,
  private val zipFile: ZipFile
) {

  /**
   * Returns either an empty list, if there are no inline members, or a list of import candidates. E.g.:
   * ```
   * [
   *   "kotlin.jdk7.*",
   *   "kotlin.jdk7.use"
   * ]
   * ```
   * An import statement with either of those would import the `kotlin.jdk7.use()` inline function, contributed by the
   * "org.jetbrains.kotlin:kotlin-stdlib-jdk7" module.
   */
  fun find(): List {
    val inMemoryCache = inMemoryCacheProvider.get()
    val alreadyFoundInlineMembers: List? = inMemoryCache.inlineMember(zipFile.name)
    if (alreadyFoundInlineMembers != null) {
      return alreadyFoundInlineMembers
    }

    val entries = zipFile.entries().toList()
    // Only look at jars that have actual Kotlin classes in them
    if (entries.none { it.name.endsWith(".kotlin_module") }) {
      return emptyList()
    }

    return entries.filterToSetOfClassFiles()
      .flatMap { entry ->
        // TODO an entry with `META-INF/proguard/androidx-annotations.pro`
        val classReader = zipFile.getInputStream(entry).use { ClassReader(it.readBytes()) }
        val metadataVisitor = KotlinMetadataVisitor(logger)
        classReader.accept(metadataVisitor, 0)

        val inlineMembers = metadataVisitor.builder?.let { header ->
          when (val metadata = KotlinClassMetadata.read(header.build())) {
            is KotlinClassMetadata.Class -> inlineMembers(metadata.toKmClass())
            is KotlinClassMetadata.FileFacade -> inlineMembers(metadata.toKmPackage())
            is KotlinClassMetadata.MultiFileClassPart -> inlineMembers(metadata.toKmPackage())
            is KotlinClassMetadata.SyntheticClass -> {
              logger.debug("Ignoring SyntheticClass $entry")
              emptyList()
            }
            is KotlinClassMetadata.MultiFileClassFacade -> {
              logger.debug("Ignoring MultiFileClassFacade $entry")
              emptyList()
            }
            is KotlinClassMetadata.Unknown -> {
              logger.debug("Ignoring Unknown $entry")
              emptyList()
            }
            null -> {
              logger.debug("Ignoring null $entry")
              emptyList()
            }
          }
        } ?: emptyList()

        if (inlineMembers.isNotEmpty()) {
          if (entry.name.contains("/")) {
            // entry is in a package
            val pn = entry.name.substring(0, entry.name.lastIndexOf("/")).replace("/", ".")
            listOf("$pn.*") + inlineMembers.map { name -> "$pn.$name" }
          } else {
            // entry is in root; no package
            inlineMembers
          }
        } else {
          emptyList()
        }
      }.also {
        inMemoryCache.inlineMembers(zipFile.name, it)
      }
  }

  private fun inlineMembers(kmDeclaration: KmDeclarationContainer): List {
    return inlineFunctions(kmDeclaration.functions) + inlineProperties(kmDeclaration.properties)
  }

  private fun inlineFunctions(functions: List): List {
    return functions
      .filter { Flag.Function.IS_INLINE(it.flags) }
      .map { it.name }
  }

  private fun inlineProperties(properties: List): List {
    return properties
      .filter { Flag.PropertyAccessor.IS_INLINE(it.flags) }
      .map { it.name }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy