Files
swift-mirror/stdlib/public/RuntimeModule/BacktraceFormatter.swift
Alastair Houghton 760cc57bef [Backtracing] Rename _Backtracing to Runtime.
Move the backtracing code into a new Runtime module.  This means renaming
the Swift Runtime's CMake target because otherwise there will be a name
clash.

rdar://124913332
2025-01-17 10:09:36 +00:00

1013 lines
32 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===--- BacktraceFormatter.swift -----------------------------*- swift -*-===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
//
// Provides functionality to format backtraces, with various additional
// options.
//
//===----------------------------------------------------------------------===//
import Swift
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
internal import Darwin
internal import BacktracingImpl.OS.Darwin
#elseif os(Windows)
internal import ucrt
#elseif canImport(Glibc)
internal import Glibc
#elseif canImport(Musl)
internal import Musl
#endif
/// A backtrace formatting theme.
@_spi(Formatting)
public protocol BacktraceFormattingTheme {
func frameIndex(_ s: String) -> String
func programCounter(_ s: String) -> String
func frameAttribute(_ s: String) -> String
func symbol(_ s: String) -> String
func offset(_ s: String) -> String
func sourceLocation(_ s: String) -> String
func lineNumber(_ s: String) -> String
func code(_ s: String) -> String
func crashedLineNumber(_ s: String) -> String
func crashedLine(_ s: String) -> String
func crashLocation() -> String
func imageName(_ s: String) -> String
func imageAddressRange(_ s: String) -> String
func imageBuildID(_ s: String) -> String
func imagePath(_ s: String) -> String
}
extension BacktraceFormattingTheme {
public func frameIndex(_ s: String) -> String { return s }
public func programCounter(_ s: String) -> String { return s }
public func frameAttribute(_ s: String) -> String { return "[\(s)]" }
public func symbol(_ s: String) -> String { return s }
public func offset(_ s: String) -> String { return s }
public func sourceLocation(_ s: String) -> String { return s }
public func lineNumber(_ s: String) -> String { return " \(s)|" }
public func code(_ s: String) -> String { return s }
public func crashedLineNumber(_ s: String) -> String { return "*\(s)|" }
public func crashedLine(_ s: String) -> String { return s }
public func crashLocation() -> String { return "^" }
public func imageName(_ s: String) -> String { return s }
public func imageAddressRange(_ s: String) -> String { return s }
public func imageBuildID(_ s: String) -> String { return s }
public func imagePath(_ s: String) -> String { return s}
}
/// Options for backtrace formatting.
///
/// This is used by chaining modifiers, e.g. .theme(.color).showSourceCode().
@_spi(Formatting)
public struct BacktraceFormattingOptions {
var _theme: BacktraceFormattingTheme = BacktraceFormatter.Themes.plain
var _showSourceCode: Bool = false
var _sourceContextLines: Int = 2
var _showAddresses: Bool = true
var _showImages: ImagesToShow = .mentioned
var _showImageNames: Bool = true
var _showFrameAttributes: Bool = true
var _skipRuntimeFailures: Bool = false
var _skipThunkFunctions: Bool = true
var _skipSystemFrames: Bool = true
var _sanitizePaths: Bool = true
var _demangle: Bool = true
var _width: Int = 80
public var selectedTheme: BacktraceFormattingTheme { return _theme }
public var shouldShowSourceCode: Bool { return _showSourceCode }
public var sourceContextLines: Int { return _sourceContextLines }
public var shouldShowAddresses: Bool { return _showAddresses }
public var imagesToShow: ImagesToShow { return _showImages }
public var shouldShowImageNames: Bool { return _showImageNames }
public var shouldShowFrameAttributes: Bool { return _showFrameAttributes }
public var shouldSkipRuntimeFailures: Bool { return _skipRuntimeFailures }
public var shouldSkipThunkFunctions: Bool { return _skipThunkFunctions }
public var shouldSkipSystemFrames: Bool { return _skipSystemFrames }
public var shouldSanitizePaths: Bool { return _sanitizePaths }
public var shouldDemangle: Bool { return _demangle }
public var formattingWidth: Int { return _width }
public init() {}
/// Theme to use for formatting.
///
/// @param theme A `BacktraceFormattingTheme` structure.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().theme(theme)
}
public func theme(_ theme: BacktraceFormattingTheme) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._theme = theme
return newOptions
}
/// Enable or disable the display of source code in the backtrace.
///
/// @param enabled Whether or not to enable source code.
///
/// @param contextLines The number of lines of context either side of the
/// line associated with the backtrace frame.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().showSourceCode(enabled,
contextLines: contextLines)
}
public func showSourceCode(_ enabled: Bool = true, contextLines: Int = 2) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._showSourceCode = enabled
newOptions._sourceContextLines = contextLines
return newOptions
}
/// Enable or disable the display of raw addresses.
///
/// @param enabled If false, we will only display a raw address in the
/// backtrace if we haven't been able to symbolicate.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().showAddresses(enabled)
}
public func showAddresses(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._showAddresses = enabled
return newOptions
}
/// Enable or disable the display of the image list.
///
/// @param enabled Says whether or not to output the image list.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public enum ImagesToShow {
case none
case mentioned
case all
}
public static func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().showImages(toShow)
}
public func showImages(_ toShow: ImagesToShow = .all) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._showImages = toShow
return newOptions
}
/// Enable or disable the display of image names in the frame list.
///
/// @param enabled If true, we will display the name of the image for
/// each frame.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().showImageNames(enabled)
}
public func showImageNames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._showImageNames = enabled
return newOptions
}
/// Enable or disable the display of frame attributes in the frame list.
///
/// @param enabled If true, we will display the frame attributes.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().showFrameAttributes(enabled)
}
public func showFrameAttributes(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._showFrameAttributes = enabled
return newOptions
}
/// Set whether or not to show Swift runtime failure frames.
///
/// @param enabled If true, we will skip Swift runtime failure frames.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().skipRuntimeFailures(enabled)
}
public func skipRuntimeFailures(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._skipRuntimeFailures = enabled
return newOptions
}
/// Set whether or not to show Swift thunk function frames.
///
/// @param enabled If true, we will skip Swift thunk function frames.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().skipThunkFunctions(enabled)
}
public func skipThunkFunctions(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._skipThunkFunctions = enabled
return newOptions
}
/// Set whether or not to show system frames.
///
/// For instance, on macOS, this will cause us to skip the "start" frame
/// at the very top of the stack.
///
/// @param enabled If true, we will skip system frames.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().skipSystemFrames(enabled)
}
public func skipSystemFrames(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._skipSystemFrames = enabled
return newOptions
}
/// Enable or disable path sanitization.
///
/// This is intended to avoid leaking PII into crash logs.
///
/// @param enabled If true, paths will be sanitized.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().sanitizePaths(enabled)
}
public func sanitizePaths(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._sanitizePaths = enabled
return newOptions
}
/// Set whether we show mangled or demangled names.
///
/// @param enabled If true, we show demangled names if we have them.
///
/// @returns A new `BacktraceFormattingOptions` structure.
public static func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().demangle(enabled)
}
public func demangle(_ enabled: Bool = true) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._demangle = enabled
return newOptions
}
/// Set the output width.
///
/// @param width The output width in characters. This is only used to
/// highlight information, and defaults to 80.
///
/// returns A new `BacktraceFormattingOptions` structure.
public static func width(_ width: Int) -> BacktraceFormattingOptions {
return BacktraceFormattingOptions().width(width)
}
public func width(_ width: Int) -> BacktraceFormattingOptions {
var newOptions = self
newOptions._width = width
return newOptions
}
}
/// Return the width of a given Unicode.Scalar.
///
/// It would be nice to have the Unicode width data, which would let us do
/// a better job of this.
private func measure(_ ch: Unicode.Scalar) -> Int {
if ch.isASCII {
return 1
}
if ch.properties.isEmoji {
return 2
}
if ch.properties.isIdeographic
&& !(ch.value >= 0xff61 && ch.value <= 0xffdc)
&& !(ch.value >= 0xffe8 && ch.value <= 0xffee) {
return 2
}
if ch.properties.canonicalCombiningClass.rawValue != 0 {
return 0
}
switch ch.properties.generalCategory {
case .control, .nonspacingMark:
return 0
default:
return 1
}
}
/// Compute the width of the given string, ignoring CSI formatting codes.
private enum MeasureState {
// Normal state
case normal
// Start of an escape
case escape
// In a CSI escape
case csi
}
private func measure<S: StringProtocol>(_ s: S) -> Int {
var totalWidth = 0
var state: MeasureState = .normal
for ch in s.unicodeScalars {
switch state {
case .normal:
if ch.value == 27 {
// This is an escape sequence
state = .escape
} else {
totalWidth += measure(ch)
}
case .escape:
if ch.value == 0x5b {
state = .csi
} else {
state = .normal
}
case .csi:
if ch.value >= 0x40 && ch.value <= 0x7e {
state = .normal
}
}
}
return totalWidth
}
/// Pad the given string to the given width using spaces.
private func pad(_ s: String, to width: Int,
aligned alignment: BacktraceFormatter.Alignment = .left)
-> String {
let currentWidth = measure(s)
let padding = max(width - currentWidth, 0)
switch alignment {
case .left:
let spaces = String(repeating: " ", count: padding)
return s + spaces
case .right:
let spaces = String(repeating: " ", count: padding)
return spaces + s
case .center:
let left = padding / 2
let right = padding - left
let leftSpaces = String(repeating: " ", count: left)
let rightSpaces = String(repeating: " ", count: right)
return "\(leftSpaces)\(s)\(rightSpaces)"
}
}
/// Untabify the given string, assuming tabs of the specified size.
///
/// @param s The string to untabify.
/// @param tabWidth The tab width to assume (default 8).
///
/// @returns A string with all the tabs replaced with appropriate numbers
/// of spaces.
private func untabify(_ s: String, tabWidth: Int = 8) -> String {
var result: String = ""
var first = true
for chunk in s.split(separator: "\t", omittingEmptySubsequences: false) {
if first {
first = false
} else {
let toTabStop = tabWidth - measure(result) % tabWidth
result += String(repeating: " ", count: toTabStop)
}
result += chunk
}
return result
}
/// Sanitize a path to remove usernames, volume names and so on.
///
/// The point of this function is to try to remove anything that might
/// contain PII before it ends up in a log file somewhere.
///
/// @param path The path to sanitize.
///
/// @returns A string containing the sanitized path.
private func sanitizePath(_ path: String) -> String {
#if os(macOS)
return CRCopySanitizedPath(path,
kCRSanitizePathGlobAllTypes
| kCRSanitizePathKeepFile)
#else
// For now, on non-macOS systems, do nothing
return path
#endif
}
/// Trim whitespace from the right hand end of a string.
///
/// @param s The string to trim.
///
/// @returns A string with the whitespace trimmed.
private func rtrim<S: StringProtocol>(_ s: S) -> S.SubSequence {
if let lastNonWhitespace = s.lastIndex(where: { !$0.isWhitespace }) {
return s.prefix(through: lastNonWhitespace)
}
return s.dropLast(0)
}
/// Responsible for formatting backtraces.
@_spi(Formatting)
public struct BacktraceFormatter {
/// The formatting options to apply when formatting data.
public var options: BacktraceFormattingOptions
public struct Themes {
/// A plain formatting theme.
public struct PlainTheme: BacktraceFormattingTheme {
}
public static let plain = PlainTheme()
}
public init(_ options: BacktraceFormattingOptions) {
self.options = options
}
public enum TableRow {
case columns([String])
case raw(String)
}
public enum Alignment {
case left
case right
case center
}
/// Output a table with each column nicely aligned.
///
/// @param rows An array of table rows, each of which holds an array
/// of table columns.
///
/// @result A `String` containing the formatted table.
public static func formatTable(_ rows: [TableRow],
alignments: [Alignment] = []) -> String {
// Work out how many columns we have
let colCount = rows.map{
if case let .columns(columns) = $0 {
return columns.count
} else {
return 0
}
}.reduce(0, max)
// Now compute their widths
var widths = Array(repeating: 0, count: colCount)
for row in rows {
if case let .columns(columns) = row {
for (n, width) in columns.lazy.map(measure).enumerated() {
widths[n] = max(widths[n], width)
}
}
}
// Generate lines for the table
var lines: [Substring] = []
for row in rows {
switch row {
case let .columns(columns):
let line = columns.enumerated().map{ n, column in
let alignment = n < alignments.count ? alignments[n] : .left
if n == colCount - 1 && alignment == .left {
return column
} else {
return pad(column, to: widths[n], aligned: alignment)
}
}.joined(separator: " ")
lines.append(rtrim(line))
case let .raw(line):
lines.append(rtrim(line))
}
}
// Trim any empty lines from the end
guard let lastNonEmpty = lines.lastIndex(where: { !$0.isEmpty }) else {
return ""
}
return lines.prefix(through: lastNonEmpty).joined(separator: "\n")
}
/// Format an individual frame into a list of columns.
///
/// @param frame The frame to format.
/// @param index The frame index, if required.
///
/// @result An array of strings, one per column.
public func formatColumns(frame: Backtrace.Frame,
index: Int? = nil) -> [String] {
let pc: String
var attrs: [String] = []
switch frame {
case let .programCounter(address):
pc = "\(address)"
case let .returnAddress(address):
pc = "\(address)"
attrs.append("ra")
case let .asyncResumePoint(address):
pc = "\(address)"
attrs.append("async")
case .omittedFrames(_), .truncated:
pc = "..."
}
var columns: [String] = []
if let index = index {
columns.append(options._theme.frameIndex("\(index)"))
}
if options._showFrameAttributes {
columns.append(attrs.map(
options._theme.frameAttribute
).joined(separator: " "))
}
columns.append(options._theme.programCounter(pc))
return columns
}
/// Format a frame into a list of rows.
///
/// @param frame The frame to format.
/// @param index The frame index, if required.
///
/// @result An array of table rows.
public func formatRows(frame: Backtrace.Frame,
index: Int? = nil) -> [TableRow] {
return [.columns(formatColumns(frame: frame,
index: index))]
}
/// Format just one frame.
///
/// @param frame The frame to format.
/// @param index The frame index, if required.
///
/// @result A `String` containing the formatted data.
public func format(frame: Backtrace.Frame,
index: Int? = nil) -> String {
let rows = formatRows(frame: frame,
index: index)
return BacktraceFormatter.formatTable(rows, alignments: [.right])
}
/// Format the frame list from a backtrace.
///
/// @param frames The frames to format.
///
/// @result A `String` containing the formatted data.
public func format(frames: some Sequence<Backtrace.Frame>) -> String {
var rows: [TableRow] = []
var n = 0
for frame in frames {
rows += formatRows(frame: frame, index: n)
if case let .omittedFrames(count) = frame {
n += count
} else {
n += 1
}
}
return BacktraceFormatter.formatTable(rows, alignments: [.right])
}
/// Format a `Backtrace`
///
/// @param backtrace The `Backtrace` object to format.
///
/// @result A `String` containing the formatted data.
public func format(backtrace: Backtrace) -> String {
return format(frames: backtrace.frames)
}
/// Grab source lines for a symbolicated backtrace.
///
/// Tries to open the file corresponding to the symbol; if successful,
/// it will return a string containing the specified lines of context,
/// with the point at which the program crashed highlighted.
private func formattedSourceLines(from sourceLocation: SymbolicatedBacktrace.SourceLocation,
indent theIndent: Int = 2) -> String? {
guard let fp = fopen(sourceLocation.path, "rt") else {
return nil
}
defer {
fclose(fp)
}
let indent = String(repeating: " ", count: theIndent)
var lines: [String] = []
var line = 1
let buffer = UnsafeMutableBufferPointer<CChar>.allocate(capacity: 4096)
var currentLine = ""
let maxLine = sourceLocation.line + options._sourceContextLines
let maxLineWidth = max("\(maxLine)".count, 4)
let doLine = { sourceLine in
if line >= sourceLocation.line - options._sourceContextLines
&& line <= sourceLocation.line + options._sourceContextLines {
let untabified = untabify(sourceLine)
let code = options._theme.code(untabified)
let theLine: String
if line == sourceLocation.line {
let lineNumber = options._theme.crashedLineNumber(pad("\(line)",
to: maxLineWidth,
aligned: .right))
let highlightWidth = options._width - 2 * theIndent
theLine = options._theme.crashedLine(pad("\(lineNumber) \(code)",
to: highlightWidth))
} else {
let lineNumber = options._theme.lineNumber(pad("\(line)",
to: maxLineWidth,
aligned: .right))
theLine = "\(lineNumber) \(code)"
}
lines.append("\(indent)\(theLine)")
if line == sourceLocation.line {
// sourceLocation.column is an index in UTF-8 code units in
// `untabified`. We should point at the grapheme cluster that
// contains that UTF-8 index.
let adjustedColumn: Int
if sourceLocation.column > 0 {
adjustedColumn = sourceLocation.column
} else {
if let ndx = code.firstIndex(where: { $0 != " " }) {
adjustedColumn = code.distance(from: code.startIndex, to: ndx) + 1
} else {
adjustedColumn = 1
}
}
let utf8Ndx
= untabified.utf8.index(untabified.utf8.startIndex,
offsetBy: adjustedColumn,
limitedBy: untabified.utf8.endIndex)
?? untabified.utf8.endIndex
// Adjust it to point at a grapheme cluster start
let strNdx = untabified.index(
untabified.index(utf8Ndx, offsetBy: 1,
limitedBy: untabified.endIndex)
?? untabified.endIndex,
offsetBy: -1,
limitedBy: untabified.startIndex) ?? untabified.startIndex
// Work out the terminal width up to that point
let terminalWidth = measure(untabified.prefix(upTo: strNdx))
let pad = String(repeating: " ",
count: max(terminalWidth - 1, 0))
let marker = options._theme.crashLocation()
let blankForNumber = options._theme.lineNumber(
String(repeating: " ", count: maxLineWidth))
lines.append("\(indent)\(blankForNumber) \(pad)\(marker)")
}
}
}
while feof(fp) == 0 && ferror(fp) == 0 {
guard let result = fgets(buffer.baseAddress,
CInt(buffer.count), fp) else {
break
}
let chunk = String(cString: result)
currentLine += chunk
if currentLine.hasSuffix("\n") {
currentLine.removeLast()
doLine(currentLine)
currentLine = ""
line += 1
}
}
doLine(currentLine)
return lines.joined(separator: "\n")
}
/// Format an individual frame into a list of columns.
///
/// @params frame The frame to format.
///
/// @result An array of strings, one per column.
public func formatColumns(frame: SymbolicatedBacktrace.Frame,
index: Int? = nil) -> [String] {
let pc: String
var attrs: [String] = []
switch frame.captured {
case let .programCounter(address):
pc = "\(address)"
case let .returnAddress(address):
pc = "\(address)"
attrs.append("ra")
case let .asyncResumePoint(address):
pc = "\(address)"
attrs.append("async")
case .omittedFrames(_), .truncated:
pc = ""
}
if frame.inlined {
attrs.append("inlined")
}
if frame.isSwiftThunk {
attrs.append("thunk")
}
if frame.isSystem {
attrs.append("system")
}
var formattedSymbol: String? = nil
var hasSourceLocation = false
if let symbol = frame.symbol {
let displayName = options._demangle ? symbol.name : symbol.rawName
let themedName = options._theme.symbol(displayName)
let offset: String
if symbol.offset > 0 {
offset = options._theme.offset(" + \(symbol.offset)")
} else if symbol.offset < 0 {
offset = options._theme.offset(" - \(-symbol.offset)")
} else {
offset = ""
}
let imageName: String
if options._showImageNames {
if symbol.imageIndex >= 0 {
imageName = " in " + options._theme.imageName(symbol.imageName)
} else {
imageName = ""
}
} else {
imageName = ""
}
let location: String
if var sourceLocation = symbol.sourceLocation {
if options._sanitizePaths {
sourceLocation.path = sanitizePath(sourceLocation.path)
}
location = " at " + options._theme.sourceLocation("\(sourceLocation)")
hasSourceLocation = true
} else {
location = ""
}
formattedSymbol = "\(themedName)\(offset)\(imageName)\(location)"
}
let location: String
if !hasSourceLocation || options._showAddresses {
let formattedPc = options._theme.programCounter(pc)
if let formattedSymbol = formattedSymbol {
location = "\(formattedPc) \(formattedSymbol)"
} else {
location = formattedPc
}
} else if let formattedSymbol = formattedSymbol {
location = formattedSymbol
} else {
location = options._theme.programCounter(pc)
}
var columns: [String] = []
if let index = index {
let frameIndex: String
switch frame.captured {
case .omittedFrames(_), .truncated:
frameIndex = options._theme.frameIndex("...")
default:
frameIndex = options._theme.frameIndex("\(index)")
}
columns.append(frameIndex)
}
if options._showFrameAttributes {
columns.append(attrs.map(
options._theme.frameAttribute
).joined(separator: " "))
}
columns.append(location)
return columns
}
/// Format a frame into a list of rows.
///
/// @param frame The frame to format.
/// @param index The frame index, if required.
///
/// @result An array of table rows.
public func formatRows(frame: SymbolicatedBacktrace.Frame,
index: Int? = nil,
showSource: Bool = true) -> [TableRow] {
let columns = formatColumns(frame: frame,
index: index)
var rows: [TableRow] = [.columns(columns)]
if showSource {
if let symbol = frame.symbol,
let sourceLocation = symbol.sourceLocation,
let lines = formattedSourceLines(from: sourceLocation) {
rows.append(.raw(""))
rows.append(.raw(lines))
rows.append(.raw(""))
}
}
return rows
}
/// Format just one frame.
///
/// @param frame The frame to format.
/// @param index The frame index, if required.
///
/// @result A `String` containing the formatted data.
public func format(frame: SymbolicatedBacktrace.Frame,
index: Int? = nil,
showSource: Bool = true) -> String {
let rows = formatRows(frame: frame, index: index, showSource: showSource)
return BacktraceFormatter.formatTable(rows, alignments: [.right])
}
/// Return `true` if we should skip the specified frame
public func shouldSkip(_ frame: SymbolicatedBacktrace.Frame) -> Bool {
return (options._skipRuntimeFailures && frame.isSwiftRuntimeFailure)
|| (options._skipSystemFrames && frame.isSystem)
|| (options._skipThunkFunctions && frame.isSwiftThunk)
}
/// Format the frame list from a symbolicated backtrace.
///
/// @param frames The frames to format.
///
/// @result A `String` containing the formatted data.
public func format(
frames: some Sequence<SymbolicatedBacktrace.Frame>
) -> String {
var rows: [TableRow] = []
var sourceLocationsShown = Set<SymbolicatedBacktrace.SourceLocation>()
var n = 0
for frame in frames {
if shouldSkip(frame) {
continue
}
var showSource = options._showSourceCode
if let symbol = frame.symbol,
let sourceLocation = symbol.sourceLocation {
if sourceLocationsShown.contains(sourceLocation) {
showSource = false
} else {
sourceLocationsShown.insert(sourceLocation)
}
}
rows += formatRows(frame: frame, index: n, showSource: showSource)
if case let .omittedFrames(count) = frame.captured {
n += count
} else {
n += 1
}
}
return BacktraceFormatter.formatTable(rows, alignments: [.right])
}
/// Format a `SymbolicatedBacktrace`
///
/// @param backtrace The `SymbolicatedBacktrace` object to format.
///
/// @result A `String` containing the formatted data.
public func format(backtrace: SymbolicatedBacktrace) -> String {
var result = format(frames: backtrace.frames)
switch options._showImages {
case .none:
break
case .all:
result += "\n\nImages:\n"
result += format(images: backtrace.images)
case .mentioned:
var mentionedImages = Set<Int>()
for frame in backtrace.frames {
if shouldSkip(frame) {
continue
}
if let symbol = frame.symbol, symbol.imageIndex >= 0 {
mentionedImages.insert(symbol.imageIndex)
}
}
let images = mentionedImages.sorted().map{ backtrace.images[$0] }
let omitted = backtrace.images.count - images.count
if omitted > 0 {
result += "\n\nImages (\(omitted) omitted):\n"
} else {
result += "\n\nImages (only mentioned):\n"
}
result += format(images: images)
}
return result
}
/// Format a `Backtrace.Image` into a list of columns.
///
/// @param image The `Image` object to format.
///
/// @result An array of strings, one per column.
public func formatColumns(image: Backtrace.Image) -> [String] {
let addressRange = "\(image.baseAddress)\(image.endOfText)"
let buildID: String
if let bytes = image.uniqueID {
buildID = hex(bytes)
} else {
buildID = "<no build ID>"
}
let imagePath: String
if let path = image.path {
if options._sanitizePaths {
imagePath = sanitizePath(path)
} else {
imagePath = path
}
} else {
imagePath = "<unknown>"
}
let imageName = image.name ?? "<unknown>"
return [
options._theme.imageAddressRange(addressRange),
options._theme.imageBuildID(buildID),
options._theme.imageName(imageName),
options._theme.imagePath(imagePath)
]
}
/// Format an array of `Backtrace.Image`s.
///
/// @param images The array of `Image` objects to format.
///
/// @result A string containing the formatted data.
public func format(images: some Sequence<Backtrace.Image>) -> String {
let rows = images.map{
TableRow.columns(
formatColumns(image: $0)
)
}
return BacktraceFormatter.formatTable(rows)
}
}