Skip to content

Commit ef3ba2e

Browse files
authored
Add ResourceContentHash annotation to generated resource accessors (#5357)
Used by Compose Hot Reload to mark a related accessor as dirty if the hash is changed after reloading of new accessors. Fixes [CMP-8513](https://youtrack.jetbrains.com/issue/CMP-8513) ## Release Notes N/A
1 parent 8ccc805 commit ef3ba2e

File tree

24 files changed

+850
-5
lines changed

24 files changed

+850
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.jetbrains.compose.resources
2+
3+
@Suppress("unused")
4+
@Retention(AnnotationRetention.BINARY)
5+
annotation class ResourceContentHash(val hash: Int)

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,15 @@ internal abstract class GenerateResourceAccessorsTask : IdeaImportTask() {
110110
}
111111

112112
val type = ResourceType.fromString(typeString) ?: error("Unknown resource type: '$typeString'.")
113-
return listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path))
113+
return listOf(
114+
ResourceItem(
115+
type,
116+
qualifiers,
117+
file.nameWithoutExtension.asUnderscoredIdentifier(),
118+
path,
119+
file.readBytes().contentHashCode()
120+
)
121+
)
114122
}
115123

116124
private fun getValueResourceItems(dataFile: File, qualifiers: List<String>, path: Path): List<ResourceItem> {
@@ -141,7 +149,15 @@ internal abstract class GenerateResourceAccessorsTask : IdeaImportTask() {
141149
path: Path
142150
): ResourceItem {
143151
val record = ValueResourceRecord.createFromString(recordString)
144-
return ResourceItem(record.type, qualifiers, record.key.asUnderscoredIdentifier(), path, offset, size)
152+
return ResourceItem(
153+
record.type,
154+
qualifiers,
155+
record.key.asUnderscoredIdentifier(),
156+
path,
157+
record.content.hashCode(),
158+
offset,
159+
size
160+
)
145161
}
146162
}
147163

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ internal data class ResourceItem(
3939
val qualifiers: List<String>,
4040
val name: String,
4141
val path: Path,
42+
val contentHash: Int,
4243
val offset: Long = -1,
4344
val size: Long = -1,
4445
)
@@ -58,6 +59,8 @@ private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "Re
5859
private val internalAnnotationClass = ClassName("org.jetbrains.compose.resources", "InternalResourceApi")
5960
private val internalAnnotation = AnnotationSpec.builder(internalAnnotationClass).build()
6061

62+
private val resourceContentHashAnnotationClass = ClassName("org.jetbrains.compose.resources", "ResourceContentHash")
63+
6164
private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder {
6265
val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier")
6366
val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier")
@@ -283,11 +286,18 @@ private fun getChunkFileSpec(
283286
.endControlFlow()
284287
.build()
285288

286-
val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier)
289+
val accessorBuilder = PropertySpec.builder(resName, type.getClassName(), resModifier)
287290
.receiver(ClassName(packageName, resClassName, type.accessorName))
288291
.delegate(initializer)
289-
.build()
290-
chunkFile.addProperty(accessor)
292+
if (System.getProperty("compose.resources.generate.ResourceContentHash.annotation") == "true") {
293+
accessorBuilder.addAnnotation(
294+
AnnotationSpec.builder(resourceContentHashAnnotationClass)
295+
.useSiteTarget(AnnotationSpec.UseSiteTarget.DELEGATE)
296+
.addMember("%L", items.fold(0) { acc, item -> ((acc * 31) + item.contentHash) })
297+
.build()
298+
)
299+
}
300+
chunkFile.addProperty(accessorBuilder.build())
291301
}
292302

293303
//__collect${chunkClassName}Resources function

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,26 @@ class ResourcesTest : GradlePluginTestBase() {
578578
}
579579
}
580580

581+
@Test
582+
fun testGeneratedAccessorsAnnotatedWithResourceContentHash(): Unit = with(testProject("misc/commonResources")) {
583+
//check generated resource's accessors
584+
gradle("prepareKotlinIdeaImport", "-Dcompose.resources.generate.ResourceContentHash.annotation=true").checks {
585+
val expected = if (System.getProperty("os.name").lowercase().contains("windows")) {
586+
// Windows has different line endings in comparison with Unixes,
587+
// thus the XML resource files differ and produce different content hashes,
588+
// so we have different test data for it.
589+
file("expected-with-hash-windows")
590+
} else {
591+
file("expected-with-hash")
592+
}
593+
594+
assertDirectoriesContentEquals(
595+
file("build/generated/compose/resourceGenerator/kotlin"),
596+
expected
597+
)
598+
}
599+
}
600+
581601
@Test
582602
fun testJvmOnlyProject(): Unit = with(testProject("misc/jvmOnlyResources")) {
583603
gradle("jar").checks {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@file:OptIn(InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.MutableMap
8+
import org.jetbrains.compose.resources.InternalResourceApi
9+
import org.jetbrains.compose.resources.ResourceContentHash
10+
import org.jetbrains.compose.resources.ResourceItem
11+
import org.jetbrains.compose.resources.StringResource
12+
13+
private const val MD: String = "composeResources/app.group.resources_test.generated.resources/"
14+
15+
@delegate:ResourceContentHash(50_967_853)
16+
internal val Res.string.android_str: StringResource by lazy {
17+
StringResource("string:android_str", "android_str", setOf(
18+
ResourceItem(setOf(), "${MD}values/strings.androidMain.cvr", 10, 39),
19+
))
20+
}
21+
22+
@InternalResourceApi
23+
internal fun _collectAndroidMainString0Resources(map: MutableMap<String, StringResource>) {
24+
map.put("android_str", Res.string.android_str)
25+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@file:OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.Map
8+
import org.jetbrains.compose.resources.DrawableResource
9+
import org.jetbrains.compose.resources.FontResource
10+
import org.jetbrains.compose.resources.PluralStringResource
11+
import org.jetbrains.compose.resources.StringArrayResource
12+
import org.jetbrains.compose.resources.StringResource
13+
14+
internal actual val Res.allDrawableResources: Map<String, DrawableResource> by lazy {
15+
val map = mutableMapOf<String, DrawableResource>()
16+
_collectCommonMainDrawable0Resources(map)
17+
return@lazy map
18+
}
19+
20+
internal actual val Res.allStringResources: Map<String, StringResource> by lazy {
21+
val map = mutableMapOf<String, StringResource>()
22+
_collectAndroidMainString0Resources(map)
23+
_collectCommonMainString0Resources(map)
24+
return@lazy map
25+
}
26+
27+
internal actual val Res.allStringArrayResources: Map<String, StringArrayResource> by lazy {
28+
val map = mutableMapOf<String, StringArrayResource>()
29+
return@lazy map
30+
}
31+
32+
internal actual val Res.allPluralStringResources: Map<String, PluralStringResource> by lazy {
33+
val map = mutableMapOf<String, PluralStringResource>()
34+
_collectCommonMainPlurals0Resources(map)
35+
return@lazy map
36+
}
37+
38+
internal actual val Res.allFontResources: Map<String, FontResource> by lazy {
39+
val map = mutableMapOf<String, FontResource>()
40+
_collectCommonMainFont0Resources(map)
41+
return@lazy map
42+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@file:OptIn(InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.MutableMap
8+
import org.jetbrains.compose.resources.DrawableResource
9+
import org.jetbrains.compose.resources.InternalResourceApi
10+
import org.jetbrains.compose.resources.LanguageQualifier
11+
import org.jetbrains.compose.resources.RegionQualifier
12+
import org.jetbrains.compose.resources.ResourceContentHash
13+
import org.jetbrains.compose.resources.ResourceItem
14+
import org.jetbrains.compose.resources.ThemeQualifier
15+
16+
private const val MD: String = "composeResources/app.group.resources_test.generated.resources/"
17+
18+
@delegate:ResourceContentHash(-1_453_679_588)
19+
internal val Res.drawable._3_strange_name: DrawableResource by lazy {
20+
DrawableResource("drawable:_3_strange_name", setOf(
21+
ResourceItem(setOf(), "${MD}drawable/3-strange-name.xml", -1, -1),
22+
))
23+
}
24+
25+
@delegate:ResourceContentHash(-1_453_679_588)
26+
internal val Res.drawable.camelCaseName: DrawableResource by lazy {
27+
DrawableResource("drawable:camelCaseName", setOf(
28+
ResourceItem(setOf(), "${MD}drawable/camelCaseName.xml", -1, -1),
29+
))
30+
}
31+
32+
@delegate:ResourceContentHash(-1_453_679_588)
33+
internal val Res.drawable.`is`: DrawableResource by lazy {
34+
DrawableResource("drawable:is", setOf(
35+
ResourceItem(setOf(), "${MD}drawable/is.xml", -1, -1),
36+
))
37+
}
38+
39+
@delegate:ResourceContentHash(-737_454_820)
40+
internal val Res.drawable.vector: DrawableResource by lazy {
41+
DrawableResource("drawable:vector", setOf(
42+
ResourceItem(setOf(LanguageQualifier("ast"), ), "${MD}drawable-ast/vector.xml", -1, -1),
43+
ResourceItem(setOf(LanguageQualifier("au"), RegionQualifier("US"), ), "${MD}drawable-au-rUS/vector.xml", -1, -1),
44+
ResourceItem(setOf(ThemeQualifier.DARK, LanguageQualifier("ge"), ), "${MD}drawable-dark-ge/vector.xml", -1, -1),
45+
ResourceItem(setOf(LanguageQualifier("en"), ), "${MD}drawable-en/vector.xml", -1, -1),
46+
ResourceItem(setOf(), "${MD}drawable/vector.xml", -1, -1),
47+
))
48+
}
49+
50+
@delegate:ResourceContentHash(-1_453_679_588)
51+
internal val Res.drawable.vector_2: DrawableResource by lazy {
52+
DrawableResource("drawable:vector_2", setOf(
53+
ResourceItem(setOf(), "${MD}drawable/vector_2.xml", -1, -1),
54+
))
55+
}
56+
57+
@InternalResourceApi
58+
internal fun _collectCommonMainDrawable0Resources(map: MutableMap<String, DrawableResource>) {
59+
map.put("_3_strange_name", Res.drawable._3_strange_name)
60+
map.put("camelCaseName", Res.drawable.camelCaseName)
61+
map.put("is", Res.drawable.`is`)
62+
map.put("vector", Res.drawable.vector)
63+
map.put("vector_2", Res.drawable.vector_2)
64+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@file:OptIn(InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.MutableMap
8+
import org.jetbrains.compose.resources.FontResource
9+
import org.jetbrains.compose.resources.InternalResourceApi
10+
import org.jetbrains.compose.resources.LanguageQualifier
11+
import org.jetbrains.compose.resources.ResourceContentHash
12+
import org.jetbrains.compose.resources.ResourceItem
13+
14+
private const val MD: String = "composeResources/app.group.resources_test.generated.resources/"
15+
16+
@delegate:ResourceContentHash(1_893_715_104)
17+
internal val Res.font.emptyFont: FontResource by lazy {
18+
FontResource("font:emptyFont", setOf(
19+
ResourceItem(setOf(LanguageQualifier("en"), ), "${MD}font-en/emptyFont.otf", -1, -1),
20+
ResourceItem(setOf(), "${MD}font/emptyFont.otf", -1, -1),
21+
))
22+
}
23+
24+
@InternalResourceApi
25+
internal fun _collectCommonMainFont0Resources(map: MutableMap<String, FontResource>) {
26+
map.put("emptyFont", Res.font.emptyFont)
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@file:OptIn(InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.MutableMap
8+
import org.jetbrains.compose.resources.InternalResourceApi
9+
import org.jetbrains.compose.resources.PluralStringResource
10+
import org.jetbrains.compose.resources.ResourceContentHash
11+
import org.jetbrains.compose.resources.ResourceItem
12+
13+
private const val MD: String = "composeResources/app.group.resources_test.generated.resources/"
14+
15+
@delegate:ResourceContentHash(-199_361_196)
16+
internal val Res.plurals.numberOfSongsAvailable: PluralStringResource by lazy {
17+
PluralStringResource("plurals:numberOfSongsAvailable", "numberOfSongsAvailable", setOf(
18+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 10, 124),
19+
))
20+
}
21+
22+
@InternalResourceApi
23+
internal fun _collectCommonMainPlurals0Resources(map: MutableMap<String, PluralStringResource>) {
24+
map.put("numberOfSongsAvailable", Res.plurals.numberOfSongsAvailable)
25+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
@file:OptIn(InternalResourceApi::class)
2+
3+
package app.group.resources_test.generated.resources
4+
5+
import kotlin.OptIn
6+
import kotlin.String
7+
import kotlin.collections.MutableMap
8+
import org.jetbrains.compose.resources.InternalResourceApi
9+
import org.jetbrains.compose.resources.ResourceContentHash
10+
import org.jetbrains.compose.resources.ResourceItem
11+
import org.jetbrains.compose.resources.StringResource
12+
13+
private const val MD: String = "composeResources/app.group.resources_test.generated.resources/"
14+
15+
@delegate:ResourceContentHash(405_464_824)
16+
internal val Res.string.PascalCase: StringResource by lazy {
17+
StringResource("string:PascalCase", "PascalCase", setOf(
18+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 172, 34),
19+
))
20+
}
21+
22+
@delegate:ResourceContentHash(-1_118_290_776)
23+
internal val Res.string._1_kebab_case: StringResource by lazy {
24+
StringResource("string:_1_kebab_case", "_1_kebab_case", setOf(
25+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 135, 36),
26+
))
27+
}
28+
29+
@delegate:ResourceContentHash(64_828_505)
30+
internal val Res.string.app_name: StringResource by lazy {
31+
StringResource("string:app_name", "app_name", setOf(
32+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 207, 44),
33+
))
34+
}
35+
36+
@delegate:ResourceContentHash(211_426_861)
37+
internal val Res.string.camelCase: StringResource by lazy {
38+
StringResource("string:camelCase", "camelCase", setOf(
39+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 252, 29),
40+
))
41+
}
42+
43+
@delegate:ResourceContentHash(466_457_346)
44+
internal val Res.string.hello: StringResource by lazy {
45+
StringResource("string:hello", "hello", setOf(
46+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 282, 37),
47+
))
48+
}
49+
50+
@delegate:ResourceContentHash(-1_288_591_563)
51+
internal val Res.string.`info_using_release_$x`: StringResource by lazy {
52+
StringResource("string:info_using_release_${'$'}x", "info_using_release_${'$'}x", setOf(
53+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 320, 57),
54+
))
55+
}
56+
57+
@delegate:ResourceContentHash(-624_025_575)
58+
internal val Res.string.multi_line: StringResource by lazy {
59+
StringResource("string:multi_line", "multi_line", setOf(
60+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 378, 178),
61+
))
62+
}
63+
64+
@delegate:ResourceContentHash(-1_332_636_786)
65+
internal val Res.string.str_template: StringResource by lazy {
66+
StringResource("string:str_template", "str_template", setOf(
67+
ResourceItem(setOf(), "${MD}values/strings.commonMain.cvr", 557, 76),
68+
))
69+
}
70+
71+
@InternalResourceApi
72+
internal fun _collectCommonMainString0Resources(map: MutableMap<String, StringResource>) {
73+
map.put("PascalCase", Res.string.PascalCase)
74+
map.put("_1_kebab_case", Res.string._1_kebab_case)
75+
map.put("app_name", Res.string.app_name)
76+
map.put("camelCase", Res.string.camelCase)
77+
map.put("hello", Res.string.hello)
78+
map.put("info_using_release_${'$'}x", Res.string.`info_using_release_$x`)
79+
map.put("multi_line", Res.string.multi_line)
80+
map.put("str_template", Res.string.str_template)
81+
}

0 commit comments

Comments
 (0)