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