1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:JvmName("SlotTreeKt")
18 
19 package androidx.compose.ui.tooling.data
20 
21 import androidx.compose.runtime.tooling.CompositionData
22 import androidx.compose.runtime.tooling.CompositionGroup
23 import androidx.compose.ui.layout.LayoutInfo
24 import androidx.compose.ui.layout.ModifierInfo
25 import androidx.compose.ui.layout.positionInWindow
26 import androidx.compose.ui.unit.IntRect
27 import java.lang.reflect.Field
28 import kotlin.math.max
29 import kotlin.math.min
30 import kotlin.math.roundToInt
31 
32 /** A group in the slot table. Represents either a call or an emitted node. */
33 @UiToolingDataApi
34 sealed class Group(
35     /** The key is the key generated for the group */
36     val key: Any?,
37 
38     /** The name of the function called, if provided */
39     val name: String?,
40 
41     /** The source location that produce the group if it can be determined */
42     val location: SourceLocation?,
43 
44     /**
45      * An optional value that identifies a Group independently of movement caused by recompositions.
46      */
47     val identity: Any?,
48 
49     /** The bounding layout box for the group. */
50     val box: IntRect,
51 
52     /** Any data that was stored in the slot table for the group */
53     val data: Collection<Any?>,
54 
55     /** The child groups of this group */
56     val children: Collection<Group>,
57 
58     /** True if the group is for an inline function call */
59     val isInline: Boolean,
60 ) {
61     /** Modifier information for the Group, or empty list if there isn't any. */
62     open val modifierInfo: List<ModifierInfo>
63         get() = emptyList()
64 
65     /** Parameter information for Groups that represent calls */
66     open val parameters: List<ParameterInformation>
67         get() = emptyList()
68 }
69 
70 @UiToolingDataApi
71 @Suppress("DataClassDefinition")
72 data class ParameterInformation(
73     val name: String,
74     val value: Any?,
75     val fromDefault: Boolean,
76     val static: Boolean,
77     val compared: Boolean,
78     val inlineClass: String?,
79     val stable: Boolean
80 )
81 
82 /** Source location of the call that produced the call group. */
83 @UiToolingDataApi
84 @Suppress("DataClassDefinition")
85 data class SourceLocation(
86     /** A 0 offset line number of the source location. */
87     val lineNumber: Int,
88 
89     /**
90      * Offset into the file. The offset is calculated as the number of UTF-16 code units from the
91      * beginning of the file to the first UTF-16 code unit of the call that produced the group.
92      */
93     val offset: Int,
94 
95     /**
96      * The length of the source code. The length is calculated as the number of UTF-16 code units
97      * that that make up the call expression.
98      */
99     val length: Int,
100 
101     /**
102      * The file name (without path information) of the source file that contains the call that
103      * produced the group. A source file names are not guaranteed to be unique, [packageHash] is
104      * included to help disambiguate files with duplicate names.
105      */
106     val sourceFile: String?,
107 
108     /**
109      * A hash code of the package name of the file. This hash is calculated by,
110      *
111      * `packageName.fold(0) { hash, current -> hash * 31 + current.toInt() }?.absoluteValue`
112      *
113      * where the package name is the dotted name of the package. This can be used to disambiguate
114      * which file is referenced by [sourceFile]. This number is -1 if there was no package hash
115      * information generated such as when the file does not contain a package declaration.
116      */
117     val packageHash: Int
118 )
119 
120 /** A group that represents the invocation of a component */
121 @UiToolingDataApi
122 class CallGroup(
123     key: Any?,
124     name: String?,
125     box: IntRect,
126     location: SourceLocation?,
127     identity: Any?,
128     override val parameters: List<ParameterInformation>,
129     data: Collection<Any?>,
130     children: Collection<Group>,
131     isInline: Boolean
132 ) : Group(key, name, location, identity, box, data, children, isInline)
133 
134 /** A group that represents an emitted node */
135 @UiToolingDataApi
136 class NodeGroup(
137     key: Any?,
138 
139     /** An emitted node */
140     val node: Any,
141     box: IntRect,
142     data: Collection<Any?>,
143     override val modifierInfo: List<ModifierInfo>,
144     children: Collection<Group>
145 ) : Group(key, null, null, null, box, data, children, false)
146 
147 @UiToolingDataApi
148 private object EmptyGroup :
149     Group(
150         key = null,
151         name = null,
152         location = null,
153         identity = null,
154         box = emptyBox,
155         data = emptyList(),
156         children = emptyList(),
157         isInline = false
158     )
159 
160 /** A key that has being joined together to form one key. */
161 @UiToolingDataApi
162 @Suppress("DataClassDefinition")
163 data class JoinedKey(val left: Any?, val right: Any?)
164 
165 internal val emptyBox = IntRect(0, 0, 0, 0)
166 
167 private val tokenizer = Regex("(\\d+)|([,])|([*])|([:])|L|(P\\([^)]*\\))|(C(\\(([^)]*)\\))?)|@")
168 
isNumbernull169 private fun MatchResult.isNumber() = groups[1] != null
170 
171 private fun MatchResult.number() = groupValues[1].parseToInt()
172 
173 private val MatchResult.text
174     get() = groupValues[0]
175 
176 private fun MatchResult.isChar(c: String) = text == c
177 
178 private fun MatchResult.isFileName() = groups[4] != null
179 
180 private fun MatchResult.isParameterInformation() = groups[5] != null
181 
182 private fun MatchResult.isCallWithName() = groups[6] != null
183 
184 private fun MatchResult.callName() = groupValues[8]
185 
186 private class SourceLocationInfo(val lineNumber: Int?, val offset: Int?, val length: Int?)
187 
188 @UiToolingDataApi
189 private class SourceInformationContext(
190     val name: String?,
191     val sourceFile: String?,
192     val packageHash: Int,
193     val locations: List<SourceLocationInfo>,
194     val repeatOffset: Int,
195     val parameters: List<Parameter>?,
196     val isCall: Boolean,
197     val isInline: Boolean
198 ) {
199     private var nextLocation = 0
200 
201     fun nextSourceLocation(): SourceLocation? {
202         if (nextLocation >= locations.size && repeatOffset >= 0) {
203             nextLocation = repeatOffset
204         }
205         if (nextLocation < locations.size) {
206             val location = locations[nextLocation++]
207             return SourceLocation(
208                 location.lineNumber ?: -1,
209                 location.offset ?: -1,
210                 location.length ?: -1,
211                 sourceFile,
212                 packageHash
213             )
214         }
215         return null
216     }
217 
218     fun sourceLocation(callIndex: Int, parentContext: SourceInformationContext?): SourceLocation? {
219         var locationIndex = callIndex
220         if (locationIndex >= locations.size && repeatOffset >= 0 && repeatOffset < locations.size) {
221             locationIndex =
222                 (callIndex - repeatOffset) % (locations.size - repeatOffset) + repeatOffset
223         }
224         if (locationIndex < locations.size) {
225             val location = locations[locationIndex]
226             return SourceLocation(
227                 location.lineNumber ?: -1,
228                 location.offset ?: -1,
229                 location.length ?: -1,
230                 sourceFile ?: parentContext?.sourceFile,
231                 (if (sourceFile == null) parentContext?.packageHash else packageHash) ?: -1
232             )
233         }
234         return null
235     }
236 }
237 
238 private val parametersInformationTokenizer = Regex("(\\d+)|,|[!P()]|:([^,!)]+)")
239 private val MatchResult.isANumber
240     get() = groups[1] != null
241 private val MatchResult.isClassName
242     get() = groups[2] != null
243 
244 private class ParseError : Exception()
245 
246 private class Parameter(val sortedIndex: Int, val inlineClass: String? = null)
247 
Stringnull248 private fun String.parseToInt(): Int =
249     try {
250         toInt()
251     } catch (_: NumberFormatException) {
252         throw ParseError()
253     }
254 
Stringnull255 private fun String.parseToInt(radix: Int): Int =
256     try {
257         toInt(radix)
258     } catch (_: NumberFormatException) {
259         throw ParseError()
260     }
261 
262 // The parameter information follows the following grammar:
263 //
264 //   parameters: (parameter|run) ("," parameter | run)*
265 //   parameter: sorted-index [":" inline-class]
266 //   sorted-index: <number>
267 //   inline-class: <chars not "," or "!">
268 //   run: "!" <number>
269 //
270 // The full description of this grammar can be found in the ComposableFunctionBodyTransformer of the
271 // compose compiler plugin.
parseParametersnull272 private fun parseParameters(parameters: String): List<Parameter> {
273     var currentResult = parametersInformationTokenizer.find(parameters)
274     val expectedSortedIndex = mutableListOf(0, 1, 2, 3)
275     var lastAdded = expectedSortedIndex.size - 1
276     val result = mutableListOf<Parameter>()
277     fun next(): MatchResult? {
278         currentResult?.let { currentResult = it.next() }
279         return currentResult
280     }
281 
282     fun expectNumber(): Int {
283         val mr = currentResult
284         if (mr == null || !mr.isANumber) throw ParseError()
285         next()
286         return mr.text.parseToInt()
287     }
288 
289     fun expectClassName(): String {
290         val mr = currentResult
291         if (mr == null || !mr.isClassName) throw ParseError()
292         next()
293         return mr.text.substring(1).replacePrefix("c#", "androidx.compose.")
294     }
295 
296     fun expect(value: String) {
297         val mr = currentResult
298         if (mr == null || mr.text != value) throw ParseError()
299         next()
300     }
301 
302     fun isChar(value: String): Boolean {
303         val mr = currentResult
304         return mr == null || mr.text == value
305     }
306 
307     fun isClassName(): Boolean {
308         val mr = currentResult
309         return mr != null && mr.isClassName
310     }
311 
312     fun ensureIndexes(index: Int) {
313         val missing = index - lastAdded
314         if (missing > 0) {
315             val minAddAmount = 4
316             val amountToAdd = if (missing < minAddAmount) minAddAmount else missing
317             repeat(amountToAdd) { expectedSortedIndex.add(it + lastAdded + 1) }
318             lastAdded += amountToAdd
319         }
320     }
321 
322     try {
323         expect("P")
324         expect("(")
325         loop@ while (!isChar(")")) {
326             when {
327                 isChar("!") -> {
328                     // run
329                     next()
330                     val count = expectNumber()
331                     ensureIndexes(result.size + count)
332                     repeat(count) {
333                         result.add(Parameter(expectedSortedIndex.first()))
334                         expectedSortedIndex.removeAt(0)
335                     }
336                 }
337                 isChar(",") -> next()
338                 else -> {
339                     val index = expectNumber()
340                     val inlineClass =
341                         if (isClassName()) {
342                             expectClassName()
343                         } else null
344                     result.add(Parameter(index, inlineClass))
345                     ensureIndexes(index)
346                     expectedSortedIndex.remove(index)
347                 }
348             }
349         }
350         expect(")")
351 
352         // Ensure there are at least as many entries as the highest referenced index.
353         while (expectedSortedIndex.size > 0) {
354             result.add(Parameter(expectedSortedIndex.first()))
355             expectedSortedIndex.removeAt(0)
356         }
357         return result
358     } catch (_: ParseError) {
359         return emptyList()
360     } catch (_: NumberFormatException) {
361         return emptyList()
362     }
363 }
364 
365 @UiToolingDataApi
sourceInformationContextOfnull366 private fun sourceInformationContextOf(
367     information: String,
368     parent: SourceInformationContext? = null
369 ): SourceInformationContext? {
370     var currentResult = tokenizer.find(information)
371 
372     fun next(): MatchResult? {
373         currentResult?.let { currentResult = it.next() }
374         return currentResult
375     }
376 
377     fun parseLocation(): SourceLocationInfo? {
378         var lineNumber: Int? = null
379         var offset: Int? = null
380         var length: Int? = null
381 
382         try {
383             var mr = currentResult
384             if (mr != null && mr.isNumber()) {
385                 // Offsets are 0 based in the data, we need 1 based.
386                 lineNumber = mr.number() + 1
387                 mr = next()
388             }
389             if (mr != null && mr.isChar("@")) {
390                 // Offset
391                 mr = next()
392                 if (mr == null || !mr.isNumber()) {
393                     return null
394                 }
395                 offset = mr.number()
396                 mr = next()
397                 if (mr != null && mr.isChar("L")) {
398                     mr = next()
399                     if (mr == null || !mr.isNumber()) {
400                         return null
401                     }
402                     length = mr.number()
403                 }
404             }
405             if (lineNumber != null && offset != null && length != null)
406                 return SourceLocationInfo(lineNumber, offset, length)
407         } catch (_: ParseError) {
408             return null
409         }
410         return null
411     }
412     val sourceLocations = mutableListOf<SourceLocationInfo>()
413     var repeatOffset = -1
414     var isCall = false
415     var isInline = false
416     var name: String? = null
417     var parameters: List<Parameter>? = null
418     var sourceFile: String? = null
419     var packageHash = -1
420     loop@ while (currentResult != null) {
421         val mr = currentResult!!
422         when {
423             mr.isNumber() || mr.isChar("@") -> {
424                 parseLocation()?.let { sourceLocations.add(it) }
425             }
426             mr.isChar("C") -> {
427                 // A redundant call marker is placed in inline functions
428                 if (isCall) isInline = true
429                 isCall = true
430                 next()
431             }
432             mr.isCallWithName() -> {
433                 // A redundant call marker is placed in inline functions
434                 if (isCall) isInline = true
435                 isCall = true
436                 name = mr.callName()
437                 next()
438             }
439             mr.isParameterInformation() -> {
440                 parameters = parseParameters(mr.text)
441                 next()
442             }
443             mr.isChar("*") -> {
444                 repeatOffset = sourceLocations.size
445                 next()
446             }
447             mr.isChar(",") -> next()
448             mr.isFileName() -> {
449                 sourceFile = information.substring(mr.range.last + 1)
450                 val hashText = sourceFile.substringAfterLast("#", "")
451                 if (hashText.isNotEmpty()) {
452                     // Remove the hash information
453                     sourceFile =
454                         sourceFile.substring(0 until sourceFile.length - hashText.length - 1)
455                     packageHash =
456                         try {
457                             hashText.parseToInt(36)
458                         } catch (_: NumberFormatException) {
459                             -1
460                         }
461                 }
462                 break@loop
463             }
464             else -> break@loop
465         }
466         if (mr == currentResult) return null
467     }
468 
469     return SourceInformationContext(
470         name = name,
471         sourceFile = sourceFile ?: parent?.sourceFile,
472         packageHash = if (sourceFile != null) packageHash else parent?.packageHash ?: packageHash,
473         locations = sourceLocations,
474         repeatOffset = repeatOffset,
475         parameters = parameters,
476         isCall = isCall,
477         isInline = isInline
478     )
479 }
480 
481 /** Iterate the slot table and extract a group tree that corresponds to the content of the table. */
482 @UiToolingDataApi
getGroupnull483 private fun CompositionGroup.getGroup(parentContext: SourceInformationContext?): Group {
484     val key = key
485     val context = sourceInfo?.let { sourceInformationContextOf(it, parentContext) }
486     val node = node
487     val data = mutableListOf<Any?>()
488     val children = mutableListOf<Group>()
489     data.addAll(this.data)
490     for (child in compositionGroups) children.add(child.getGroup(context))
491 
492     val modifierInfo =
493         if (node is LayoutInfo) {
494             node.getModifierInfo()
495         } else {
496             emptyList()
497         }
498 
499     // Calculate bounding box
500     val box =
501         when (node) {
502             is LayoutInfo -> boundsOfLayoutNode(node)
503             else ->
504                 if (children.isEmpty()) emptyBox
505                 else children.map { g -> g.box }.reduce { acc, box -> box.union(acc) }
506         }
507     val location =
508         if (context?.isCall == true) {
509             parentContext?.nextSourceLocation()
510         } else {
511             null
512         }
513     return if (node != null) NodeGroup(key, node, box, data, modifierInfo, children)
514     else
515         CallGroup(
516             key,
517             context?.name,
518             box,
519             location,
520             identity =
521                 if (
522                     !context?.name.isNullOrEmpty() &&
523                         (box.bottom - box.top > 0 || box.right - box.left > 0)
524                 ) {
525                     this.identity
526                 } else {
527                     null
528                 },
529             extractParameterInfo(data, context),
530             data,
531             children,
532             context?.isInline == true
533         )
534 }
535 
boundsOfLayoutNodenull536 private fun boundsOfLayoutNode(node: LayoutInfo): IntRect {
537     val coordinates = node.coordinates
538     if (!node.isAttached || !coordinates.isAttached) {
539         return IntRect(left = 0, top = 0, right = node.width, bottom = node.height)
540     }
541     val position = coordinates.positionInWindow()
542     val size = coordinates.size
543     val left = position.x.roundToInt()
544     val top = position.y.roundToInt()
545     val right = left + size.width
546     val bottom = top + size.height
547     return IntRect(left = left, top = top, right = right, bottom = bottom)
548 }
549 
550 @UiToolingDataApi
551 private class CompositionCallStack<T>(
552     private val factory: (CompositionGroup, SourceContext, List<T>) -> T?,
553     private val contexts: MutableMap<String, Any?>
554 ) : SourceContext {
555     private val stack = ArrayDeque<CompositionGroup>()
556     private var currentCallIndex = 0
557 
convertnull558     fun convert(group: CompositionGroup, callIndex: Int, out: MutableList<T>): IntRect {
559         val children = mutableListOf<T>()
560         var box = emptyBox
561         push(group)
562         var childCallIndex = 0
563         group.compositionGroups.forEach { child ->
564             box = box.union(convert(child, childCallIndex, children))
565             if (isCall(child)) {
566                 childCallIndex++
567             }
568         }
569         box = (group.node as? LayoutInfo)?.let { boundsOfLayoutNode(it) } ?: box
570         currentCallIndex = callIndex
571         bounds = box
572         factory(group, this, children)?.let { out.add(it) }
573         pop()
574         return box
575     }
576 
577     override val name: String?
578         get() {
579             val info = current.sourceInfo ?: return null
580             val startIndex =
581                 when {
582                     info.startsWith("CC(") -> 3
583                     info.startsWith("C(") -> 2
584                     else -> return null
585                 }
586             val endIndex = info.indexOf(')')
587             return if (endIndex > 2) info.substring(startIndex, endIndex) else null
588         }
589 
590     override val isInline: Boolean
591         get() = current.sourceInfo?.startsWith("CC") == true
592 
593     override var bounds: IntRect = emptyBox
594         private set
595 
596     override val location: SourceLocation?
597         get() {
<lambda>null598             val context = parentGroup(1)?.sourceInfo?.let { contextOf(it) } ?: return null
599             var parentContext: SourceInformationContext? = context
600             var index = 2
601             while (index < stack.size && parentContext?.sourceFile == null) {
<lambda>null602                 parentContext = parentGroup(index++)?.sourceInfo?.let { contextOf(it) }
603             }
604             return context.sourceLocation(currentCallIndex, parentContext)
605         }
606 
607     override val parameters: List<ParameterInformation>
608         get() {
609             val group = current
<lambda>null610             val context = group.sourceInfo?.let { contextOf(it) } ?: return emptyList()
611             val data = mutableListOf<Any?>()
612             data.addAll(group.data)
613             return extractParameterInfo(data, context)
614         }
615 
616     override val depth: Int
617         get() = stack.size
618 
pushnull619     private fun push(group: CompositionGroup) = stack.addLast(group)
620 
621     private fun pop() = stack.removeLast()
622 
623     private val current: CompositionGroup
624         get() = stack.last()
625 
626     private fun parentGroup(parentDepth: Int): CompositionGroup? =
627         if (stack.size > parentDepth) stack[stack.size - parentDepth - 1] else null
628 
629     private fun contextOf(information: String): SourceInformationContext? =
630         contexts.getOrPut(information) { sourceInformationContextOf(information) }
631             as? SourceInformationContext
632 
isCallnull633     private fun isCall(group: CompositionGroup): Boolean =
634         group.sourceInfo?.startsWith("C") ?: false
635 }
636 
637 /** A cache of [SourceInformationContext] that optionally can be specified when using [mapTree]. */
638 @UiToolingDataApi
639 class ContextCache {
640     /** Clears the cache. */
641     fun clear() {
642         contexts.clear()
643     }
644 
645     internal val contexts = mutableMapOf<String, Any?>()
646 }
647 
648 /**
649  * Context with data for creating group nodes.
650  *
651  * See the factory argument of [mapTree].
652  */
653 @UiToolingDataApi
654 interface SourceContext {
655     /** The name of the Composable or null if not applicable. */
656     val name: String?
657 
658     /** The bounds of the Composable if known. */
659     val bounds: IntRect
660 
661     /** The [SourceLocation] of where the Composable was called. */
662     val location: SourceLocation?
663 
664     /** The parameters of the Composable. */
665     val parameters: List<ParameterInformation>
666 
667     /** The current depth into the [CompositionGroup] tree. */
668     val depth: Int
669 
670     /** The source context is for a call to an inline composable function */
671     val isInline: Boolean
672         get() = false
673 }
674 
675 /**
676  * Return a tree of custom nodes for the slot table.
677  *
678  * The [factory] method will be called for every [CompositionGroup] in the slot tree and can be used
679  * to create custom nodes based on the passed arguments. The [SourceContext] argument gives access
680  * to additional information encoded in the [CompositionGroup.sourceInfo]. A return of null from
681  * [factory] means that the entire subtree will be ignored.
682  *
683  * A [cache] can optionally be specified. If a client is calling [mapTree] multiple times, this can
684  * save some time if the values of [CompositionGroup.sourceInfo] are not unique.
685  */
686 @UiToolingDataApi
mapTreenull687 fun <T> CompositionData.mapTree(
688     factory: (CompositionGroup, SourceContext, List<T>) -> T?,
689     cache: ContextCache = ContextCache()
690 ): T? {
691     val group = compositionGroups.firstOrNull() ?: return null
692     val callStack = CompositionCallStack(factory, cache.contexts)
693     val out = mutableListOf<T>()
694     callStack.convert(group, 0, out)
695     return out.firstOrNull()
696 }
697 
698 /** Return the parameters found for this [CompositionGroup]. */
699 @UiToolingDataApi
findParametersnull700 fun CompositionGroup.findParameters(cache: ContextCache? = null): List<ParameterInformation> {
701     val information = sourceInfo ?: return emptyList()
702     val context =
703         if (cache == null) sourceInformationContextOf(information)
704         else
705             cache.contexts.getOrPut(information) { sourceInformationContextOf(information) }
706                 as? SourceInformationContext
707     val data = mutableListOf<Any?>()
708     data.addAll(this.data)
709     return extractParameterInfo(data, context)
710 }
711 
712 /**
713  * Return a group tree for for the slot table that represents the entire content of the slot table.
714  */
715 @UiToolingDataApi
asTreenull716 fun CompositionData.asTree(): Group = compositionGroups.firstOrNull()?.getGroup(null) ?: EmptyGroup
717 
718 internal fun IntRect.union(other: IntRect): IntRect {
719     if (this == emptyBox) return other else if (other == emptyBox) return this
720 
721     return IntRect(
722         left = min(left, other.left),
723         top = min(top, other.top),
724         bottom = max(bottom, other.bottom),
725         right = max(right, other.right)
726     )
727 }
728 
729 @UiToolingDataApi
keyPositionnull730 private fun keyPosition(key: Any?): String? =
731     when (key) {
732         is String -> key
733         is JoinedKey -> keyPosition(key.left) ?: keyPosition(key.right)
734         else -> null
735     }
736 
737 private const val parameterPrefix = "${'$'}"
738 private const val internalFieldPrefix = parameterPrefix + parameterPrefix
739 private const val defaultFieldName = "${internalFieldPrefix}default"
740 private const val changedFieldName = "${internalFieldPrefix}changed"
741 private const val jacocoDataField = "${parameterPrefix}jacoco"
742 private const val recomposeScopeNameSuffix = ".RecomposeScopeImpl"
743 
744 @UiToolingDataApi
extractParameterInfonull745 private fun extractParameterInfo(
746     data: List<Any?>,
747     context: SourceInformationContext?
748 ): List<ParameterInformation> {
749     val recomposeScope =
750         data.firstOrNull { it != null && it.javaClass.name.endsWith(recomposeScopeNameSuffix) }
751             ?: return emptyList()
752 
753     val block =
754         recomposeScope.javaClass.accessibleField("block")?.get(recomposeScope) ?: return emptyList()
755 
756     val parametersMetadata = context?.parameters.orEmpty()
757     val blockClass = block.javaClass
758 
759     return try {
760         val inlineFields = filterParameterFields(blockClass.declaredFields, isIndyLambda = true)
761 
762         if (inlineFields.isNotEmpty()) {
763             extractFromIndyLambdaFields(inlineFields, block, parametersMetadata)
764         } else {
765             val legacyFields =
766                 filterParameterFields(blockClass.declaredFields, isIndyLambda = false)
767             extractFromLegacyFields(legacyFields, block, parametersMetadata)
768         }
769     } catch (e: Exception) {
770         emptyList()
771     }
772 }
773 
774 @OptIn(UiToolingDataApi::class)
extractFromIndyLambdaFieldsnull775 private fun extractFromIndyLambdaFields(
776     fields: List<Field>,
777     block: Any,
778     metadata: List<Parameter>
779 ): List<ParameterInformation> {
780     val sortedFields =
781         fields.sortedBy { it.name.substringAfter("f$").toIntOrNull() ?: Int.MAX_VALUE }
782 
783     val blockClass = block.javaClass
784     val defaults = blockClass.accessibleField(defaultFieldName)?.get(block) as? Int ?: 0
785     val changed = blockClass.accessibleField(changedFieldName)?.get(block) as? Int ?: 0
786 
787     return sortedFields.mapIndexed { index, field ->
788         buildParameterInfo(field, block, index, defaults, changed, metadata.getOrNull(index))
789     }
790 }
791 
792 @OptIn(UiToolingDataApi::class)
extractFromLegacyFieldsnull793 private fun extractFromLegacyFields(
794     fields: List<Field>,
795     block: Any,
796     metadata: List<Parameter>
797 ): List<ParameterInformation> {
798     val blockClass = block.javaClass
799     val defaults = blockClass.accessibleField(defaultFieldName)?.get(block) as? Int ?: 0
800     val changed = blockClass.accessibleField(changedFieldName)?.get(block) as? Int ?: 0
801 
802     return fields.mapIndexedNotNull { index, _ ->
803         val paramMeta = metadata.getOrNull(index) ?: Parameter(index)
804         val sortedIndex = paramMeta.sortedIndex
805         if (sortedIndex >= fields.size) return@mapIndexedNotNull null
806 
807         val field = fields[sortedIndex]
808         buildParameterInfo(field, block, index, defaults, changed, paramMeta)
809     }
810 }
811 
812 @UiToolingDataApi
buildParameterInfonull813 private fun buildParameterInfo(
814     field: Field,
815     block: Any,
816     index: Int,
817     defaults: Int,
818     changed: Int,
819     metadata: Parameter?
820 ): ParameterInformation {
821     field.isAccessible = true
822     val value = field.get(block)
823 
824     val fromDefault = (1 shl index) and defaults != 0
825     val changedOffset = index * BITS_PER_SLOT + 1
826     val parameterChanged = ((SLOT_MASK shl changedOffset) and changed) shr changedOffset
827 
828     val static = parameterChanged and STATIC_BITS == STATIC_BITS
829     val compared = parameterChanged and STATIC_BITS == 0
830     val stable = parameterChanged and STABLE_BITS == 0
831 
832     return ParameterInformation(
833         name = field.name.substring(1),
834         value = value,
835         fromDefault = fromDefault,
836         static = static,
837         compared = compared && !fromDefault,
838         inlineClass = metadata?.inlineClass,
839         stable = stable
840     )
841 }
842 
filterParameterFieldsnull843 private fun filterParameterFields(fields: Array<Field>, isIndyLambda: Boolean): List<Field> {
844     return fields.filter { field ->
845         val name = field.name
846         val matchesInlinePattern = name.matches(Regex("^f\\$\\d+$"))
847         val matchesLegacyPattern = name.startsWith(parameterPrefix)
848 
849         val validPrefix =
850             isIndyLambda && matchesInlinePattern || !isIndyLambda && matchesLegacyPattern
851 
852         validPrefix && !name.startsWith(internalFieldPrefix) && !name.startsWith(jacocoDataField)
853     }
854 }
855 
856 private const val BITS_PER_SLOT = 3
857 private const val SLOT_MASK = 0b111
858 private const val STATIC_BITS = 0b011
859 private const val STABLE_BITS = 0b100
860 
861 /** The source position of the group extracted from the key, if one exists for the group. */
862 @UiToolingDataApi
863 val Group.position: String?
864     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") @UiToolingDataApi get() = keyPosition(key)
865 
Classnull866 private fun Class<*>.accessibleField(name: String): Field? =
867     declaredFields.firstOrNull { it.name == name }?.apply { isAccessible = true }
868 
Stringnull869 private fun String.replacePrefix(prefix: String, replacement: String) =
870     if (startsWith(prefix)) replacement + substring(prefix.length) else this
871