Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/typed throws support #1401

Conversation

alexandre-pod
Copy link
Contributor

@alexandre-pod alexandre-pod commented Feb 15, 2025

This PR solves #1371 by adding basic support for Typed Throws (SE-0413) introduced with Swift 6 and usable without Swift 6 language mode.

Here a recap of what has been added to support typed throws:

  • Updating swift-syntax to 600.0.1 (from 510.0.3) for the new syntax
  • Adding throwsTypeName to Method, Variable, Subscript and Closure to expose the optional throws type
  • Adding isThrowsTypeGeneric to Method, to tell if the thrown type error is a generic parameter
  • Adding isNever on TypeName to facilitate the handling of throws(Never)
  • Updating AutoMockable.stencil to add support for typed throws. Typed throws in subscript and generic constrained typed throws are not supported, but the generated mocks correctly conforms to the protocol

Usage example

Protocol we want to mock

protocol AutoMockable {}
struct CustomError: Error {}

protocol TypedThrowableProtocol: AutoMockable {
    var value: Int { get throws(CustomError) }
    func doOrThrow() throws(CustomError) -> String
    func doOrThrowAnyError() throws(any Error)
    func doOrRethrows<E>(_ block: () throws(E) -> Void) throws(E) -> Int where E: Error
}
Generated code from AutoMockable
class TypedThrowableProtocolMock: TypedThrowableProtocol {


    var valueCallsCount = 0
    var valueCalled: Bool {
        return valueCallsCount > 0
    }

    var value: Int {
        get throws(CustomError) {
            valueCallsCount += 1
            if let error = valueThrowableError {
                throw error
            }
            if let valueClosure = valueClosure {
                return try valueClosure()
            } else {
                return underlyingValue
            }
        }
    }
    var underlyingValue: Int!
    var valueThrowableError: (CustomError)?
    var valueClosure: (() throws(CustomError) -> Int)?


    //MARK: - doOrThrow

    var doOrThrowStringThrowableError: (CustomError)?
    var doOrThrowStringCallsCount = 0
    var doOrThrowStringCalled: Bool {
        return doOrThrowStringCallsCount > 0
    }
    var doOrThrowStringReturnValue: String!
    var doOrThrowStringClosure: (() throws(CustomError) -> String)?

    func doOrThrow() throws(CustomError) -> String {
        doOrThrowStringCallsCount += 1
        if let error = doOrThrowStringThrowableError {
            throw error
        }
        if let doOrThrowStringClosure = doOrThrowStringClosure {
            return try doOrThrowStringClosure()
        } else {
            return doOrThrowStringReturnValue
        }
    }

    //MARK: - doOrThrowAnyError

    var doOrThrowAnyErrorVoidThrowableError: (any Error)?
    var doOrThrowAnyErrorVoidCallsCount = 0
    var doOrThrowAnyErrorVoidCalled: Bool {
        return doOrThrowAnyErrorVoidCallsCount > 0
    }
    var doOrThrowAnyErrorVoidClosure: (() throws(any Error) -> Void)?

    func doOrThrowAnyError() throws(any Error) {
        doOrThrowAnyErrorVoidCallsCount += 1
        if let error = doOrThrowAnyErrorVoidThrowableError {
            throw error
        }
        try doOrThrowAnyErrorVoidClosure?()
    }

    //MARK: - doOrRethrows<E>


    func doOrRethrows<E>(_ block: () throws(E) -> Void) throws(E) -> Int where E: Error {
        fatalError("Generic typed throws are not fully supported yet")
    }


}

Mock usage example:

let mock = TypedThrowableProtocolMock()
let sut = mock as TypedThrowableProtocol

// Typed Throws variable getter

print("Test value getter")
do {
    mock.underlyingValue = 42
    print(try sut.value)
    mock.valueThrowableError = CustomError()
    print(try sut.value)
} catch {
    print("Expected thrown error", error)
}
print("value call count", mock.valueCallsCount)

// Typed Throws method

print("Test method doOrThrows")

do {
    mock.doOrThrowStringThrowableError = CustomError()
    print(try sut.doOrThrow())
} catch {
    print("Expected thrown error", error)
}
mock.doOrThrowStringThrowableError = nil

