From 889f1bd91d51bd4ba72b156ecc809827ba0ce784 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 10 Aug 2026 15:52:23 +0800 Subject: [PATCH 1/4] refactor: consume the ObjC rendering and indexing layers from the library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ObjC renderer and interface indexer were never RuntimeViewer-specific — they turn ObjC metadata into semantic declarations and build a queryable index, which any MachOObjCSection consumer wants — yet ~1500 lines of them sat in this app's core layer. They now ship with MachOObjCSection, so RuntimeObjCSection drops from 617 lines to 471 and is left doing what it should: translating library results into RuntimeViewer domain types. The library reports progress and discovered relationships through a single ObjCIndexingEvent channel where this code had two; the adapter back onto RuntimeObjectsLoadingEvent lives in ObjCIndexingEvent+LoadingProgress. The concrete transformer modules likewise move to the libraries that own their vocabulary. What stays is the aggregate Transformer.Configuration: it spans both halves, and nothing outside RuntimeViewer persists them as a unit. Re-exports keep every existing Transformer.… reference compiling. --- RuntimeViewerCore/Package.swift | 20 +- .../ObjCIndexingEvent+LoadingProgress.swift | 20 + .../Core/RuntimeObjCSection.swift | 362 +++---- .../RuntimeObjCInterfaceIndexer.swift | 628 ------------ .../RuntimeSwiftInterfaceIndexer.swift | 18 +- .../RuntimeRelationshipsResolver.swift | 4 +- .../Transformer/Transformer+CType.swift | 210 ---- .../Transformer+Configuration.swift | 43 + .../Transformer+ObjCIvarOffset.swift | 87 -- .../Transformer/Transformer.swift | 65 +- .../Utils/MachOImage+AddressFormatting.swift | 44 - .../Utils/ObjCDump+SemanticString.swift | 896 ------------------ .../TransformerAdditionalTests.swift | 410 -------- .../TransformerConfigurationTests.swift | 48 + .../TransformerTests.swift | 109 --- 15 files changed, 262 insertions(+), 2702 deletions(-) create mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Core/ObjCIndexingEvent+LoadingProgress.swift delete mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeObjCInterfaceIndexer.swift delete mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+CType.swift create mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+Configuration.swift delete mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+ObjCIvarOffset.swift delete mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/MachOImage+AddressFormatting.swift delete mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/ObjCDump+SemanticString.swift delete mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerAdditionalTests.swift create mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerConfigurationTests.swift delete mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerTests.swift diff --git a/RuntimeViewerCore/Package.swift b/RuntimeViewerCore/Package.swift index 05bf12d3..505c923c 100644 --- a/RuntimeViewerCore/Package.swift +++ b/RuntimeViewerCore/Package.swift @@ -107,6 +107,16 @@ let package = Package( exact: "0.14.1", ), ), + .package( + local: .package( + path: "../../swift-semantic-string", + isRelative: true, + ), + remote: .package( + url: "https://github.com/MxIris-Reverse-Engineering/swift-semantic-string", + from: "0.3.0", + ), + ), .package( url: "https://github.com/MxIris-Library-Forks/Asynchrone", from: "0.23.0-fork.1", @@ -173,8 +183,13 @@ let package = Package( "RuntimeViewerCommunication", .product(name: "MachOKit", package: "MachOKit"), .product(name: "MachOObjCSection", package: "MachOObjCSection"), + .product(name: "ObjCDeclarationRendering", package: "MachOObjCSection"), + .product(name: "ObjCIndexing", package: "MachOObjCSection"), + .product(name: "ObjCInterface", package: "MachOObjCSection"), .product(name: "MachOSwiftSection", package: "MachOSwiftSection"), - .product(name: "OutputTransformer", package: "MachOSwiftSection"), + .product(name: "OutputTransformer", package: "swift-semantic-string"), + .product(name: "ObjCOutputTransformer", package: "MachOObjCSection"), + .product(name: "SwiftOutputTransformer", package: "MachOSwiftSection"), .product(name: "SwiftDeclaration", package: "MachOSwiftSection"), .product(name: "SwiftDeclarationRendering", package: "MachOSwiftSection"), .product(name: "SwiftIndexing", package: "MachOSwiftSection"), @@ -225,6 +240,9 @@ let package = Package( dependencies: [ "RuntimeViewerCore", "RuntimeViewerCommunication", + .product(name: "OutputTransformer", package: "swift-semantic-string"), + .product(name: "ObjCOutputTransformer", package: "MachOObjCSection"), + .product(name: "SwiftOutputTransformer", package: "MachOSwiftSection"), ], ), .testTarget( diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/ObjCIndexingEvent+LoadingProgress.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/ObjCIndexingEvent+LoadingProgress.swift new file mode 100644 index 00000000..1f260a78 --- /dev/null +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/ObjCIndexingEvent+LoadingProgress.swift @@ -0,0 +1,20 @@ +import Foundation +import ObjCIndexing + +extension ObjCIndexingEvent.Phase { + /// The RuntimeViewer loading phase this indexing phase maps onto. + /// + /// The library names its phases without the `ObjC` qualifier — it only + /// ever indexes Objective-C — while `RuntimeObjectsLoadingProgress.Phase` + /// spans both the ObjC and Swift halves of a load, so it keeps the + /// qualifier to stay unambiguous. + var loadingPhase: RuntimeObjectsLoadingProgress.Phase { + switch self { + case .indexingSubclasses: .indexingObjCSubclasses + case .loadingClasses: .loadingObjCClasses + case .loadingProtocols: .loadingObjCProtocols + case .indexingConformances: .indexingObjCConformances + case .loadingCategories: .loadingObjCCategories + } + } +} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift index a86f529e..cc54d79d 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift @@ -9,34 +9,15 @@ import Semantic import Utilities import MetaCodable -typealias LoadingEventContinuation = AsyncThrowingStream.Continuation +/// `ObjCGenerationOptions`, the ObjC interface indexer and the whole +/// `ObjCDumpInfo → SemanticString` renderer now live library-side in +/// MachOObjCSection. This re-export keeps every existing +/// `ObjCGenerationOptions` reference in RuntimeViewer compiling unchanged. +@_exported import ObjCDeclarationRendering +@_exported import ObjCIndexing +import ObjCInterface -@Codable -@MemberInit -public struct ObjCGenerationOptions: Sendable, Equatable { - @Default(false) - public var stripProtocolConformance: Bool - @Default(false) - public var stripOverrides: Bool - @Default(false) - public var stripSynthesizedIvars: Bool - @Default(false) - public var stripSynthesizedMethods: Bool - @Default(false) - public var stripCtorMethod: Bool - @Default(false) - public var stripDtorMethod: Bool - @Default(false) - public var addIvarOffsetComments: Bool - @Default(false) - public var addPropertyAttributesComments: Bool - @Default(false) - public var addMethodIMPAddressComments: Bool - @Default(false) - public var addPropertyAccessorAddressComments: Bool - - public static let `default` = Self() -} +typealias LoadingEventContinuation = AsyncThrowingStream.Continuation @Loggable(.private) actor RuntimeObjCSection { @@ -61,9 +42,9 @@ actor RuntimeObjCSection { /// /// `nonisolated let` so `RuntimeRelationshipsResolver` and the factory's /// aggregate can read its query methods without an actor hop — - /// `RuntimeObjCInterfaceIndexer` is `Sendable` and protects its own - /// state with `@Mutex`. - nonisolated let objcIndexer: RuntimeObjCInterfaceIndexer + /// `ObjCInterfaceIndexer` is `Sendable` and protects its own state with + /// its own lock. + nonisolated let objcIndexer: ObjCInterfaceIndexer init(imagePath: String, factory: RuntimeObjCSectionFactory, progressContinuation: LoadingEventContinuation? = nil) async throws { #log(.info, "Initializing ObjC section for image: \(imagePath, privacy: .public)") @@ -74,8 +55,12 @@ actor RuntimeObjCSection { self.machO = machO self.imagePath = imagePath self.factory = factory - self.objcIndexer = RuntimeObjCInterfaceIndexer(machO: machO, imagePath: imagePath) - try await objcIndexer.prepare(progressContinuation: progressContinuation) + self.objcIndexer = ObjCInterfaceIndexer( + machO: machO, + imagePath: imagePath, + eventHandler: Self.makeEventHandler(forwardingTo: progressContinuation) + ) + try await objcIndexer.prepare() } init(machO: MachOImage, factory: RuntimeObjCSectionFactory, progressContinuation: LoadingEventContinuation? = nil) async throws { @@ -83,8 +68,40 @@ actor RuntimeObjCSection { self.machO = machO self.imagePath = machO.imagePath self.factory = factory - self.objcIndexer = RuntimeObjCInterfaceIndexer(machO: machO, imagePath: machO.imagePath) - try await objcIndexer.prepare(progressContinuation: progressContinuation) + self.objcIndexer = ObjCInterfaceIndexer( + machO: machO, + imagePath: machO.imagePath, + eventHandler: Self.makeEventHandler(forwardingTo: progressContinuation) + ) + try await objcIndexer.prepare() + } + + /// Adapts the library's single ``ObjCIndexingEvent`` channel back onto + /// RuntimeViewer's loading-progress stream. + /// + /// Only the `.progress` cases have a counterpart here; the relationship + /// events (`subclassIndexed` and friends) are consumed by the indexer's + /// own reverse tables, which `RuntimeRelationshipsResolver` queries + /// directly, so they need no forwarding. + private static func makeEventHandler( + forwardingTo progressContinuation: LoadingEventContinuation? + ) -> (@Sendable (ObjCIndexingEvent) -> Void)? { + guard let progressContinuation else { return nil } + return { event in + guard case .progress(let phase, let itemDescription, let currentCount, let totalCount) = event else { + return + } + progressContinuation.yield( + RuntimeObjectsLoadingEvent.progress( + RuntimeObjectsLoadingProgress( + phase: phase.loadingPhase, + itemDescription: itemDescription, + currentCount: currentCount, + totalCount: totalCount + ) + ) + ) + } } func allObjects() async throws -> [RuntimeObject] { @@ -119,234 +136,71 @@ actor RuntimeObjCSection { func interface(for object: RuntimeObject, using options: ObjCGenerationOptions, transformer: Transformer.ObjCConfiguration) async throws -> RuntimeObjectInterface { #log(.debug, "Generating interface for: \(object.name, privacy: .public)") let name = object.withImagePath(imagePath) - let cTypeReplacements = transformer.cType.isEnabled ? transformer.cType.replacements : [:] - let ivarOffsetTransformer = transformer.ivarOffset.isEnabled ? transformer.ivarOffset : nil - let objcDumpContext = ObjCDumpContext(machO: machO, options: options, cTypeReplacements: cTypeReplacements) { name, isStruct in - guard let name else { return true } - if isStruct { - return !self.objcIndexer.containsStruct(named: name) - } else { - return !self.objcIndexer.containsUnion(named: name) + + // The strip switches, the C-type substitution and the rendering all + // live in MachOObjCSection's `ObjCInterface`; this section only maps + // RuntimeViewer's `Transformer` settings onto that API and wraps the + // result back into a `RuntimeObjectInterface`. + let builder = ObjCInterfaceBuilder(indexer: objcIndexer, machO: machO) + let cTypeReplacements = transformer.cType.isEnabled + ? transformer.cType.replacements.reduce(into: [ObjCPrimitiveTypePattern: String]()) { result, pair in + guard let pattern = ObjCPrimitiveTypePattern(rawValue: pair.key.rawValue) else { return } + result[pattern] = pair.value } + : [:] + let ivarOffsetCommentBuilder: (@Sendable (Int) -> String)? + if transformer.ivarOffset.isEnabled { + let module = transformer.ivarOffset + ivarOffsetCommentBuilder = { offset in module.transform(.init(offset: offset)) } + } else { + ivarOffsetCommentBuilder = nil } - objcDumpContext.ivarOffsetTransformer = ivarOffsetTransformer + let interfaceString: SemanticString? switch name.kind { case .objc(.type(.class)): - if let classGroup = objcIndexer.classGroup(forName: name.name), let currentClassInfo = classGroup.info.first { - let superclassInfos = classGroup.info.dropFirst() - var finalClassInfo = classGroup.info.first - var needsStripClassProperties: Set = [] - var needsStripProperties: Set = [] - var needsStripClassMethods: Set = [] - var needsStripMethods: Set = [] - var needsStripIvars: Set = [] - - if options.stripCtorMethod { - needsStripMethods.insert(".cxx_construct") - } - - if options.stripDtorMethod { - needsStripMethods.insert(".cxx_destruct") - } - - if options.stripOverrides { - for superclassInfo in superclassInfos { - needsStripClassProperties.insert(contentsOf: superclassInfo.classProperties.map(\.name)) - needsStripProperties.insert(contentsOf: superclassInfo.properties.map(\.name)) - needsStripClassMethods.insert(contentsOf: superclassInfo.classMethods.map(\.name)) - needsStripMethods.insert(contentsOf: superclassInfo.methods.map(\.name)) - } - } - if options.stripProtocolConformance { - for protocolInfo in currentClassInfo.protocols { - needsStripClassProperties.insert(contentsOf: protocolInfo.classProperties.map(\.name)) - needsStripProperties.insert(contentsOf: protocolInfo.properties.map(\.name)) - needsStripClassMethods.insert(contentsOf: protocolInfo.classMethods.map(\.name)) - needsStripMethods.insert(contentsOf: protocolInfo.methods.map(\.name)) - } - } - if options.stripSynthesizedIvars || options.stripSynthesizedMethods { - var needsStripIvarNames: Set = [] - - for property in currentClassInfo.properties + currentClassInfo.classProperties { - if options.stripSynthesizedMethods { - let propertyName = property.name - if let customGetter = property.customGetter { - if property.isClassProperty { - needsStripClassMethods.insert(customGetter) - } else { - needsStripMethods.insert(customGetter) - } - } else { - if property.isClassProperty { - needsStripClassMethods.insert(propertyName) - } else { - needsStripMethods.insert(propertyName) - } - } - - if let customSetter = property.customSetter { - if property.isClassProperty { - needsStripClassMethods.insert(customSetter) - } else { - needsStripMethods.insert(customSetter) - } - } else { - let setterMethodName = "set" + propertyName.uppercasedFirst - if property.isClassProperty { - needsStripClassMethods.insert(setterMethodName) - } else { - needsStripMethods.insert(setterMethodName) - } - } - } - - if options.stripSynthesizedIvars, !property.isClassProperty { - if let ivar = property.ivar { - needsStripIvarNames.insert(ivar) - } - } - } - - if options.stripSynthesizedIvars { - for ivar in currentClassInfo.ivars { - if needsStripIvarNames.contains(ivar.name) { - needsStripIvars.insert(ivar.name) - } - } - } - } - - finalClassInfo = ObjCClassInfo( - name: currentClassInfo.name, - version: currentClassInfo.version, - imageName: currentClassInfo.imageName, - instanceSize: currentClassInfo.instanceSize, - superClassName: currentClassInfo.superClassName, - protocols: currentClassInfo.protocols, - ivars: currentClassInfo.ivars.removingAll { needsStripIvars.contains($0.name) }, - classProperties: currentClassInfo.classProperties.removingAll { needsStripClassProperties.contains($0.name) }, - properties: currentClassInfo.properties.removingAll { needsStripProperties.contains($0.name) }, - classMethods: currentClassInfo.classMethods.removingAll { needsStripClassMethods.contains($0.name) }, - methods: currentClassInfo.methods.removingAll { needsStripMethods.contains($0.name) } - ) - - if let finalClassInfo { - if options.addPropertyAccessorAddressComments { - for method in currentClassInfo.methods where method.imp != 0 { - objcDumpContext.methodIMPs[method.name] = method.imp - } - for method in currentClassInfo.classMethods where method.imp != 0 { - objcDumpContext.classMethodIMPs[method.name] = method.imp - } - } - return .init(object: name, interfaceString: finalClassInfo.semanticString(using: objcDumpContext)) - } - } + interfaceString = builder.classInterface( + named: name.name, + options: options, + cTypeReplacements: cTypeReplacements, + ivarOffsetCommentBuilder: ivarOffsetCommentBuilder + ) case .objc(.type(.protocol)): - if let currentProtocolInfo = objcIndexer.protocolGroup(forName: name.name)?.info { - var finalProtocolInfo = currentProtocolInfo - - var needsStripClassProperties: Set = [] - var needsStripClassMethods: Set = [] - var needsStripProperties: Set = [] - var needsStripMethods: Set = [] - - if options.stripCtorMethod { - needsStripMethods.insert(".cxx_construct") - } - - if options.stripDtorMethod { - needsStripMethods.insert(".cxx_destruct") - } - - if options.stripProtocolConformance { - for protocolInfo in currentProtocolInfo.protocols { - needsStripClassProperties.insert(contentsOf: protocolInfo.classProperties.map(\.name)) - needsStripProperties.insert(contentsOf: protocolInfo.properties.map(\.name)) - needsStripClassMethods.insert(contentsOf: protocolInfo.classMethods.map(\.name)) - needsStripMethods.insert(contentsOf: protocolInfo.methods.map(\.name)) - - needsStripClassProperties.insert(contentsOf: protocolInfo.optionalClassProperties.map(\.name)) - needsStripProperties.insert(contentsOf: protocolInfo.optionalProperties.map(\.name)) - needsStripClassMethods.insert(contentsOf: protocolInfo.optionalClassMethods.map(\.name)) - needsStripMethods.insert(contentsOf: protocolInfo.optionalMethods.map(\.name)) - } - } - - if options.stripSynthesizedMethods { - for property in currentProtocolInfo.properties + currentProtocolInfo.classProperties + currentProtocolInfo.optionalProperties + currentProtocolInfo.optionalClassProperties { - let propertyName = property.name - if let customGetter = property.customGetter { - if property.isClassProperty { - needsStripClassMethods.insert(customGetter) - } else { - needsStripMethods.insert(customGetter) - } - } else { - if property.isClassProperty { - needsStripClassMethods.insert(propertyName) - } else { - needsStripMethods.insert(propertyName) - } - } - - if let customSetter = property.customSetter { - if property.isClassProperty { - needsStripClassMethods.insert(customSetter) - } else { - needsStripMethods.insert(customSetter) - } - } else { - let setterMethodName = "set" + propertyName.uppercasedFirst - if property.isClassProperty { - needsStripClassMethods.insert(setterMethodName) - } else { - needsStripMethods.insert(setterMethodName) - } - } - } - } - - finalProtocolInfo = ObjCProtocolInfo( - name: currentProtocolInfo.name, - protocols: currentProtocolInfo.protocols, - classProperties: currentProtocolInfo.classProperties.removingAll { needsStripClassProperties.contains($0.name) }, - properties: currentProtocolInfo.properties.removingAll { needsStripProperties.contains($0.name) }, - classMethods: currentProtocolInfo.classMethods.removingAll { needsStripClassMethods.contains($0.name) }, - methods: currentProtocolInfo.methods.removingAll { needsStripMethods.contains($0.name) }, - optionalClassProperties: currentProtocolInfo.optionalClassProperties.removingAll { needsStripClassProperties.contains($0.name) }, - optionalProperties: currentProtocolInfo.optionalProperties.removingAll { needsStripProperties.contains($0.name) }, - optionalClassMethods: currentProtocolInfo.optionalClassMethods.removingAll { needsStripClassMethods.contains($0.name) }, - optionalMethods: currentProtocolInfo.optionalMethods.removingAll { needsStripMethods.contains($0.name) } - ) - - return .init(object: name, interfaceString: finalProtocolInfo.semanticString(using: objcDumpContext)) - } + interfaceString = builder.protocolInterface( + named: name.name, + options: options, + cTypeReplacements: cTypeReplacements, + ivarOffsetCommentBuilder: ivarOffsetCommentBuilder + ) case .objc(.category(.class)): - if let categoryInfo = objcIndexer.categoryGroup(forName: name.name)?.info { - if options.addPropertyAccessorAddressComments { - for method in categoryInfo.methods where method.imp != 0 { - objcDumpContext.methodIMPs[method.name] = method.imp - } - for method in categoryInfo.classMethods where method.imp != 0 { - objcDumpContext.classMethodIMPs[method.name] = method.imp - } - } - return .init(object: name, interfaceString: categoryInfo.semanticString(using: objcDumpContext)) - } + interfaceString = builder.categoryInterface( + uniqueName: name.name, + options: options, + cTypeReplacements: cTypeReplacements, + ivarOffsetCommentBuilder: ivarOffsetCommentBuilder + ) case .c(.struct): - if let interfaceString = objcIndexer.structSemanticString(forName: name.name, context: objcDumpContext) { - return .init(object: name, interfaceString: interfaceString) - } + interfaceString = builder.structInterface( + named: name.name, + options: options, + cTypeReplacements: cTypeReplacements, + ivarOffsetCommentBuilder: ivarOffsetCommentBuilder + ) case .c(.union): - if let interfaceString = objcIndexer.unionSemanticString(forName: name.name, context: objcDumpContext) { - return .init(object: name, interfaceString: interfaceString) - } + interfaceString = builder.unionInterface( + named: name.name, + options: options, + cTypeReplacements: cTypeReplacements, + ivarOffsetCommentBuilder: ivarOffsetCommentBuilder + ) default: - break + interfaceString = nil } + + if let interfaceString { + return .init(object: name, interfaceString: interfaceString) + } + #log(.default, "Invalid runtime object: \(object.name, privacy: .public) kind: \(String(describing: object.kind), privacy: .public)") throw Error.invalidRuntimeObject } @@ -517,16 +371,16 @@ actor RuntimeObjCSectionFactory { /// the section is created, so queries against this aggregate fan out /// across all loaded ObjC sections. Mirrors `RuntimeSwiftSectionFactory.indexer`. /// - /// `RuntimeObjCInterfaceIndexer` binds a `MachOImage` at `init`; this + /// `ObjCInterfaceIndexer` binds a `MachOImage` at `init`; this /// aggregate never parses one of its own (`prepare()` is never called on /// it), so it is constructed against the current process image as a /// placeholder — mirroring `RuntimeSwiftSectionFactory`'s aggregate, /// which is likewise built `in: .current()`. - let objcInterfaceIndexer: RuntimeObjCInterfaceIndexer + let objcInterfaceIndexer: ObjCInterfaceIndexer init() { let currentMachO = MachOImage.current() - objcInterfaceIndexer = RuntimeObjCInterfaceIndexer(machO: currentMachO, imagePath: currentMachO.imagePath) + objcInterfaceIndexer = ObjCInterfaceIndexer(machO: currentMachO, imagePath: currentMachO.imagePath) } func existingSection(for imagePath: String) -> RuntimeObjCSection? { diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeObjCInterfaceIndexer.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeObjCInterfaceIndexer.swift deleted file mode 100644 index 86c30e4f..00000000 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeObjCInterfaceIndexer.swift +++ /dev/null @@ -1,628 +0,0 @@ -import Foundation -import MachOKit -import MachOObjCSection -import ObjCDump -import ObjCTypeDecodeKit -import OrderedCollections -import Semantic -import SwiftStdlibToolbox - -/// A reference to an Objective-C class (or a Swift class that surfaces a -/// `class_t` record through `__objc_classlist`) that was found to subclass -/// another class or to adopt a protocol. -/// -/// `isSwiftStable` carries the structural signal that lets -/// `RuntimeRelationshipsResolver` decide whether to materialize the -/// reference as a Swift `RuntimeObject` (kind `.swift(.type(.class))`) or as -/// an Objective-C one (kind `.objc(.type(.class))`). Mirrors the same field -/// that `RuntimeObjCSection.allObjects()` already uses to mark bridged classes. -public struct ObjCClassReference: Hashable, Sendable, Codable { - public let className: String - public let imagePath: String - public let isSwiftStable: Bool - - public init(className: String, imagePath: String, isSwiftStable: Bool) { - self.className = className - self.imagePath = imagePath - self.isSwiftStable = isSwiftStable - } -} - -/// Per-image Objective-C interface index: the parsed data store for one -/// Mach-O image's classes, protocols, categories and C struct/union -/// definitions, plus the class-inheritance and protocol-adoption reverse -/// tables that back `RuntimeRelationshipsResolver`. -/// -/// This is the Objective-C counterpart of `SwiftDeclarationIndexer` on the -/// Swift side: it takes the image's `MachOImage` at `init` and owns *all* -/// of the raw `MachOObjCSection` / `ObjCDump` extraction (`prepare()`), so -/// `RuntimeObjCSection` is left as a thin translation layer that turns this -/// index into `RuntimeViewerCore` domain types (`RuntimeObject`, -/// `RuntimeObjectInterface`, `RuntimeMemberAddress`). -/// -/// Aggregation: a `RuntimeObjCSectionFactory` keeps one empty aggregate -/// instance and registers every per-image indexer as a sub-indexer via -/// `addSubIndexer(_:)`, so relationship queries can fan out across all -/// loaded ObjC images. Mirrors `SwiftDeclarationIndexer.addSubIndexer(_:)`. -/// -/// `@unchecked Sendable`: the `MachOImage` supplied at `init` and the -/// stored `ObjCDump` / `MachOObjCSection` values are not themselves -/// `Sendable`, but `machO` is an immutable `let` and every dictionary is -/// `@Mutex`-guarded and immutable once `prepare()` returns — mirroring the -/// `SwiftDeclarationIndexer` reference-type indexer pattern (a shared -/// `Sendable` data store), though the exact isolation annotations on each -/// side differ. -public final class RuntimeObjCInterfaceIndexer: @unchecked Sendable { - - // MARK: - Group Types - - /// A class paired with its own `ObjCClassInfo` plus the `ObjCClassInfo` - /// of every superclass (resolved across images), `info.first` being the - /// class itself. `internal` so `RuntimeObjCSection` can read the tuple. - typealias ObjCClassGroup = (objcClass: any ObjCClassProtocol, info: [ObjCClassInfo]) - - typealias ObjCProtocolGroup = (objcProtocol: any ObjCProtocolProtocol, info: ObjCProtocolInfo) - - typealias ObjCCategoryGroup = (objcCategory: any ObjCCategoryProtocol, info: ObjCCategoryInfo) - - // MARK: - C Struct / Union - - /// A C `struct` / `union` definition harvested from the ivar / method / - /// property type encodings of the image's ObjC metadata. - private struct CStructOrUnion: Hashable { - let name: String - - let fields: [ObjCField] - - var hasBitFieldOnly: Bool { - fields.allSatisfy { $0.bitWidth != nil } - } - - var numberOfHasNameFields: Int { - fields.count { $0.name != nil } - } - - @SemanticStringBuilder - func semanticString(isStruct: Bool, context: ObjCDumpContext) -> SemanticString { - Keyword(isStruct ? "struct" : "union") - Space() - TypeName(kind: .other, name) - Joined { - MemberList(level: 1) { - for (index, field) in fields.enumerated() { - field.semanticString(fallbackName: "x\(index)", level: 1, context: context) - } - } - } prefix: { - " {" - } suffix: { - Indent(level: 0) - "}" - } - } - } - - // MARK: - Indexed Image - - /// The Mach-O image this indexer parses. Bound at `init` and never - /// reassigned — mirrors `SwiftDeclarationIndexer`, which likewise binds - /// its `MachOImage` at construction. `prepare()` reads it to populate - /// the data store below. - private let machO: MachOImage - - /// The image path recorded into every `ObjCClassReference` and - /// translated `RuntimeObject`. Passed explicitly rather than derived - /// from `machO.imagePath`: `RuntimeObjCSection` may be constructed from - /// a path that differs from the resolved image's own path — e.g. in - /// Debug builds the main-executable stub path resolves to a sibling - /// `.debug.dylib` image whose `imagePath` is not the stub's. - private let imagePath: String - - // MARK: - Interface Data Store - - @Mutex - private var classes: [String: ObjCClassGroup] = [:] - - @Mutex - private var protocols: [String: ObjCProtocolGroup] = [:] - - @Mutex - private var categories: [String: ObjCCategoryGroup] = [:] - - @Mutex - private var structs: [String: CStructOrUnion] = [:] - - @Mutex - private var unions: [String: CStructOrUnion] = [:] - - // MARK: - Relationship Reverse Tables - - @Mutex - private var subclassesByClassName: [String: OrderedSet] = [:] - - @Mutex - private var conformingClassesByProtocolName: [String: OrderedSet] = [:] - - @Mutex - private var subIndexers: [RuntimeObjCInterfaceIndexer] = [] - - private let eventHandler: RuntimeObjCInterfaceEvents.Handler? - - /// `internal`, not `public`: the parameter type `MachOImage` comes from - /// a non-`public` import, and every construction site - /// (`RuntimeObjCSection`, `RuntimeObjCSectionFactory`) lives in this - /// module anyway. `prepare()` is likewise `internal`. - init(machO: MachOImage, imagePath: String, eventHandler: RuntimeObjCInterfaceEvents.Handler? = nil) { - self.machO = machO - self.imagePath = imagePath - self.eventHandler = eventHandler - } - - // MARK: - Preparation - - /// Parse this indexer's Mach-O image (`machO`, bound at `init`) into the - /// data store: every class / protocol / category, the C struct / union - /// definitions harvested from their type encodings, and — inline as the - /// `__objc_classlist` walk proceeds — the class-inheritance and - /// protocol-adoption reverse tables. - /// - /// Called once by `RuntimeObjCSection.init`, after which the store is - /// immutable. The aggregate indexer held by `RuntimeObjCSectionFactory` - /// never calls this — it only aggregates per-image sub-indexers. - func prepare(progressContinuation: LoadingEventContinuation? = nil) async throws { - var classByName: [String: ObjCClassGroup] = [:] - var protocolByName: [String: ObjCProtocolGroup] = [:] - var categoryByName: [String: ObjCCategoryGroup] = [:] - var structsByName: [String: CStructOrUnion] = [:] - var unionsByName: [String: CStructOrUnion] = [:] - var classInfoCache: [String: ObjCClassInfo] = [:] - - func setObjCType(_ type: ObjCType) { - switch type { - case .struct(let name, let fields): - if let name { - let newStruct = CStructOrUnion(name: name, fields: fields ?? []) - guard !newStruct.hasBitFieldOnly else { return } - if let existStruct = structsByName[name] { - if existStruct.numberOfHasNameFields < newStruct.numberOfHasNameFields { - structsByName[name] = newStruct - } - } else { - structsByName[name] = newStruct - } - } - case .union(let name, let fields): - if let name { - let newUnion = CStructOrUnion(name: name, fields: fields ?? []) - guard !newUnion.hasBitFieldOnly else { return } - if let existUnion = unionsByName[name] { - if existUnion.numberOfHasNameFields < newUnion.numberOfHasNameFields { - unionsByName[name] = newUnion - } - } else { - unionsByName[name] = newUnion - } - } - default: - break - } - } - - func setObjCTypeFromMethods(_ methods: [ObjCMethodInfo]) { - for method in methods { - if let returnType = method.returnType { - setObjCType(returnType) - } - - if let argumentInfos = method.argumentInfos { - for argumentInfo in argumentInfos { - setObjCType(argumentInfo.type) - } - } - } - } - - func setObjCTypeFromProperties(_ properties: [ObjCPropertyInfo]) { - for property in properties { - for attribute in property.attributes { - if let type = attribute.type { - setObjCType(type) - } - } - } - } - - let objcClasses: [any ObjCClassProtocol] = machO.objc.classes64.orEmpty + machO.objc.classes32.orEmpty + machO.objc.nonLazyClasses64.orEmpty + machO.objc.nonLazyClasses32.orEmpty - - // One-shot progress marker so the loading indicator can surface - // "Indexing Objective-C subclasses…" before the per-class loop - // starts pushing `.loadingObjCClasses` updates. Inheritance and - // protocol-adoption indexing happens inline below — every class in - // `__objc_classlist` (including Swift-derived ones via the same - // record format) is fed to the reverse tables as we walk the list. - progressContinuation?.yield(RuntimeObjectsLoadingEvent.progress(RuntimeObjectsLoadingProgress( - phase: .indexingObjCSubclasses, - itemDescription: "", - currentCount: 0, - totalCount: objcClasses.count - ))) - - for objcClass in objcClasses { - let objcClassGroup: ObjCClassGroup = (objcClass, infoWithSuperclasses(class: objcClass, in: machO, cache: &classInfoCache)) - guard let objcClassInfo = objcClassGroup.info.first else { continue } - classByName[objcClassInfo.name] = objcClassGroup - progressContinuation?.yield(RuntimeObjectsLoadingEvent.progress(RuntimeObjectsLoadingProgress( - phase: .loadingObjCClasses, - itemDescription: objcClassInfo.name, - currentCount: classByName.count, - totalCount: objcClasses.count - ))) - - // Feed the reverse tables. We pass the already-extracted class - // info — `superClassName` is resolved through MachO's bind/rebase - // walking by `infoWithSuperclasses`, so we don't redo that work - // here. `isSwiftStable` comes off the raw class_t record itself, - // exactly matching the field used by `RuntimeObjCSection` to mark - // bridged classes' `secondaryKind`. - indexClass( - className: objcClassInfo.name, - superClassName: objcClassInfo.superClassName, - adoptedProtocolNames: objcClassInfo.protocols.map(\.name), - imagePath: imagePath, - isSwiftStable: objcClass.isSwiftStable - ) - - for ivar in objcClassInfo.ivars { - if let type = ivar.type { - setObjCType(type) - } - } - - setObjCTypeFromProperties(objcClassInfo.properties + objcClassInfo.classProperties) - setObjCTypeFromMethods(objcClassInfo.methods + objcClassInfo.classMethods) - } - - // `__objc_protolist` carries a full `protocol_t` record for *every* - // protocol whose `@protocol` declaration was in scope at compile - // time — including ones merely imported from dependencies (`NSObject`, - // `NSCopying`, …), not just this image's own. We list all of them: - // there is no authoritative "defining image" recorded for a protocol - // (unlike classes, which are emitted exactly once in their own image's - // `__objc_classlist`), so every attempt at attributing ownership is a - // heuristic that can silently *drop* an image's real protocols. A - // previous dependency-closure heuristic did exactly that — see - // `Documentations/ResolvedIssues/2026-08-05-objc-protocol-ownership-filter.md`. - let objcProtocols: [any ObjCProtocolProtocol] = machO.objc.protocols64.orEmpty + machO.objc.protocols32.orEmpty - - for objcProtocol in objcProtocols { - guard let objcProtocolInfo = objcProtocol.info(in: machO) else { continue } - protocolByName[objcProtocolInfo.name] = (objcProtocol, objcProtocolInfo) - progressContinuation?.yield(RuntimeObjectsLoadingEvent.progress(RuntimeObjectsLoadingProgress( - phase: .loadingObjCProtocols, - itemDescription: objcProtocolInfo.name, - currentCount: protocolByName.count, - totalCount: objcProtocols.count - ))) - setObjCTypeFromProperties(objcProtocolInfo.properties + objcProtocolInfo.classProperties) - setObjCTypeFromMethods(objcProtocolInfo.methods + objcProtocolInfo.classMethods) - } - - var objcCategories: [any ObjCCategoryProtocol] = [] - - objcCategories.append(contentsOf: machO.objc.categories64.orEmpty) - objcCategories.append(contentsOf: machO.objc.categories32.orEmpty) - objcCategories.append(contentsOf: machO.objc.nonLazyCategories64.orEmpty) - objcCategories.append(contentsOf: machO.objc.nonLazyCategories32.orEmpty) - objcCategories.append(contentsOf: machO.objc.categories2_64.orEmpty) - objcCategories.append(contentsOf: machO.objc.categories2_32.orEmpty) - - // One-shot marker that conformance indexing starts; each category - // extends the conformer set of its target class for every protocol - // the category adopts. - progressContinuation?.yield(RuntimeObjectsLoadingEvent.progress(RuntimeObjectsLoadingProgress( - phase: .indexingObjCConformances, - itemDescription: "", - currentCount: 0, - totalCount: objcCategories.count - ))) - - for objcCategory in objcCategories { - guard let objcCategoryInfo = objcCategory.info(in: machO) else { continue } - categoryByName[objcCategoryInfo.uniqueName] = (objcCategory, objcCategoryInfo) - progressContinuation?.yield(RuntimeObjectsLoadingEvent.progress(RuntimeObjectsLoadingProgress( - phase: .loadingObjCCategories, - itemDescription: objcCategoryInfo.uniqueName, - currentCount: categoryByName.count, - totalCount: objcCategories.count - ))) - setObjCTypeFromProperties(objcCategoryInfo.properties + objcCategoryInfo.classProperties) - setObjCTypeFromMethods(objcCategoryInfo.methods + objcCategoryInfo.classMethods) - - // Feed category data to the reverse tables. The target class' - // Swift stable flag is read from the already-resolved class - // record so category adoptions on bridged classes (e.g. NSError - // extending Swift error protocols) carry `isSwiftStable == true` - // and surface as Swift `RuntimeObject` at query time. - let targetClassName = objcCategoryInfo.className - let targetIsSwiftStable: Bool - if let (_, targetClass) = objcCategory.class(in: machO) { - targetIsSwiftStable = targetClass.isSwiftStable - } else { - targetIsSwiftStable = false - } - indexCategory( - targetClassName: targetClassName, - targetIsSwiftStable: targetIsSwiftStable, - adoptedProtocolNames: objcCategoryInfo.protocols.map(\.name), - imagePath: imagePath - ) - } - - classes = classByName - protocols = protocolByName - categories = categoryByName - structs = structsByName - unions = unionsByName - } - - /// Resolve `cls` to its own `ObjCClassInfo` followed by the - /// `ObjCClassInfo` of every superclass, walking `superClass(in:)` across - /// image boundaries. `cache` memoizes `info(in:)` extraction so a deep - /// inheritance chain shared by many classes is decoded only once. - private func infoWithSuperclasses(class cls: Class, in machO: MachOImage, cache: inout [String: ObjCClassInfo]) -> [ObjCClassInfo] { - guard let className = cls.name(in: machO) else { return [] } - - var currentInfo: ObjCClassInfo? - - if let cacheInfo = cache[className] { - currentInfo = cacheInfo - } else { - let info = cls.info(in: machO) - currentInfo = info - cache[className] = info - } - - guard let currentInfo else { return [] } - - var resultInfos: [ObjCClassInfo] = [currentInfo] - - var machOAndSuperclass = cls.superClass(in: machO) - - while let currentMachOAndSuperclass = machOAndSuperclass { - let currentMachO = currentMachOAndSuperclass.0 - let currentSuperclass = currentMachOAndSuperclass.1 - - machOAndSuperclass = currentSuperclass.superClass(in: currentMachO) - - guard let superClassName = currentSuperclass.name(in: currentMachO) else { continue } - - var superclassInfo: ObjCClassInfo? - if let cacheInfo = cache[superClassName] { - superclassInfo = cacheInfo - } else { - let info = currentSuperclass.info(in: currentMachO) - superclassInfo = info - cache[superClassName] = info - } - if let superclassInfo { - resultInfos.append(superclassInfo) - } - } - - return resultInfos - } - - // MARK: - Reverse-table Feed - - /// Records one Objective-C class record from `__objc_classlist`: - /// - its superclass name -> add this class as a subclass entry - /// - each protocol it adopts inline -> add this class as a conformer - /// - /// `__objc_classlist` automatically contains a `class_t` record for every - /// Swift class with an Objective-C ancestor (`class Foo: NSObject`, - /// whether or not annotated `@objc`). Pass `isSwiftStable: true` for those - /// so the resolver can materialize the reference as a Swift `RuntimeObject` - /// at query time without doing any string-name bridging. - private func indexClass( - className: String, - superClassName: String?, - adoptedProtocolNames: [String], - imagePath: String, - isSwiftStable: Bool - ) { - let reference = ObjCClassReference( - className: className, - imagePath: imagePath, - isSwiftStable: isSwiftStable - ) - - if let superClassName, !superClassName.isEmpty { - // `_ =` drops `OrderedSet.append`'s `(inserted:index:)` tuple so - // the `withLock` closure stays `Void`-returning; a repeated - // reference being deduped by `OrderedSet` is the intended behavior. - _subclassesByClassName.withLock { dictionary in - _ = dictionary[superClassName, default: []].append(reference) - } - eventHandler?( - RuntimeObjCInterfaceEvents.Event( - kind: .subclassIndexed( - className: className, - superclass: superClassName, - imagePath: imagePath - ) - ) - ) - } - - for protocolName in adoptedProtocolNames { - _conformingClassesByProtocolName.withLock { dictionary in - _ = dictionary[protocolName, default: []].append(reference) - } - eventHandler?( - RuntimeObjCInterfaceEvents.Event( - kind: .conformanceIndexed( - className: className, - protocolName: protocolName, - imagePath: imagePath - ) - ) - ) - } - } - - /// Records one Objective-C category. Categories extend the conformance - /// set of the target class: every protocol the category adopts gets the - /// target class added as a conformer (with the target's `isSwiftStable` - /// flag carried through, so a category on a bridged class still surfaces - /// the class as Swift). - private func indexCategory( - targetClassName: String, - targetIsSwiftStable: Bool, - adoptedProtocolNames: [String], - imagePath: String - ) { - let reference = ObjCClassReference( - className: targetClassName, - imagePath: imagePath, - isSwiftStable: targetIsSwiftStable - ) - - for protocolName in adoptedProtocolNames { - _conformingClassesByProtocolName.withLock { dictionary in - _ = dictionary[protocolName, default: []].append(reference) - } - eventHandler?( - RuntimeObjCInterfaceEvents.Event( - kind: .categoryConformanceIndexed( - targetClassName: targetClassName, - protocolName: protocolName, - imagePath: imagePath - ) - ) - ) - } - } - - // MARK: - Interface Query - - /// The class plus its superclass chain for `name`, or `nil` if `name` is - /// not a class in this image. `info.first` is the class itself. - func classGroup(forName name: String) -> ObjCClassGroup? { - classes[name] - } - - /// The protocol record for `name`, or `nil` if `name` is not a protocol - /// in this image. - func protocolGroup(forName name: String) -> ObjCProtocolGroup? { - protocols[name] - } - - /// The category record for `uniqueName`, or `nil` if absent. - func categoryGroup(forName uniqueName: String) -> ObjCCategoryGroup? { - categories[uniqueName] - } - - /// Names of every class in this image (`__objc_classlist` order is not - /// preserved — dictionary iteration order). - var classNames: [String] { - Array(classes.keys) - } - - var protocolNames: [String] { - Array(protocols.keys) - } - - var categoryNames: [String] { - Array(categories.keys) - } - - var structNames: [String] { - Array(structs.keys) - } - - var unionNames: [String] { - Array(unions.keys) - } - - /// Whether a C `struct` named `name` was harvested from this image — - /// used by `ObjCDumpContext` to decide whether a referenced struct - /// should be emitted inline or left as a forward declaration. - func containsStruct(named name: String) -> Bool { - structs[name] != nil - } - - func containsUnion(named name: String) -> Bool { - unions[name] != nil - } - - /// The rendered interface of the C `struct` named `name`, or `nil` if - /// absent. The `context` is supplied by `RuntimeObjCSection` because it - /// depends on per-request generation options. - func structSemanticString(forName name: String, context: ObjCDumpContext) -> SemanticString? { - structs[name]?.semanticString(isStruct: true, context: context) - } - - func unionSemanticString(forName name: String, context: ObjCDumpContext) -> SemanticString? { - unions[name]?.semanticString(isStruct: false, context: context) - } - - // MARK: - Relationship Query - - /// All directly subclassing references for the given Objective-C class - /// name, gathered from this indexer's own per-image data plus every - /// sub-indexer registered via `addSubIndexer`. Insertion order is - /// preserved across a single image; cross-image order follows - /// `subIndexers` registration order. - public func subclasses(of className: String) -> [ObjCClassReference] { - var result: OrderedSet = subclassesByClassName[className] ?? [] - for subIndexer in subIndexers { - for reference in subIndexer.subclasses(of: className) { - result.append(reference) - } - } - return Array(result) - } - - /// All classes (across all sub-indexers) that adopt the given protocol - /// either inline (`@interface …

