1 /*
<lambda>null2  * Copyright 2024 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 package androidx.compose.runtime.tooling
18 
19 import androidx.compose.runtime.snapshots.fastForEach
20 
21 /**
22  * A diagnostic exception with a composition stack trace. This exception is usually appended to the
23  * suppressed exceptions when [androidx.compose.runtime.Composer.setDiagnosticStackTraceEnabled]
24  * flag is set to true.
25  */
26 internal expect class DiagnosticComposeException(
27     trace: List<ComposeStackTraceFrame>,
28 ) : RuntimeException
29 
30 internal data class ComposeStackTraceFrame(
31     val sourceInfo: ParsedSourceInformation,
32     val groupOffset: Int?
33 )
34 
35 internal class ParsedSourceInformation(
36     val isCall: Boolean,
37     val functionName: String?,
38     val fileName: String?,
39     val packageHash: String?,
40     val lineNumbers: IntArray,
41     val dataString: String
42 )
43 
44 internal fun Throwable.tryAttachComposeStackTrace(
45     trace: () -> List<ComposeStackTraceFrame>
46 ): Boolean {
47     var result = false
48     if (suppressedExceptions.none { it is DiagnosticComposeException }) {
49         val traceException =
50             try {
51                 val frames = trace()
52                 result = frames.isNotEmpty()
53                 if (result) DiagnosticComposeException(frames) else null
54             } catch (e: Throwable) {
55                 // Attach the exception thrown while collecting trace.
56                 // Usually this means that the slot table is malformed.
57                 e
58             }
59         if (traceException != null) {
60             addSuppressed(traceException)
61         }
62     }
63     return result
64 }
65 
attachComposeStackTracenull66 internal fun Throwable.attachComposeStackTrace(
67     trace: () -> List<ComposeStackTraceFrame>
68 ): Throwable = apply { tryAttachComposeStackTrace(trace) }
69 
appendStackTracenull70 internal fun StringBuilder.appendStackTrace(trace: List<ComposeStackTraceFrame>) {
71     var currentFunction: String? = null
72     var currentFile: String? = null
73     val lines = buildList {
74         trace.asReversed().fastForEach { frame ->
75             val sourceInfo = frame.sourceInfo
76             val functionName = sourceInfo.functionName ?: currentFunction ?: "<unknown function>"
77             val fileName = sourceInfo.fileName ?: currentFile ?: "<unknown file>"
78             val lineNumbers = sourceInfo.lineNumbers
79             val resolvedLine =
80                 if (frame.groupOffset != null && frame.groupOffset < lineNumbers.size) {
81                     lineNumbers[frame.groupOffset].toString()
82                 } else {
83                     if (IncludeDebugInfo) {
84                         "<no offset ${frame.groupOffset} in $lineNumbers>"
85                     } else {
86                         "<unknown line>"
87                     }
88                 }
89 
90             val traceLine = buildString {
91                 append(functionName)
92                 append('(')
93                 append(fileName)
94                 append(':')
95                 append(resolvedLine)
96                 append(')')
97 
98                 if (IncludeDebugInfo) {
99                     append(", parsed from ")
100                     append(sourceInfo.dataString)
101                     append(", group offset: ")
102                     append(frame.groupOffset)
103                 }
104             }
105 
106             if (!sourceInfo.isCall) {
107                 // replace previous line for source info, since this line will provide more
108                 // precise info for line numbers from previous entry
109                 val line = removeLastOrNull()
110                 if (IncludeDebugInfo) {
111                     add("$line (collapsed)")
112                 }
113             }
114 
115             // Filter first subcomposition frames that point to rememberCompositionContext.
116             if (
117                 sourceInfo.functionName == "rememberCompositionContext" &&
118                     sourceInfo.packageHash == RuntimePackageHash
119             ) {
120                 if (IncludeDebugInfo) {
121                     add("$traceLine (ignored)")
122                 }
123             } else {
124                 add(traceLine)
125             }
126 
127             currentFunction = functionName
128             currentFile = fileName
129         }
130     }
131     lines.asReversed().fastForEach { appendLine("\tat $it") }
132 }
133 
134 private const val RuntimePackageHash = "9igjgp"
135 
136 private const val IncludeDebugInfo = false
137