diff --git a/Example/InterposeKitExample/Sources/AppDelegate.swift b/Example/InterposeKitExample/Sources/AppDelegate.swift index f5cbd63..2fe098f 100644 --- a/Example/InterposeKitExample/Sources/AppDelegate.swift +++ b/Example/InterposeKitExample/Sources/AppDelegate.swift @@ -64,6 +64,31 @@ class AppDelegate: NSObject, NSApplicationDelegate { ) -> Hook { do { switch example { + case .NSWindow_setTitle: + return try Interpose.prepareHook( + on: self.window, + for: #selector(setter: NSWindow.title), + methodSignature: (@convention(c) (NSWindow, Selector, String) -> Void).self, + hookSignature: (@convention(block) (NSWindow, String) -> Void).self + ) { hook in + return { `self`, title in + hook.original(self, hook.selector, "## \(title.uppercased()) ##") + } + } + case .NSWindow_miniaturize: + return try Interpose.prepareHook( + on: self.window, + for: #selector(NSWindow.miniaturize(_:)), + methodSignature: (@convention(c) (NSWindow, Selector, Any?) -> Void).self, + hookSignature: (@convention(block) (NSWindow, Any?) -> Void).self + ) { hook in + return { `self`, sender in + let alert = NSAlert() + alert.messageText = "Miniaturization Intercepted" + alert.informativeText = "This window refused to minimize because a hook was applied to\n -[NSWindow miniaturize:]." + alert.runModal() + } + } case .NSApplication_sendEvent: return try Interpose.prepareHook( on: NSApplication.shared, @@ -76,17 +101,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { hook.original(self, hook.selector, event) } } - case .NSWindow_setTitle: - return try Interpose.prepareHook( - on: self.window, - for: #selector(setter: NSWindow.title), - methodSignature: (@convention(c) (NSWindow, Selector, String) -> Void).self, - hookSignature: (@convention(block) (NSWindow, String) -> Void).self - ) { hook in - return { `self`, title in - hook.original(self, hook.selector, "## \(title.uppercased()) ##") - } - } case .NSMenuItem_title: return try Interpose.prepareHook( on: NSMenuItem.self, @@ -99,8 +113,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { return "## \(title) ##" } } - case .NSColor_controlAccentColor: - fatalError("Not implemented") + case .NSColor_labelColor: + return try Interpose.prepareHook( + on: NSColor.self, + for: #selector(getter: NSColor.labelColor), + methodKind: .class, + methodSignature: (@convention(c) (NSColor.Type, Selector) -> NSColor).self, + hookSignature: (@convention(block) (NSColor.Type) -> NSColor).self + ) { hook in + return { `self` in + return self.systemPink + } + } } } catch { fatalError("\(error)") diff --git a/Example/InterposeKitExample/Sources/ContentView.swift b/Example/InterposeKitExample/Sources/ContentView.swift index 6f84998..7278179 100644 --- a/Example/InterposeKitExample/Sources/ContentView.swift +++ b/Example/InterposeKitExample/Sources/ContentView.swift @@ -44,16 +44,13 @@ struct ContentView: View { .labelsHidden() .padding(.leading, 20) } label: { - Group { - Text(example.selector) - .monospaced() - - Text(example.description) - .font(.subheadline) - } - .opacity(example == .NSColor_controlAccentColor ? 0.5 : 1) + Text(example.selector) + .monospaced() + .foregroundStyle(Color(NSColor.labelColor)) + + Text(example.description) + .font(.subheadline) } - .disabled(example == .NSColor_controlAccentColor) } } } diff --git a/Example/InterposeKitExample/Sources/HookExample.swift b/Example/InterposeKitExample/Sources/HookExample.swift index 167b57c..048dd8c 100644 --- a/Example/InterposeKitExample/Sources/HookExample.swift +++ b/Example/InterposeKitExample/Sources/HookExample.swift @@ -1,45 +1,52 @@ enum HookExample: CaseIterable { - case NSApplication_sendEvent case NSWindow_setTitle + case NSWindow_miniaturize + case NSApplication_sendEvent case NSMenuItem_title - case NSColor_controlAccentColor + case NSColor_labelColor } extension HookExample { var selector: String { switch self { - case .NSApplication_sendEvent: - return "-[NSApplication sendEvent:]" case .NSWindow_setTitle: return "-[NSWindow setTitle:]" + case .NSWindow_miniaturize: + return "-[NSWindow miniaturize:]" + case .NSApplication_sendEvent: + return "-[NSApplication sendEvent:]" case .NSMenuItem_title: return "-[NSMenuItem title]" - case .NSColor_controlAccentColor: - return "+[NSColor controlAccentColor]" + case .NSColor_labelColor: + return "+[NSColor labelColor]" } } var description: String { switch self { - case .NSApplication_sendEvent: - return """ - An object hook on the shared NSApplication instance that logs all events passed \ - through sendEvent(_:). - """ case .NSWindow_setTitle: return """ An object hook on the main NSWindow that uppercases the title and wraps it with \ decorative markers whenever it’s set. This can be tested using the text field below. """ + case .NSWindow_miniaturize: + return """ + An object hook on the main NSWindow that intercepts miniaturization and shows \ + an alert instead of minimizing the window. + """ + case .NSApplication_sendEvent: + return """ + A class hook on NSApplication that logs all events passed through sendEvent(_:). + """ case .NSMenuItem_title: return """ A class hook on NSMenuItem that wraps all menu item titles with decorative markers, \ visible in the main menu and the text field’s context menu. """ - case .NSColor_controlAccentColor: + case .NSColor_labelColor: return """ - A class hook that overrides the system accent color by hooking the corresponding \ - class method on NSColor. (Not implemented.) + A class hook that overrides the standard label color by hooking the corresponding \ + class method on NSColor. Affects text in this window and menus. """ } } diff --git a/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift b/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift index d1ec95d..1c0a114 100644 --- a/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift +++ b/Sources/InterposeKit/Deprecated/NSObject+Deprecated.swift @@ -35,7 +35,7 @@ extension NSObject { _ build: @escaping HookBuilder ) throws -> Hook { let hook = try Hook( - target: .class(self), + target: .class(self, .instance), selector: selector, build: build ) diff --git a/Sources/InterposeKit/Hooks/Hook.swift b/Sources/InterposeKit/Hooks/Hook.swift index bce4c9f..fb9a31e 100644 --- a/Sources/InterposeKit/Hooks/Hook.swift +++ b/Sources/InterposeKit/Hooks/Hook.swift @@ -71,13 +71,14 @@ public final class Hook { } switch target { - case .class(let `class`): + case let .class(`class`, methodKind): return ClassHookStrategy( class: `class`, + methodKind: methodKind, selector: selector, makeHookIMP: makeHookIMP ) - case .object(let object): + case let .object(object): return ObjectHookStrategy( object: object, selector: selector, @@ -222,7 +223,8 @@ extension Hook: CustomDebugStringConvertible { case .failed: description += "Failed" } - description.append(" hook for -[\(self.class) \(self.selector)]") + let symbolPrefix = self.scope.methodKind.symbolPrefix + description.append(" hook for \(symbolPrefix)[\(self.class) \(self.selector)]") if case .object(let object) = self.scope { description.append(" on \(Unmanaged.passUnretained(object).toOpaque())") @@ -236,16 +238,6 @@ extension Hook: CustomDebugStringConvertible { } } -public enum HookScope { - - /// The scope that targets all instances of the class. - case `class` - - /// The scope that targets a specific instance of the class. - case object(NSObject) - -} - public enum HookState: Equatable { /// The hook is ready to be applied. @@ -259,7 +251,13 @@ public enum HookState: Equatable { } +/// Represents the target of a hook operation—either a class type or a specific object instance. internal enum HookTarget { - case `class`(AnyClass) + + /// A hook targeting a method defined on a class, either an instance method or a class method. + case `class`(AnyClass, MethodKind) + + /// A hook targeting a method on a specific object instance. case object(NSObject) + } diff --git a/Sources/InterposeKit/Hooks/HookScope.swift b/Sources/InterposeKit/Hooks/HookScope.swift new file mode 100644 index 0000000..8beb6c6 --- /dev/null +++ b/Sources/InterposeKit/Hooks/HookScope.swift @@ -0,0 +1,25 @@ +import ObjectiveC + +public enum HookScope { + + /// The scope that targets a method on a class type (instance or class method). + case `class`(MethodKind) + + /// The scope that targets a specific object instance. + case object(NSObject) + +} + +extension HookScope { + + /// Returns the kind of the method targeted by the hook scope. + public var methodKind: MethodKind { + switch self { + case .class(let methodKind): + return methodKind + case .object: + return .instance + } + } + +} diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift index 8acb4a8..ceddf18 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ClassHookStrategy/ClassHookStrategy.swift @@ -8,11 +8,13 @@ internal final class ClassHookStrategy: HookStrategy { internal init( `class`: AnyClass, + methodKind: MethodKind, selector: Selector, makeHookIMP: @escaping () -> IMP ) { self.class = `class` self.selector = selector + self.methodKind = methodKind self.makeHookIMP = makeHookIMP } @@ -21,9 +23,10 @@ internal final class ClassHookStrategy: HookStrategy { // ============================================================================ // internal let `class`: AnyClass - internal var scope: HookScope { .class } + internal var scope: HookScope { .class(self.methodKind) } internal let selector: Selector + private let methodKind: MethodKind private let makeHookIMP: () -> IMP // ============================================================================ // @@ -33,23 +36,41 @@ internal final class ClassHookStrategy: HookStrategy { private(set) internal var appliedHookIMP: IMP? private(set) internal var storedOriginalIMP: IMP? + // ============================================================================ // + // MARK: Target Class + // ============================================================================ // + + /// The target class resolved for the configured method kind. + /// + /// This is the class itself for instance methods, or the metaclass for class methods. + private lazy var targetClass: AnyClass = { + switch self.methodKind { + case .instance: + return self.class + case .class: + return object_getClass(self.class) + } + }() + // ============================================================================ // // MARK: Validation // ============================================================================ // internal func validate() throws { // Ensure that the method exists. - guard class_getInstanceMethod(self.class, self.selector) != nil else { + guard class_getInstanceMethod(self.targetClass, self.selector) != nil else { throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } // Ensure that the class directly implements the method. - guard class_implementsInstanceMethod(self.class, self.selector) else { + guard class_implementsInstanceMethod(self.targetClass, self.selector) else { throw InterposeError.methodNotDirectlyImplemented( class: self.class, + kind: self.methodKind, selector: self.selector ) } @@ -62,17 +83,18 @@ internal final class ClassHookStrategy: HookStrategy { internal func replaceImplementation() throws { let hookIMP = self.makeHookIMP() - guard let method = class_getInstanceMethod(self.class, self.selector) else { + guard let method = class_getInstanceMethod(self.targetClass, self.selector) else { // This should not happen under normal circumstances, as we perform validation upon // creating the hook strategy, which itself checks for the presence of the method. throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } guard let originalIMP = class_replaceMethod( - self.class, + self.targetClass, self.selector, hookIMP, method_getTypeEncoding(method) @@ -80,7 +102,8 @@ internal final class ClassHookStrategy: HookStrategy { // This should not happen under normal circumstances, as we perform validation upon // creating the hook strategy, which checks if the class directly implements the method. throw InterposeError.implementationNotFound( - class: self.class, + class: self.targetClass, + kind: self.methodKind, selector: self.selector ) } @@ -88,7 +111,10 @@ internal final class ClassHookStrategy: HookStrategy { self.appliedHookIMP = hookIMP self.storedOriginalIMP = originalIMP - Interpose.log("Replaced implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP) -> \(hookIMP)") + Interpose.log({ + let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]" + return "Replaced implementation for \(selector) IMP: \(originalIMP) -> \(hookIMP)" + }()) } internal func restoreImplementation() throws { @@ -101,15 +127,16 @@ internal final class ClassHookStrategy: HookStrategy { self.storedOriginalIMP = nil } - guard let method = class_getInstanceMethod(self.class, self.selector) else { + guard let method = class_getInstanceMethod(self.targetClass, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: self.methodKind, selector: self.selector ) } let previousIMP = class_replaceMethod( - self.class, + self.targetClass, self.selector, originalIMP, method_getTypeEncoding(method) @@ -118,12 +145,16 @@ internal final class ClassHookStrategy: HookStrategy { guard previousIMP == hookIMP else { throw InterposeError.revertCorrupted( class: self.class, + kind: self.methodKind, selector: self.selector, imp: previousIMP ) } - Interpose.log("Restored implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP)") + Interpose.log({ + let selector = "\(self.methodKind.symbolPrefix)[\(self.class) \(self.selector)]" + return "Restored implementation for \(selector) IMP: \(originalIMP)" + }()) } } diff --git a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift index 0c294fb..576f976 100644 --- a/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift +++ b/Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift @@ -50,6 +50,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard class_getInstanceMethod(self.class, self.selector) != nil else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -58,6 +59,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard self.lookUpIMP() != nil else { throw InterposeError.implementationNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -92,6 +94,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let method = class_getInstanceMethod(self.class, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -137,6 +140,7 @@ internal final class ObjectHookStrategy: HookStrategy { // or an existing hook. throw InterposeError.implementationNotFound( class: subclass, + kind: .instance, selector: self.selector ) } @@ -171,6 +175,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let method = class_getInstanceMethod(self.class, self.selector) else { throw InterposeError.methodNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -178,6 +183,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard let currentIMP = class_getMethodImplementation(dynamicSubclass, self.selector) else { throw InterposeError.implementationNotFound( class: self.class, + kind: .instance, selector: self.selector ) } @@ -194,6 +200,7 @@ internal final class ObjectHookStrategy: HookStrategy { guard previousIMP == hookIMP else { throw InterposeError.revertCorrupted( class: dynamicSubclass, + kind: .instance, selector: self.selector, imp: previousIMP ) diff --git a/Sources/InterposeKit/Interpose.swift b/Sources/InterposeKit/Interpose.swift index a2f7c3d..0c5bcef 100644 --- a/Sources/InterposeKit/Interpose.swift +++ b/Sources/InterposeKit/Interpose.swift @@ -14,12 +14,13 @@ public enum Interpose { public static func prepareHook( on `class`: AnyClass, for selector: Selector, + methodKind: MethodKind = .instance, methodSignature: MethodSignature.Type, hookSignature: HookSignature.Type, build: @escaping HookBuilder ) throws -> Hook { try Hook( - target: .class(`class`), + target: .class(`class`, methodKind), selector: selector, build: build ) @@ -29,6 +30,7 @@ public enum Interpose { public static func applyHook( on `class`: AnyClass, for selector: Selector, + methodKind: MethodKind = .instance, methodSignature: MethodSignature.Type, hookSignature: HookSignature.Type, build: @escaping HookBuilder @@ -36,6 +38,7 @@ public enum Interpose { let hook = try prepareHook( on: `class`, for: selector, + methodKind: methodKind, methodSignature: methodSignature, hookSignature: hookSignature, build: build diff --git a/Sources/InterposeKit/InterposeError.swift b/Sources/InterposeKit/InterposeError.swift index 0211b67..ca3486b 100644 --- a/Sources/InterposeKit/InterposeError.swift +++ b/Sources/InterposeKit/InterposeError.swift @@ -5,35 +5,38 @@ public enum InterposeError: Error, @unchecked Sendable { /// A hook operation failed and the hook is no longer usable. case hookInFailedState - /// No instance method found for the selector on the specified class. + /// No method of the given kind found for the selector on the specified class. /// - /// This typically occurs when mistyping a stringified selector or attempting to interpose - /// a class method, which is not supported. + /// This typically occurs when mistyping a stringified selector or attempting to hook + /// a method that does not exist on the class. case methodNotFound( class: AnyClass, + kind: MethodKind, selector: Selector ) - - /// The method for the selector is not directly implemented on the specified class - /// but inherited from a superclass. + + /// The method for the selector is inherited from a superclass rather than directly implemented + /// by the specified class. /// - /// Class-based interposing only supports instance methods implemented directly by the class - /// itself. This restriction ensures safe reverting via `revert()`, which cannot remove - /// dynamically added methods. + /// Class-based interposing only supports methods directly implemented by the class itself. + /// This restriction ensures safe reverting via `revert()`, which cannot remove dynamically + /// added methods. /// /// To interpose this method, consider hooking the superclass that provides the implementation, /// or use object-based hooking on a specific instance instead. case methodNotDirectlyImplemented( class: AnyClass, + kind: MethodKind, selector: Selector ) - - /// No implementation found for the method matching the specified selector on the class. + + /// No implementation found for a method of the given kind matching the selector on the class. /// /// This should not occur under normal conditions and may indicate an invalid or misconfigured /// runtime state. case implementationNotFound( class: AnyClass, + kind: MethodKind, selector: Selector ) @@ -44,6 +47,7 @@ public enum InterposeError: Error, @unchecked Sendable { /// In such cases, `Hook.revert()` is unsafe and should be avoided. case revertCorrupted( class: AnyClass, + kind: MethodKind, selector: Selector, imp: IMP? ) @@ -102,34 +106,43 @@ public enum InterposeError: Error, @unchecked Sendable { extension InterposeError: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { switch lhs { - case let .methodNotFound(lhsClass, lhsSelector): + case let .methodNotFound(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .methodNotFound(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .methodNotFound(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .methodNotDirectlyImplemented(lhsClass, lhsSelector): + case let .methodNotDirectlyImplemented(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .methodNotDirectlyImplemented(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .methodNotDirectlyImplemented(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .implementationNotFound(lhsClass, lhsSelector): + case let .implementationNotFound(lhsClass, lhsKind, lhsSelector): switch rhs { - case let .implementationNotFound(rhsClass, rhsSelector): - return lhsClass == rhsClass && lhsSelector == rhsSelector + case let .implementationNotFound(rhsClass, rhsKind, rhsSelector): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector default: return false } - case let .revertCorrupted(lhsClass, lhsSelector, lhsIMP): + case let .revertCorrupted(lhsClass, lhsKind, lhsSelector, lhsIMP): switch rhs { - case let .revertCorrupted(rhsClass, rhsSelector, rhsIMP): - return lhsClass == rhsClass && lhsSelector == rhsSelector && lhsIMP == rhsIMP + case let .revertCorrupted(rhsClass, rhsKind, rhsSelector, rhsIMP): + return lhsClass == rhsClass + && lhsKind == rhsKind + && lhsSelector == rhsSelector + && lhsIMP == rhsIMP default: return false } diff --git a/Sources/InterposeKit/MethodKind.swift b/Sources/InterposeKit/MethodKind.swift new file mode 100644 index 0000000..d35dbaa --- /dev/null +++ b/Sources/InterposeKit/MethodKind.swift @@ -0,0 +1,24 @@ +public enum MethodKind: Equatable { + + /// An instance method, e.g. `-[MyClass doSomething]`. + case instance + + /// A class method, e.g. `+[MyClass doSomething]`. + case `class` + +} + +extension MethodKind { + + /// Returns the Objective-C method prefix symbol for this kind, `-` for instance methods + /// and `+` for class methods. + internal var symbolPrefix: String { + switch self { + case .instance: + return "-" + case .class: + return "+" + } + } + +} diff --git a/Tests/InterposeKitTests/ClassHookTests.swift b/Tests/InterposeKitTests/ClassHookTests.swift index f00d3a8..3dbb668 100644 --- a/Tests/InterposeKitTests/ClassHookTests.swift +++ b/Tests/InterposeKitTests/ClassHookTests.swift @@ -3,6 +3,7 @@ import XCTest fileprivate class ExampleClass: NSObject { @objc static dynamic func doSomethingStatic() {} + @objc static dynamic let intValueStatic = 1 @objc dynamic func doSomething() {} @objc dynamic var intValue = 1 @objc dynamic var arrayValue: [String] { ["base"] } @@ -200,6 +201,38 @@ final class ClassHookTests: XCTestCase { XCTAssertEqual(object.arrayValue, ["base"]) } + func testClassMethod() throws { + let hook = try Interpose.prepareHook( + on: ExampleClass.self, + for: #selector(getter: ExampleClass.intValueStatic), + methodKind: .class, + methodSignature: (@convention(c) (ExampleClass.Type, Selector) -> Int).self, + hookSignature: (@convention(block) (ExampleClass.Type) -> Int).self + ) { hook in + return { `self` in 2 } + } + + XCTAssertEqual(ExampleClass.intValueStatic, 1) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Pending hook for \+\[ExampleClass intValueStatic\]$"# + ) + + try hook.apply() + XCTAssertEqual(ExampleClass.intValueStatic, 2) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Active hook for \+\[ExampleClass intValueStatic\] \(originalIMP: 0x[0-9a-fA-F]+\)$"# + ) + + try hook.revert() + XCTAssertEqual(ExampleClass.intValueStatic, 1) + XCTAssertMatchesRegex( + hook.debugDescription, + #"^Pending hook for \+\[ExampleClass intValueStatic\]$"# + ) + } + func testValidationFailure_methodNotFound_nonExistent() throws { XCTAssertThrowsError( try Interpose.prepareHook( @@ -212,33 +245,36 @@ final class ClassHookTests: XCTestCase { }, expected: InterposeError.methodNotFound( class: ExampleClass.self, + kind: .instance, selector: Selector(("doSomethingNotFound")) ) ) } - func testValidationFailure_methodNotFound_classMethod() throws { + func testValidationFailure_methodNotDirectlyImplemented_instanceMethod() throws { XCTAssertThrowsError( try Interpose.prepareHook( - on: ExampleClass.self, - for: #selector(ExampleClass.doSomethingStatic), + on: ExampleSubclass.self, + for: #selector(ExampleClass.doSomething), methodSignature: (@convention(c) (NSObject, Selector) -> Void).self, hookSignature: (@convention(block) (NSObject) -> Void).self ) { hook in return { `self` in } }, - expected: InterposeError.methodNotFound( - class: ExampleClass.self, - selector: #selector(ExampleClass.doSomethingStatic) + expected: InterposeError.methodNotDirectlyImplemented( + class: ExampleSubclass.self, + kind: .instance, + selector: #selector(ExampleClass.doSomething) ) ) } - func testValidationFailure_methodNotDirectlyImplemented() throws { + func testValidationFailure_methodNotDirectlyImplemented_classMethod() throws { XCTAssertThrowsError( try Interpose.prepareHook( on: ExampleSubclass.self, - for: #selector(ExampleClass.doSomething), + for: #selector(ExampleClass.doSomethingStatic), + methodKind: .class, methodSignature: (@convention(c) (NSObject, Selector) -> Void).self, hookSignature: (@convention(block) (NSObject) -> Void).self ) { hook in @@ -246,7 +282,8 @@ final class ClassHookTests: XCTestCase { }, expected: InterposeError.methodNotDirectlyImplemented( class: ExampleSubclass.self, - selector: #selector(ExampleClass.doSomething) + kind: .class, + selector: #selector(ExampleClass.doSomethingStatic) ) ) } @@ -283,6 +320,7 @@ final class ClassHookTests: XCTestCase { try hook.revert(), expected: InterposeError.revertCorrupted( class: ExampleClass.self, + kind: .instance, selector: #selector(ExampleClass.doSomething), imp: externalIMP )