mock.doOrThrowStringClosure = { () throws(CustomError) in
    return "String return value"
}
print(try! sut.doOrThrow())

do {
    mock.doOrThrowStringClosure = { () throws(CustomError) in
        throw CustomError()
    }
    print(try sut.doOrThrow())
} catch {
    print("Expected thrown error", error)
}

// Output:
// Test value getter
// 42
// Expected thrown error CustomError()
// value call count 2
// Test method doOrThrows
// Expected thrown error CustomError()
// String return value
// Expected thrown error CustomError()

Limitations

  1. Methods using generics to define thrown error, like func doOrRethrows<E>(_ block: () throws(E) -> Void) throws(E) -> Int where E: Error are not supported as handling those error types are not easy. Also The where clause was causing me an issue because it was included in the return type, this causing issues with generated variables used to control mock behavior.
  2. Inits using generics to define thrown error, like init<E>() throws(E) where E: Error, for the same reason as Methods
  3. Typed throws in subscript, because subscript are not supported right now in AutoMockable

Even if those limitation does not allow complete configuration of mocked methods, this makes possible the generation of a mock class that still allow compilation. Those limitations can be managed manually by subclassing the generated mock for now, and it should be possible to handle that in AutoMockable but right now it is too much work for me.

If in the future we want to support those generic types, an approach could be to attempt the generation of this code:

This is NOT what is proposed in this Pull Request, but may be useful for a future evolution
protocol TypedThrowableNotYetSupportedProtocol: AutoMockable {
    func doOrRethrows<E>(_ block: () throws(E) -> Void) throws(E) -> Int where E: Error
}

class TypedThrowableNotYetSupportedProtocolMock: TypedThrowableNotYetSupportedProtocol {

    //MARK: - doOrRethrows<E>

    var doOrRethrowsEBlockThrowsEVoidIntWhereEErrorThrowableError: (any Error)?
    var doOrRethrowsEBlockThrowsEVoidIntWhereEErrorCallsCount = 0
    var doOrRethrowsEBlockThrowsEVoidIntWhereEErrorCalled: Bool {
        return doOrRethrowsEBlockThrowsEVoidIntWhereEErrorCallsCount > 0
    }
    var doOrRethrowsEBlockThrowsEVoidIntWhereEErrorReturnValue: Int!
    var doOrRethrowsEBlockThrowsEVoidIntWhereEErrorClosure: ((() throws(any Error) -> Void) throws(any Error) -> Int)?

    func doOrRethrows<E>(_ block: () throws(E) -> Void) throws(E) -> Int where E: Error {
        doOrRethrowsEBlockThrowsEVoidIntWhereEErrorCallsCount += 1
        if let error = doOrRethrowsEBlockThrowsEVoidIntWhereEErrorThrowableError {
            throw error as! E
        }
        if let doOrRethrowsEBlockThrowsEVoidIntWhereEErrorClosure = doOrRethrowsEBlockThrowsEVoidIntWhereEErrorClosure {
            do {
            	return try doOrRethrowsEBlockThrowsEVoidIntWhereEErrorClosure(block)
        	} catch {
        		throw error as! E
        	}
        } else {
            return doOrRethrowsEBlockThrowsEVoidIntWhereEErrorReturnValue
        }
    }
}

let mockNotYetSupported = TypedThrowableNotYetSupportedProtocolMock()

mockNotYetSupported.doOrRethrowsEBlockThrowsEVoidIntWhereEErrorThrowableError = CustomError()
mockNotYetSupported.doOrRethrowsEBlockThrowsEVoidIntWhereEErrorClosure = { (block) throws(any Error) -> Int in
	throw CustomError()
}
do {
	_ = try mockNotYetSupported.doOrRethrows { () throws(CustomError) in
		throw CustomError()
	}
} catch {
	print("Expected throws", error)
}

@krzysztofzablocki krzysztofzablocki merged commit d2e3d8b into krzysztofzablocki:master Feb 18, 2025
2 checks passed
@krzysztofzablocki
Copy link
Owner

krzysztofzablocki commented Feb 18, 2025

great work, thank you🙇

@alvaromurillo
Copy link

Hi, first of all, thanks for merging this PR! I was wondering when the next release including these changes is planned. Do you have an estimated date for it?

Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants