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

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

There is a newer version: 2.6.1
Show newest version
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
@file:Suppress("UnstableApiUsage")

package com.autonomousapps.tasks

import com.autonomousapps.internal.KotlinMetadataVisitor
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.*
import com.autonomousapps.model.internal.InlineMemberCapability
import com.autonomousapps.model.internal.KtFile
import com.autonomousapps.model.internal.PhysicalArtifact
import com.autonomousapps.model.internal.PhysicalArtifact.Mode
import com.autonomousapps.model.internal.TypealiasCapability
import com.autonomousapps.model.internal.intermediates.InlineMemberDependency
import com.autonomousapps.model.internal.intermediates.TypealiasDependency
import com.autonomousapps.services.InMemoryCache
import kotlinx.metadata.*
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
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.io.File
import java.util.zip.ZipFile
import javax.inject.Inject

@CacheableTask
abstract class FindKotlinMagicTask @Inject constructor(
  private val workerExecutor: WorkerExecutor,
) : DefaultTask() {

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

  @get:Internal
  abstract val inMemoryCacheProvider: Property

  /** Not used by the task action, but necessary for correct input-output tracking, for reasons I do not recall. */
  @get:Classpath
  abstract val compileClasspath: ConfigurableFileCollection

  /** [PhysicalArtifact]s used to compile this project. */
  @get:PathSensitive(PathSensitivity.RELATIVE)
  @get:InputFile
  abstract val artifacts: RegularFileProperty

  /** Inline members in this project's dependencies. */
  @get:OutputFile
  abstract val outputInlineMembers: RegularFileProperty

  /** typealiases in this project's dependencies. */
  @get:OutputFile
  abstract val outputTypealiases: RegularFileProperty

  /**
   * Errors analyzing class files.
   *
   * @see Issue 1035
   * @see KT-60870
   */
  @get:OutputFile
  abstract val outputErrors: RegularFileProperty

  @TaskAction
  fun action() {
    workerExecutor.noIsolation().submit(FindKotlinMagicWorkAction::class.java) {
      artifacts.set([email protected])
      inlineUsageReport.set([email protected])
      typealiasReport.set([email protected])
      errorsReport.set([email protected])
      inMemoryCacheProvider.set([email protected])
    }
  }

  interface FindKotlinMagicParameters : WorkParameters {
    val artifacts: RegularFileProperty
    val inlineUsageReport: RegularFileProperty
    val typealiasReport: RegularFileProperty
    val errorsReport: RegularFileProperty
    val inMemoryCacheProvider: Property
  }

  abstract class FindKotlinMagicWorkAction : WorkAction {

    private val logger = getLogger()

    override fun execute() {
      val inlineUsageReportFile = parameters.inlineUsageReport.getAndDelete()
      val typealiasReportFile = parameters.typealiasReport.getAndDelete()
      val errorsReport = parameters.errorsReport.getAndDelete()

      val finder = KotlinMagicFinder(
        inMemoryCache = parameters.inMemoryCacheProvider.get(),
        artifacts = parameters.artifacts.fromJsonList(),
        errorsReport = errorsReport,
      )
      val inlineMembers = finder.inlineMembers
      val typealiases = finder.typealiases

      inlineUsageReportFile.bufferWriteJsonSet(inlineMembers)
      typealiasReportFile.bufferWriteJsonSet(typealiases)

      if (finder.didWriteErrors) {
        logger.warn("There were errors during inline member analysis. See ${errorsReport.toPath().toUri()}")
      } else {
        // This file must always exist, even if empty
        errorsReport.writeText("")
      }
    }
  }
}

