Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.autonomousapps.model.internal.Source.kt Maven / Gradle / Ivy
// 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}'"
}
}