`) or via a category that adopts the - /// protocol on the class. - public func conformingClasses(toProtocol protocolName: String) -> [ObjCClassReference] { - var result: OrderedSet = conformingClassesByProtocolName[protocolName] ?? [] - for subIndexer in subIndexers { - for reference in subIndexer.conformingClasses(toProtocol: protocolName) { - result.append(reference) - } - } - return Array(result) - } - - // MARK: - Aggregation - - /// Registers a per-image indexer with this aggregate. Mirrors - /// `SwiftDeclarationIndexer.addSubIndexer(_:)` and is called by - /// `RuntimeObjCSectionFactory` immediately after a new per-image - /// `RuntimeObjCSection` has been constructed. - public func addSubIndexer(_ subIndexer: RuntimeObjCInterfaceIndexer) { - _subIndexers.withLock { $0.append(subIndexer) } - } -} - -// MARK: - Events - -public enum RuntimeObjCInterfaceEvents { - public struct Event: Sendable { - public enum Kind: Sendable { - case subclassIndexed(className: String, superclass: String, imagePath: String) - case conformanceIndexed(className: String, protocolName: String, imagePath: String) - case categoryConformanceIndexed(targetClassName: String, protocolName: String, imagePath: String) - } - - public let kind: Kind - - public init(kind: Kind) { - self.kind = kind - } - } - - public typealias Handler = @Sendable (Event) -> Void -} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeSwiftInterfaceIndexer.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeSwiftInterfaceIndexer.swift index 3b92e1cd..7cb0fba2 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeSwiftInterfaceIndexer.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Indexing/RuntimeSwiftInterfaceIndexer.swift @@ -13,8 +13,8 @@ import SwiftStdlibToolbox /// upstream `MachOSwiftSection` `SwiftDeclarationIndexer` that layers on the /// relationship reverse tables backing the Inspector's Relationships tab. /// -/// This is the Swift counterpart of `RuntimeObjCInterfaceIndexer`, but the -/// two are not built the same way. On the ObjC side `RuntimeObjCInterfaceIndexer` +/// This is the Swift counterpart of `ObjCInterfaceIndexer`, but the +/// two are not built the same way. On the ObjC side `ObjCInterfaceIndexer` /// *is* the indexer — it parses `MachOObjCSection` / `ObjCDump` itself. On the /// Swift side the heavy parsing is already done by the upstream /// `SwiftDeclarationIndexer`, which we neither own nor can extend; so this type @@ -43,19 +43,19 @@ import SwiftStdlibToolbox /// `typeName(forMangledName:)`) fan out across `self` plus every registered /// sub-indexer — so a query against the `RuntimeSwiftSectionFactory` /// aggregate, which holds every per-image indexer, spans all loaded images. -/// Mirrors `RuntimeObjCInterfaceIndexer`. +/// Mirrors `ObjCInterfaceIndexer`. /// /// `@unchecked Sendable`: the `MachOImage` and `SwiftDeclaration.TypeName` /// values held here are not themselves `Sendable`, but `machO` / `upstream` /// are immutable `let`s and the reverse tables plus `subIndexers` are all -/// `@Mutex`-guarded — mirroring `RuntimeObjCInterfaceIndexer`. +/// `@Mutex`-guarded — mirroring `ObjCInterfaceIndexer`. @dynamicMemberLookup final class RuntimeSwiftInterfaceIndexer: @unchecked Sendable { // MARK: - Indexed Image /// The Mach-O image this indexer parses. Bound at `init`, never - /// reassigned — mirrors `RuntimeObjCInterfaceIndexer`, which likewise + /// reassigned — mirrors `ObjCInterfaceIndexer`, which likewise /// binds its `MachOImage` at construction. private let machO: MachOImage @@ -107,7 +107,7 @@ final class RuntimeSwiftInterfaceIndexer: @unchecked Sendable { /// section's own indexer; on the `RuntimeSwiftSectionFactory` aggregate it /// holds every loaded image's indexer, so the query methods fan out across /// all of them. `@Mutex`-guarded because the factory keeps registering as - /// images load. Mirrors `RuntimeObjCInterfaceIndexer.subIndexers`. + /// images load. Mirrors `ObjCInterfaceIndexer.subIndexers`. @Mutex private var subIndexers: [RuntimeSwiftInterfaceIndexer] = [] @@ -115,7 +115,7 @@ final class RuntimeSwiftInterfaceIndexer: @unchecked Sendable { /// `machO` is bound here and never changes; `eventHandlers` is forwarded /// straight to the upstream indexer (`RuntimeSwiftSection` builds the - /// progress-event handler). Mirrors `RuntimeObjCInterfaceIndexer.init`, + /// progress-event handler). Mirrors `ObjCInterfaceIndexer.init`, /// where the image is likewise bound at construction. init(machO: MachOImage, eventHandlers: [SwiftIndexEvents.Handler] = []) { self.machO = machO @@ -144,7 +144,7 @@ final class RuntimeSwiftInterfaceIndexer: @unchecked Sendable { // Build into locals, then assign through the `@Mutex` once each — so // no lock is held across the `await mangleAsString` suspension points. - // Mirrors `RuntimeObjCInterfaceIndexer.prepare()`. + // Mirrors `ObjCInterfaceIndexer.prepare()`. var subclassTable: [String: OrderedSet] = [:] var typeNameTable: [String: SwiftDeclaration.TypeName] = [:] var protocolNameTable: [String: SwiftDeclaration.ProtocolName] = [:] @@ -276,7 +276,7 @@ final class RuntimeSwiftInterfaceIndexer: @unchecked Sendable { /// sub-indexer's `upstream` to `upstream.addSubIndexer` so the upstream's /// own cross-image lookups (`allAllTypeDefinitions`, …) see it too. /// Callers pass `RuntimeSwiftInterfaceIndexer` values and never reach for - /// `.upstream`. Mirrors `RuntimeObjCInterfaceIndexer.addSubIndexer(_:)`; + /// `.upstream`. Mirrors `ObjCInterfaceIndexer.addSubIndexer(_:)`; /// `RuntimeSwiftSectionFactory` calls it as each section is created. func addSubIndexer(_ subIndexer: RuntimeSwiftInterfaceIndexer) { upstream.addSubIndexer(subIndexer.upstream) diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift index effc32e8..3f17d732 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift @@ -58,8 +58,8 @@ actor RuntimeRelationshipsResolver { // // For ObjC class/protocol targets, `object.name` is the raw ObjC // class/protocol name (the same string used as the key in - // `RuntimeObjCInterfaceIndexer.classes`/`.protocols` and as the - // `superclassByClassName` key in `RuntimeObjCInterfaceIndexer`). + // `ObjCInterfaceIndexer.classes`/`.protocols` and as the + // `superclassByClassName` key in `ObjCInterfaceIndexer`). // // For Swift class targets, `object.name` is the mangled string // produced by `mangleAsString(typeName.node)`, which is the diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+CType.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+CType.swift deleted file mode 100644 index b84dc013..00000000 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+CType.swift +++ /dev/null @@ -1,210 +0,0 @@ -import Foundation -import MetaCodable -public import Semantic - -// MARK: - C Type Transformer Module - -extension Transformer { - /// Replaces C primitive types with custom types. - /// - /// Example: - /// ```swift - /// var module = Transformer.CType() - /// module.isEnabled = true - /// module.replacements[.double] = "CGFloat" - /// module.replacements[.longLong] = "NSInteger" - /// ``` - @Codable - public struct CType: Module { - public typealias Parameter = Pattern - public typealias Input = SemanticString - public typealias Output = SemanticString - - public static let displayName = "C Type Replacement" - - @Default(ifMissing: false) - public var isEnabled: Bool - - @Default(ifMissing: [:]) - public var replacements: [Pattern: String] - - public init(isEnabled: Bool = false, replacements: [Pattern: String] = [:]) { - self.isEnabled = isEnabled - self.replacements = replacements - } - - public func transform(_ input: SemanticString) -> SemanticString { - let sorted = sortedReplacements - guard !sorted.isEmpty else { return input } - - let components = input.components - guard !components.isEmpty else { return input } - - var result: [AtomicComponent] = [] - var index = 0 - - while index < components.count { - if let (replacement, consumed) = match(in: components, at: index, patterns: sorted) { - result.append(AtomicComponent(string: replacement, type: .type(.other, .name))) - index += consumed - } else { - result.append(components[index]) - index += 1 - } - } - - return SemanticString(components: result) - } - - // Sorted by pattern length (longest first) - private var sortedReplacements: [(Pattern, String)] { - replacements - .filter { !$0.value.isEmpty } - .sorted { $0.key.keywords.count > $1.key.keywords.count } - .map { ($0.key, $0.value) } - } - - private func match( - in components: [AtomicComponent], - at startIndex: Int, - patterns: [(Pattern, String)] - ) -> (String, Int)? { - for (pattern, replacement) in patterns { - if let consumed = matchKeywords(pattern.keywords, in: components, at: startIndex) { - return (replacement, consumed) - } - } - return nil - } - - private func matchKeywords( - _ keywords: [String], - in components: [AtomicComponent], - at startIndex: Int - ) -> Int? { - guard !keywords.isEmpty else { return nil } - - var ci = startIndex - var ki = 0 - var consumed = 0 - - while ki < keywords.count && ci < components.count { - let c = components[ci] - - // Skip whitespace - if c.type == .standard && c.string.allSatisfy(\.isWhitespace) { - ci += 1 - consumed += 1 - continue - } - - guard c.type == .keyword, c.string == keywords[ki] else { return nil } - - ki += 1 - ci += 1 - consumed += 1 - } - - return ki == keywords.count ? consumed : nil - } - } -} - -// MARK: - Pattern - -extension Transformer.CType { - /// C primitive type patterns. - public enum Pattern: String, CaseIterable, Codable, Sendable, Hashable { - case char - case uchar - case short - case ushort - case int - case uint - case long - case ulong - case longLong - case ulongLong - case float - case double - case longDouble - - public var displayName: String { - switch self { - case .char: "char" - case .uchar: "unsigned char" - case .short: "short" - case .ushort: "unsigned short" - case .int: "int" - case .uint: "unsigned int" - case .long: "long" - case .ulong: "unsigned long" - case .longLong: "long long" - case .ulongLong: "unsigned long long" - case .float: "float" - case .double: "double" - case .longDouble: "long double" - } - } - - var keywords: [String] { - switch self { - case .char: ["char"] - case .uchar: ["unsigned", "char"] - case .short: ["short"] - case .ushort: ["unsigned", "short"] - case .int: ["int"] - case .uint: ["unsigned", "int"] - case .long: ["long"] - case .ulong: ["unsigned", "long"] - case .longLong: ["long", "long"] - case .ulongLong: ["unsigned", "long", "long"] - case .float: ["float"] - case .double: ["double"] - case .longDouble: ["long", "double"] - } - } - } -} - -// MARK: - Presets - -extension Transformer.CType { - public enum Presets { - public static let stdint: [Pattern: String] = [ - .uchar: "uint8_t", - .ushort: "uint16_t", - .uint: "uint32_t", - .ulong: "uint64_t", - .ulongLong: "uint64_t", - - .char: "int8_t", - .short: "int16_t", - .int: "int32_t", - .long: "int64_t", - .longLong: "int64_t", - ] - - public static let foundation: [Pattern: String] = [ - .double: "CGFloat", - .long: "NSInteger", - .ulong: "NSUInteger", - .longLong: "NSInteger", - .ulongLong: "NSUInteger", - ] - - public static let mixed: [Pattern: String] = [ - .uchar: "uint8_t", - .ushort: "uint16_t", - .uint: "uint32_t", - .char: "int8_t", - .short: "int16_t", - .int: "int32_t", - .long: "NSInteger", - .ulong: "NSUInteger", - .longLong: "NSInteger", - .ulongLong: "NSUInteger", - .double: "CGFloat", - ] - } -} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+Configuration.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+Configuration.swift new file mode 100644 index 00000000..a4cb308b --- /dev/null +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+Configuration.swift @@ -0,0 +1,43 @@ +import Foundation +public import OutputTransformer +public import ObjCOutputTransformer +public import SwiftOutputTransformer + +// MARK: - Aggregated Configuration + +extension Transformer { + /// Aggregated configuration for every transformer module, used for + /// persistence. + /// + /// This lives here rather than library-side because it is the only place + /// that spans both halves: the ObjC modules ship with MachOObjCSection and + /// the Swift ones with MachOSwiftSection, and nothing but RuntimeViewer + /// needs to persist and edit them as a single unit. + public struct Configuration: Sendable, Equatable, Hashable, Codable { + public var objc: Transformer.ObjCConfiguration + public var swift: Transformer.SwiftConfiguration + + public init( + objc: Transformer.ObjCConfiguration = .init(), + swift: Transformer.SwiftConfiguration = .init() + ) { + self.objc = objc + self.swift = swift + } + + // Missing-key-tolerant decoding (compatible with the previous + // MetaCodable `@Default(_:)` persistence). + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.objc = try container.decodeIfPresent(ObjCConfiguration.self, forKey: .objc) ?? .init() + self.swift = try container.decodeIfPresent(SwiftConfiguration.self, forKey: .swift) ?? .init() + } + + public static let `default` = Self() + + /// Whether any module is enabled. + public var hasEnabledModules: Bool { + objc.hasEnabledModules || swift.hasEnabledModules + } + } +} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+ObjCIvarOffset.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+ObjCIvarOffset.swift deleted file mode 100644 index 33f4a72c..00000000 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer+ObjCIvarOffset.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import MetaCodable - -// MARK: - ObjC Ivar Offset Transformer Module - -extension Transformer { - /// Customizes ObjC ivar offset comment format using token templates. - @Codable - public struct ObjCIvarOffset: Module { - public typealias Parameter = Token - public typealias Output = String - - public static let displayName = "ObjC Ivar Offset Comment" - - @Default(ifMissing: false) - public var isEnabled: Bool - - @Default(ifMissing: Templates.standard) - public var template: String - - @Default(ifMissing: true) - public var useHexadecimal: Bool - - public init(isEnabled: Bool = false, template: String = Templates.standard, useHexadecimal: Bool = true) { - self.isEnabled = isEnabled - self.template = template - self.useHexadecimal = useHexadecimal - } - - public func transform(_ input: Input) -> String { - template - .replacingOccurrences(of: Token.offset.placeholder, with: formatValue(input.offset)) - } - - private func formatValue(_ value: Int) -> String { - useHexadecimal ? "0x\(String(value, radix: 16, uppercase: true))" : String(value) - } - - public func contains(_ token: Token) -> Bool { - template.contains(token.placeholder) - } - } -} - -// MARK: - Input - -extension Transformer.ObjCIvarOffset { - public struct Input: Sendable { - public let offset: Int - - public init(offset: Int) { - self.offset = offset - } - } -} - -// MARK: - Token - -extension Transformer.ObjCIvarOffset { - public enum Token: String, CaseIterable, Sendable { - case offset - - public var placeholder: String { "${\(rawValue)}" } - - public var displayName: String { - switch self { - case .offset: "Offset" - } - } - } -} - -// MARK: - Templates - -extension Transformer.ObjCIvarOffset { - public enum Templates { - public static let standard = "offset: ${offset}" - public static let labeled = "ivar offset: ${offset}" - public static let bare = "${offset}" - - public static let all: [(name: String, template: String)] = [ - ("Standard", standard), - ("Labeled", labeled), - ("Bare", bare), - ] - } -} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer.swift index 664d4b00..7bc6a1cb 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Transformer/Transformer.swift @@ -1,54 +1,15 @@ -import Foundation -import MetaCodable - -/// The Swift-side transformer template mechanism (the `Transformer` namespace: -/// comment token templates, presets, the `Module` protocol, and -/// `SwiftConfiguration`) moved library-side into MachOSwiftSection's -/// `OutputTransformer` module, so the templates render inside the library -/// and RuntimeViewer keeps only the settings UI. This re-export keeps every -/// existing `Transformer.…` reference compiling unchanged. +/// The `Transformer` namespace and its `Module` protocol come from +/// swift-semantic-string, while the concrete modules ship with the library +/// that owns their vocabulary — `CType` / `ObjCIvarOffset` with +/// MachOObjCSection, the Swift comment kinds with MachOSwiftSection. All of +/// them extend the same namespace. /// -/// The ObjC-side modules (`CType`, `ObjCIvarOffset`) and the aggregate -/// persistence `Configuration` remain here for now (declared as extensions of -/// the imported namespace), pending a library-side home for the ObjC -/// rendering pipeline. +/// Only the aggregate `Transformer.Configuration` remains here (see +/// `Transformer+Configuration.swift`): it is the one piece that spans both +/// halves, and nothing outside RuntimeViewer needs to persist them as a unit. +/// +/// These re-exports keep every existing `Transformer.…` reference in +/// RuntimeViewer compiling unchanged. @_exported import OutputTransformer - -// MARK: - ObjC Configuration - -extension Transformer { - /// Configuration for ObjC-specific transformer modules. - @Codable - public struct ObjCConfiguration: Sendable, Equatable, Hashable { - @Default(ifMissing: Transformer.CType()) - public var cType: Transformer.CType - @Default(ifMissing: Transformer.ObjCIvarOffset()) - public var ivarOffset: Transformer.ObjCIvarOffset - - public init(cType: CType = .init(), ivarOffset: ObjCIvarOffset = .init()) { - self.cType = cType - self.ivarOffset = ivarOffset - } - } -} - -// MARK: - Aggregated Configuration - -extension Transformer { - /// Aggregated configuration for all transformer modules (used for persistence). - @Codable - @MemberInit - public struct Configuration: Sendable, Equatable, Hashable { - @Default(Transformer.ObjCConfiguration()) - public var objc: Transformer.ObjCConfiguration - @Default(Transformer.SwiftConfiguration()) - public var swift: Transformer.SwiftConfiguration - - public static let `default` = Self() - - /// Whether any module is enabled. - public var hasEnabledModules: Bool { - objc.cType.isEnabled || objc.ivarOffset.isEnabled || swift.hasEnabledModules - } - } -} +@_exported import ObjCOutputTransformer +@_exported import SwiftOutputTransformer diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/MachOImage+AddressFormatting.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/MachOImage+AddressFormatting.swift deleted file mode 100644 index 4e3c7a0c..00000000 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/MachOImage+AddressFormatting.swift +++ /dev/null @@ -1,44 +0,0 @@ -import MachOKit -import MachOExtensions -import Semantic - -extension MachOImage { - /// Safely format an IMP address into a resolved virtual address string. - /// - /// Returns `nil` when the raw value is zero or falls below the image base, - /// meaning the caller should treat it as invalid. - func formattedAddress(forRawValue rawValue: UInt64) -> String? { - let value = UInt(rawValue) - let baseAddress = UInt(bitPattern: ptr) - guard value != 0, value >= baseAddress else { - return nil - } - return "0x\(addressString(forOffset: .init(value &- baseAddress)))" - } - - /// Build a ``Comment`` component for an IMP address. - /// - /// Produces a normal comment (e.g. `// IMP: 0x1A2B3C`) when the address - /// is valid, or an ``Error`` component (e.g. `// IMP: `) - /// when it is not. - @SemanticStringBuilder - func impAddressComment(label: String, rawValue: UInt64) -> SemanticString { - if let resolved = formattedAddress(forRawValue: rawValue) { - Comment("\(label): \(resolved)") - } else { - Comment("\(label): ") - Error("") - } - } - - /// Format an IMP address into a plain string suitable for data models. - /// - /// Returns a resolved virtual address when valid, or a raw hex - /// representation prefixed with `` when not. - func formattedAddressString(forRawValue rawValue: UInt64) -> String { - if let resolved = formattedAddress(forRawValue: rawValue) { - return resolved - } - return "" - } -} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/ObjCDump+SemanticString.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/ObjCDump+SemanticString.swift deleted file mode 100644 index 77639825..00000000 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Utils/ObjCDump+SemanticString.swift +++ /dev/null @@ -1,896 +0,0 @@ -import Foundation -import MachOKit -import MachOExtensions -import Semantic -import ObjCDump -import ObjCTypeDecodeKit -import MemberwiseInit - -@MemberwiseInit() -final class ObjCDumpContext { - let machO: MachOImage - var options: ObjCGenerationOptions - var cTypeReplacements: [Transformer.CType.Pattern: String] = [:] - var ivarOffsetTransformer: Transformer.ObjCIvarOffset? - var currentArray: SemanticString? - var methodIMPs: [String: UInt64] = [:] - var classMethodIMPs: [String: UInt64] = [:] - var isExpandHandler: (_ name: String?, _ isStruct: Bool) -> Bool = { _, _ in true } -} - -extension ObjCClassInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - Keyword("@interface") - Space() - TypeDeclaration(kind: .class, name) - - if let superClassName { - " : " - TypeName(kind: .class, superClassName) - } - - Joined(separator: ", ", prefix: " <", suffix: ">") { - for `protocol` in protocols { - TypeName(kind: .protocol, `protocol`.name) - } - } - - Joined { - MemberList(level: 1) { - for ivar in ivars { - ivar.semanticString(using: context) - } - } - } prefix: { - Space() - "{" - } suffix: { - "}" - } - - BreakLine() - - Joined(suffix: BreakLine()) { - BlockList { - for property in classProperties { - property.semanticString(using: context) - } - } - BlockList { - for property in properties { - property.semanticString(using: context) - } - } - BlockList { - for method in classMethods { - method.semanticString(using: context) - } - } - BlockList { - for method in methods { - method.semanticString(using: context) - } - } - } - - Keyword("@end") - } -} - -extension ObjCProtocolInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - Keyword("@protocol") - Space() - TypeDeclaration(kind: .protocol, name) - - Joined(separator: ", ", prefix: " <", suffix: ">") { - for `protocol` in protocols { - TypeName(kind: .protocol, `protocol`.name) - } - } - - BreakLine() - - Joined(separator: BreakLine(), prefix: BreakLine(), suffix: BreakLine()) { - Joined { - BlockList { - for property in classProperties { - property.semanticString(using: context) - } - } - BlockList { - for property in properties { - property.semanticString(using: context) - } - } - BlockList { - for method in classMethods { - method.semanticString(using: context) - } - } - BlockList { - for method in methods { - method.semanticString(using: context) - } - } - } prefix: { - Keyword("@required") - BreakLine() - } - - Joined { - BlockList { - for property in optionalClassProperties { - property.semanticString(using: context) - } - } - BlockList { - for property in optionalProperties { - property.semanticString(using: context) - } - } - BlockList { - for method in optionalClassMethods { - method.semanticString(using: context) - } - } - BlockList { - for method in optionalMethods { - method.semanticString(using: context) - } - } - } prefix: { - Keyword("@optional") - BreakLine() - } - } - - Keyword("@end") - } -} - -extension ObjCCategoryInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - Keyword("@interface") - Space() - TypeName(kind: .class, className) - Space() - "(\(name))" - - Joined(separator: ", ", prefix: " <", suffix: ">") { - for `protocol` in protocols { - TypeName(kind: .protocol, `protocol`.name) - } - } - - BreakLine() - - Joined(suffix: BreakLine()) { - BlockList { - for property in classProperties { - property.semanticString(using: context) - } - } - - BlockList { - for property in properties { - property.semanticString(using: context) - } - } - - BlockList { - for method in classMethods { - method.semanticString(using: context) - } - } - - BlockList { - for method in methods { - method.semanticString(using: context) - } - } - } - - Keyword("@end") - } -} - -extension ObjCIvarInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - if let type, case .bitField(let width) = type { - ObjCField(type: .int, name: name, bitWidth: width) - .semanticString(fallbackName: name, context: context) - } else { - if [.char, .uchar].contains(type) { - Keyword("BOOL") - Space() - Variable(name) - ";" - } else { - if let type = type?.semanticDecoded(context: context) { - type - if type.string.last != "*" { - Space() - } - Variable(name) - if let currentArray = context.currentArray { - currentArray - context.currentArray = nil - } - ";" - } else { - UnknownError() - Space() - Variable(name) - ";" - } - } - } - - if context.options.addIvarOffsetComments { - Space() - if let ivarOffsetTransformer = context.ivarOffsetTransformer { - Comment(ivarOffsetTransformer.transform(.init(offset: offset))) - } else { - Comment("offset: \(offset)") - } - } - } -} - -extension ObjCPropertyInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - Keyword("@property") - - Joined(separator: ", ", prefix: " (", suffix: ")") { - if attributes.contains(.nonatomic) { - Keyword("nonatomic") - } - - if attributes.contains(.weak) { - Keyword("weak") - } - - if attributes.contains(.copy) { - Keyword("copy") - } - - if attributes.contains(.retain) { - Keyword("strong") - } - - if isClassProperty { - Keyword("class") - } - - if let getter = attributes.compactMap(\.getter).first { - Group { - Keyword("getter") - "=" - getter - } - } - - if let setter = attributes.compactMap(\.setter).first { - Group { - Keyword("setter") - "=" - setter - } - } - - if attributes.contains(.readonly) { - Keyword("readonly") - } - } - - Space() - - let typeString = attributes.compactMap(\.type).first?.semanticDecodedForArgument(context: context) - - if let typeString { - typeString - if typeString.string.last != "*" { - Space() - } - } else { - UnknownError() - Space() - } - - MemberDeclaration(name) - ";" - - if context.options.addPropertyAttributesComments { - Joined(separator: " ", prefix: " ") { - if attributes.contains(.dynamic) { - Comment("@dynamic \(name)") - } - - if let ivar { - if ivar == name { - Comment("@synthesize \(ivar)") - } else { - Comment("@synthesize \(name) = \(ivar)") - } - } - } - } - - if context.options.addPropertyAccessorAddressComments { - let imps = isClassProperty ? context.classMethodIMPs : context.methodIMPs - let getterName = customGetter ?? name - let setterName = customSetter ?? "set\(name.uppercasedFirst):" - - Joined(separator: " ", prefix: " ") { - if let getterIMP = imps[getterName] { - context.machO.impAddressComment(label: "getter IMP", rawValue: getterIMP) - } - if let setterIMP = imps[setterName] { - context.machO.impAddressComment(label: "setter IMP", rawValue: setterIMP) - } - } - } - } -} - -extension ObjCMethodInfo { - @SemanticStringBuilder - func semanticString(using context: ObjCDumpContext) -> SemanticString { - if isClassMethod { - "+" - } else { - "-" - } - - Space() - - "(" - if let returnType = type?.returnType { - returnType.semanticDecodedForArgument(context: context) - } else { - UnknownError() - } - ")" - - let numberOfArguments = name.filter { $0 == ":" }.count - - if numberOfArguments == 0 { - FunctionDeclaration(name) - } else { - let nameAndLabels = name.split(separator: ":") - let argumentInfos = type?.argumentInfos ?? [] - - for (index, label) in nameAndLabels.enumerated() { - if index > 0 { - Space() - } - let labelString = String(label) - FunctionDeclaration(labelString) - ":" - "(" - if index < argumentInfos.count { - argumentInfos[index].type.semanticDecodedForArgument(context: context) - } else { - UnknownError() - } - ")" - Argument(NamingIntelligent.parameterName(from: labelString)) - } - } - - ";" - - if context.options.addMethodIMPAddressComments { - Space() - context.machO.impAddressComment(label: "IMP", rawValue: imp) - } - } -} - -extension ObjCField { - @SemanticStringBuilder - func semanticString(fallbackName: String, level: Int = 1, context: ObjCDumpContext) -> SemanticString { - type.semanticDecoded(level: level, context: context) - Space() - Variable(name ?? fallbackName) - if let bitWidth { - " : " - Numeric(bitWidth) - } - ";" - } -} - -extension ObjCModifier { - @SemanticStringBuilder - func semanticDecoded(level: Int = 1) -> SemanticString { - switch self { - case .complex: - Keyword("_Complex") - case .atomic: - Keyword("_Atomic") - case .const: - Keyword("const") - case .in: - Keyword("in") - case .inout: - Keyword("inout") - case .out: - Keyword("out") - case .bycopy: - Keyword("bycopy") - case .byref: - Keyword("byref") - case .oneway: - Keyword("oneway") - case .register: - Keyword("register") - } - } -} - -extension ObjCType { - @SemanticStringBuilder - func semanticDecodedForArgument(context: ObjCDumpContext) -> SemanticString { - switch self { - case .struct(let name, let fields), - .union(let name, let fields): - Keyword(isStruct ? "struct" : "union") - if let name { - Space() - TypeName(kind: isStruct ? .struct : .other, name) - } - - if context.isExpandHandler(name, isStruct) { - Joined { - if let fields { - Joined(separator: " ") { - for (index, field) in fields.enumerated() { - Group { - field.type.semanticDecodedForArgument(context: context) - Space() - Variable(field.name ?? "x\(index)") - if let bitWidth = field.bitWidth { - " : " - Numeric(bitWidth) - } - ";" - } - } - } - } - } prefix: { - " { " - } suffix: { - " }" - } - } - case .char: - Keyword("BOOL") - case .pointer(let type): - type.semanticDecodedForArgument(context: context) - Space() - "*" - case .modified(let modifier, let type): - modifier.semanticDecoded(level: 0) - Space() - type.semanticDecodedForArgument(context: context) - default: - semanticDecoded(level: 0, context: context) - } - } - - @SemanticStringBuilder - func semanticDecoded(level: Int = 1, context: ObjCDumpContext) -> SemanticString { - switch self { - case .class: - TypeName(kind: .class, "Class") - case .selector: - Keyword("SEL") - case .char: - if let r = context.cTypeReplacements[.char] { TypeName(kind: .other, r) } else { Keyword("char") } - case .uchar: - if let r = context.cTypeReplacements[.uchar] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("unsigned") - Keyword("char") - } - } - case .short: - if let r = context.cTypeReplacements[.short] { TypeName(kind: .other, r) } else { Keyword("short") } - case .ushort: - if let r = context.cTypeReplacements[.ushort] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("unsigned") - Keyword("short") - } - } - case .int: - if let r = context.cTypeReplacements[.int] { TypeName(kind: .other, r) } else { Keyword("int") } - case .uint: - if let r = context.cTypeReplacements[.uint] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("unsigned") - Keyword("int") - } - } - case .long: - if let r = context.cTypeReplacements[.long] { TypeName(kind: .other, r) } else { Keyword("long") } - case .ulong: - if let r = context.cTypeReplacements[.ulong] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("unsigned") - Keyword("long") - } - } - case .longLong: - if let r = context.cTypeReplacements[.longLong] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("long") - Keyword("long") - } - } - case .ulongLong: - if let r = context.cTypeReplacements[.ulongLong] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("unsigned") - Keyword("long") - Keyword("long") - } - } - case .int128: - TypeName(kind: .other, "__int128_t") - case .uint128: - TypeName(kind: .other, "__uint128_t") - case .float: - if let r = context.cTypeReplacements[.float] { TypeName(kind: .other, r) } else { Keyword("float") } - case .double: - if let r = context.cTypeReplacements[.double] { TypeName(kind: .other, r) } else { Keyword("double") } - case .longDouble: - if let r = context.cTypeReplacements[.longDouble] { - TypeName(kind: .other, r) - } else { - Joined(separator: Space()) { - Keyword("long") - Keyword("double") - } - } - case .bool: - Keyword("BOOL") - case .void: - Keyword("void") - case .unknown: - UnknownError() - case .charPtr: - Keyword("char") - Space() - "*" - case .atom: - Keyword("atom") - case .object(let name): - if let name { - // eg. id - if name.first == "<" && name.last == ">" { - let components = name.components(separatedBy: "><") - Keyword("id") - if components.count > 1 { - Joined(separator: ", ", prefix: "<", suffix: ">") { - for (offset, component) in components.offsetEnumerated() { - if offset.isStart { - TypeName(kind: .protocol, String(component.dropFirst(1))) - } else if offset.isEnd { - TypeName(kind: .protocol, String(component.dropLast(1))) - } else { - TypeName(kind: .protocol, String(component)) - } - } - } - } else { - "<" - TypeName(kind: .protocol, String(name.dropFirst(1).dropLast(1))) - ">" - } - } else { - // eg. NSObject - if let protocolPrefixIndex = name.firstIndex(of: "<"), let protocolSuffixIndex = name.lastIndex(of: ">") { - let protocolStartIndex = name.index(after: protocolPrefixIndex) - let protocols = name[protocolStartIndex..<") - TypeName(kind: .class, String(name[name.startIndex.. 1 { - Joined(separator: ", ", prefix: "<", suffix: ">") { - for (offset, component) in components.offsetEnumerated() { - if offset.isStart { - TypeName(kind: .protocol, String(component.dropFirst(1))) - } else if offset.isEnd { - TypeName(kind: .protocol, String(component.dropLast(1))) - } else { - TypeName(kind: .protocol, String(component)) - } - } - } - } else { - // eg. NSObject - "<" - TypeName(kind: .protocol, String(protocols)) - ">" - } - Space() - "*" - } else { - TypeName(kind: .class, name) - Space() - "*" - } - } - } else { - Keyword("id") - } - case .block(let ret, let args): - if let ret, let args { - ret.semanticDecoded(level: level, context: context) - " (^)(" - Joined(separator: ", ") { - for arg in args { - arg.semanticDecoded(level: level, context: context) - } - } - ")" - } else { - Keyword("id") - Space() - InlineComment("block") - } - case .functionPointer: - Keyword("void") - Space() - "*" - Space() - InlineComment("function pointer") - case .array(let type, let size): - type.semanticDecoded(level: level, context: context) - context.currentArray = SemanticString { - "[" - if let size { - Numeric(size) - } - "]" - } - case .pointer(let type): - type.semanticDecoded(level: level, context: context) - Space() - "*" - case .bitField(let width): - Keyword("int") - Space() - Variable("x") - " : " - Numeric(width) - case .struct(let name, let fields), - .union(let name, let fields): - Keyword(isStruct ? "struct" : "union") - if let name { - Space() - TypeName(kind: isStruct ? .struct : .other, name) - } - if context.isExpandHandler(name, isStruct) { - Joined { - if let fields { - MemberList(level: level + 1) { - for (index, field) in fields.enumerated() { - field.semanticString(fallbackName: "x\(index)", level: level + 1, context: context) - } - } - } - } prefix: { - " {" - } suffix: { - Indent(level: level) - "}" - }.if(fields != nil || name == nil) - } - case .modified(let modifier, let type): - modifier.semanticDecoded(level: level) - Space() - type.semanticDecoded(level: level, context: context) - case .other(let string): - string - } - } -} - -extension ObjCCategoryInfo { - var uniqueName: String { - "\(className)(\(name))" - } -} - -extension ObjCPropertyInfo { - var ivar: String? { - attributes.compactMap(\.ivar).first - } - - var customGetter: String? { - attributes.compactMap(\.getter).first - } - - var customSetter: String? { - attributes.compactMap(\.setter).first - } -} - -// MARK: - Naming Intelligent - -/// A utility for intelligently guessing parameter names from Objective-C method labels. -/// -/// Examples: -/// - `initWithTitle` -> `title` -/// - `objectForKey` -> `key` -/// - `valueAtIndex` -> `index` -/// - `setFrame` -> `frame` -/// - `setMaximumNumberOfLines` -> `lines` -/// - `name` -> `name` -private enum NamingIntelligent { - /// Common prepositions used in Objective-C method names (lowercase). - /// Ordered by length (longest first) to match longer prepositions before shorter ones. - private static let prepositions: [String] = [ - "withcontentsof", - "byappending", - "byreplacing", - "fromstring", - "tostring", - "containing", - "including", - "excluding", - "replacing", - "returning", - "matching", - "starting", - "between", - "through", - "without", - "within", - "during", - "before", - "behind", - "except", - "under", - "using", - "after", - "about", - "above", - "along", - "among", - "below", - "named", - "called", - "having", - "where", - "until", - "since", - "with", - "from", - "into", - "onto", - "upon", - "over", - "like", - "near", - "past", - "for", - "and", - "but", - "nor", - "yet", - "via", - "per", - "at", - "by", - "in", - "of", - "on", - "to", - "as", - ] - - /// Prefixes that should be stripped before looking for prepositions. - private static let prefixes: [String] = [ - "_set", - "_get", - "set", - "get", - ] - - /// Guesses a parameter name from an Objective-C method label. - /// - /// - Parameter label: The method label (e.g., "initWithTitle", "objectForKey") - /// - Returns: The guessed parameter name (e.g., "title", "key") - static func parameterName(from label: String) -> String { - guard !label.isEmpty else { return "arg" } - - var workingLabel = label - let lowercasedLabel = label.lowercased() - - // First, strip known prefixes like set/get - for prefix in prefixes { - if lowercasedLabel.hasPrefix(prefix) && label.count > prefix.count { - let afterPrefix = label.index(label.startIndex, offsetBy: prefix.count) - // Make sure the next character is uppercase (word boundary) - if label[afterPrefix].isUppercase { - workingLabel = String(label[afterPrefix...]) - break - } - } - } - - // Now search for prepositions from the beginning, find the LAST match - let lowercasedWorking = workingLabel.lowercased() - var lastMatchEnd: String.Index? - - for preposition in prepositions { - // Search for all occurrences from the beginning - var searchStart = lowercasedWorking.startIndex - while let range = lowercasedWorking.range(of: preposition, range: searchStart ..< lowercasedWorking.endIndex) { - // Calculate the corresponding range in the working label - let startDistance = lowercasedWorking.distance(from: lowercasedWorking.startIndex, to: range.lowerBound) - let endDistance = lowercasedWorking.distance(from: lowercasedWorking.startIndex, to: range.upperBound) - let originalStart = workingLabel.index(workingLabel.startIndex, offsetBy: startDistance) - let originalEnd = workingLabel.index(workingLabel.startIndex, offsetBy: endDistance) - - // Check word boundary for camelCase: - // 1. The preposition must start with uppercase (e.g., "With" in "initWithTitle") - // 2. After: must be uppercase letter (the next word starts) - let prepositionStartChar = workingLabel[originalStart] - let startsWithUppercase = prepositionStartChar.isUppercase - - let hasValidEnd: Bool - if originalEnd >= workingLabel.endIndex { - // Preposition at the end of the label is not valid - hasValidEnd = false - } else { - let nextChar = workingLabel[originalEnd] - hasValidEnd = nextChar.isUppercase - } - - if startsWithUppercase && hasValidEnd { - // Use the last (rightmost) preposition match - if lastMatchEnd == nil || originalEnd > lastMatchEnd! { - lastMatchEnd = originalEnd - } - } - - // Move search start forward - searchStart = range.upperBound - } - } - - // Extract the part after the last preposition - if let end = lastMatchEnd { - let afterPreposition = String(workingLabel[end...]) - if !afterPreposition.isEmpty { - return afterPreposition.lowercasedFirst - } - } - - // No preposition found, use the working label - return workingLabel.lowercasedFirst - } -} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerAdditionalTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerAdditionalTests.swift deleted file mode 100644 index 54955410..00000000 --- a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerAdditionalTests.swift +++ /dev/null @@ -1,410 +0,0 @@ -import Testing -import Foundation -import RuntimeViewerCore - -// MARK: - SwiftVTableOffset Tests - -@Suite("Transformer.SwiftVTableOffset") -struct TransformerSwiftVTableOffsetTests { - // MARK: - Basic Properties - - @Test("Display name") - func displayName() { - #expect(Transformer.SwiftVTableOffset.displayName == "Swift VTable Offset Comment") - } - - @Test("Default initialization") - func defaultInit() { - let module = Transformer.SwiftVTableOffset() - #expect(module.isEnabled == false) - #expect(module.template == Transformer.SwiftVTableOffset.Templates.standard) - #expect(module.labeledTemplate == Transformer.SwiftVTableOffset.Templates.standardLabeled) - #expect(module.useHexadecimal == false) - } - - @Test("Custom initialization") - func customInit() { - let module = Transformer.SwiftVTableOffset( - isEnabled: true, - template: "custom ${slotOffset}", - labeledTemplate: "custom labeled ${slotOffset} ${label}", - useHexadecimal: true - ) - #expect(module.isEnabled == true) - #expect(module.template == "custom ${slotOffset}") - #expect(module.labeledTemplate == "custom labeled ${slotOffset} ${label}") - #expect(module.useHexadecimal == true) - } - - // MARK: - Transform - - @Test("Transform with standard template (no label)") - func transformStandard() { - let module = Transformer.SwiftVTableOffset() - let result = module.transform(.init(slotOffset: 42, label: nil)) - #expect(result == "VTable Offset: 42") - } - - @Test("Transform with standard labeled template") - func transformStandardLabeled() { - let module = Transformer.SwiftVTableOffset() - let result = module.transform(.init(slotOffset: 42, label: "getter")) - #expect(result == "VTable Offset (getter): 42") - } - - @Test("Transform with compact template (no label)") - func transformCompact() { - let module = Transformer.SwiftVTableOffset(template: Transformer.SwiftVTableOffset.Templates.compact) - let result = module.transform(.init(slotOffset: 10, label: nil)) - #expect(result == "VTable[10]") - } - - @Test("Transform with compact labeled template") - func transformCompactLabeled() { - let module = Transformer.SwiftVTableOffset( - labeledTemplate: Transformer.SwiftVTableOffset.Templates.compactLabeled - ) - let result = module.transform(.init(slotOffset: 10, label: "setter")) - #expect(result == "VTable[10] (setter)") - } - - @Test("Transform with offset-only template") - func transformOffsetOnly() { - let module = Transformer.SwiftVTableOffset(template: Transformer.SwiftVTableOffset.Templates.offsetOnly) - let result = module.transform(.init(slotOffset: 255, label: nil)) - #expect(result == "255") - } - - @Test("Transform with hexadecimal formatting") - func transformHexadecimal() { - let module = Transformer.SwiftVTableOffset(useHexadecimal: true) - let result = module.transform(.init(slotOffset: 255, label: nil)) - #expect(result == "VTable Offset: 0xFF") - } - - @Test("Transform with hexadecimal and label") - func transformHexadecimalLabeled() { - let module = Transformer.SwiftVTableOffset(useHexadecimal: true) - let result = module.transform(.init(slotOffset: 16, label: "getter")) - #expect(result == "VTable Offset (getter): 0x10") - } - - @Test("Transform with zero offset") - func transformZeroOffset() { - let module = Transformer.SwiftVTableOffset() - let result = module.transform(.init(slotOffset: 0, label: nil)) - #expect(result == "VTable Offset: 0") - } - - @Test("Transform uses template when label is nil, labeledTemplate when label is provided") - func transformTemplateSelection() { - let module = Transformer.SwiftVTableOffset( - template: "UNLABELED: ${slotOffset}", - labeledTemplate: "LABELED: ${slotOffset} ${label}" - ) - let unlabeledResult = module.transform(.init(slotOffset: 5, label: nil)) - #expect(unlabeledResult == "UNLABELED: 5") - - let labeledResult = module.transform(.init(slotOffset: 5, label: "test")) - #expect(labeledResult == "LABELED: 5 test") - } - - @Test("Transform with empty label still uses labeledTemplate") - func transformEmptyLabel() { - let module = Transformer.SwiftVTableOffset() - let result = module.transform(.init(slotOffset: 42, label: "")) - // label is non-nil (empty string), so labeledTemplate is used - #expect(result == "VTable Offset (): 42") - } - - // MARK: - Contains - - @Test("Contains token checks both templates") - func containsToken() { - let module = Transformer.SwiftVTableOffset() - #expect(module.contains(.slotOffset) == true) - #expect(module.contains(.label) == true) // label is in labeledTemplate - } - - @Test("Contains returns false for absent token") - func containsAbsentToken() { - let module = Transformer.SwiftVTableOffset( - template: "no tokens here", - labeledTemplate: "still no tokens" - ) - #expect(module.contains(.slotOffset) == false) - #expect(module.contains(.label) == false) - } - - @Test("Contains returns true when token is only in one template") - func containsInOneTemplate() { - let module = Transformer.SwiftVTableOffset( - template: "${slotOffset}", - labeledTemplate: "no offset token" - ) - #expect(module.contains(.slotOffset) == true) - } - - // MARK: - Token - - @Test("Token placeholders", arguments: [ - (Transformer.SwiftVTableOffset.Token.slotOffset, "${slotOffset}"), - (.label, "${label}"), - ] as [(Transformer.SwiftVTableOffset.Token, String)]) - func tokenPlaceholder(token: Transformer.SwiftVTableOffset.Token, expected: String) { - #expect(token.placeholder == expected) - } - - @Test("Token display names", arguments: [ - (Transformer.SwiftVTableOffset.Token.slotOffset, "Slot Offset"), - (.label, "Label"), - ] as [(Transformer.SwiftVTableOffset.Token, String)]) - func tokenDisplayName(token: Transformer.SwiftVTableOffset.Token, expected: String) { - #expect(token.displayName == expected) - } - - @Test("Token allCases count") - func tokenAllCases() { - #expect(Transformer.SwiftVTableOffset.Token.allCases.count == 2) - } - - // MARK: - Templates - - @Test("Templates.all count") - func templatesAllCount() { - #expect(Transformer.SwiftVTableOffset.Templates.all.count == 3) - } - - @Test("Templates.allLabeled count") - func templatesAllLabeledCount() { - #expect(Transformer.SwiftVTableOffset.Templates.allLabeled.count == 3) - } - - @Test("All templates contain slotOffset token") - func templatesContainOffset() { - for (_, template) in Transformer.SwiftVTableOffset.Templates.all { - #expect(template.contains("${slotOffset}")) - } - } - - @Test("All labeled templates contain slotOffset token") - func labeledTemplatesContainOffset() { - for (_, template) in Transformer.SwiftVTableOffset.Templates.allLabeled { - #expect(template.contains("${slotOffset}")) - } - } - - @Test("Standard templates match expected values") - func standardTemplateValues() { - #expect(Transformer.SwiftVTableOffset.Templates.standard == "VTable Offset: ${slotOffset}") - #expect(Transformer.SwiftVTableOffset.Templates.standardLabeled == "VTable Offset (${label}): ${slotOffset}") - #expect(Transformer.SwiftVTableOffset.Templates.compact == "VTable[${slotOffset}]") - #expect(Transformer.SwiftVTableOffset.Templates.compactLabeled == "VTable[${slotOffset}] (${label})") - #expect(Transformer.SwiftVTableOffset.Templates.offsetOnly == "${slotOffset}") - } - - // MARK: - Codable - - @Test("Codable round-trip") - func codable() throws { - let original = Transformer.SwiftVTableOffset( - isEnabled: true, - template: "custom ${slotOffset}", - labeledTemplate: "labeled ${slotOffset} ${label}", - useHexadecimal: true - ) - let data = try JSONEncoder().encode(original) - let decoded = try JSONDecoder().decode(Transformer.SwiftVTableOffset.self, from: data) - #expect(decoded == original) - } - - @Test("Codable decoding with missing fields uses defaults") - func codableDefaults() throws { - let json = "{}".data(using: .utf8)! - let decoded = try JSONDecoder().decode(Transformer.SwiftVTableOffset.self, from: json) - #expect(decoded.isEnabled == false) - #expect(decoded.template == Transformer.SwiftVTableOffset.Templates.standard) - #expect(decoded.labeledTemplate == Transformer.SwiftVTableOffset.Templates.standardLabeled) - #expect(decoded.useHexadecimal == false) - } - - // MARK: - Equatable / Hashable - - @Test("Equatable") - func equatable() { - let moduleA = Transformer.SwiftVTableOffset(isEnabled: true, useHexadecimal: true) - let moduleB = Transformer.SwiftVTableOffset(isEnabled: true, useHexadecimal: true) - let moduleC = Transformer.SwiftVTableOffset(isEnabled: false, useHexadecimal: true) - #expect(moduleA == moduleB) - #expect(moduleA != moduleC) - } -} - -// MARK: - SwiftMemberAddress Tests - -@Suite("Transformer.SwiftMemberAddress") -struct TransformerSwiftMemberAddressTests { - // MARK: - Basic Properties - - @Test("Display name") - func displayName() { - #expect(Transformer.SwiftMemberAddress.displayName == "Swift Member Address Comment") - } - - @Test("Default initialization") - func defaultInit() { - let module = Transformer.SwiftMemberAddress() - #expect(module.isEnabled == false) - #expect(module.template == Transformer.SwiftMemberAddress.Templates.standard) - #expect(module.useHexadecimal == true) - } - - @Test("Custom initialization") - func customInit() { - let module = Transformer.SwiftMemberAddress( - isEnabled: true, - template: "custom ${offset}", - useHexadecimal: false - ) - #expect(module.isEnabled == true) - #expect(module.template == "custom ${offset}") - #expect(module.useHexadecimal == false) - } - - // MARK: - Transform - - @Test("Transform with standard template (hex)") - func transformStandardHex() { - let module = Transformer.SwiftMemberAddress() - let result = module.transform(.init(offset: 0x1234)) - #expect(result == "Address: 0x1234") - } - - @Test("Transform with standard template (decimal)") - func transformStandardDecimal() { - let module = Transformer.SwiftMemberAddress(useHexadecimal: false) - let result = module.transform(.init(offset: 4660)) - #expect(result == "Address: 4660") - } - - @Test("Transform with compact template") - func transformCompact() { - let module = Transformer.SwiftMemberAddress(template: Transformer.SwiftMemberAddress.Templates.compact) - let result = module.transform(.init(offset: 0xFF)) - #expect(result == "0xFF") - } - - @Test("Transform with labeled template") - func transformLabeled() { - let module = Transformer.SwiftMemberAddress(template: Transformer.SwiftMemberAddress.Templates.labeled) - let result = module.transform(.init(offset: 0xAB)) - #expect(result == "addr: 0xAB") - } - - @Test("Transform with zero offset") - func transformZeroOffset() { - let module = Transformer.SwiftMemberAddress() - let result = module.transform(.init(offset: 0)) - #expect(result == "Address: 0x0") - } - - @Test("Transform with large offset (hex)") - func transformLargeOffset() { - let module = Transformer.SwiftMemberAddress() - let result = module.transform(.init(offset: 0xDEADBEEF)) - #expect(result == "Address: 0xDEADBEEF") - } - - @Test("Transform with custom template") - func transformCustomTemplate() { - let module = Transformer.SwiftMemberAddress(template: "offset=${offset}") - let result = module.transform(.init(offset: 256)) - #expect(result == "offset=0x100") - } - - // MARK: - Contains - - @Test("Contains offset token in standard template") - func containsOffsetToken() { - let module = Transformer.SwiftMemberAddress() - #expect(module.contains(.offset) == true) - } - - @Test("Contains returns false for absent token") - func containsAbsentToken() { - let module = Transformer.SwiftMemberAddress(template: "no tokens here") - #expect(module.contains(.offset) == false) - } - - // MARK: - Token - - @Test("Token placeholder") - func tokenPlaceholder() { - #expect(Transformer.SwiftMemberAddress.Token.offset.placeholder == "${offset}") - } - - @Test("Token display name") - func tokenDisplayName() { - #expect(Transformer.SwiftMemberAddress.Token.offset.displayName == "Offset") - } - - @Test("Token allCases count") - func tokenAllCases() { - #expect(Transformer.SwiftMemberAddress.Token.allCases.count == 1) - } - - // MARK: - Templates - - @Test("Templates.all count") - func templatesAllCount() { - #expect(Transformer.SwiftMemberAddress.Templates.all.count == 3) - } - - @Test("All templates contain offset token") - func templatesContainOffset() { - for (_, template) in Transformer.SwiftMemberAddress.Templates.all { - #expect(template.contains("${offset}")) - } - } - - @Test("Standard template values") - func standardTemplateValues() { - #expect(Transformer.SwiftMemberAddress.Templates.standard == "Address: ${offset}") - #expect(Transformer.SwiftMemberAddress.Templates.compact == "${offset}") - #expect(Transformer.SwiftMemberAddress.Templates.labeled == "addr: ${offset}") - } - - // MARK: - Codable - - @Test("Codable round-trip") - func codable() throws { - let original = Transformer.SwiftMemberAddress( - isEnabled: true, - template: "custom ${offset}", - useHexadecimal: false - ) - let data = try JSONEncoder().encode(original) - let decoded = try JSONDecoder().decode(Transformer.SwiftMemberAddress.self, from: data) - #expect(decoded == original) - } - - @Test("Codable decoding with missing fields uses defaults") - func codableDefaults() throws { - let json = "{}".data(using: .utf8)! - let decoded = try JSONDecoder().decode(Transformer.SwiftMemberAddress.self, from: json) - #expect(decoded.isEnabled == false) - #expect(decoded.template == Transformer.SwiftMemberAddress.Templates.standard) - #expect(decoded.useHexadecimal == true) - } - - // MARK: - Equatable / Hashable - - @Test("Equatable") - func equatable() { - let moduleA = Transformer.SwiftMemberAddress(isEnabled: true, useHexadecimal: false) - let moduleB = Transformer.SwiftMemberAddress(isEnabled: true, useHexadecimal: false) - let moduleC = Transformer.SwiftMemberAddress(isEnabled: false, useHexadecimal: false) - #expect(moduleA == moduleB) - #expect(moduleA != moduleC) - } -} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerConfigurationTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerConfigurationTests.swift new file mode 100644 index 00000000..7db99048 --- /dev/null +++ b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerConfigurationTests.swift @@ -0,0 +1,48 @@ +import Foundation +import Testing +import OutputTransformer +import ObjCOutputTransformer +import SwiftOutputTransformer +import RuntimeViewerCore + +/// The concrete transformer modules live with their subject matter — the ObjC +/// ones in MachOObjCSection, the Swift ones in MachOSwiftSection — and are +/// tested there. What is exercised here is the aggregate that only +/// RuntimeViewer needs: it spans both halves and is what gets persisted. +@Suite("Transformer.Configuration") +struct TransformerConfigurationTests { + @Test("Both halves are reachable through one namespace") + func bothHalvesShareOneNamespace() { + // The modules come from two different packages but extend the same + // `Transformer` namespace, so neither needs qualifying. + #expect(Transformer.CType.displayName == "C Type Replacement") + #expect(Transformer.SwiftEnumLayout.displayName == "Enum Layout Comment") + } + + @Test("Persistence round-trips and tolerates missing keys") + func persistenceRoundTrips() throws { + var configuration = Transformer.Configuration() + configuration.swift.swiftFieldOffset.isEnabled = true + configuration.swift.swiftEnumLayout = .explained + configuration.objc.cType = .init(isEnabled: true, replacements: Transformer.CType.Presets.foundation) + + let encoded = try JSONEncoder().encode(configuration) + let decoded = try JSONDecoder().decode(Transformer.Configuration.self, from: encoded) + #expect(decoded == configuration) + + // Settings stored by older versions may lack any key. + let emptyDecoded = try JSONDecoder().decode(Transformer.Configuration.self, from: Data("{}".utf8)) + #expect(emptyDecoded == .default) + } + + @Test("hasEnabledModules covers both sides") + func hasEnabledModulesCoversBothSides() { + var configuration = Transformer.Configuration.default + #expect(!configuration.hasEnabledModules) + configuration.objc.ivarOffset.isEnabled = true + #expect(configuration.hasEnabledModules) + configuration = .default + configuration.swift.swiftTypeLayout.isEnabled = true + #expect(configuration.hasEnabledModules) + } +} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerTests.swift deleted file mode 100644 index e209a688..00000000 --- a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/TransformerTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -import Foundation -import Testing -import Semantic -@testable import RuntimeViewerCore - -/// The Swift-side transformer template engine moved library-side -/// (MachOSwiftSection's `OutputTransformer` module), where its behavior is -/// exhaustively unit tested (`TransformerModuleTests`, -/// `EnumLayoutCommentTemplateTests`). This package still owns the ObjC-side -/// modules (`CType`, `ObjCIvarOffset`) and the aggregate persistence -/// `Configuration`, so these tests cover those plus the re-export seam. -@Suite("Transformer") -struct TransformerTests { - // MARK: - Re-export seam - - @Test("re-exported namespace is visible with the historical spelling") - func reExportedNamespaceIsVisible() { - #expect(!Transformer.Configuration.default.hasEnabledModules) - #expect(Transformer.SwiftEnumLayout.displayName == "Enum Layout Comment") - #expect(!Transformer.SwiftEnumLayout.CaseTemplates.all.isEmpty) - } - - @Test("enabled modules render through the library engine") - func enabledModulesRenderThroughLibraryEngine() { - var fieldOffsetModule = Transformer.SwiftFieldOffset(isEnabled: true) - fieldOffsetModule.template = Transformer.SwiftFieldOffset.Templates.range - #expect(fieldOffsetModule.transform(.init(startOffset: 0, endOffset: 8)) == "0x0 ..< 0x8") - - let compactEnumModule = Transformer.SwiftEnumLayout.compact - let caseInput = Transformer.SwiftEnumLayout.CaseInput( - caseIndex: 1, - caseName: "payload case #1", - declaredName: "value", - isPayloadCase: true, - tagValue: 1, - payloadValue: 0 - ) - #expect(compactEnumModule.transformCase(caseInput) == "[0x01] `value` — payload case, tag 1") - } - - // MARK: - ObjC-side modules (still owned here) - - private func semanticKeywords(_ keywords: [String]) -> SemanticString { - var components: [AtomicComponent] = [] - for (keywordIndex, keyword) in keywords.enumerated() { - if keywordIndex > 0 { - components.append(AtomicComponent(string: " ", type: .standard)) - } - components.append(AtomicComponent(string: keyword, type: .keyword)) - } - return SemanticString(components: components) - } - - @Test("CType replaces the longest pattern first") - func cTypeReplacesLongestPatternFirst() { - var module = Transformer.CType(isEnabled: true) - module.replacements = Transformer.CType.Presets.stdint - // "unsigned long long" must map to uint64_t, not "unsigned" + int64_t. - #expect(module.transform(semanticKeywords(["unsigned", "long", "long"])).string == "uint64_t") - #expect(module.transform(semanticKeywords(["long"])).string == "int64_t") - } - - @Test("CType leaves non-keyword components untouched") - func cTypeLeavesNonKeywordComponentsUntouched() { - var module = Transformer.CType(isEnabled: true) - module.replacements = [.double: "CGFloat"] - let input = SemanticString(components: [ - AtomicComponent(string: "double", type: .keyword), - AtomicComponent(string: " ", type: .standard), - AtomicComponent(string: "value", type: .variable), - ]) - #expect(module.transform(input).string == "CGFloat value") - } - - @Test("ObjCIvarOffset renders its template") - func objcIvarOffsetRendersTemplate() { - let module = Transformer.ObjCIvarOffset(isEnabled: true) - #expect(module.transform(.init(offset: 8)) == "offset: 0x8") - } - - // MARK: - Aggregate persistence - - @Test("configuration persistence round-trips and tolerates missing keys") - func configurationPersistenceRoundTrips() throws { - var configuration = Transformer.Configuration() - configuration.swift.swiftFieldOffset.isEnabled = true - configuration.swift.swiftEnumLayout = .explained - configuration.objc.cType = .init(isEnabled: true, replacements: Transformer.CType.Presets.foundation) - - let encoded = try JSONEncoder().encode(configuration) - let decoded = try JSONDecoder().decode(Transformer.Configuration.self, from: encoded) - #expect(decoded == configuration) - - // Settings stored by older versions may lack any key. - let emptyDecoded = try JSONDecoder().decode(Transformer.Configuration.self, from: Data("{}".utf8)) - #expect(emptyDecoded == .default) - } - - @Test("hasEnabledModules covers both sides") - func hasEnabledModulesCoversBothSides() { - var configuration = Transformer.Configuration.default - #expect(!configuration.hasEnabledModules) - configuration.objc.ivarOffset.isEnabled = true - #expect(configuration.hasEnabledModules) - configuration = .default - configuration.swift.swiftTypeLayout.isEnabled = true - #expect(configuration.hasEnabledModules) - } -} From 55fc3a01efee91b12f4488dcadaeef9c4e3b30de Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 10 Aug 2026 16:41:42 +0800 Subject: [PATCH 2/4] chore(deps): move to the released rendering and indexing libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Points at the tags that carry the extracted layers: MachOObjCSection 0.8.102, MachOSwiftSection 0.15.0, swift-semantic-string 0.3.0. MachOKit goes to 0.52.100 because 0.8.102 requires it — the old 0.51.101 pin only held because the previously referenced MachOObjCSection 0.7.103 asked for far less. Package.resolved is regenerated with everything resolving remotely, so its pins are complete. --- RuntimeViewerCore/Package.resolved | 54 +++++++++++++++--------------- RuntimeViewerCore/Package.swift | 6 ++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/RuntimeViewerCore/Package.resolved b/RuntimeViewerCore/Package.resolved index 58080d97..a443eda6 100644 --- a/RuntimeViewerCore/Package.resolved +++ b/RuntimeViewerCore/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5c3d5d7723e43596fdf07100a113291c3931f7539a940f5005b0ffba2fc2687d", + "originHash" : "ad07950f3a36cd48081d9113d0221bfe898ef384dfe68490dc4fb00453a8921e", "pins" : [ { "identity" : "associatedobject", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Mx-Iris/FrameworkToolbox", "state" : { - "revision" : "6ee952112ea50f21194a56f805ae158e5de1bdb8", - "version" : "0.7.5" + "revision" : "3be067f4280e0764cd4b6cd3ed623698ac981114", + "version" : "0.9.0" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/MachInjector", "state" : { - "revision" : "e558c427e13f3b7abf97fa4b46e96d65413ff7c4", - "version" : "0.4.2" + "revision" : "c65d9c26af12c6d90e114eba6896eb1cf56dd64f", + "version" : "0.4.3" } }, { @@ -60,8 +60,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/MachOKit", "state" : { - "revision" : "4374bafbe4c628c05e068382a12467e7971b29ac", - "version" : "0.51.101" + "revision" : "5223d0958aa5aa810e8f77b6c6763f019254a322", + "version" : "0.52.100" + } + }, + { + "identity" : "machokitextensions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MxIris-Reverse-Engineering/MachOKitExtensions", + "state" : { + "revision" : "fde952e9b9af76e32e179f37098b6d259102769e", + "version" : "0.1.0" } }, { @@ -69,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/MachOObjCSection", "state" : { - "revision" : "63e4b8a61d54fe90bb30e87c0c93f6a8b9c181f9", - "version" : "0.7.103" + "revision" : "415430845bcafd2309cba7c822e260a3cb811523", + "version" : "0.8.102" } }, { @@ -78,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection", "state" : { - "revision" : "3396cfd7332d40edb6aa3b0600758916701a8be4", - "version" : "0.14.1" + "revision" : "a440905b64494771cb67fa4bdf1ce10e688f9fda", + "version" : "0.15.0" } }, { @@ -231,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Mx-Iris/swift-helper-service", "state" : { - "revision" : "f0b834f47f876f41e39a691fb177ff76d3910c49", - "version" : "0.1.4" + "revision" : "454ad61ada4a25c43ed74fc8551684fd92e8f8ae", + "version" : "0.2.0" } }, { @@ -244,15 +253,6 @@ "version" : "0.6.0" } }, - { - "identity" : "swift-macro-toolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-macro-toolkit", - "state" : { - "revision" : "d6ed555bb83c9f21a292e9769952e8f52610a6e2", - "version" : "0.9.0" - } - }, { "identity" : "swift-memberwise-init-macro", "kind" : "remoteSourceControl", @@ -294,14 +294,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/swift-semantic-string", "state" : { - "revision" : "daea78f0c94305d69b3c59af337ea69eda53603b", - "version" : "0.2.0" + "revision" : "1021589b85fc647435b383307aa6aea2af1079ce", + "version" : "0.3.0" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "version" : "603.0.2" @@ -312,8 +312,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-macOS-Library-Forks/SwiftyXPC", "state" : { - "revision" : "8c0a1738ba68540e6e911eb409fca8a4603d3efd", - "version" : "0.5.103" + "revision" : "4f67b80d2d21b0968ec6c6c45aa01389f1f1ed05", + "version" : "0.5.104" } }, { diff --git a/RuntimeViewerCore/Package.swift b/RuntimeViewerCore/Package.swift index 505c923c..c0362197 100644 --- a/RuntimeViewerCore/Package.swift +++ b/RuntimeViewerCore/Package.swift @@ -84,7 +84,7 @@ let package = Package( ), remote: .package( url: "https://github.com/MxIris-Reverse-Engineering/MachOKit", - exact: "0.51.101", + exact: "0.52.100", ), ), .package( @@ -94,7 +94,7 @@ let package = Package( ), remote: .package( url: "https://github.com/MxIris-Reverse-Engineering/MachOObjCSection", - exact: "0.7.103", + exact: "0.8.102", ), ), .package( @@ -104,7 +104,7 @@ let package = Package( ), remote: .package( url: "https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection", - exact: "0.14.1", + exact: "0.15.0", ), ), .package( From 42267d407278d7d41f7c9fd377db050b5050555e Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Mon, 10 Aug 2026 17:40:26 +0800 Subject: [PATCH 3/4] chore(deps): repin MachOSwiftSection 0.15.0 The 0.15.0 tag was recut to include the release commit (changelog plus the BundledVersion bump its release workflow requires), so the version now resolves to a different revision than the one recorded here. --- RuntimeViewerCore/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RuntimeViewerCore/Package.resolved b/RuntimeViewerCore/Package.resolved index a443eda6..def6b7ed 100644 --- a/RuntimeViewerCore/Package.resolved +++ b/RuntimeViewerCore/Package.resolved @@ -87,7 +87,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MxIris-Reverse-Engineering/MachOSwiftSection", "state" : { - "revision" : "a440905b64494771cb67fa4bdf1ce10e688f9fda", + "revision" : "aa38ff50bcf0ba89173da4ea0a8417c8bc478aa4", "version" : "0.15.0" } }, From 33c1f8a02f99550671e1edeccda66d230b1b5101 Mon Sep 17 00:00:00 2001 From: Mx-Iris Date: Tue, 11 Aug 2026 08:28:59 +0800 Subject: [PATCH 4/4] refactor: rebuild the ObjC relationship tables from the indexer's events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MachOObjCSection 0003 removed the inheritance and protocol-adoption reverse tables from ObjCIndexing: the library parses and broadcasts what it finds, and no longer remembers how classes relate. RuntimeObjCRelationshipIndex is where those broadcasts become tables again, one per image, fed by the event stream during prepare(). The hazard here is not the three deleted APIs — the compiler catches those. It is that eventHandler quietly changed from an observer into the sole channel carrying relationship data, and RuntimeObjCSection installed one only when a progress stream was supplied. Six of the seven section-creation call sites pass no progress stream, background indexing among them, so keeping that condition would have emptied the Relationships pane for nearly every image while still compiling and still passing most tests. The handler is now unconditional; only the forwarding of .progress events depends on there being a stream. Equivalence is pinned by a baseline captured through relationships(for:) before the pin moved, since that is the only vantage point existing on both sides of the change. Output matches it verbatim: 307 NSObject subclasses, 9 NSCoding and 68 NSCopying conformers, identical in order and in imagePath. Three properties the match rests on get their own unit tests — inline and category adoptions sharing one table, arrival-order replay, and dedup keyed on all three fields so a class reaching one protocol twice with differing isSwiftStable stays twice. RuntimeObjCSectionFactory's aggregate indexer goes away rather than moving: it was only ever written to, since RuntimeRelationshipsResolver fans out across images itself. That also settles what to do about f41648a on feature/node-store-adoption, which added removeSubIndexer to plug the leak the aggregate caused — with no aggregate there is nothing to detach. Tables build on first query instead of behind a seal-the-index call. Requiring one would have reintroduced exactly the failure this change exists to remove: forget it, or miss it because prepare() threw, and every query silently returns nothing. Note: this cannot resolve against the remote pins yet. MachOSwiftSection 0.15.0 holds MachOObjCSection at exact 0.8.102, so building and testing this branch needs USING_LOCAL_DEPENDENCIES=1 until that pin moves to 0.8.103. --- ...lationship-index-returns-to-application.md | 506 ++++++++++++++++++ Documentations/Evolutions/README.md | 1 + Documentations/README.md | 2 +- RuntimeViewerCore/Package.swift | 2 +- .../Core/RuntimeObjCSection.swift | 89 +-- .../RuntimeObjCRelationshipIndex.swift | 182 +++++++ .../RuntimeRelationshipsResolver.swift | 6 +- ...elationshipsEquivalenceSnapshotTests.swift | 141 +++++ ...ationshipsWithoutProgressStreamTests.swift | 78 +++ .../RuntimeObjCRelationshipIndexTests.swift | 181 +++++++ .../Snapshots/relationships-baseline.txt | 390 ++++++++++++++ 11 files changed, 1537 insertions(+), 41 deletions(-) create mode 100644 Documentations/Evolutions/0007-objc-relationship-index-returns-to-application.md create mode 100644 RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeObjCRelationshipIndex.swift create mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsEquivalenceSnapshotTests.swift create mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsWithoutProgressStreamTests.swift create mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RuntimeObjCRelationshipIndexTests.swift create mode 100644 RuntimeViewerCore/Tests/RuntimeViewerCoreTests/Snapshots/relationships-baseline.txt diff --git a/Documentations/Evolutions/0007-objc-relationship-index-returns-to-application.md b/Documentations/Evolutions/0007-objc-relationship-index-returns-to-application.md new file mode 100644 index 00000000..38c31c93 --- /dev/null +++ b/Documentations/Evolutions/0007-objc-relationship-index-returns-to-application.md @@ -0,0 +1,506 @@ +# 0007 - ObjC 关系索引归还应用侧 + +- **状态**: In Progress(代码完成、测试通过;合入 main 阻塞于 MachOSwiftSection 的 pin,见「落地记录」) +- **作者**: JH +- **日期**: 2026-08-10 +- **关联提案**: MachOObjCSection [0003 - ObjC 关系反向表移出索引层,归还应用](https://github.com/MxIris-Reverse-Engineering/MachOObjCSection/blob/main/Documentations/Evolutions/0003-objc-relationship-tables-return-to-application.md) + —— **本提案是它的下游适配**,库侧的设计论证不在此重复 +- **实现分支 / PR**: 待定(依赖 MachOObjCSection `0.8.103` 发版) + +## 摘要 + +MachOObjCSection 0003 把 ObjC 的继承 / 协议遵守反向表移出 `ObjCIndexing`,库改为只通过 +`ObjCIndexingEvent` 广播发现、不再自己存表。本提案是 RuntimeViewer 侧的对应改动: + +1. 新增 `RuntimeObjCRelationshipIndex`,在 `prepare()` 期间由事件流填充,取代原先直接查库的两个方法; +2. **把事件 handler 的安装与进度转发解耦** —— 这是本提案真正的风险点,不改会让绝大多数 image + 的关系数据静默变空; +3. 删除 `RuntimeObjCSectionFactory.objcInterfaceIndexer` 这个只写不读的聚合。 + +对用户没有任何可见变化:Relationships 标签页的内容与顺序保持逐条等价。 + +## 动机 + +### 一、库不再存表,应用必须自己存 + +0003 之后 `ObjCInterfaceIndexer` 不再有 `subclasses(of:)` / `conformingClasses(toProtocol:)`, +`ObjCClassReference` 类型也一并移除。`RuntimeRelationshipsResolver.swift:85` 与 `:118` 附近对 +ObjC 关系的查询失去后端,必须在应用侧重建。 + +方向与取舍(为什么是事件流而不是重新走查、为什么 `isSwiftStable` 进 payload)已在 0003 +论证完毕,本提案直接采用其结论,不重复。 + +### 二、事件 handler 现在是**条件安装**的,照搬会静默丢掉几乎全部关系数据 + +这是本提案存在的主要理由,也是唯一一处不改就出错的地方。 + +`RuntimeObjCSection.makeEventHandler(forwardingTo:)` 目前在没有进度流时直接返回 `nil`: + +```swift +private static func makeEventHandler( + forwardingTo progressContinuation: LoadingEventContinuation? +) -> (@Sendable (ObjCIndexingEvent) -> Void)? { + guard let progressContinuation else { return nil } // ← 没有进度流就完全不装 handler + ... +} +``` + +今天这没有任何后果 —— 库无条件建表,handler 只负责转发进度。0003 之后,**没装 handler 就等于 +放弃关系数据**。 + +而实际的调用点分布是压倒性的:创建 ObjC section 的 7 处调用里,只有 1 处传了 +`progressContinuation`。 + +| 调用点 | 传 `progressContinuation`? | +|---|---| +| `RuntimeEngine.swift:883` | ✅ 唯一一处 | +| `RuntimeEngine+BackgroundIndexing.swift:62` | ❌ | +| `RuntimeEngine.swift:534` | ❌ | +| `RuntimeEngine.swift:810` | ❌ | +| `RuntimeEngine.swift:572 / 574 / 626 / 628`(按名字查的四处) | ❌ | + +其中 `RuntimeEngine+BackgroundIndexing.swift:62` 是**后台索引**的路径 —— 绝大多数 image 是被 +它带进来的。所以直接照搬现有 handler 的结果不是「某个冷门路径缺数据」,而是「只有用户手动触发 +那一次带进度条的加载有关系数据,其余全空」,且**完全静默**:不报错,Relationships 标签页看起来 +就像"这个类确实没有子类"。 + +结论:handler 的安装必须与 progressContinuation 解绑。 + +### 三、工厂聚合是只写不读的死重,一并删除 + +`RuntimeObjCSectionFactory` 持有一个聚合 `ObjCInterfaceIndexer` 并在每次建 section 时 +`addSubIndexer` 注册,但全仓库对它的引用只有四处 —— 声明(`RuntimeObjCSection.swift:379`)、 +构造(`:383`)、两处 `addSubIndexer`(`:411`、`:432`),**没有任何一处读它**。真正的跨 image +合并是 `RuntimeRelationshipsResolver` 在调用点自己做的:遍历已索引 image 路径,逐个取该 image +自己的 indexer 查表。 + +0003 删掉库侧的 `addSubIndexer(_:)` 后这个聚合无法再构造,正好一并删除。附带收益是那个 +「拿 `MachOImage.current()` 当占位」的构造 hack(`:381-384`)随之消失。 + +**Swift 侧不受影响**:`RuntimeSwiftSectionFactory.indexer` 是真被读的(`GenericSpecializer` / +`IndexerConformanceProvider` 走 `factory.indexer.upstream`,泛型特化还查 `allAllTypeDefinitions`), +是承重结构,本提案不碰。 + +### 四、它解掉一个已埋下的跨分支冲突 + +feeder 分支 `feature/node-store-adoption`(同时在 `next` 上)的 commit `f41648a` 修的是聚合造成的 +内存泄漏 —— `addSubIndexer` 没有逆操作,聚合活得和 engine 一样长,浏览过的每个 image 的索引状态 +永远释放不掉。它的修法是给本地的 `RuntimeObjCInterfaceIndexer` 加 `removeSubIndexer(_:)`,并在 +`removeSection` / `removeAllSections` 里先摘再删。 + +但 main 上的 `889f1bd` 已经把 `RuntimeObjCInterfaceIndexer` 整个删了,顶替它的库类型没有 +`removeSubIndexer`。两条线迟早要撞。 + +本提案把聚合整个删掉,泄漏源消失,逆操作不再需要 —— 冲突以「删除」的方式解决,而不是回头给库补 +一个即将过时的 API。 + +> **合并提示**:`feature/node-store-adoption` 与 main 合并时,`RuntimeObjCSectionFactory` +> 的 `removeSection` / `removeAllSections` 里那两行 `objcInterfaceIndexer.removeSubIndexer(...)` +> 应当**随聚合一并删除**,不要试图移植到库类型上。Swift 侧那一半(`RuntimeSwiftSectionFactory`) +> 不受影响,正常保留。 + +## 提议方案 + +### 一、`RuntimeObjCRelationshipIndex`:应用侧的关系索引 + +新增一个 per-image 的关系索引,职责单一:接收 `ObjCIndexingEvent` 的三个关系事件,攒成两张表, +并提供原先由库提供的两个查询。 + +- 位置:`RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/` + (放在 Relationships 而非 Indexing —— 它服务的是关系解析,不是接口解析) +- 与 `ObjCInterfaceIndexer` 一一对应:每个 `RuntimeObjCSection` 持有自己那一份 +- 同时新增 `RuntimeObjCClassReference`,字段与原库类型逐字相同(`className` / `imagePath` / + `isSwiftStable`),因为 `RuntimeRelationshipsResolver.materializeRelationshipReference(_:)` + 已经按这三个字段消费 + +### 二、handler 解耦:永远安装,进度转发才看 continuation + +`makeEventHandler` 改为**总是**返回 handler。handler 内部分流:关系事件喂给 +`RuntimeObjCRelationshipIndex`,`.progress` 事件仅在 continuation 存在时转发。 + +### 三、删除工厂聚合 + +删除 `RuntimeObjCSectionFactory.objcInterfaceIndexer` 的声明、构造与两处 `addSubIndexer` 调用。 + +### 非目标 + +- **不改 Relationships 的任何用户可见行为。** 顺序、内容、去重规则逐条等价。 +- **不改 category 的 `imagePath` 语义。** 库侧 0003 已说明:category 产生的引用里 `className` + 与 `imagePath` 不属于同一个 image(`imagePath` 是 category 所在 image)。这是既有行为, + 原样保留;想改属行为变更,另开提案。 +- **不动 Swift 侧。** `RuntimeSwiftInterfaceIndexer` 及其三张表保持现状。 +- **不改 `RuntimeRelationshipsResolver` 的跨 image 扇出结构。** 它在调用点遍历 image 的做法不变, + 只把查询对象从 `objcSection.objcIndexer` 换成 `objcSection.objcRelationshipIndex`。 +- **不引入新的关系查询能力**(如反向的「某类的所有超类」)。等价迁移,不夹带。 +- **不"顺手"修同类双 `isSwiftStable` 的重复条目。** 见「详细设计」等价性细节第 3 条 —— + 那是现有行为,修它属于行为变更,会让等价性测试失败。 + +## 详细设计 + +### `RuntimeObjCRelationshipIndex` + +```swift +/// An Objective-C class or bridged Swift class discovered to subclass another +/// class or to adopt a protocol. +/// +/// `isSwiftStable` carries the structural signal (`class_t`'s +/// `FAST_IS_SWIFT_STABLE` bit) that lets `RuntimeRelationshipsResolver` decide +/// whether to materialize the reference as a Swift `RuntimeObject` or an +/// Objective-C one. The library reports the bit; the domain decision is made here. +struct RuntimeObjCClassReference: Hashable, Sendable { + let className: String + let imagePath: String + let isSwiftStable: Bool +} + +/// Per-image Objective-C relationship index, populated from the +/// `ObjCIndexingEvent` stream that `ObjCInterfaceIndexer.prepare()` emits. +/// +/// MachOObjCSection 0003 removed the library-side reverse tables; the library +/// now only broadcasts what it discovers. This type is where those broadcasts +/// become the tables that back the Inspector's Relationships pane. +/// +/// An instance is handed to the indexer as its event handler *before* +/// `prepare()` runs and accumulates throughout the walk. The tables are built +/// on first query; `prewarm()` merely pays that cost up front and is never +/// required for correctness. +final class RuntimeObjCRelationshipIndex: @unchecked Sendable { + func record(_ event: ObjCIndexingEvent) + + /// Optional: build the tables now instead of on first query. Never required + /// — skipping it, or failing to reach it because `prepare()` threw, costs + /// nothing but the deferred build. + func prewarm() + + func subclasses(of className: String) -> [RuntimeObjCClassReference] + func conformingClasses(toProtocol protocolName: String) -> [RuntimeObjCClassReference] +} +``` + +`RuntimeObjCClassReference` 不带 `Codable`,也不是 `public`:全仓库对原库类型的引用只有两处 +(`RuntimeRelationshipsResolver` 的一处 doc comment 和一个 `private` 方法签名),关系引用 +从不跨进程 —— 越过 XPC 边界的是已经物化好的 `RuntimeObject`。搬迁正是收紧这类多余 +conformance 的窗口。 + +**建表时机:惰性,不是外部契约。** 查询发现尚未建表就先建。**不采用「`prepare()` 之后必须调用 +`freeze()`」的写法** —— 那会新造两条静默失效路径,而它们恰恰是本提案动机第二条要消灭的那一类: +忘了调 → 查询返回空、不报错;`prepare()` 是 `async throws`,抛错就跳过了调用点(除非包 `defer`), +该 section 的索引从此永远空着。惰性构建把这两条路径都变成不可能,代价只是查询路径上一次布尔检查。 + +**累积策略:单一队列,一次 `append`。** `record(_:)` 在锁下向**同一个**待处理队列追加,建表时 +按队列顺序回放。单队列不只是为了锁内工作量小,更是行为等价的前提 —— 见下节。 + +> 锁保留。当前库侧走查是单线程的(`ObjCInterfaceIndexer` 内无 `TaskGroup` / +> `concurrentPerform` / `DispatchQueue` / `async let`),所以竞争接近于零、锁的成本可忽略; +> 但**这是当前实现的事实,不是库的承诺** —— 0003 的契约四明确不承诺 `eventHandler` 的执行 +> 上下文,handler 声明为 `@Sendable` 正是为 0002 之后按 image / section 并行走查留的余地。 +> 因此锁是必需的,不能以"反正是单线程"为由去掉。 + +### 与库侧原实现的三处等价性细节 + +「顺序、内容、去重规则逐条等价」不是一句口号,落到实现上有三条必须照做: + +1. **两个 conformance 事件写进同一张表。** 库侧 `indexClass` 的 inline 采纳与 `indexCategory` + 的 category 采纳写的是**同一个** `_conformingClassesByProtocolName`,一次 + `conformingClasses(toProtocol:)` 同时返回两者。不得按事件 case 分成两张表。 +2. **顺序是「inline 整体在前,category 整体在后」,不是交错。** 库侧 `prepare()` 先走完整个 + class 列表(`:272`)再走 category 列表(`:350`),所以同一协议的 conformer 里 inline 采纳 + 的类整体排在 category 贡献的之前。按到达顺序回放单一队列天然保持这个顺序;一旦按 case 分组, + 合并时无论怎么拼都不等价。这条顺序由 0003 的**契约五**承诺(同一 image、同一库版本, + 两次 `prepare()` 事件序列完全相同;class 阶段整体先于 category 阶段),改动它算库的破坏性 + 变更 —— 因此本提案可以放心依赖它,而不是依赖一个碰巧成立的实现细节。 +3. **去重按全部三个字段,保留首次插入位置 —— 不要"顺手"改成按 `className` 去重。** + 同一个类可能同时通过 inline 与 category 采纳同一协议,而两条路径的 `isSwiftStable` 来源不同: + inline 读类自己 `class_t` 上的 flag,category 读 `objcCategory.class(in: machO)` 跨 image + 解析的结果,**解析失败兜底 `false`**。因此在「解析失败 + 该类是 Swift 类 + 同时 inline 与 + category 采纳同一协议」的组合下,会出现 `className` 相同而 `isSwiftStable` 不同的两条引用, + `OrderedSet` 不去重,Relationships 里该类出现两次、一次标 Swift 一次标 ObjC。 + + 场景罕见(category 的 target 通常在别的 image,那种情况下 inline 路径根本不会在本 indexer + 产生条目),但它是**现有行为**。按 `className` 去重、或在冲突时合并 `isSwiftStable`, + 看起来像修 bug,实际是行为变更,会让等价性测试失败。真要修另开提案。 + +### handler 安装 + +```swift +private static func makeEventHandler( + relationshipIndex: RuntimeObjCRelationshipIndex, + forwardingTo progressContinuation: LoadingEventContinuation? +) -> @Sendable (ObjCIndexingEvent) -> Void { + { event in + // Relationship events always build the index — this must NOT depend on + // whether a progress stream exists. Only one of the seven section-creation + // call sites passes a continuation; gating the handler on it would leave + // every background-indexed image without relationship data, silently. + relationshipIndex.record(event) + + guard let progressContinuation, + case .progress(let phase, let itemDescription, let currentCount, let totalCount) = event + else { return } + + progressContinuation.yield(...) + } +} +``` + +两个 `init` 都改为:先构造 `relationshipIndex`,传入 handler,再 `await objcIndexer.prepare()`。 +**`prepare()` 之后无需任何收尾调用** —— 建表是惰性的,`prepare()` 抛错也不会让索引卡在半成品状态。 + +### 调用点改动 + +`RuntimeRelationshipsResolver` 里两处,查询对象替换,其余结构不动: + +```swift +// 改前 +for reference in objcSection.objcIndexer.subclasses(of: objcKey) { ... } +for reference in objcSection.objcIndexer.conformingClasses(toProtocol: object.name) { ... } + +// 改后 +for reference in objcSection.objcRelationshipIndex.subclasses(of: objcKey) { ... } +for reference in objcSection.objcRelationshipIndex.conformingClasses(toProtocol: object.name) { ... } +``` + +`materializeRelationshipReference(_:)` 的参数类型从 `ObjCClassReference` 改为 +`RuntimeObjCClassReference`,函数体不变(三个字段同名同义)。 + +## 影响(App 型) + +- **用户可见变化**:无。Relationships 标签页的内容、顺序、去重行为逐条等价,这是本提案的硬性约束 + (见「验收标准」)。 +- **可发现性**:无新增入口、无设置项、无菜单变化。 +- **数据与配置兼容**:不涉及持久化格式,无迁移。 +- **平台与最低版本**:不变。 +- **发布影响**:需要 MachOObjCSection `0.8.103`。适配代码与 `exact: "0.8.102"` → `"0.8.103"` + 的 pin bump **同批次**提交;在库发版前本改动不能进 main。 +- **内存**:预期小幅下降 —— 聚合删除后,`subIndexers` 不再为 engine 生命周期钉住每个 image 的 + 索引状态(这正是 `f41648a` 想解决的问题)。具体数值不作承诺,落地后按需实测。 + +## 验收标准 + +1. Relationships 标签页对同一目标的结果,与改动前**逐条等价**(内容、顺序均一致)。 +2. **不带 `progressContinuation` 创建的 section 同样能查到关系数据** —— 对应动机第二条。 +3. 后台索引带进来的 image,其关系查询与手动加载的 image 无差别。 +4. `RuntimeObjCSectionFactory` 中不再存在聚合 indexer。 +5. `RuntimeViewerCore` 与 `RuntimeViewerPackages` 编译通过,测试退出码为 0。 + +## 测试策略 + +### 基线快照走公开 API,仍须在升级依赖之前采集 + +**落地时的修正(2026-08-11)**:本节原先假定必须比对内部的 `objcIndexer.subclasses(of:)`, +因而断言「没有任何一次运行能同时拿到新旧两个序列」。核实后发现更干净的路子 —— +`RuntimeEngine.relationships(for:)` 这个**公开 API 在重构前后都存在且签名不变**, +现有的 `RelationshipsTests` 就是走它。基线因此可以直接采集用户可见的输出, +既不依赖任何将被删除的内部接口,也正好锁住真正要保住的东西。 + +采集仍须在升级 pin 之前完成 —— 升级后旧实现就没了,拿不到对照基线。 + +**同时修正一处对影响面的判断**:`RuntimeRelationshipsResolver` 在返回前把 +`subclasses` 与 `conformingTypes` **都按 `displayName` 排过序** +(`RuntimeRelationshipsResolver.swift:128-133`),所以库侧的走查顺序 / 事件发射顺序 +**到不了用户界面** —— Relationships 列表一直是字母序。 + +由此,「详细设计」等价性细节的三条中: +- 第 1 条(两个 conformance 事件写进同一张表)**仍然承重** —— 它关乎结果集的*内容*,分表会丢结果; +- 第 3 条(按三个字段去重)**仍然承重** —— 同样关乎内容,会影响某个类是否出现两次; +- 第 2 条(inline 整体在前)**只关乎内部表的保真度,不影响用户可见输出**。仍然照做(单队列回放 + 是最自然的实现,没有额外成本),但它不再是本提案的正确性支点。 + +0003 的契约五依旧成立,只是支撑它的是库侧自己的两条理由(顺序承诺的载体随方法一起被删、 +不给承诺会让 0003 否决「重新走查」的论证塌掉一半),而**不是**本提案原先声称的 +「并行化会静默改变 Relationships 顺序」—— 那条声称是错的,已向库侧更正。 + +### 快照覆盖 + +- `NSObject` 的子类集(跨 libobjc + Foundation,数量大) +- `NSCoding`、`NSCopying` 的遵守者集(inline 采纳与 category 采纳并存,验第 1 条等价性) + +### 三条测试 + +- **回归测试(必须先红后绿)**:构造一个**不传 `progressContinuation`** 的 + `RuntimeObjCSection`,断言其关系查询非空。这条直接钉住动机第二条那个静默失效。 + 它只有在依赖升级之后、handler 解耦之前才会红,因此落地步骤把它排在那个位置 —— **必须实际 + 观察到红色**,否则它不构成回归防线。 +- **等价性测试**:比对上述基线快照,序列完全一致(含顺序与重复项)。 +- **category 路径测试**:用测试 bundle 自带的 fixture,而不是去 Apple 的二进制里翻。 + 同模块内为 Swift 类写 `@objc extension` **通常不产出** `__objc_catlist` 条目(编译器掌控 + 自己模块里的类,会把成员直接并进 method list);可靠产出 category 的是**跨模块** extension: + + ```swift + // The explicit runtime name is REQUIRED, not stylistic: an `@objc protocol` + // without one is exposed to the ObjC runtime under its mangled spelling + // (`_TtP_`), which is what the indexer reads out of the binary + // — so the event payload would carry a name the test does not recognize. + @objc(ObjCIndexingFixtureProtocol) + protocol ObjCIndexingFixtureProtocol { func fixtureMethod() } + + @objc extension NSString: ObjCIndexingFixtureProtocol { + func fixtureMethod() {} + } + ``` + + 断言两条(形状取自库侧 0003 落地时的实测版本): + + ```swift + #expect(conformance.imagePath == fixturePath) // 测试 bundle,不是 Foundation + #expect(!indexer.classNames.contains("NSString")) // 目标类确实不在本 image + ``` + + 这就把「category 的 `imagePath` 是 category 所在 image、不是 target class 所在 image」 + 变成了可执行断言 —— 在系统 image 里碰运气拿不到这个。 + + 它钉不住 `isSwiftStable == true`(`NSString` 不是 Swift 类),也无法区分「解析成功且目标 + 非 Swift」与「解析失败兜底」—— 两者 payload 同形。要同时满足「必然产 category」和「target + 是 Swift 类」需要两个模块,成本另计,落地时视需要再定。 + +- 测试位置 `RuntimeViewerCore/Tests/RuntimeViewerCoreTests/`。落地时确认该 target 现有的 + image 加载设施可复用。 +- **判定只认 `swift test` 退出码**,不看 xcsift 摘要(见 AGENTS.md)。 + +## 风险与假设 + +- **假设**:库侧三个关系事件覆盖了原两张表的全部写入点。0003 的前期调研已确认「三者一一对应, + 没有第四条写入路径」。若落地时发现遗漏,等价性测试会立刻失败。 +- **风险**:库侧 `prepare()` 无幂等保护,重复调用会重放事件流,而本提案的累积队列不会自行去重。 + RuntimeViewer 每个 section 只在 `init` 里 prepare 一次且按 imagePath 缓存,当前不受影响; + 建表时的 `OrderedSet` 也会吸收完全相同的重复条目。仍需在类文档中写明这一前提。 +- **已关闭的风险 —— 并行化打破顺序等价**:本提案的逐条等价建立在「事件按走查顺序到达」之上, + 而 0003 初稿的契约四只说不承诺执行上下文,连带把顺序也放掉了。经反馈,0003 已增补**契约五**: + 同一 image、同一库版本,两次 `prepare()` 产生完全相同的事件序列;class 阶段全部事件先于 + category 阶段;**改变该顺序算破坏性变更,须走提案**。因此并行化不再会静默改变 Relationships + 的顺序,本提案的等价性目标与测试策略均无需调整。 +- **残留风险**:库侧若并行化走查(契约四允许),`record(_:)` 的锁会从"零竞争"变成真正的竞争点。 + 单队列 append 仍然正确、顺序仍由契约五保证,只是那时值得重新评估累积策略的性能。 + 不在本提案范围内。 + +## 替代方案考量 + +### 保留 handler 的条件安装,另外补一条无条件的关系 handler + +**为什么否**:两个 handler 意味着两条事件订阅路径,而库只接受一个 `eventHandler`。真要做只能在 +外层再包一层分发,比直接在单一 handler 内分流更绕,且把「关系必须无条件收集」这个契约藏得更深。 + +### 关系索引挂在工厂上,做成跨 image 的单一大表 + +**为什么否**:那等于把库刚删掉的聚合原样搬到应用侧。`RuntimeRelationshipsResolver` 现有的 +「遍历 image、逐个查」结构已经能工作,且天然随 section 的移除而释放;单一大表会重新引入 +`f41648a` 修过的那类生命周期问题。 + +### 等 0002(`MachOFile` 泛型化)一起做 + +**为什么否**:0002 在库侧尚是 Draft,且 0003 明确排在它之前落地。把下游适配压到 0002 之后, +意味着 main 长期停在旧 pin 上,与「库发版后同批次落地」的交付约定冲突。 + +## 落地步骤 + +每一步的验收都是单一确定的状态,按编号顺序执行即可。 + +0. **采集基线快照** —— 在动任何依赖之前,用现有实现 dump 目标序列落盘。见「测试策略」, + 这是一次性窗口。验收:快照文件存在且非空。 +1. **升级依赖**:MachOObjCSection pin `0.8.102` → `0.8.103`。 + 验收:编译**失败**(两个查询方法与 `ObjCClassReference` 已不存在),属预期。 +2. **新增 `RuntimeObjCClassReference` 与 `RuntimeObjCRelationshipIndex`**,在 + `RuntimeObjCSection` 的两个 `init` 中构造并作为 handler 传入;handler **暂时保持**现有的 + 条件安装(这是下一步要观察的失效条件)。同时切换 `RuntimeRelationshipsResolver` 的两处查询 + 与 `materializeRelationshipReference(_:)` 的参数类型。 + 验收:编译通过。 +3. **写回归测试并跑出红色** —— 不带 `progressContinuation` 创建的 section,断言其关系查询非空。 + 验收:该测试**失败**。红色本身就是这一步的验收标准;跑不出红说明测试没打中,先修测试。 +4. **handler 解耦**:`makeEventHandler` 改为总是返回 handler,`.progress` 的转发才看 + continuation。验收:第 3 步的测试转绿。 +5. **补齐等价性测试与 category fixture 测试**,比对第 0 步的快照。 + 验收:全部通过。 +6. **删除工厂聚合**:`objcInterfaceIndexer` 的声明、构造与两处 `addSubIndexer`。 + 验收:编译通过,`swift test` 退出码为 0。 +7. **写契约文档**:`RuntimeObjCRelationshipIndex` 的类文档写明「关系数据只经 `eventHandler` + 进入,未安装 handler 即放弃关系数据」以及事件重放的前提。**不允许留到最后补** —— 这是 + 本改动唯一一处靠文档而非类型系统兜住的地方。 +8. **收尾**:更新本篇状态;判断是否值得在 `Documentations/Internal/` 单独成篇 + (判据是它是否包含代码本身看不出来的决策,目前倾向值得 —— handler 必须无条件安装这一点 + 从签名上完全看不出来)。 + +## 落地记录(2026-08-11) + +代码已完成,**尚未提交,且暂时无法用远端 pin 构建**,见下方「阻塞」。 + +### 红绿两态都观察到了 + +- **红**:`RelationshipsWithoutProgressStreamTests` 在 handler 仍为条件安装时失败, + 失败形态正是预期的静默空集 —— `relationships.subclasses → []`,无任何报错。 + 同一时刻等价性快照也红了。 +- **绿**:handler 改为无条件安装后,两者同时转绿。 + +### 等价性:逐字一致 + +基线快照(升级依赖前用旧实现采集,`Snapshots/relationships-baseline.txt`)与迁移后输出 +**完全相同**:`NSObject` 307 个子类、`NSCoding` 9 个遵守者、`NSCopying` 68 个遵守者, +顺序与 `imagePath` 均一致。 + +测试:`RuntimeViewerCore` 全量 392 tests / 78 suites 通过,`swift test` 退出码 0 +(不看 xcsift 摘要);`RuntimeViewerPackages` 编译通过。 + +### 与提案的差异 + +1. **聚合删除提前到第 2 步。** 提案排在第 6 步,但 `addSubIndexer` 的调用点在库升级后 + 直接编译不过,不删就无法进入任何可运行状态。不影响 handler 解耦的红绿观察顺序。 +2. **category 测试改为对索引的单元测试。** 提案原计划用测试 bundle 的 + `@objc extension NSString` fixture 走公开 API 断言。实际跑下来 fixture 协议能被索引到, + 但 `NSString` **不会**作为遵守者出现 —— 原因见下条发现。断言层次因此下移到 + `RuntimeObjCRelationshipIndex` 本身(`RuntimeObjCRelationshipIndexTests`,8 个用例), + 用合成事件精确钉住三条等价性 + category 的 `imagePath` 语义;真实二进制的端到端保真 + 由等价性快照负责。 +3. **`freeze()` 的替代实现多了一条路径。** 惰性建表后,若事件在建表**之后**到达, + 不能作废重建(待处理队列已在建表时释放,重建会丢掉先前全部事件),改为直接折进已建好的表。 + 已加测试 `lateEventsStillRegister` 钉住。 + +### 落地中发现的既有缺陷(未修,不属本提案范围) + +**目标类不在本镜像的 category 遵守关系,在物化阶段被静默丢弃。** + +`RuntimeRelationshipsResolver.materializeRelationshipReference(_:)` 按 `reference.imagePath` +去定位类,而 category 记录的是**自己所在镜像**。于是「A 框架给 B 框架的类加 category 并声明 +协议遵守」这种关系,索引里有、界面上没有 —— 物化时在 A 镜像里找不到那个类,返回 `nil` 丢弃。 + +这是既有行为,与本次迁移无关(物化逻辑和 `imagePath` 语义都未改动),等价性快照也证明 +迁移前后完全一致。修它属于行为变更(会让 Relationships 出现此前从未出现过的条目), +应另开提案。 + +### 阻塞:远端 pin 暂时升不上去 + +`MachOSwiftSection` 0.15.0(含其当前 main)把 `MachOObjCSection` 钉在 `exact: "0.8.102"`, +而本改动需要 `0.8.103`。两者都用 `exact:`,因此 RuntimeViewer 无法同时满足: + +``` +'machoswiftsection' 0.15.0 depends on 'machoobjcsection' 0.8.102 +and root depends on 'machoobjcsection' 0.8.103 +``` + +**本分支因此暂时只能用 `USING_LOCAL_DEPENDENCIES=1` 走本地 checkout 构建与测试** +(上述全部验收均在该模式下完成)。合入 main 的前置条件是 MachOSwiftSection 把它的 +`MachOObjCSection` pin 提到 `0.8.103` 或更高并发版。 + +`Package.swift` 已写成目标状态 `exact: "0.8.103"`,等上游发版后即可直接远端构建验证。 + +### 未采纳:直接升到 `0.8.104` + +库侧建议 pin 直接指向 `0.8.104`(含一个 setter strip 的行为修复)。**未采纳** —— +那会让本次纯等价迁移夹带一个用户可见的输出变更,与提案「非目标」冲突。 +`0.8.104` 应作为独立改动落地,并单独评估它对 ObjC 接口输出基线的影响。 + +## 决策日志 + +| 日期 | 变更 | 说明 | +|------|------|------| +| 2026-08-10 | Created as Draft | 用户定方向「把 ObjC 搬进来,Swift 不动」,库侧对应 MachOObjCSection 0003。本篇只做下游适配,方向论证不重复 | +| 2026-08-10 | handler 无条件安装 | 核实调用点后确认:7 处 section 创建只有 1 处传 `progressContinuation`,后台索引那条也不传。沿用条件安装会让绝大多数 image 静默失去关系数据。这是本提案的主要风险点,也是它值得单独成篇的理由 | +| 2026-08-10 | 关系索引 per-image,不做工厂级大表 | 沿用 `RuntimeRelationshipsResolver` 现有的「遍历 image 逐个查」结构。做成跨 image 大表等于把库刚删的聚合搬过来,并重新引入 `f41648a` 修过的生命周期问题 | +| 2026-08-10 | 工厂聚合一并删除 | 该聚合只写不读(四处引用全是声明/构造/注册)。0003 删掉库侧 `addSubIndexer` 后它也无法构造。附带解掉 `feature/node-store-adoption` 与 main 之间关于 `removeSubIndexer` 的合并冲突 | +| 2026-08-10 | 回归测试必须先观察到红 | 该失效是静默的(无报错、结果为空),只有先跑出红色才能证明测试抓住了它。因此落地步骤把测试排在依赖升级之后、handler 解耦之前 | +| 2026-08-10 | 建表改为惰性,不引入 `freeze()` 时序契约 | 原设计要求 `prepare()` 之后调用 `freeze()`。库侧评审指出这会新造两条静默失效路径 —— 忘了调、以及 `prepare()` 抛错跳过调用点(它是 `async throws`)—— 而这正是本提案动机第二条要消灭的病症。改为查询时惰性建表,`prewarm()` 降级为可选预热。少一条契约优于多一条文档 | +| 2026-08-10 | 单一累积队列是行为等价的前提,不只是性能选择 | 库侧先走完 class 列表(`:272`)再走 category 列表(`:350`),故同一协议的 conformer 中 inline 采纳整体排在 category 贡献之前。按 case 分成两张表再合并,无论怎么拼都无法还原该顺序 | +| 2026-08-10 | 不"顺手"修同类双 `isSwiftStable` 重复条目 | inline 与 category 两条路径的 `isSwiftStable` 来源不同(后者跨 image 解析、失败兜底 `false`),罕见组合下会产生 `className` 相同而标志不同的两条引用。这是现有行为;按 `className` 去重看似修 bug,实为行为变更 | +| 2026-08-10 | 基线快照必须在升级依赖前采集 | 库侧评审指出「改动前后各跑一次」做不到 —— 改动后旧查询方法已不存在,没有一次运行能同时拿到新旧序列。这是一次性窗口,错过需回退依赖重跑 | +| 2026-08-10 | 保留 `record(_:)` 的锁 | 原文写「锁只为满足 `@Sendable` 检查而非防竞争」,把当前实现当成了 API 契约。0003 契约四明确库不承诺 `eventHandler` 的执行上下文,`@Sendable` 正是为 0002 之后并行化走查留的余地。锁必需,理由改为「当前实现下竞争接近零,成本可忽略」 | +| 2026-08-10 | category fixture 用跨模块 extension,不用同模块 | 原建议在测试 bundle 内为自有 Swift 类写 `@objc extension`,库侧评审指出同模块 extension 通常被直接并进 class 的 method list、不产出 `__objc_catlist` 条目。改为对 `NSString` 这类跨模块 ObjC 类写 extension。附带收益:其 `imagePath` 是测试 bundle,正好把 category 的 imagePath 语义变成可执行断言 | +| 2026-08-10 | 反向要求库承诺发射顺序,0003 增补契约五 | 契约四只说不承诺执行上下文,但连带把发射顺序也放掉了 —— 而本提案的逐条等价正建立在顺序之上,并行化会静默改变 Relationships 列表顺序。库侧接受拆分,并给出更强的理由:顺序承诺**今天已经存在**(`OrderedSet` 插入序 + doc comment + README 白纸黑字),表一移出只是失去载体,不转移到事件上等于静默撤销一个已对外给出的保证。因此本提案的等价性目标得以保住 | +| 2026-08-10 | `RuntimeObjCClassReference` 不带 `Codable`、不设 `public` | 核实全仓库对原库类型的引用只有两处(一处 doc comment、一个 `private` 方法签名),关系引用从不跨进程 —— 越过 XPC 的是已物化的 `RuntimeObject` | diff --git a/Documentations/Evolutions/README.md b/Documentations/Evolutions/README.md index acad6e06..89c69d1c 100644 --- a/Documentations/Evolutions/README.md +++ b/Documentations/Evolutions/README.md @@ -17,6 +17,7 @@ | [0003](0003-generic-type-specialization.md) | 泛型类型特化 | In Progress | 用户在 Inspector 的 Specialization tab 为泛型类型选定具体类型组合,特化结果作为 sidebar 子节点呈现,泛型参数被替换且 metadata 字段填上真实数值。 | | [0004](0004-differentiable-box-lazy-cellvm.md) | DifferentiableBox 与 Lazy Cell ViewModel 渲染范式 | Draft | 在 `RuntimeViewerArchitectures` 引入 `DifferentiableBox`,把任意 `Hashable` 领域模型适配为 DifferenceKit 的 `Differentiable`,使表格与大纲视图的 Rx 数据源走「轻量身份元素 + cell 级惰性 ViewModel」。 | | [0006](0006-mcp-transport-bind-failure-teardown.md) | MCP Transport 绑定失败的资源回收与状态如实化 | Implemented | 绑定失败改为显式 `start()` 判定:失败即回收 transport(线程 56→0)、`serverState` 如实 `.stopped`、端口文件带所有权守卫不误删他人文件。残余 5.57 MiB 为上游 SwiftMCP adapter↔engine 引用环,与线程数硬编码一并列为上游跟进项。 | +| [0007](0007-objc-relationship-index-returns-to-application.md) | ObjC 关系索引归还应用侧 | In Progress | MachOObjCSection 0003 的下游适配:库不再存继承 / 遵守反向表,改由应用在 `prepare()` 期间从 `ObjCIndexingEvent` 事件流重建。核心风险是事件 handler 目前条件安装(7 处 section 创建仅 1 处传进度流),不解耦会让后台索引的 image 静默失去关系数据。同批次删除只写不读的工厂聚合。 | > 0000 与 0001 采用早期格式,正文没有状态字段,此处如实标为「未标注」。按「旧文档原地不动」的约定不回填。 diff --git a/Documentations/README.md b/Documentations/README.md index ed3bc454..5c85d02a 100644 --- a/Documentations/README.md +++ b/Documentations/README.md @@ -18,7 +18,7 @@ ## 提案(Evolutions) -见 [`Evolutions/README.md`](Evolutions/README.md)。当前 5 篇:Bonjour 可靠性、IDA 兼容导出、后台索引、泛型类型特化、DifferentiableBox 渲染范式。 +见 [`Evolutions/README.md`](Evolutions/README.md)。当前 7 篇:Bonjour 可靠性、IDA 兼容导出、后台索引、泛型类型特化、DifferentiableBox 渲染范式、MCP Transport 绑定失败回收、ObjC 关系索引归还应用侧。 ## 设计与实现计划(Plans,归档) diff --git a/RuntimeViewerCore/Package.swift b/RuntimeViewerCore/Package.swift index c0362197..d4753d53 100644 --- a/RuntimeViewerCore/Package.swift +++ b/RuntimeViewerCore/Package.swift @@ -94,7 +94,7 @@ let package = Package( ), remote: .package( url: "https://github.com/MxIris-Reverse-Engineering/MachOObjCSection", - exact: "0.8.102", + exact: "0.8.103", ), ), .package( diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift index cc54d79d..8435d446 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Core/RuntimeObjCSection.swift @@ -34,18 +34,27 @@ actor RuntimeObjCSection { /// Per-image Objective-C interface index: the parsed data store for /// this image (classes, protocols, categories, C struct/union - /// definitions) plus the inheritance / protocol-adoption reverse - /// tables. Constructed in `init` with this image's `MachOImage` and + /// definitions). Constructed in `init` with this image's `MachOImage` and /// populated by `objcIndexer.prepare()`; afterwards this section only /// *reads* it back to translate into `RuntimeViewerCore` domain types /// (`RuntimeObject`, `RuntimeObjectInterface`, `RuntimeMemberAddress`). /// - /// `nonisolated let` so `RuntimeRelationshipsResolver` and the factory's - /// aggregate can read its query methods without an actor hop — - /// `ObjCInterfaceIndexer` is `Sendable` and protects its own state with - /// its own lock. + /// Relationships are **not** in here — since MachOObjCSection 0003 the + /// library keeps no reverse tables; see `objcRelationshipIndex`. + /// + /// `nonisolated let` so `RuntimeRelationshipsResolver` can read its query + /// methods without an actor hop — `ObjCInterfaceIndexer` is `Sendable` and + /// protects its own state with its own lock. nonisolated let objcIndexer: ObjCInterfaceIndexer + /// Inheritance and protocol-adoption reverse tables for this image, + /// accumulated from the indexer's event stream during `prepare()`. + /// + /// `nonisolated let` for the same reason as `objcIndexer`: + /// `RuntimeRelationshipsResolver` reads it without an actor hop, and the + /// index guards its own state. + nonisolated let objcRelationshipIndex: RuntimeObjCRelationshipIndex + init(imagePath: String, factory: RuntimeObjCSectionFactory, progressContinuation: LoadingEventContinuation? = nil) async throws { #log(.info, "Initializing ObjC section for image: \(imagePath, privacy: .public)") guard let machO = DyldUtilities.machOImage(forPath: imagePath) else { @@ -55,10 +64,15 @@ actor RuntimeObjCSection { self.machO = machO self.imagePath = imagePath self.factory = factory + let objcRelationshipIndex = RuntimeObjCRelationshipIndex() + self.objcRelationshipIndex = objcRelationshipIndex self.objcIndexer = ObjCInterfaceIndexer( machO: machO, imagePath: imagePath, - eventHandler: Self.makeEventHandler(forwardingTo: progressContinuation) + eventHandler: Self.makeEventHandler( + recordingInto: objcRelationshipIndex, + forwardingTo: progressContinuation + ) ) try await objcIndexer.prepare() } @@ -68,27 +82,41 @@ actor RuntimeObjCSection { self.machO = machO self.imagePath = machO.imagePath self.factory = factory + let objcRelationshipIndex = RuntimeObjCRelationshipIndex() + self.objcRelationshipIndex = objcRelationshipIndex self.objcIndexer = ObjCInterfaceIndexer( machO: machO, imagePath: machO.imagePath, - eventHandler: Self.makeEventHandler(forwardingTo: progressContinuation) + eventHandler: Self.makeEventHandler( + recordingInto: objcRelationshipIndex, + forwardingTo: progressContinuation + ) ) try await objcIndexer.prepare() } - /// Adapts the library's single ``ObjCIndexingEvent`` channel back onto - /// RuntimeViewer's loading-progress stream. + /// Fans the library's single ``ObjCIndexingEvent`` channel out to its two + /// consumers: the relationship index, and RuntimeViewer's loading-progress + /// stream. /// - /// Only the `.progress` cases have a counterpart here; the relationship - /// events (`subclassIndexed` and friends) are consumed by the indexer's - /// own reverse tables, which `RuntimeRelationshipsResolver` queries - /// directly, so they need no forwarding. + /// **The handler is installed unconditionally**, and the returned closure is + /// non-optional on purpose. Since MachOObjCSection 0003 the relationship + /// events are the *only* channel carrying inheritance and protocol adoption: + /// an indexer built without a handler keeps none of it. Gating installation + /// on `progressContinuation` — as this did while the library still owned the + /// tables — would leave every image loaded through `_loadImage(at:)` or + /// background indexing with an empty Relationships pane and no error to show + /// for it. Only the *forwarding* of `.progress` events depends on a stream + /// being there to forward to. private static func makeEventHandler( + recordingInto objcRelationshipIndex: RuntimeObjCRelationshipIndex, forwardingTo progressContinuation: LoadingEventContinuation? - ) -> (@Sendable (ObjCIndexingEvent) -> Void)? { - guard let progressContinuation else { return nil } + ) -> @Sendable (ObjCIndexingEvent) -> Void { return { event in - guard case .progress(let phase, let itemDescription, let currentCount, let totalCount) = event else { + objcRelationshipIndex.record(event) + guard let progressContinuation, + case .progress(let phase, let itemDescription, let currentCount, let totalCount) = event + else { return } progressContinuation.yield( @@ -364,24 +392,15 @@ actor RuntimeObjCSection { @Loggable(.private) actor RuntimeObjCSectionFactory { - private var sections: [String: RuntimeObjCSection] = [:] - - /// Aggregate Objective-C interface indexer. Each per-image - /// `RuntimeObjCSection.objcIndexer` is registered as a sub-indexer when - /// the section is created, so queries against this aggregate fan out - /// across all loaded ObjC sections. Mirrors `RuntimeSwiftSectionFactory.indexer`. + /// Per-image sections, keyed by the dyld-canonical image path. /// - /// `ObjCInterfaceIndexer` binds a `MachOImage` at `init`; this - /// aggregate never parses one of its own (`prepare()` is never called on - /// it), so it is constructed against the current process image as a - /// placeholder — mirroring `RuntimeSwiftSectionFactory`'s aggregate, - /// which is likewise built `in: .current()`. - let objcInterfaceIndexer: ObjCInterfaceIndexer - - init() { - let currentMachO = MachOImage.current() - objcInterfaceIndexer = ObjCInterfaceIndexer(machO: currentMachO, imagePath: currentMachO.imagePath) - } + /// There is deliberately no aggregate indexer here, unlike + /// `RuntimeSwiftSectionFactory`. One existed until Evolution 0007 and was + /// only ever written to — every per-image indexer was registered with it and + /// nothing ever queried it, because `RuntimeRelationshipsResolver` fans out + /// across images itself. Keeping it alive would only pin each image's index + /// state for the engine's lifetime. + private var sections: [String: RuntimeObjCSection] = [:] func existingSection(for imagePath: String) -> RuntimeObjCSection? { sections[imagePath] @@ -408,7 +427,6 @@ actor RuntimeObjCSectionFactory { #log(.debug, "Creating ObjC section for: \(imagePath, privacy: .public)") let section = try await RuntimeObjCSection(imagePath: imagePath, factory: self, progressContinuation: progressContinuation) sections[imagePath] = section - objcInterfaceIndexer.addSubIndexer(section.objcIndexer) #log(.debug, "ObjC section created and cached") return (false, section) } @@ -429,7 +447,6 @@ actor RuntimeObjCSectionFactory { #log(.debug, "Creating ObjC section from MachO: \(machO.imagePath, privacy: .public)") let objcSection = try await RuntimeObjCSection(machO: machO, factory: self) sections[machO.imagePath] = objcSection - objcInterfaceIndexer.addSubIndexer(objcSection.objcIndexer) return objcSection } catch { #log(.error, "Failed to create ObjC section: \(error, privacy: .public)") diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeObjCRelationshipIndex.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeObjCRelationshipIndex.swift new file mode 100644 index 00000000..84df6b3e --- /dev/null +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeObjCRelationshipIndex.swift @@ -0,0 +1,182 @@ +import Foundation +import ObjCIndexing +import OrderedCollections + +/// An Objective-C class — or a Swift class carrying an Objective-C ancestor — +/// found to subclass another class or to adopt a protocol. +/// +/// `isSwiftStable` is the structural signal (`class_t`'s `FAST_IS_SWIFT_STABLE` +/// bit) that lets `RuntimeRelationshipsResolver` decide whether to materialize +/// the reference as a Swift `RuntimeObject` or an Objective-C one. The library +/// reports the bit; the domain judgement is made here. +/// +/// Deliberately neither `public` nor `Codable`: relationship references never +/// leave the process. What crosses the XPC boundary is the already-materialized +/// `RuntimeObject`. +struct RuntimeObjCClassReference: Hashable, Sendable { + let className: String + let imagePath: String + let isSwiftStable: Bool +} + +/// Per-image Objective-C relationship index: the inheritance and +/// protocol-adoption reverse tables backing the Inspector's Relationships pane. +/// +/// MachOObjCSection 0003 removed these tables from `ObjCIndexing` — the library +/// parses and broadcasts what it finds, and no longer remembers how classes +/// relate. This is where those broadcasts become tables again. +/// +/// ## Relationship data arrives only through the event stream +/// +/// An instance is installed as the indexer's `eventHandler` *before* +/// `prepare()` runs and accumulates throughout the walk. An `ObjCInterfaceIndexer` +/// constructed without a handler keeps no relationship data at all, so +/// `RuntimeObjCSection` installs one unconditionally — never gated on whether a +/// progress stream happens to exist. +/// +/// ## Tables are built on first query +/// +/// `record(_:)` only appends; the tables are materialized lazily. There is +/// deliberately no "sealing" call the caller must remember to make: forgetting +/// it, or skipping it because `prepare()` threw, would leave the index +/// permanently empty *without any error* — the same silent-emptiness failure +/// this whole design exists to rule out. `prewarm()` is available to pay the +/// build cost up front, and is never required for correctness. +/// +/// ## Equivalence with the library's former tables +/// +/// Three properties of the old implementation are reproduced deliberately: +/// +/// 1. Inline adoptions and category-contributed adoptions land in **one** +/// conformer table, so a single query returns both. +/// 2. Events are replayed from a **single** queue in arrival order, so inline +/// adoptions precede category ones exactly as they did when the library +/// walked classes before categories. +/// 3. `OrderedSet` deduplicates on all three fields, not on `className`. A class +/// can legitimately appear twice for one protocol with differing +/// `isSwiftStable` — inline adoption reads the class' own flag while a +/// category resolves its target across images and falls back to `false`. +/// Collapsing those would look like a bug fix and would be a behaviour change. +/// +/// `@unchecked Sendable`: every stored property is guarded by `lock`. +final class RuntimeObjCRelationshipIndex: @unchecked Sendable { + private struct Tables { + var subclassesByClassName: [String: OrderedSet] = [:] + var conformingClassesByProtocolName: [String: OrderedSet] = [:] + } + + private let lock = NSLock() + + /// Events in arrival order, awaiting replay. Cleared once `tables` is built. + private var pendingEvents: [ObjCIndexingEvent] = [] + + private var tables: Tables? + + init() {} + + // MARK: - Accumulation + + /// Append one event. Progress events are ignored; only the three + /// relationship cases carry table data. + /// + /// A single `append` under the lock is all the work done on the parse hot + /// path — cheaper than the two dictionary lookups plus `OrderedSet.append` + /// the library used to perform here. The lock is required: the library + /// promises the *order* of events but explicitly not the thread they arrive + /// on, leaving room to parallelize the walk later. + func record(_ event: ObjCIndexingEvent) { + switch event { + case .progress: + return + case .subclassIndexed, .conformanceIndexed, .categoryConformanceIndexed: + lock.lock() + defer { lock.unlock() } + if tables != nil { + // Already materialized — fold the event straight in rather than + // invalidating, since `pendingEvents` was released at build time + // and could no longer reproduce the earlier events. Arrival + // order still holds: this event genuinely comes last. + apply(event, into: &tables!) + } else { + pendingEvents.append(event) + } + } + } + + /// Build the tables now rather than on first query. Optional: skipping it + /// costs nothing but deferring the same work. + func prewarm() { + lock.lock() + defer { lock.unlock() } + _ = materializedTables() + } + + // MARK: - Query + + /// Direct subclasses of `className` recorded for this image, in the order + /// the library walked `__objc_classlist`. + func subclasses(of className: String) -> [RuntimeObjCClassReference] { + lock.lock() + defer { lock.unlock() } + return Array(materializedTables().subclassesByClassName[className] ?? []) + } + + /// Classes adopting `protocolName` in this image — inline adoptions first, + /// then those contributed by categories. + func conformingClasses(toProtocol protocolName: String) -> [RuntimeObjCClassReference] { + lock.lock() + defer { lock.unlock() } + return Array(materializedTables().conformingClassesByProtocolName[protocolName] ?? []) + } + + // MARK: - Materialization + + /// Caller must hold `lock`. + private func materializedTables() -> Tables { + if let tables { return tables } + + var built = Tables() + for event in pendingEvents { + apply(event, into: &built) + } + + tables = built + pendingEvents = [] + return built + } + + /// Fold one event into `tables`. Caller must hold `lock`. + private func apply(_ event: ObjCIndexingEvent, into tables: inout Tables) { + switch event { + case .progress: + return + + case .subclassIndexed(let className, let superclass, let imagePath, let isSwiftStable): + let reference = RuntimeObjCClassReference( + className: className, + imagePath: imagePath, + isSwiftStable: isSwiftStable + ) + tables.subclassesByClassName[superclass, default: []].append(reference) + + case .conformanceIndexed(let className, let protocolName, let imagePath, let isSwiftStable): + let reference = RuntimeObjCClassReference( + className: className, + imagePath: imagePath, + isSwiftStable: isSwiftStable + ) + tables.conformingClassesByProtocolName[protocolName, default: []].append(reference) + + case .categoryConformanceIndexed(let targetClassName, let protocolName, let imagePath, let targetIsSwiftStable): + // `imagePath` is the image declaring the *category*, not the one + // declaring `targetClassName`. Preserved verbatim: it is what the + // library's table recorded, and changing it is a behaviour change. + let reference = RuntimeObjCClassReference( + className: targetClassName, + imagePath: imagePath, + isSwiftStable: targetIsSwiftStable + ) + tables.conformingClassesByProtocolName[protocolName, default: []].append(reference) + } + } +} diff --git a/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift index 3f17d732..66935023 100644 --- a/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift +++ b/RuntimeViewerCore/Sources/RuntimeViewerCore/Relationships/RuntimeRelationshipsResolver.swift @@ -82,7 +82,7 @@ actor RuntimeRelationshipsResolver { if wantsSubclasses { if let objcKey { if let objcSection = await objcSectionFactory.existingSection(for: imagePath) { - for reference in objcSection.objcIndexer.subclasses(of: objcKey) { + for reference in objcSection.objcRelationshipIndex.subclasses(of: objcKey) { if let runtimeObject = await materializeRelationshipReference(reference) { subclasses.append(runtimeObject) } @@ -103,7 +103,7 @@ actor RuntimeRelationshipsResolver { if wantsConformers { if isObjCProtocol { if let objcSection = await objcSectionFactory.existingSection(for: imagePath) { - for reference in objcSection.objcIndexer.conformingClasses(toProtocol: object.name) { + for reference in objcSection.objcRelationshipIndex.conformingClasses(toProtocol: object.name) { if let runtimeObject = await materializeRelationshipReference(reference) { conformers.append(runtimeObject) } @@ -161,7 +161,7 @@ actor RuntimeRelationshipsResolver { /// section. When that lookup fails (e.g. an `@objc(customName)` class /// whose raw name isn't a Swift mangling), the entry is dropped rather /// than fall back to `.objc(.type(.class))`. - private func materializeRelationshipReference(_ reference: ObjCClassReference) async -> RuntimeObject? { + private func materializeRelationshipReference(_ reference: RuntimeObjCClassReference) async -> RuntimeObject? { if reference.isSwiftStable { // `demangleAsNode` / `mangleAsString` each ship a sync and an async // overload; the compiler picks the async one inside this `async` diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsEquivalenceSnapshotTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsEquivalenceSnapshotTests.swift new file mode 100644 index 00000000..3c38752e --- /dev/null +++ b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsEquivalenceSnapshotTests.swift @@ -0,0 +1,141 @@ +import Testing +import Foundation +import RuntimeViewerCore + +/// Golden-file equivalence guard for Evolution 0007, which moves the Objective-C +/// relationship reverse tables out of `ObjCIndexing` and rebuilds them here from +/// the library's event stream. +/// +/// The snapshot is taken through `RuntimeEngine.relationships(for:)` — the public +/// API whose output is what users actually see, and the only vantage point that +/// exists both before and after the migration. (The internal +/// `ObjCInterfaceIndexer.subclasses(of:)` disappears with the library change, so +/// it cannot serve as a shared baseline.) +/// +/// Capture the baseline **before** bumping the MachOObjCSection pin: once the +/// old implementation is gone there is nothing left to compare against. Missing +/// snapshot files are written on the spot and reported as a failure, so a +/// forgotten baseline is loud rather than silent. +@Suite("Relationships equivalence snapshot", .serialized) +struct RelationshipsEquivalenceSnapshotTests { + private enum Anchors { + static let foundationPath = "/System/Library/Frameworks/Foundation.framework/Foundation" + static let libobjcPath = "/usr/lib/libobjc.A.dylib" + } + + /// Anchors chosen for what each one proves about equivalence: + /// + /// - `NSObject` — the largest subclass set available, spanning two images, + /// so a dropped image or a broken cross-image union shows up immediately. + /// - `NSCoding` / `NSCopying` — Objective-C protocols adopted both inline and + /// through categories. The library wrote both kinds into one table; if the + /// rebuilt index splits them apart and queries only one, these shrink. + private static let subclassAnchors = ["NSObject"] + private static let protocolAnchors = ["NSCoding", "NSCopying"] + + @Test("Relationships output matches the pre-migration baseline") + func matchesBaseline() async throws { + let engine = RuntimeEngine(source: .local, engineID: "test-rel-equivalence-snapshot") + try await engine.connect() + try await engine.loadImage(at: Anchors.libobjcPath) + try await engine.loadImage(at: Anchors.foundationPath) + + var report = "" + + for className in Self.subclassAnchors { + let anchor = try #require( + await findObject(named: className, kind: .objc(.type(.class)), in: engine), + "Anchor class \(className) not found in the loaded images." + ) + let relationships = try await engine.relationships(for: anchor) + report += render(section: "subclasses of \(className)", objects: relationships.subclasses) + } + + for protocolName in Self.protocolAnchors { + let anchor = try #require( + await findObject(named: protocolName, kind: .objc(.type(.protocol)), in: engine), + "Anchor protocol \(protocolName) not found in the loaded images." + ) + let relationships = try await engine.relationships(for: anchor) + report += render(section: "conformers of \(protocolName)", objects: relationships.conformingTypes) + } + + try compare(report, againstSnapshotNamed: "relationships-baseline.txt") + } + + // MARK: - Rendering + + /// One line per entry: `kind|displayName|imagePath`. + /// + /// `imagePath` is included deliberately — a category-contributed conformer + /// carries the image declaring the *category*, not the one declaring the + /// class, and that asymmetry is existing behaviour the migration must + /// preserve verbatim. + private func render(section: String, objects: [RuntimeObject]) -> String { + var rendered = "## \(section) (\(objects.count))\n" + for object in objects { + rendered += "\(object.kind)|\(object.displayName)|\(object.imagePath)\n" + } + return rendered + "\n" + } + + private func findObject( + named name: String, + kind: RuntimeObjectKind, + in engine: RuntimeEngine + ) async -> RuntimeObject? { + for imagePath in await engine.loadedImagePaths { + guard let objects = try? await engine.objects(in: imagePath) else { continue } + if let match = objects.first(where: { $0.name == name && $0.kind == kind }) { + return match + } + } + return nil + } + + // MARK: - Snapshot IO + + /// Snapshots live next to this source file so they are reviewable in diffs + /// and travel with the branch; `#filePath` avoids depending on test-bundle + /// resource plumbing. + private func snapshotDirectory() -> URL { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .appendingPathComponent("Snapshots", isDirectory: true) + } + + private func compare(_ report: String, againstSnapshotNamed fileName: String) throws { + let snapshotURL = snapshotDirectory().appendingPathComponent(fileName) + + guard let recorded = try? String(contentsOf: snapshotURL, encoding: .utf8) else { + try FileManager.default.createDirectory( + at: snapshotDirectory(), + withIntermediateDirectories: true + ) + try report.write(to: snapshotURL, atomically: true, encoding: .utf8) + Issue.record( + """ + No baseline snapshot existed; one was just written to \(snapshotURL.path). + Review it, commit it, and re-run. This failure is expected exactly once — \ + on the run that captures the baseline. + """ + ) + return + } + + guard recorded != report else { return } + + let recordedLines = recorded.components(separatedBy: "\n") + let reportLines = report.components(separatedBy: "\n") + let missing = Set(recordedLines).subtracting(reportLines).sorted() + let unexpected = Set(reportLines).subtracting(recordedLines).sorted() + + Issue.record( + """ + Relationships output diverged from the baseline. + Missing \(missing.count) line(s): \(missing.prefix(20).joined(separator: ", ")) + Unexpected \(unexpected.count) line(s): \(unexpected.prefix(20).joined(separator: ", ")) + """ + ) + } +} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsWithoutProgressStreamTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsWithoutProgressStreamTests.swift new file mode 100644 index 00000000..5560f10f --- /dev/null +++ b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RelationshipsWithoutProgressStreamTests.swift @@ -0,0 +1,78 @@ +import Testing +import Foundation +import RuntimeViewerCore + +/// Regression guard for the one hazard Evolution 0007 could not delegate to the +/// compiler. +/// +/// Since MachOObjCSection 0003 the library keeps no relationship tables of its +/// own — inheritance and protocol adoption leave it *only* through the event +/// handler. An `ObjCInterfaceIndexer` built without a handler silently keeps +/// nothing, and nothing about that is a compile error. +/// +/// `RuntimeObjCSection` used to install its handler only when a progress stream +/// was supplied, which was harmless while the library owned the tables. Six of +/// the seven section-creation call sites pass no progress stream — including +/// `_loadImage(at:)` and background indexing, i.e. how most images are indexed — +/// so leaving that condition in place would empty the Relationships pane for +/// nearly every image, with no error anywhere. +/// +/// This test therefore drives the *no-progress-stream* route deliberately: +/// `loadImage(at:)` creates its sections without a continuation. +@Suite("Relationships without a progress stream") +struct RelationshipsWithoutProgressStreamTests { + private enum Anchors { + static let foundationPath = "/System/Library/Frameworks/Foundation.framework/Foundation" + static let libobjcPath = "/usr/lib/libobjc.A.dylib" + } + + @Test("Images loaded without a progress stream still resolve relationships") + func relationshipsSurviveWithoutProgressStream() async throws { + let engine = RuntimeEngine(source: .local, engineID: "test-rel-no-progress") + try await engine.connect() + + // `loadImage(at:)` builds its sections with no progress continuation — + // the route that loses relationship data if the handler is conditional. + try await engine.loadImage(at: Anchors.libobjcPath) + try await engine.loadImage(at: Anchors.foundationPath) + + var nsObject: RuntimeObject? + for imagePath in await engine.loadedImagePaths { + let objects = try await engine.objects(in: imagePath) + if let match = objects.first(where: { $0.name == "NSObject" && $0.kind == .objc(.type(.class)) }) { + nsObject = match + break + } + } + let anchor = try #require(nsObject, "NSObject not found in the loaded images.") + + let relationships = try await engine.relationships(for: anchor) + + #expect( + !relationships.subclasses.isEmpty, + """ + NSObject resolved zero subclasses. The relationship index is only fed \ + by the indexer's event handler, so this is what an unconditionally \ + required handler being installed conditionally looks like — no error, \ + just an empty pane. + """ + ) + #expect(relationships.subclasses.contains { $0.displayName == "NSString" }) + } + + @Test("ObjC protocol conformers survive without a progress stream") + func conformersSurviveWithoutProgressStream() async throws { + let engine = RuntimeEngine(source: .local, engineID: "test-rel-no-progress-conformers") + try await engine.connect() + try await engine.loadImage(at: Anchors.foundationPath) + + let objects = try await engine.objects(in: Anchors.foundationPath) + let anchor = try #require( + objects.first(where: { $0.name == "NSCopying" && $0.kind == .objc(.type(.protocol)) }), + "NSCopying not found in Foundation." + ) + + let relationships = try await engine.relationships(for: anchor) + #expect(!relationships.conformingTypes.isEmpty) + } +} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RuntimeObjCRelationshipIndexTests.swift b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RuntimeObjCRelationshipIndexTests.swift new file mode 100644 index 00000000..413ea8ad --- /dev/null +++ b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/RuntimeObjCRelationshipIndexTests.swift @@ -0,0 +1,181 @@ +import Testing +import Foundation +import ObjCIndexing +@testable import RuntimeViewerCore + +/// Unit tests for the reverse tables Evolution 0007 brought back from +/// MachOObjCSection. +/// +/// The end-to-end equivalence is covered by `RelationshipsEquivalenceSnapshotTests`, +/// which compares real output against a baseline captured before the migration. +/// What that snapshot cannot show is *why* the output matches — these tests pin +/// the three properties it depends on, using synthetic events so each one fails +/// in isolation when broken. +@Suite("RuntimeObjCRelationshipIndex") +struct RuntimeObjCRelationshipIndexTests { + private static let imagePath = "/fixture/Image.framework/Image" + private static let categoryImagePath = "/fixture/Other.framework/Other" + + // MARK: - Property 1: both conformance kinds share one table + + @Test("Inline and category adoptions answer the same query") + func inlineAndCategoryAdoptionsShareOneTable() { + let index = RuntimeObjCRelationshipIndex() + index.record(.conformanceIndexed( + className: "InlineAdopter", + protocolName: "FixtureProtocol", + imagePath: Self.imagePath, + isSwiftStable: false + )) + index.record(.categoryConformanceIndexed( + targetClassName: "CategoryAdopter", + protocolName: "FixtureProtocol", + imagePath: Self.categoryImagePath, + targetIsSwiftStable: false + )) + + let conformers = index.conformingClasses(toProtocol: "FixtureProtocol") + // The library wrote both kinds into one dictionary and answered both + // from a single query. Splitting them into per-case tables would drop + // half the results here. + #expect(conformers.map(\.className) == ["InlineAdopter", "CategoryAdopter"]) + } + + // MARK: - Property 2: inline adoptions precede category ones + + @Test("Replay preserves arrival order across both phases") + func replayPreservesArrivalOrder() { + let index = RuntimeObjCRelationshipIndex() + // The library walks every class before any category, so a consumer + // replaying a single queue in arrival order reproduces that grouping. + for className in ["ClassA", "ClassB", "ClassC"] { + index.record(.conformanceIndexed( + className: className, + protocolName: "FixtureProtocol", + imagePath: Self.imagePath, + isSwiftStable: false + )) + } + for targetClassName in ["CategoryTargetA", "CategoryTargetB"] { + index.record(.categoryConformanceIndexed( + targetClassName: targetClassName, + protocolName: "FixtureProtocol", + imagePath: Self.categoryImagePath, + targetIsSwiftStable: false + )) + } + + #expect( + index.conformingClasses(toProtocol: "FixtureProtocol").map(\.className) + == ["ClassA", "ClassB", "ClassC", "CategoryTargetA", "CategoryTargetB"] + ) + } + + // MARK: - Property 3: dedup keys on all three fields + + @Test("Same class differing only in isSwiftStable is kept twice") + func dedupKeysOnEveryField() { + let index = RuntimeObjCRelationshipIndex() + // A class can reach one protocol twice: inline adoption reads its own + // `class_t` flag, while a category resolves the target across images and + // falls back to `false` when that fails. The library's `OrderedSet` kept + // both entries; collapsing them by class name would look like a fix and + // would be a behaviour change. + index.record(.conformanceIndexed( + className: "BridgedClass", + protocolName: "FixtureProtocol", + imagePath: Self.imagePath, + isSwiftStable: true + )) + index.record(.categoryConformanceIndexed( + targetClassName: "BridgedClass", + protocolName: "FixtureProtocol", + imagePath: Self.imagePath, + targetIsSwiftStable: false + )) + + let conformers = index.conformingClasses(toProtocol: "FixtureProtocol") + #expect(conformers.count == 2) + #expect(conformers.map(\.isSwiftStable) == [true, false]) + } + + @Test("Fully identical records collapse to one") + func identicalRecordsCollapse() { + let index = RuntimeObjCRelationshipIndex() + for _ in 0 ..< 3 { + index.record(.subclassIndexed( + className: "Subclass", + superclass: "Superclass", + imagePath: Self.imagePath, + isSwiftStable: false + )) + } + #expect(index.subclasses(of: "Superclass").count == 1) + } + + // MARK: - Category imagePath asymmetry + + @Test("A category records its own image, not the target class' image") + func categoryRecordsItsOwnImage() { + let index = RuntimeObjCRelationshipIndex() + index.record(.categoryConformanceIndexed( + targetClassName: "NSString", + protocolName: "FixtureProtocol", + imagePath: Self.categoryImagePath, + targetIsSwiftStable: false + )) + + let conformer = try? #require(index.conformingClasses(toProtocol: "FixtureProtocol").first) + // `className` and `imagePath` deliberately do not belong to the same + // image here. Anything keying off `imagePath` to locate the class will + // not find it; that is existing behaviour, preserved verbatim by 0007. + #expect(conformer?.className == "NSString") + #expect(conformer?.imagePath == Self.categoryImagePath) + } + + // MARK: - Lazy build + + @Test("Events recorded after the tables were built still register") + func lateEventsStillRegister() { + let index = RuntimeObjCRelationshipIndex() + index.record(.subclassIndexed( + className: "First", + superclass: "Superclass", + imagePath: Self.imagePath, + isSwiftStable: false + )) + // Force materialization, then keep recording. The build releases the + // pending queue, so a late event has to be folded straight into the + // tables — rebuilding from an emptied queue would lose "First". + #expect(index.subclasses(of: "Superclass").map(\.className) == ["First"]) + + index.record(.subclassIndexed( + className: "Second", + superclass: "Superclass", + imagePath: Self.imagePath, + isSwiftStable: false + )) + #expect(index.subclasses(of: "Superclass").map(\.className) == ["First", "Second"]) + } + + @Test("prewarm does not change what queries return") + func prewarmIsObservationallyNeutral() { + let index = RuntimeObjCRelationshipIndex() + index.record(.subclassIndexed( + className: "Subclass", + superclass: "Superclass", + imagePath: Self.imagePath, + isSwiftStable: false + )) + index.prewarm() + #expect(index.subclasses(of: "Superclass").map(\.className) == ["Subclass"]) + } + + @Test("Progress events contribute nothing") + func progressEventsAreIgnored() { + let index = RuntimeObjCRelationshipIndex() + index.record(.progress(phase: .loadingClasses, itemDescription: "Whatever", currentCount: 1, totalCount: 2)) + #expect(index.subclasses(of: "Superclass").isEmpty) + #expect(index.conformingClasses(toProtocol: "FixtureProtocol").isEmpty) + } +} diff --git a/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/Snapshots/relationships-baseline.txt b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/Snapshots/relationships-baseline.txt new file mode 100644 index 00000000..9ee191b3 --- /dev/null +++ b/RuntimeViewerCore/Tests/RuntimeViewerCoreTests/Snapshots/relationships-baseline.txt @@ -0,0 +1,390 @@ +## subclasses of NSObject (307) +Objective-C Class|__IncompleteProtocol|/usr/lib/libobjc.A.dylib +Objective-C Class|__NSBundleTables|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|__NSObserver|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|__NSOperationInternalObserver|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|__NSSKGraphE|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|__NSUnrecognizedTaggedPointer|/usr/lib/libobjc.A.dylib +Objective-C Class|_NSActivityAssertion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAECoercerData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAETranslatorData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributeDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributedStringFromMarkdownCreatorConcrete|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributedStringGrammar|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributedStringGrammarInflection|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributedStringReplacement|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAuxiliaryUndoManagerReference|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSBundleODRDataCommon|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSBundleODRTag|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSBundleOnDemandResourceClientExportedObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSCloudSharingDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSDataCompressor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSDiskOperation|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSDOConversationInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSExtensionContextVendor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSFCFakelinkGroupInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSFileAccessAsynchronousProcessAssertion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSFileAccessAsynchronousProcessAssertionScheduler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSFileAccessClaimPresenterRelinquishment|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSFileWatcherFileHandleInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSIPCallbackSerialization|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSItemProviderTypeCoercion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSJSONReader|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSJSONRoundTrippingNumber|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSJSONWriter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyedCoderOldStyleArray|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyedUnarchiverHelper|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueDebugging|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueDebuggingDeallocSentinel|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueDidWillStats|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueObjectAndKeyPair|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueObjectBox|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueReturnedValueConsistencyStats|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKVOCompatibility|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKVODeallocSentinel|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSLexiconMorphunDictionary|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSLocalizedStringResource|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSMetadataItemPrivateIvars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSMetadataQueryPrivateIvars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSMetadataQuerySortingPseudoItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSObserverList|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSOrderedCollectionDifferenceMoves|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPerformanceMeter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPersonNameComponentsData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPersonNameComponentsFormatterData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPersonNameComponentsStyleFormatter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPredicateOperatorUtilities|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPredicateUtilities|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSProgressSubscriber|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSSharedKeySetS|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSStringFormattingOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSThreadData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSThreadPerformInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSTimerBlockTarget|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoActionInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoManagerAuxiliaryExportedObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoManagerMainExportedObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoStack|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXMLDocumentExtraIvars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXMLPlaceholderNode|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCAllowListHolder|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCBoost|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionClassCache|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionExpectedReplies|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionExpectedReplyInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionExportedObjectTable|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionImportInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCConnectionRequestedReplies|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCDistantObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSXPCRemoteTransport|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAEDescriptorTranslator|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAffineTransform|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAKDeserializer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAKDeserializerStream|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAKSerializer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAKSerializerStream|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleEventDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleEventHandling|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleEventManager|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleScript|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSArrayChange|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAssertionHandler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedString|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedStringMarkdownParsingOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedStringMarkdownSourcePosition|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAutoreleasePool|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSBackgroundActivityScheduler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSBoundKeyPath|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSBundle|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSBundleResourceRequest|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCFStreamWeakDelegateWrapper|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCFType|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCharacterSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSClassDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCoder|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCondition|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSConditionLock|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSConnection|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSConnectionHelper|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCreateCommandMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDateInterval|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDecimalNumberHandler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDeserializer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDictionaryEntry|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDistantObjectRequest|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDistantObjectTableEntry|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDistributedLock|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDocInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSEncodingDetectionBuffer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSEncodingDetectionPlaceholder|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSEncodingDetector|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSError|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExpression|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtension|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionContext|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionService_Subsystem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionURLResult|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessArbiter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessArbiterProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessClaim|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessIntent|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessNode|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessProcessManager|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessProcessMonitor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileAccessSubarbiter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileCoordinator|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileCoordinatorAccessorBlockCompletion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileCoordinatorReacquisitionBlockCompletion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileHandle|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileManager|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFilePresenterManagedProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFilePresenterOperationRecord|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFilePresenterRelinquishment|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFilePresenterXPCMessenger|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFilePromiseWriteToken|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderKernelFileMaterializationInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderKernelMaterializationInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderKernelPartialFolderMaterializationInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderMessageInterface|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderMessenger|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderMovingInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderMovingResponse|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderPresenterInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderService|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProviderXPCMessenger|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProvidingInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileProvidingResponse|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileReactorProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileVersion|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileWatcher|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileWatcherObservations|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFileWrapper|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFormatter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSGarbageCollector|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSHashTable|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSIndexPath|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSIndexSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSInflectionRule|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSItemProvider|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSItemProviderRepresentation|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSItemRepresentationLoadResult|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSJSONSerialization|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyBinding|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueAccessor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueContainerClass|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueMutatingCollectionMethodSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueNonmutatingCollectionMethodSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueObservance|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueObservationInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueProperty|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueProxyShareKey|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueShareableObservationInfoKey|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueSharedObservers|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueSharedObserversSnapshot|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSLinguisticTagger|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSLocalizedNumberFormatRule|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSLock|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSLookupMatch|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMapTable|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMeasurement|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMetadataItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMetadataQuery|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMetadataQueryAttributeValueTuple|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMetadataQueryResultGroup|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphology|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphologyCustomPronoun|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphologyPronoun|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMultiReadUniWriteLock|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSNotification|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSNotificationCenter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSNotificationQueue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSObservation|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSObservationSink|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSObservationSource|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSObservedValue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOperation|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOperationQueue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrderedCollectionChange|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrderedCollectionDifference|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrderedSetChange|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrthography|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPersonNameComponents|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPipe|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPointerArray|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPointerFunctions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPortMessage|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPortNameServer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPositionalSpecifier|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPositionalSpecifierMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPredicate|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPredicateOperator|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPredicateValidator|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPresentationIntent|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProcessInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProgress|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProgressPublisherProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProgressRegistrar|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProgressSubscriberProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSProgressValues|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPropertyListSerialization|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSRecursiveLock|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSRegularExpression|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSRLEArray|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScanner|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptArgumentDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptClassDescriptionMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCoercionHandler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommand|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommandConstructionContext|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommandDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommandDescriptionMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommandMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptEnumeratorDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptExecutionContext|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptExecutionContextMoreIVars|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptingAppleEventHandler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptObjectSpecifier|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptPropertyDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptRecordFieldDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptSDEFElement|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptSDEFParser|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptSuiteDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptSuiteRegistry|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptSynonymDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptTypeDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptWhoseTest|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSecurityScopedURLWrapper|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSerializer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSetChange|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSmartPunctuationController|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSmartQuoteOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSortDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSpellServer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSString|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTask|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTermOfAddress|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTextCheckingKeyEvent|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTextCheckingResult|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSThread|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUbiquitousKeyValueStore|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUndoManager|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUnit|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUnitConverter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLComponents|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLConnectionDelegateProxy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLFileTypeMappings|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLHandle|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLHostNameAddressInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLKeyValuePair|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLPromisePair|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLQueryItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLQueue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLQueueNode|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserActivity|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotification|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotificationAction|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotificationCenter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserScriptTask|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserScriptTaskRunner|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserScriptTaskServiceDelegate|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUUID|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSValue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSValueTransformer|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLContext|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLNode|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLParser|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLSAXParser|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLSchemaType|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLTidy|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLTreeReader|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXPCConnection|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXPCInterface|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXPCListener|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXPCListenerEndpoint|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSZipFileArchive|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|Protocol|/usr/lib/libobjc.A.dylib +Objective-C Class|UIKit_PKSubsystem|/System/Library/Frameworks/Foundation.framework/Foundation + +## conformers of NSCoding (9) +Objective-C Class|NSDecimalNumberHandler|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDistantObject|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFormatter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSNotification|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommand|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptCommandDescription|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptObjectSpecifier|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScriptWhoseTest|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotification|/System/Library/Frameworks/Foundation.framework/Foundation + +## conformers of NSCopying (68) +Objective-C Class|_NSAttributedStringGrammar|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSAttributedStringGrammarInflection|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueObjectAndKeyPair|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSKeyValueObjectBox|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSLocalizedStringResource|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPersonNameComponentsData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSPersonNameComponentsFormatterData|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSSharedKeySetS|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSStringFormattingOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUndoActionInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|_NSUUIDBridge|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAffineTransform|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleEventDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAppleScript|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSArrayChanges|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedString|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedStringMarkdownParsingOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSAttributedStringMarkdownSourcePosition|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSCharacterSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDateInterval|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSDocInfo|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSError|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExpression|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionContext|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSExtensionItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSFormatter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSHashTable|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSIndexPath|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSIndexSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSInflectionRule|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSItemProvider|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSItemRepresentationLoadResult|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSKeyValueProperty|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSLocalizedNumberFormatRule|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMapTable|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMeasurement|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphology|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphologyCustomPronoun|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMorphologyPronoun|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSMutableCharacterSet|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSNotification|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrderedSetChanges|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSOrthography|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPersonNameComponents|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPersonNameComponentsFormatter|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPointerArray|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPointerFunctions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPredicate|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPredicateOperator|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSPresentationIntent|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSRegularExpression|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSRLEArray|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSScanner|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSetChanges|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSmartQuoteOptions|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSSortDescriptor|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSString|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTermOfAddress|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTextCheckingKeyEvent|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSTextCheckingResult|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUnit|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLComponents|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSURLQueryItem|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotification|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUserNotificationAction|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSUUID|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSValue|/System/Library/Frameworks/Foundation.framework/Foundation +Objective-C Class|NSXMLNode|/System/Library/Frameworks/Foundation.framework/Foundation +