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

com.autonomousapps.internal.BytecodeParsers.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
package com.autonomousapps.internal

import com.autonomousapps.internal.ClassNames.canonicalize
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.JAVA_FQCN_REGEX_SLASHY
import com.autonomousapps.internal.utils.asSequenceOfClassFiles
import com.autonomousapps.internal.utils.getLogger
import com.autonomousapps.model.internal.intermediates.consumer.ExplodingBytecode
import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
import org.gradle.api.logging.Logger
import java.io.File

internal sealed class ClassReferenceParser(private val buildDir: File) {

  /** Source is either a jar or set of class files. */
  protected abstract fun parseBytecode(): Set

  protected fun relativize(file: File) = file.toRelativeString(buildDir)

  // TODO some jars only have metadata. What to do about them?
  // 1. e.g. kotlin-stdlib-common-1.3.50.jar
  // 2. e.g. legacy-support-v4-1.0.0/jars/classes.jar
  internal fun analyze(): Set {
    return parseBytecode()
  }
}

/** Given a set of .class files, produce a set of FQCN references present in that set. */
internal class ClassFilesParser(
  private val classes: Set,
  buildDir: File
) : ClassReferenceParser(buildDir) {

  private val logger = getLogger()

  override fun parseBytecode(): Set {
    return classes.asSequenceOfClassFiles()
      .map { classFile ->
        val classFilePath = classFile.path
        val explodedClass = classFile.inputStream().use {
          BytecodeReader(it.readBytes(), logger, classFilePath).parse()
        }

        ExplodingBytecode(
          relativePath = relativize(classFile),
          className = explodedClass.className,
          sourceFile = explodedClass.source,
          nonAnnotationClasses = explodedClass.nonAnnotationClasses,
          annotationClasses = explodedClass.annotationClasses,
          invisibleAnnotationClasses = explodedClass.invisibleAnnotationClasses,
          binaryClassAccesses = explodedClass.binaryClasses,
        )
      }
      .toSet()
  }
}

private class BytecodeReader(
  private val bytes: ByteArray,
  private val logger: Logger,
  private val classFilePath: String,
) {
  /**
   * This (currently, maybe forever) fails to detect constant usage in Kotlin-generated class files.
   * Works just fine for Java (but
   * [not the ecj compiler](https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/issues/735)).
   *
   * Returns a pair of values:
   * 1. The "source" of the class file (the source file name, like "Main.kt").
   * 2. The classes used by that class file.
   */
  fun parse(): ExplodedClass {
    val constantPool = ConstantPoolParser.getConstantPoolClassReferences(bytes, classFilePath)
      // Constant pool has a lot of weird bullshit in it
      .filter { JAVA_FQCN_REGEX_SLASHY.matches(it) }

    val classAnalyzer = ClassReader(bytes).let { classReader ->
      ClassAnalyzer(logger).apply {
        classReader.accept(this, 0)
      }
    }

    val usedVisibleAnnotationClasses = classAnalyzer.classes.asSequence()
      .filter { it.kind == ClassRef.Kind.ANNOTATION_VISIBLE }
      .map { it.classRef }
      .toSet()
    // TODO(tsr): use this somehow? I think these should be considered compileOnly candidates
    //  Look at `CompileOnlySpec#annotations can be compileOnly`. It detects usage of Producer because it is imported,
    //  but doesn't see it in the bytecode. I think this can be improved. Finding it in the bytecode is preferable to
    //  the import heuristic. We'll need to differentiate in/visible annotations though.
    val usedInvisibleAnnotationClasses = classAnalyzer.classes.asSequence()
      .filter { it.kind == ClassRef.Kind.ANNOTATION_HIDDEN }
      .map { it.classRef }
      .toSet()
    val usedNonAnnotationClasses = classAnalyzer.classes.asSequence()
      .filter { it.kind == ClassRef.Kind.NOT_ANNOTATION }
      .map { it.classRef }
      .toSet()

    return ExplodedClass(
      source = classAnalyzer.source,
      className = canonicalize(classAnalyzer.className),
      nonAnnotationClasses = constantPool.asSequence().plus(usedNonAnnotationClasses).fixup(classAnalyzer),
      annotationClasses = usedVisibleAnnotationClasses.asSequence().fixup(classAnalyzer),
      invisibleAnnotationClasses = usedInvisibleAnnotationClasses.asSequence().fixup(classAnalyzer),
      binaryClasses = classAnalyzer.getBinaryClasses().fixup(classAnalyzer),
    )
  }

  // Change this in concert with the Map.fixup() function below
  private fun Sequence.fixup(classAnalyzer: ClassAnalyzer): Set {
    return this
      // Filter out `java` packages, but not `javax`
      .filterNot { it.startsWith("java/") }
      // Filter out a "used class" that is exactly the class under analysis
      .filterNot { it == classAnalyzer.className }
      // More human-readable
      .map { canonicalize(it) }
      .toSortedSet()
  }

  // TODO(tsr): decide whether to dottify (canonicalize) the class names or leave them slashy
  // Change this in concert with the Sequence.fixup() function above
  private fun Map>.fixup(classAnalyzer: ClassAnalyzer): Map> {
    return this
      // Filter out `java` packages, but not `javax`
      .filterKeys { !it.startsWith("java/") }
      // Filter out a "used class" that is exactly the class under analysis
      .filterKeys { it != classAnalyzer.className }
  }
}

private class ExplodedClass(
  val source: String?,
  val className: String,
  val nonAnnotationClasses: Set,
  val annotationClasses: Set,
  val invisibleAnnotationClasses: Set,
  val binaryClasses: Map>,
)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy