Browse Source

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

pull/4215/head
Konstantin 10 months ago committed by GitHub
parent
commit
b1e86ade36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 113
      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. 148
      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. 20
      gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

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

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

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 package app.group.empty_res.generated.resources
import kotlin.ByteArray import kotlin.ByteArray
@ -6,7 +11,6 @@ import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi @ExperimentalResourceApi
internal object Res { internal object Res {
/** /**

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