blob: ab57d62eb8902b2ea94854be4fe426d61646cb86 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("SymbolTableUtil")
package android.databinding.tool.util
import android.databinding.tool.expr.ResourceExpr
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMultimap
import java.io.File
private const val STYLEABLE: String = "styleable"
val EMPTY_RESOURCES: Resources = Resources(null)
data class Resources(val symbolTables: ImmutableList<SymbolTable>?) {
/**
* Returns the prefix to be used in R class references in Java/Kotlin. For example: "android."
* for "android.R.color.white", "com.my.lib." for "com.my.lib.R.string.text" and an empty string
* for default R class reference e.g. "R.attr.my_attr".
*
* This method accepts data-binding specific "types", e.g. using "text" (for "string" resources)
* or "intArray" ( for "array" resources).
*/
fun getRPackagePrefix(packageName: String?, type: String, name: String): String {
when {
"android" == packageName -> {
return "android."
}
symbolTables != null -> {
for (table in symbolTables) {
if (table.contains(type, name)) {
// For local module references, return empty prefix as the R class will use
// the same local package.
if (table.rPackage.isEmpty()) return ""
// For any non-local references, use the full package plus a "." for the
// prefix.
return "${table.rPackage}."
}
}
throw RuntimeException("Resource not found: $type $name.")
}
else -> {
// If we don't have a list of resources, it means the local R class contains all
// resources, both local and from dependencies, so just use the local R class.
return ""
}
}
}
}
data class SymbolTable constructor(
val rPackage: String,
val resources: ImmutableMultimap<String, String>) {
fun contains(type: String, name: String) : Boolean {
return resources[type].orEmpty().contains(name)
}
}
fun parseRTxtFiles(
localRFile: File?,
dependenciesRFiles: List<File>?,
mergedDependenciesRFile: File?
) : Resources {
// If not using non-transitive R, return empty Resources (only local R class should be used)
if (localRFile == null) return EMPTY_RESOURCES
if (dependenciesRFiles != null && mergedDependenciesRFile != null) {
error("Unexpected error: Both listed and merged dependencies R files present.")
}
val symbolTables = ImmutableList.builder<SymbolTable>()
// local resources at the front of the list
symbolTables.add(parseLocalRTxt(localRFile))
when {
dependenciesRFiles != null -> {
// then add the rest of the dependencies, in order
dependenciesRFiles.forEach { symbolTables.add(parsePackageAwareRTxt(it)) }
}
mergedDependenciesRFile != null -> {
parseMergedPackageAwareRTxt(mergedDependenciesRFile, symbolTables)
}
else -> {
error("Unexpected error: Missing dependency resources")
}
}
return Resources(symbolTables.build())
}
fun parseLocalRTxt(file: File): SymbolTable {
file.useLines {
val iterator: Iterator<String> = it.iterator()
// First line is a comment
if (!iterator.hasNext())
error("Incorrect package-aware R.txt format. " +
"Failed to parse file: ${file.absolutePath}")
iterator.next()
// Second line is local package we can ignore
if (!iterator.hasNext())
error("Resource list needs to contain the local package. " +
"Failed to parse file: ${file.absolutePath}")
val localPackage = iterator.next()
if (localPackage != "local")
error("Illegal local package '$localPackage' in file ${file.absolutePath}")
// Finally we can start parsing the resources
val resources = try {
readResources(iterator)
} catch (e: java.lang.IllegalStateException) {
throw IllegalStateException("Failed to parse file: ${file.absolutePath}", e)
}
// Local table should use a blank package to use default R class
return SymbolTable("", resources)
}
}
fun parsePackageAwareRTxt(file: File) : SymbolTable {
// R.txt is verified before being written, no need to re-verify package or resource names.
file.useLines {
val iterator: Iterator<String> = it.iterator()
// First line contains the package
if (!iterator.hasNext())
error("Resource list needs to contain the local package. " +
"Failed to parse file: ${file.absolutePath}")
val pckg: String = iterator.next()
val resources = try {
readResources(iterator)
} catch (e: IllegalStateException) {
throw IllegalStateException("Failed to parse file: ${file.absolutePath}", e)
}
return SymbolTable(pckg, resources)
}
}
fun parseMergedPackageAwareRTxt(file: File, symbolTables: ImmutableList.Builder<SymbolTable>) {
file.useLines {
val iterator: Iterator<String> = it.iterator()
// As a workaround for KAPT resolving files at configuration time, dependencies' R files are
// merged into one file and then separated by an empty line, for example:
// com.mid.lib
// string foo
//
// com.leaf.lib1
// string bar
//
// com.empty.lib
//
// com.final.lib
// string hello
//
// Each dependency's chunk will start with a line with that R package, followed by the list
// of resources defined in that dependency (non-transitive) - or empty if there were no
// resources.
// Loop through all the lines.
while (iterator.hasNext()) {
// First line contains the dependency's package
if (!iterator.hasNext())
error("Resource list needs to contain the local package. " +
"Failed to parse file: ${file.absolutePath}")
val pckg: String = iterator.next()
val resources = try {
// Until the next empty line, this method will parse the resources from the current
// dependency.
readResources(iterator)
} catch (e: IllegalStateException) {
throw IllegalStateException("Failed to parse file: ${file.absolutePath}", e)
}
symbolTables.add(SymbolTable(pckg, resources))
}
}
}
fun readResources(lines: Iterator<String>) : ImmutableMultimap<String, String> {
val resources = ImmutableMultimap.builder<String, String>()
// Loop through the resources for a single dependency. The package has already been consumed,
// so read the resources until an empty line or EOF.
while (lines.hasNext()) {
val line = lines.next()
if (line.isEmpty())
// No more resources within this dependency.
return resources.build()
val chunks = line.split(" ")
// Format is <type> <name> for all resources apart from Styleables.
if (chunks.size < 2 || (chunks[0] != STYLEABLE && chunks.size != 2))
error("Illegal line in R.txt: '$line'")
addResource(chunks[0], sanitizeName(chunks[1]), resources)
if (chunks[0] == STYLEABLE) {
// For styleables the format is <type> <name> <child1> <child2> ... <childN>
// The resulting children need to be added as Styleable <parent>_<child>.
// It's possible for a styleable to not have children at all.
val parent = sanitizeName(chunks[1])
for (i in 2 until chunks.size) {
// Styleable children exist in the R class as R.styleable.parent_child.
resources.put(STYLEABLE, "${parent}_${sanitizeName(chunks[i])}")
}
}
}
return resources.build()
}
fun addResource(
type: String,
name: String,
resourcesBuilder: ImmutableMultimap.Builder<String, String>) {
// Some expressions in data-binding correspond to different names in the R class. To be able
// to verify them, we need to add the resource to all matching expressions, e.g:
// - "string" to "text"
// - "array" to "intArray" AND "stringArray" AND "typedArray"
ResourceExpr.R_OBJECT_TO_RESOURCE_TYPE[type]?.forEach {
resourcesBuilder.put(it, name)
}
resourcesBuilder.put(type, name)
}
private fun sanitizeName(name: String): String {
return name.replace('.', '_').replace(':', '_')
}