Browse Source

Generate initializer functions in the Res file to avoid the MethodTooLargeException (#4205)

pull/4215/head
Konstantin 3 months ago committed by GitHub
parent
commit
b1e86ade36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 155
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  2. 87
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt
  3. 194
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  4. 6
      gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt
  5. 26
      gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

155
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

@ -2,6 +2,8 @@ package org.jetbrains.compose.resources
import com.squareup.kotlinpoet.*
import java.nio.file.Path
import java.util.SortedMap
import java.util.TreeMap
import kotlin.io.path.invariantSeparatorsPathString
internal enum class ResourceType(val typeName: String) {
@ -26,12 +28,14 @@ internal data class ResourceItem(
val path: Path
)
private fun ResourceItem.getClassName(): ClassName = when (type) {
private fun ResourceType.getClassName(): ClassName = when (this) {
ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource")
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource")
}
private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder {
val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier")
val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier")
@ -101,85 +105,118 @@ internal fun getResFileSpec(
//type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
packageName: String
): FileSpec = FileSpec.builder(packageName, "Res").apply {
addType(TypeSpec.objectBuilder("Res").apply {
addModifiers(KModifier.INTERNAL)
): FileSpec =
FileSpec.builder(packageName, "Res").apply {
addAnnotation(
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
.addMember("org.jetbrains.compose.resources.InternalResourceApi::class")
.build()
)
addAnnotation(
AnnotationSpec.builder(ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi"))
.addMember("org.jetbrains.compose.resources.ExperimentalResourceApi::class")
.build()
)
//readFileBytes
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
addFunction(
FunSpec.builder("readBytes")
.addKdoc("""
//we need to sort it to generate the same code on different platforms
val sortedResources = sortResources(resources)
addType(TypeSpec.objectBuilder("Res").apply {
addModifiers(KModifier.INTERNAL)
addAnnotation(
AnnotationSpec.builder(
ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi")
).build()
)
//readFileBytes
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
addFunction(
FunSpec.builder("readBytes")
.addKdoc(
"""
Reads the content of the resource file at the specified path and returns it as a byte array.
Example: `val bytes = Res.readBytes("files/key.bin")`
@param path The path of the file to read in the compose resource's directory.
@return The content of the file as a byte array.
""".trimIndent())
.addParameter("path", String::class)
.addModifiers(KModifier.SUSPEND)
.returns(ByteArray::class)
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
.build()
)
""".trimIndent()
)
.addParameter("path", String::class)
.addModifiers(KModifier.SUSPEND)
.returns(ByteArray::class)
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
.build()
)
val types = sortedResources.map { (type, idToResources) ->
getResourceTypeObject(type, idToResources)
}
addTypes(types)
}.build())
val types = resources.map { (type, idToResources) ->
getResourceTypeObject(type, idToResources)
}.sortedBy { it.name }
addTypes(types)
}.build())
}.build()
sortedResources
.flatMap { (type, idToResources) ->
idToResources.map { (name, items) ->
getResourceInitializer(name, type, items)
}
}
.forEach { addFunction(it) }
}.build()
private fun getResourceTypeObject(type: ResourceType, nameToResources: Map<String, List<ResourceItem>>) =
TypeSpec.objectBuilder(type.typeName).apply {
nameToResources.entries
.sortedBy { it.key }
.forEach { (name, items) ->
addResourceProperty(name, items.sortedBy { it.path })
nameToResources.keys
.forEach { name ->
addProperty(
PropertySpec
.builder(name, type.getClassName())
.initializer("get_$name()")
.build()
)
}
}.build()
private fun TypeSpec.Builder.addResourceProperty(name: String, items: List<ResourceItem>) {
val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
val first = items.first()
val propertyClassName = first.getClassName()
val resourceId = first.let { "${it.type}:${it.name}" }
val initializer = CodeBlock.builder()
.add("%T(\n", propertyClassName).withIndent {
add("\"$resourceId\",\n")
if (first.type == ResourceType.STRING) {
add("\"${first.name}\",\n")
}
add("setOf(\n").withIndent {
items.forEach { item ->
add("%T(\n", resourceItemClass).withIndent {
add("setOf(").addQualifiers(item).add("),\n")
//file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"\n") //todo: add module ID here
private fun getResourceInitializer(name: String, type: ResourceType, items: List<ResourceItem>): FunSpec {
val propertyTypeName = type.getClassName()
val resourceId = "${type}:${name}"
return FunSpec.builder("get_$name")
.addModifiers(KModifier.PRIVATE)
.returns(propertyTypeName)
.addStatement(
CodeBlock.builder()
.add("return %T(\n", propertyTypeName).withIndent {
add("\"$resourceId\",")
if (type == ResourceType.STRING) add(" \"$name\",")
withIndent {
add("\nsetOf(\n").withIndent {
items.forEach { item ->
add("%T(", resourceItemClass)
add("setOf(").addQualifiers(item).add("), ")
//file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"") //todo: add module ID here
add("),\n")
}
}
add(")\n")
}
add("),\n")
}
}
add(")\n")
}
.add(")")
.add(")")
.build().toString()
)
.build()
}
addProperty(
PropertySpec.builder(name, propertyClassName)
.initializer(initializer)
.build()
)
private fun sortResources(
resources: Map<ResourceType, Map<String, List<ResourceItem>>>
): TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>> {
val result = TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>>()
resources
.entries
.forEach { (type, items) ->
val typeResult = TreeMap<String, List<ResourceItem>>()
items
.entries
.forEach { (name, resItems) ->
typeResult[name] = resItems.sortedBy { it.path }
}
result[type] = typeResult
}
return result
}

87
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

@ -9,7 +9,7 @@ import kotlin.io.path.Path
class ResourcesTest : GradlePluginTestBase() {
@Test
fun testGeneratedAccessorsAndCopiedFonts(): Unit = with(testProject("misc/commonResources")) {
fun testGeneratedAccessors(): Unit = with(testProject("misc/commonResources")) {
//check generated resource's accessors
gradle("generateComposeResClass").checks {
assertEqualTextFiles(
@ -137,4 +137,89 @@ class ResourcesTest : GradlePluginTestBase() {
}
gradle("jar")
}
//https://github.com/JetBrains/compose-multiplatform/issues/4194
@Test
fun testHugeNumberOfStrings(): Unit = with(
//disable cache for the test because the generateStringFiles task doesn't support it
testProject("misc/commonResources", defaultTestEnvironment.copy(useGradleConfigurationCache = false))
) {
file("build.gradle.kts").let { f ->
val originText = f.readText()
f.writeText(
buildString {
appendLine("import java.util.Locale")
append(originText)
appendLine()
append("""
val template = ""${'"'}
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="multi_line">Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</string>
<string name="str_template">Hello, %1${'$'}{"$"}s! You have %2${'$'}{"$"}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
[ADDITIONAL_STRINGS]
</resources>
""${'"'}.trimIndent()
val generateStringFiles = tasks.register("generateStringFiles") {
val numberOfLanguages = 20
val numberOfStrings = 500
val langs = Locale.getAvailableLocales()
.map { it.language }
.filter { it.count() == 2 }
.sorted()
.distinct()
.take(numberOfLanguages)
.toList()
val resourcesFolder = project.file("src/commonMain/composeResources")
doLast {
// THIS REMOVES THE `values` FOLDER IN `composeResources`
// THIS REMOVES THE `values` FOLDER IN `composeResources`
// Necessary when reducing the number of languages.
resourcesFolder.listFiles()?.filter { it.name.startsWith("values") }?.forEach {
it.deleteRecursively()
}
langs.forEachIndexed { langIndex, lang ->
val additionalStrings =
(0 until numberOfStrings).joinToString(System.lineSeparator()) { index ->
""${'"'}
<string name="string_${'$'}{index.toString().padStart(4, '0')}">String ${'$'}index in lang ${'$'}lang</string>
""${'"'}.trimIndent()
}
val langFile = if (langIndex == 0) {
File(resourcesFolder, "values/strings.xml")
} else {
File(resourcesFolder, "values-${'$'}lang/strings.xml")
}
langFile.parentFile.mkdirs()
langFile.writeText(template.replace("[ADDITIONAL_STRINGS]", additionalStrings))
}
}
}
tasks.named("generateComposeResClass") {
dependsOn(generateStringFiles)
}
""".trimIndent())
}
)
}
gradle("desktopJar").checks {
check.taskSuccessful(":generateStringFiles")
check.taskSuccessful(":generateComposeResClass")
assert(file("src/commonMain/composeResources/values/strings.xml").readLines().size == 513)
}
}
}

194
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package app.group.resources_test.generated.resources
import kotlin.ByteArray
@ -6,14 +11,9 @@ import kotlin.String
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.FontResource
import org.jetbrains.compose.resources.LanguageQualifier
import org.jetbrains.compose.resources.RegionQualifier
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.ThemeQualifier
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**
@ -27,115 +27,99 @@ internal object Res {
public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path)
public object drawable {
public val _3_strange_name: DrawableResource = DrawableResource(
"drawable:_3_strange_name",
setOf(
ResourceItem(
setOf(),
"drawable/3-strange-name.xml"
),
)
)
public val _3_strange_name: DrawableResource = get__3_strange_name()
public val vector: DrawableResource = DrawableResource(
"drawable:vector",
setOf(
ResourceItem(
setOf(LanguageQualifier("au"), RegionQualifier("US"), ),
"drawable-au-rUS/vector.xml"
),
ResourceItem(
setOf(ThemeQualifier.DARK, LanguageQualifier("ge"), ),
"drawable-dark-ge/vector.xml"
),
ResourceItem(
setOf(LanguageQualifier("en"), ),
"drawable-en/vector.xml"
),
ResourceItem(
setOf(),
"drawable/vector.xml"
),
)
)
public val vector: DrawableResource = get_vector()
public val vector_2: DrawableResource = DrawableResource(
"drawable:vector_2",
setOf(
ResourceItem(
setOf(),
"drawable/vector_2.xml"
),
)
)
public val vector_2: DrawableResource = get_vector_2()
}
public object string {
public val app_name: StringResource = get_app_name()
public val hello: StringResource = get_hello()
public val multi_line: StringResource = get_multi_line()
public val str_arr: StringResource = get_str_arr()
public val str_template: StringResource = get_str_template()
}
public object font {
public val emptyfont: FontResource = FontResource(
"font:emptyfont",
setOf(
ResourceItem(
setOf(),
"font/emptyFont.otf"
),
)
)
public val emptyfont: FontResource = get_emptyfont()
}
}
public object string {
public val app_name: StringResource = StringResource(
"string:app_name",
"app_name",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
private fun get__3_strange_name(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:_3_strange_name",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml"),
)
)
public val hello: StringResource = StringResource(
"string:hello",
"hello",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
)
private fun get_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource(
"drawable:vector",
setOf(
public val multi_line: StringResource = StringResource(
"string:multi_line",
"multi_line",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
)
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("au"),
org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml"),
public val str_arr: StringResource = StringResource(
"string:str_arr",
"str_arr",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
)
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.ThemeQualifier.DARK,
org.jetbrains.compose.resources.LanguageQualifier("ge"), ), "drawable-dark-ge/vector.xml"),
public val str_template: StringResource = StringResource(
"string:str_template",
"str_template",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
)
}
}
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"),
), "drawable-en/vector.xml"),
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"),
)
)
private fun get_vector_2(): DrawableResource = org.jetbrains.compose.resources.DrawableResource(
"drawable:vector_2",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml"),
)
)
private fun get_app_name(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:app_name", "app_name",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_hello(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:hello", "hello",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_multi_line(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:multi_line", "multi_line",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_str_arr(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:str_arr", "str_arr",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_str_template(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:str_template", "str_template",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_emptyfont(): FontResource = org.jetbrains.compose.resources.FontResource(
"font:emptyfont",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf"),
)
)

6
gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package app.group.empty_res.generated.resources
import kotlin.ByteArray
@ -6,7 +11,6 @@ import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**

26
gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package me.app.jvmonlyresources.generated.resources
import kotlin.ByteArray
@ -5,10 +10,8 @@ import kotlin.OptIn
import kotlin.String
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**
@ -22,14 +25,13 @@ internal object Res {
public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path)
public object drawable {
public val vector: DrawableResource = DrawableResource(
"drawable:vector",
setOf(
ResourceItem(
setOf(),
"drawable/vector.xml"
),
)
)
public val vector: DrawableResource = get_vector()
}
}
}
private fun get_vector(): DrawableResource = org.jetbrains.compose.resources.DrawableResource(
"drawable:vector",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"),
)
)
Loading…
Cancel
Save