internal class KotlinMagicFinder(
  private val inMemoryCache: InMemoryCache,
  artifacts: List,
  private val errorsReport: File,
) {

  private val logger = getLogger()
  var didWriteErrors = false

  val inlineMembers: Set
  val typealiases: Set

  init {
    val inlineMembersMut = mutableSetOf()
    val typealiasesMut = mutableSetOf()

    artifacts.asSequence()
      .filter {
        it.isJar() || it.containsClassFiles()
      }.map { artifact ->
        artifact to findKotlinMagic(artifact, artifact.mode)
      }.forEach { (artifact, capabilities) ->
        if (capabilities.inlineMembers.isNotEmpty()) {
          inlineMembersMut += InlineMemberDependency(artifact.coordinates, capabilities.inlineMembers)
        }
        if (capabilities.typealiases.isNotEmpty()) {
          typealiasesMut += TypealiasDependency(artifact.coordinates, capabilities.typealiases)
        }
      }

    inlineMembers = inlineMembersMut
    typealiases = typealiasesMut
  }

  // private fun analyzeDependencies(): Set {
  //   val inlineMembers = mutableSetOf()
  //   val typealiases = mutableSetOf()
  //
  //   artifacts.asSequence()
  //     .filter {
  //       it.isJar() || it.containsClassFiles()
  //     }.map { artifact ->
  //       artifact to findKotlinMagic(artifact, artifact.mode)
  //     }.forEach { (artifact, capabilities) ->
  //       if (capabilities.inlineMembers.isNotEmpty()) {
  //         inlineMembers += InlineMemberDependency(artifact.coordinates, capabilities.inlineMembers)
  //       }
  //       if (capabilities.typealiases.isNotEmpty()) {
  //         typealiases += TypealiasDependency(artifact.coordinates, capabilities.typealiases)
  //       }
  //     }
  //
  //   // return artifacts.asSequence()
  //   //   .filter {
  //   //     it.isJar() || it.containsClassFiles()
  //   //   }.map { artifact ->
  //   //     artifact to findKotlinMagic(artifact, artifact.mode)
  //   //   }.map { (artifact, capabilities) ->
  //   //     InlineMemberDependency(artifact.coordinates, inlineMembers)
  //   //   }.toSortedSet()
  // }

  /**
   * Returns either an empty set, if there are no inline members, or a set of [InlineMemberCapability.InlineMember]s
   * (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.
   */
  private fun findKotlinMagic(artifact: PhysicalArtifact, mode: Mode): KotlinCapabilities {
    val cached = findInCache(artifact)
    if (cached != null) return cached

    fun packageName(fileLike: String): String {
      return if (fileLike.contains('/')) {
        // entry is in a package
        fileLike.substringBeforeLast('/').replace('/', '.')
      } else {
        // entry is in root; no package
        ""
      }
    }

    val inlineMembers = mutableSetOf()
    val typealiases = mutableSetOf()

    when (mode) {
      Mode.ZIP -> {
        val zipFile = ZipFile(artifact.file)
        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 KotlinCapabilities.EMPTY
        }

        entries.asSequenceOfClassFiles()
          .mapNotNull { entry ->
            // TODO an entry with `META-INF/proguard/androidx-annotations.pro`
            val kotlinMagic = readClass(
              zipFile.getInputStream(entry).use { ClassReader(it.readBytes()) },
              entry.toString()
            ) ?: return@mapNotNull null

            entry to kotlinMagic
          }
          .forEach { (entry, kotlinMagic) ->
            if (kotlinMagic.inlineMembers != null) {
              inlineMembers += InlineMemberCapability.InlineMember(
                packageName = packageName(entry.name),
                // Guaranteed to be non-empty
                inlineMembers = kotlinMagic.inlineMembers
              )
            }

            if (kotlinMagic.typealiases != null) {
              typealiases += TypealiasCapability.Typealias(
                packageName = packageName(entry.name),
                typealiases = kotlinMagic.typealiases
              )
            }
          }
      }

      Mode.CLASSES -> {
        if (KtFile.fromDirectory(artifact.file).isEmpty()) {
          return KotlinCapabilities.EMPTY
        }

        artifact.file.walkBottomUp()
          .filter { it.isFile && it.name.endsWith(".class") }
          .mapNotNull { classFile ->
            val kotlinMagic = readClass(
              classFile.inputStream().use { ClassReader(it.readBytes()) },
              classFile.toString()
            ) ?: return@mapNotNull null

            classFile to kotlinMagic
          }
          .forEach { (classFile, kotlinMagic) ->
            if (kotlinMagic.inlineMembers != null) {
              inlineMembers += InlineMemberCapability.InlineMember(
                packageName = packageName(Files.asPackagePath(classFile)),
                // Guaranteed to be non-empty
                inlineMembers = kotlinMagic.inlineMembers
              )
            }

            if (kotlinMagic.typealiases != null) {
              typealiases += TypealiasCapability.Typealias(
                packageName = packageName(Files.asPackagePath(classFile)),
                typealiases = kotlinMagic.typealiases
              )
            }
          }
      }
    }

    val kotlinCapabilities = KotlinCapabilities(inlineMembers, typealiases)

    // cache
    putInCache(artifact, kotlinCapabilities)

    return kotlinCapabilities
  }

  private fun findInCache(artifact: PhysicalArtifact): KotlinCapabilities? {
    return inMemoryCache.kotlinCapabilities(artifact.file.absolutePath)
  }

  private fun putInCache(artifact: PhysicalArtifact, capabilities: KotlinCapabilities) {
    inMemoryCache.inlineMembers(artifact.file.absolutePath, capabilities)
  }

  /** Returned set is either null or non-empty. */
  private fun readClass(classReader: ClassReader, classFile: String): KotlinMagic? {
    val metadataVisitor = KotlinMetadataVisitor(logger)
    classReader.accept(metadataVisitor, 0)

    var inlineMembers: Set? = null
    var typealiases: Set? = null

    metadataVisitor.builder?.let { header ->
      // Can throw `kotlinx.metadata.InconsistentKotlinMetadataException`, which is unfortunately `internal` to its
      // module. It extends `IllegalArgumentException`, so we catch that. This can happen if we attempt to read a class
      // file compiled by a "very old" version of Kotlin.
      // See https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1035
      // See https://youtrack.jetbrains.com/issue/KT-60870
      val metadata = try {
        KotlinClassMetadata.readLenient(header.build())
      } catch (e: IllegalArgumentException) {
        logger.debug("Can't read class file '$classFile'")
        errorsReport.appendText("Can't read class file '$classFile'\n")
        didWriteErrors = true
        return null
      }

      when (metadata) {
        is KotlinClassMetadata.Class -> {
          inlineMembers = inlineMembers(metadata.kmClass)
          typealiases = typealiases(metadata.kmClass)
        }

        is KotlinClassMetadata.FileFacade -> {
          inlineMembers = inlineMembers(metadata.kmPackage)
          typealiases = typealiases(metadata.kmPackage)
        }

        is KotlinClassMetadata.MultiFileClassPart -> {
          inlineMembers = inlineMembers(metadata.kmPackage)
          typealiases = typealiases(metadata.kmPackage)
        }

        is KotlinClassMetadata.SyntheticClass -> logger.debug("Ignoring SyntheticClass $classFile")
        is KotlinClassMetadata.MultiFileClassFacade -> logger.debug("Ignoring MultiFileClassFacade $classFile")
        is KotlinClassMetadata.Unknown -> logger.debug("Ignoring Unknown $classFile")
      }
    } ?: return null

    // It's part of the contract to never return an empty set
    return KotlinMagic(
      inlineMembers = inlineMembers?.ifEmpty { null },
      typealiases = typealiases?.ifEmpty { null },
    )

    // It's part of the contract to never return an empty set
    // return inlineMembers?.ifEmpty { null }
  }

  private class KotlinMagic(
    val inlineMembers: Set?,
    val typealiases: Set?,
  )

  private fun inlineMembers(kmDeclaration: KmDeclarationContainer): Set {
    fun inlineFunctions(functions: List): Sequence {
      return functions.asSequence()
        .filter { it.isInline }
        .map { it.name }
    }

    fun inlineProperties(properties: List): Sequence {
      return properties.asSequence()
        .filter { it.getter.isInline }
        .map { it.name }
    }

    return (inlineFunctions(kmDeclaration.functions) + inlineProperties(kmDeclaration.properties)).toSortedSet()
  }

  private fun typealiases(kmDeclaration: KmDeclarationContainer): Set {
    fun KmType.name(): String {
      // classifier is variable, so we can't smartcast in the when statement without something like this
      return classifier.run {
        when (this) {
          is KmClassifier.Class -> name
          is KmClassifier.TypeAlias -> name
          is KmClassifier.TypeParameter -> id.toString()
        }
      }
    }

    return kmDeclaration.typeAliases.mapToOrderedSet {
      TypealiasCapability.Typealias.Alias(it.name, it.expandedType.name())
    }
  }
}

internal class KotlinCapabilities(
  val inlineMembers: Set,
  val typealiases: Set,
) {
  companion object {
    val EMPTY = KotlinCapabilities(emptySet(), emptySet())
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy