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

com.autonomousapps.model.internal.Source.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.model.internal

import com.autonomousapps.internal.parse.AndroidResParser
import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel

@JsonClass(generateAdapter = false, generator = "sealed:type")
internal sealed class Source(
  /** Source file path relative to project dir (e.g. `src/main/com/foo/Bar.kt`). */
  open val relativePath: String,
) : Comparable {

  override fun compareTo(other: Source): Int = when (this) {
    is AndroidAssetSource -> {
      when (other) {
        is AndroidAssetSource -> defaultCompareTo(other)
        is AndroidResSource -> 1
        is CodeSource -> 1
      }
    }

    is AndroidResSource -> {
      when (other) {
        is AndroidAssetSource -> -1
        is AndroidResSource -> defaultCompareTo(other)
        is CodeSource -> 1
      }
    }

    is CodeSource -> {
      when (other) {
        is AndroidAssetSource -> -1
        is AndroidResSource -> -1
        is CodeSource -> defaultCompareTo(other)
      }
    }
  }

  private fun defaultCompareTo(other: Source): Int = relativePath.compareTo(other.relativePath)
}

/** A single `.class` file in this project. */
@TypeLabel("code")
@JsonClass(generateAdapter = false)
internal data class CodeSource(
  override val relativePath: String,
  /** Source language. */
  val kind: Kind,

  /** The name of this class. */
  val className: String,

  /** Every class discovered in the bytecode of [className], and not as an annotation. */
  val usedNonAnnotationClasses: Set,

  /** Every class discovered in the bytecode of [className], and as a visible annotation. */
  val usedAnnotationClasses: Set,

  /** Every class discovered in the bytecode of [className], and as an invisible annotation. */
  val usedInvisibleAnnotationClasses: Set,

  /** Every class discovered in the bytecode of [className], and which is exposed as part of the ABI. */
  val exposedClasses: Set,

  /** Every import in this source file. */
  val imports: Set,

  // /** Every [MemberAccess] to another class from [this class][className]. */
  // val binaryClassAccesses: Map>,
) : Source(relativePath) {

  enum class Kind {
    JAVA,
    KOTLIN,
    GROOVY,
    SCALA,

    /** Probably generated source. */
    UNKNOWN,
  }
}

/** A single `.xml` (Android resource) file in this project. */
@TypeLabel("android_res")
@JsonClass(generateAdapter = false)
internal data class AndroidResSource(
  override val relativePath: String,
  val styleParentRefs: Set,
  val attrRefs: Set,
  /** Layout files have class references. */
  val usedClasses: Set,
) : Source(relativePath) {

  @JsonClass(generateAdapter = false)
  /** The parent of a style resource, e.g. "Theme.AppCompat.Light.DarkActionBar". */
  data class StyleParentRef(val styleParent: String) : Comparable {

    init {
      assertNoDots("styleParent", styleParent)
    }

    override fun compareTo(other: StyleParentRef): Int = styleParent.compareTo(other.styleParent)

    internal companion object {
      fun of(styleParent: String): StyleParentRef {
        // Transform Theme.AppCompat.Light.DarkActionBar to Theme_AppCompat_Light_DarkActionBar
        return StyleParentRef(styleParent.toCanonicalResString())
      }
    }
  }

  /** Any attribute that looks like a reference to another resource. */
  @JsonClass(generateAdapter = false)
  data class AttrRef(val type: String, val id: String) : Comparable {

    init {
      assertNoDots("id", id)
    }

    override fun compareTo(other: AttrRef): Int = compareBy(
      { it.type },
      { it.id }
    ).compare(this, other)

    companion object {

      /**
       * This will match references to resources `@[:]/`:
       *
       * - `@drawable/foo`
       * - `@android:drawable/foo`
       *
       * @see Accessing resources from XML
       */
      private val TYPE_REGEX = Regex("""@(\w+:)?(?\w+)/(\w+)""")

      /**
       * TODO(tsr): this regex is too permissive. I only want `@+id/...`, but I lazily just copied the above with a
       *  small tweak.
       *
       * This will match references to resources `@+[:]/`:
       *
       * - `@+drawable/foo`
       * - `@+android:drawable/foo`
       *
       * @see Accessing resources from XML
       */
      private val NEW_ID_REGEX = Regex("""@\+(\w+:)?(?\w+)/(\w+)""")

      /**
       * This will match references to style attributes `?[:][/]`:
       *
       * - `?foo`
       * - `?attr/foo`
       * - `?android:foo`
       * - `?android:attr/foo`
       *
       * @see Referencing style attributes
       */
      private val ATTR_REGEX = Regex("""\?(\w+:)?(\w+/)?(?\w+)""")

      fun style(name: String): AttrRef? {
        return if (name.isBlank()) null else AttrRef("style", name.toCanonicalResString())
      }

      /**
       * Push [AttrRef]s into the [container], either as external references or as internal "new IDs" (`@+id`). The
       * purpose of this approach is to avoid parsing the XML file twice.
       */
      internal fun from(mapEntry: Pair, container: AndroidResParser.Container) {
        if (mapEntry.isNewId()) {
          newId(mapEntry)?.let { container.newIds += it }
        }

        from(mapEntry)?.let { container.attrRefs += it }
      }

      /**
       * On consumer side, only get attrs from the XML document when:
       * 1. They're not a new ID (don't start with `@+id`)
       * 2. They're not a tools namespace (don't start with `tools:`)
       * 3. They're not a data binding expression (don't start with `@{` and end with `}`)
       * 4. Their value starts with `?`, like `?themeColor`.
       * 5. Their value starts with `@`, like `@drawable/`.
       *
       * Will return `null` if the map entry doesn't match an expected pattern.
       */
      fun from(mapEntry: Pair): AttrRef? {
        if (mapEntry.isNewId()) return null
        if (mapEntry.isToolsAttr()) return null
        if (mapEntry.isDataBindingExpression()) return null

        val id = mapEntry.second
        return when {
          ATTR_REGEX.matchEntire(id) != null -> AttrRef(
            type = "attr",
            id = id.attr().toCanonicalResString()
          )

          TYPE_REGEX.matchEntire(id) != null -> AttrRef(
            type = id.type(),
            // @drawable/some_drawable => some_drawable
            id = id.substringAfterLast('/').toCanonicalResString()
          )
          // Swipe refresh layout defines an attr (https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:swiperefreshlayout/swiperefreshlayout/src/main/res-public/values/attrs.xml;l=19):
          //   
          // A consumer may provide a value for this attr:
          //   ...
          // See ResSpec.detects attr usage in res file.
          mapEntry.first == "name" -> AttrRef(
            type = "attr",
            id = id.toCanonicalResString()
          )

          else -> null
        }
      }

      /**
       * Returns an [AttrRef] when [AttrRef.type] is a new id ("@+id"), so that we can strip references to that id in
       * the current res file being analyzed. Such references are local, not from a dependency.
       */
      private fun newId(mapEntry: Pair): AttrRef? {
        if (!mapEntry.isNewId()) return null

        val id = mapEntry.second
        return when {
          NEW_ID_REGEX.matchEntire(id) != null -> AttrRef(
            type = "id",
            // @drawable/some_drawable => some_drawable
            id = id.substringAfterLast('/').toCanonicalResString()
          )

          else -> null
        }
      }

      private fun Pair.isNewId() = second.startsWith("@+id")
      private fun Pair.isToolsAttr() = first.startsWith("tools:")
      private fun Pair.isDataBindingExpression() = first.startsWith("@{") && first.endsWith("}")

      // @drawable/some_drawable => drawable
      // @android:drawable/some_drawable => drawable
      private fun String.type(): String = TYPE_REGEX.find(this)!!.groups["type"]!!.value

      // ?themeColor => themeColor
      // ?attr/themeColor => themeColor
      private fun String.attr(): String = ATTR_REGEX.find(this)!!.groups["attr"]!!.value
    }
  }
}

@TypeLabel("android_assets")
@JsonClass(generateAdapter = false)
internal data class AndroidAssetSource(
  override val relativePath: String,
) : Source(relativePath)

private fun String.toCanonicalResString(): String = replace('.', '_')

private fun assertNoDots(name: String, value: String) {
  require(!value.contains('.')) {
    "'$name' field must not contain dot characters. Was '${value}'"
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy