diff --git a/supacode/Support/DebugCaseOutput.swift b/supacode/Support/DebugCaseOutput.swift index cd7d0e3c..e5044ddb 100644 --- a/supacode/Support/DebugCaseOutput.swift +++ b/supacode/Support/DebugCaseOutput.swift @@ -16,9 +16,9 @@ struct LogActionsReducer: Reducer where Base.State: Equatable { private let logger = SupaLogger("TCA") func reduce(into state: inout Base.State, action: Base.Action) -> Effect { - let actionLabel = debugCaseOutput(action) - logger.debug("Action: \(actionLabel)") #if DEBUG + let actionLabel = debugCaseOutput(action) + logger.debug("Action: \(actionLabel)") let previousState = state let effects = base.reduce(into: &state, action: action) if previousState != state, let diff = CustomDump.diff(previousState, state) { @@ -26,6 +26,8 @@ struct LogActionsReducer: Reducer where Base.State: Equatable { } return effects #else + let actionLabel = releaseActionLabel(action) + logger.debug("Action: \(actionLabel)") SentrySDK.logger.info("Action: \(actionLabel)") let breadcrumb = Breadcrumb(level: .debug, category: "action") breadcrumb.message = actionLabel @@ -66,10 +68,57 @@ func debugCaseOutput( ?? "\(abbreviated ? "" : typeName(type(of: value)))\(debugCaseOutputHelp(value))" } +func releaseActionLabel(_ value: Any) -> String { + let rootType = shortTypeName(type(of: value)) + let casePath = releaseEnumCasePath(value) + guard !casePath.isEmpty else { + return rootType + } + return "\(rootType).\(casePath.joined(separator: "."))" +} + private func isUnlabeledArgument(_ label: String) -> Bool { label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil } +private func releaseEnumCasePath(_ value: Any) -> [String] { + var labels: [String] = [] + var currentValue = value + + while true { + let mirror = Mirror(reflecting: currentValue) + guard mirror.displayStyle == .enum else { + return labels + } + if let child = mirror.children.first, let label = child.label { + labels.append(label) + let childMirror = Mirror(reflecting: child.value) + guard childMirror.displayStyle == .enum else { + return labels + } + currentValue = child.value + } else { + labels.append(caseName(String(describing: currentValue))) + return labels + } + } +} + +private func caseName(_ description: String) -> String { + if let parenIndex = description.firstIndex(of: "(") { + return String(description[.. String { + let components = String(reflecting: type) + .split(separator: ".") + .filter { !$0.hasPrefix("(unknown context at $") } + .suffix(2) + return components.isEmpty ? String(reflecting: type) : components.joined(separator: ".") +} + private func typeName( _ type: Any.Type, qualified: Bool = true, diff --git a/supacodeTests/ReleaseActionLabelTests.swift b/supacodeTests/ReleaseActionLabelTests.swift new file mode 100644 index 00000000..1b620fdd --- /dev/null +++ b/supacodeTests/ReleaseActionLabelTests.swift @@ -0,0 +1,40 @@ +import Testing + +@testable import supacode + +struct ReleaseActionLabelTests { + private enum OuterAction { + case idle + case inner(InnerAction) + case payload(id: Int, title: String) + } + + private enum InnerAction { + case ready + case deep(DeepAction) + } + + private enum DeepAction { + case done(worktreeID: String, added: Int, removed: Int) + } + + @Test func nestedEnumLabelUsesCasePathWithoutPayloads() { + let label = releaseActionLabel( + OuterAction.inner(.deep(.done(worktreeID: "wt-1", added: 3, removed: 1))) + ) + + #expect(label == "ReleaseActionLabelTests.OuterAction.inner.deep.done") + } + + @Test func payloadCaseDoesNotExpandAssociatedValues() { + let label = releaseActionLabel(OuterAction.payload(id: 42, title: "repo")) + + #expect(label == "ReleaseActionLabelTests.OuterAction.payload") + } + + @Test func payloadlessCaseKeepsTypeAndCase() { + let label = releaseActionLabel(OuterAction.idle) + + #expect(label == "ReleaseActionLabelTests.OuterAction.idle") + } +}