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

com.autonomousapps.internal.classReferenceParsers.kt Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
package com.autonomousapps.internal

import com.autonomousapps.advice.VariantFile
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.*
import org.gradle.api.logging.Logger
import java.io.File
import java.util.zip.ZipFile

internal sealed class ProjectClassReferenceParser(
  protected val variantFiles: Set,
  private val layouts: Set,
  private val testFiles: Set
) {

  private val logger = getLogger()

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

  protected fun variantsFromFile(file: File): Set {
    return variantsFromPath(file.path)
  }

  /**
   * Associate file paths to variants/source sets.
   */
  protected fun variantsFromPath(path: String): Set {
    val fileExtension = path.substring(path.lastIndexOf("."))
    return variantFiles.filter {
      path.endsWith("${it.filePath}${fileExtension}")
    }.mapToOrderedSet {
      it.variant
    }
  }

  private fun parseLayouts(): List {
    return layouts.flatMap { layoutFile ->
      val variants = variantsFromFile(layoutFile)
      buildDocument(layoutFile).getElementsByTagName("*")
        .map { it.nodeName }
        .filter { nodeName ->
          nodeName.contains(".")
        }.map {
          VariantClass(it, variants)
        }
    }
  }

  private fun parseTestSource(): List {
    return testFiles
      .filter { it.extension == "class" }
      .map { classFile ->
        val variants = variantsFromFile(classFile)
        val usedClasses = classFile.inputStream().use { BytecodeParser(it.readBytes(), logger).parse() }
        variants to usedClasses
      }.flatMap { (variants, classes) ->
        classes.map {
          VariantClass(it, variants)
        }
      }
  }

  // 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 {
    val variants = parseBytecode().plus(parseLayouts()).plus(parseTestSource())
    return variants.merge()
  }

  private fun List.merge(): Set {
    // a Collection is functionally a map
    val map = LinkedHashMap>()
    forEach {
      val theClass = it.theClass
      val variants = it.variants
      map.merge(theClass, variants.toSortedSet()) { oldSet, newSet ->
        oldSet.apply { addAll(newSet) }
      }
    }
    return map.map { (theClass, variants) ->
      VariantClass(theClass, variants)
    }.toSortedSet()
  }
}

/**
 * Given a jar and, optionally, and a set of Android layout files, produce a set of FQCN references
 * present in these inputs, as strings. These inputs are part of a single logical whole, viz., the
 * Gradle project being analyzed.
 */
internal class JarReader(
  variantFiles: Set,
  jarFile: File,
  layouts: Set,
  testFiles: Set
) : ProjectClassReferenceParser(
  variantFiles = variantFiles,
  layouts = layouts,
  testFiles = testFiles
) {

  private val logger = getLogger()
  private val zipFile = ZipFile(jarFile)

  override fun parseBytecode(): List {
    return zipFile.asClassFiles()
      .map { classEntry ->
        val variants = variantsFromPath(classEntry.name)
        val usedClasses = zipFile.getInputStream(classEntry).use { BytecodeParser(it.readBytes(), logger).parse() }
        variants to usedClasses
      }.flatMap { (variants, classes) ->
        classes.map { VariantClass(it, variants) }
      }
  }
}

/**
 * Given a set of .class files and, optionally, and a set of Android layout files, produce a set of
 * FQCN references present in these inputs, as strings. These inputs are part of a single logical
 * whole, viz., the Gradle project being analyzed.
 */
internal class ClassSetReader(
  private val classes: Set,
  variantFiles: Set,
  layouts: Set,
  testFiles: Set
) : ProjectClassReferenceParser(
  variantFiles = variantFiles,
  layouts = layouts,
  testFiles = testFiles
) {

  private val logger = getLogger()

  override fun parseBytecode(): List {
    return classes.map { classFile ->
      val variants = variantsFromFile(classFile)
      val usedClasses = classFile.inputStream().use { BytecodeParser(it.readBytes(), logger).parse() }
      variants to usedClasses
    }.flatMap { (variants, classes) ->
      classes.map {
        VariantClass(it, variants)
      }
    }
  }
}

private class BytecodeParser(
  private val bytes: ByteArray,
  private val logger: Logger
) {
  /**
   * This (currently, maybe forever) fails to detect constant usage in Kotlin-generated class files. Works just fine
   * for Java.
   */
  fun parse(): Set {
    val constantPool = ConstantPoolParser.getConstantPoolClassReferences(bytes)
      // Constant pool has a lot of weird bullshit in it
      .filter { JAVA_FQCN_REGEX_SLASHY.matches(it) }

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

    return constantPool.plus(classEntries)
      // Filter out `java` packages, but not `javax`
      .filterNot { it.startsWith("java/") }
      .mapToSet { it.replace("/", ".") }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy