com.autonomousapps.internal.parse.explodeAndroidResSource.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dependency-analysis-gradle-plugin Show documentation
Show all versions of dependency-analysis-gradle-plugin Show documentation
Analyzes dependency usage in Android and JVM projects
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal.parse
import com.autonomousapps.internal.ManifestParser
import com.autonomousapps.internal.ManifestParser.ManifestParseException
import com.autonomousapps.internal.ManifestParser.ParseResult
import com.autonomousapps.internal.utils.JAVA_FQCN_REGEX_DOTTY
import com.autonomousapps.internal.utils.buildDocument
import com.autonomousapps.internal.utils.document.attrs
import com.autonomousapps.internal.utils.document.contentReferences
import com.autonomousapps.internal.utils.document.map
import com.autonomousapps.internal.utils.document.mapNotNull
import com.autonomousapps.internal.utils.filterToOrderedSet
import com.autonomousapps.internal.utils.mapToSet
import com.autonomousapps.model.internal.AndroidResSource
import org.w3c.dom.Document
import org.xml.sax.SAXParseException
import java.io.File
internal class AndroidLayoutParser(
private val projectDir: File,
private val layouts: Set,
) {
val explodedLayouts: Set = parseLayouts()
private fun parseLayouts(): Set {
return layouts.asSequence()
.map { layoutFile ->
layoutFile to buildDocument(layoutFile).getElementsByTagName("*")
.map { it.nodeName }
.filterToOrderedSet { JAVA_FQCN_REGEX_DOTTY.matches(it) }
}.map { (file, classes) ->
ExplodedLayout(
relativePath = file.toRelativeString(projectDir),
usedClasses = classes
)
}
.toSet()
}
}
internal class AndroidResParser(
projectDir: File,
resources: Iterable,
) {
internal class Container {
val attrRefs = mutableSetOf()
val newIds = mutableSetOf()
fun nonLocalAttrRefs(): Set = attrRefs - newIds
}
private val container = Container()
val androidResSource: Set = resources
.mapNotNull {
try {
it to buildDocument(it)
} catch (_: SAXParseException) {
// https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1211
null
}
}
.mapToSet { (file, doc) ->
// Populate the container
extractAttrsFromResourceXml(doc)
extractContentReferencesFromResourceXml(doc)
ExplodedRes(
relativePath = file.toRelativeString(projectDir),
styleParentRefs = extractStyleParentsFromResourceXml(doc),
attrRefs = container.nonLocalAttrRefs()
)
}
// e.g., "Theme.AppCompat.Light.DarkActionBar"
private fun extractStyleParentsFromResourceXml(doc: Document): Set =
doc.getElementsByTagName("style").mapNotNull {
it.attributes.getNamedItem("parent")?.nodeValue
}.mapToSet {
AndroidResSource.StyleParentRef.of(it)
}
private fun extractAttrsFromResourceXml(doc: Document) {
doc.attrs().forEach {
AndroidResSource.AttrRef.from(it, container)
}
}
private fun extractContentReferencesFromResourceXml(doc: Document) {
doc.contentReferences().entries.forEach {
AndroidResSource.AttrRef.from(it.key to it.value, container)
}
}
}
internal class AndroidManifestParser(
private val manifests: Iterable,
private val projectDir: File,
private val namespace: String,
) {
private val parser = ManifestParser(namespace)
val explodedManifests: List = compute()
private fun compute(): List {
// If we have a namespace defined by the Android DSL, it is safe to parse the manifests immediately.
if (namespace.isNotBlank()) return parseManifests()
// Otherwise, parsing may result in an exception. We attempt to parse each file. If there's a failure, we catch it
// and put it in a queue for a second attempt. One of the manifests should parse correctly. We'll get the namespace
// from that one (probably src/main/AndroidManifest.xml).
val parseResults = mutableListOf>()
val malformedManifests = mutableListOf()
for (manifest in manifests) {
if (!manifest.exists()) continue
try {
parseResults += manifest to parser.parse(manifest)
} catch (_: ManifestParseException) {
malformedManifests += manifest
}
}
val mainNamespace = parseResults.firstOrNull()?.second?.packageName.orEmpty()
val malformedParser = ManifestParser(mainNamespace)
for (remainder in malformedManifests) {
parseResults += remainder to malformedParser.parse(remainder)
}
return parseResults.toExplodedManifests()
}
private fun parseManifests(): List = manifests
.filter { it.exists() }
.map { it to parser.parse(it) }
.toExplodedManifests()
private fun Iterable>.toExplodedManifests(): List {
return map { it.toExplodedManifest() }
}
private fun Pair.toExplodedManifest(): ExplodedManifest {
val file = first
val parseResult = second
val applicationName = parseResult.applicationName
val theme = AndroidResSource.AttrRef.style(parseResult.theme)
return ExplodedManifest(
relativePath = file.toRelativeString(projectDir),
applicationName = applicationName,
theme = theme,
)
}
}
internal class AndroidResBuilder(private val relativePath: String) {
val styleParentRefs = mutableSetOf()
val attrRefs = mutableSetOf()
val usedClasses = mutableSetOf()
fun concat(other: AndroidResBuilder): AndroidResBuilder {
styleParentRefs.addAll(other.styleParentRefs)
attrRefs.addAll(other.attrRefs)
usedClasses.addAll(other.usedClasses)
return this
}
fun build() = AndroidResSource(
relativePath = relativePath,
styleParentRefs = styleParentRefs.toSortedSet(),
attrRefs = attrRefs.toSortedSet(),
usedClasses = usedClasses.toSortedSet()
)
}
internal class ExplodedLayout(
val relativePath: String,
val usedClasses: Set,
)
internal class ExplodedRes(
val relativePath: String,
val styleParentRefs: Set,
val attrRefs: Set,
)
internal class ExplodedManifest(
val relativePath: String,
val applicationName: String,
val theme: AndroidResSource.AttrRef?,
)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy