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

Generate mock method with Typed Throw #262

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.library(name: "MockoloFramework", targets: ["MockoloFramework"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.1"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-prerelease-2024-06-12"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to use ThrowsClauseSyntax, I updated swift-syntax.

.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
],
targets: [
Expand Down
15 changes: 13 additions & 2 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ extension FunctionDeclSyntax {
genericTypeParams: genericTypeParams,
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsSpecifier?.text,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsClause?.text,
asyncOrReasync: self.signature.effectSpecifiers?.asyncSpecifier?.text,
isStatic: isStatic,
offset: self.offset,
Expand Down Expand Up @@ -473,7 +473,7 @@ extension InitializerDeclSyntax {
genericTypeParams: genericTypeParams,
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsSpecifier?.text,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsClause?.text,
asyncOrReasync: self.signature.effectSpecifiers?.asyncSpecifier?.text,
isStatic: false,
offset: self.offset,
Expand Down Expand Up @@ -570,6 +570,17 @@ extension TypeAliasDeclSyntax {
}
}

extension ThrowsClauseSyntax {

var text: String {
if let type {
"\(throwsSpecifier.text)(\(type.description))"
fummicc1 marked this conversation as resolved.
Show resolved Hide resolved
} else {
throwsSpecifier.text
}
}
}

final class EntityVisitor: SyntaxVisitor {
var entities: [Entity] = []
var imports: [String: [String]] = [:]
Expand Down
4 changes: 2 additions & 2 deletions Sources/MockoloFramework/Utils/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ extension String {
///
"""


var hasThrowsOrRethrows: Bool {
return components(separatedBy: .whitespaces).contains { component in
return component == .throws || component == .rethrows
let hasTypedThrow = hasPrefix(String.throws.withLeftParen) && hasSuffix(")")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have trailing trivia to throws keyword so this expression is not enough.

func f() throws (any Error) {
}

(However, it will work because the component == .throws clause matches 😓)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be the time to extract this from the FunctionEffectSpecifiersSyntax instead of the String.

return component == .throws || hasTypedThrow || component == .rethrows
}
}

Expand Down
30 changes: 29 additions & 1 deletion Sources/MockoloFramework/Utils/TypeParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,25 @@ public final class `Type` {
return mutableArg
}

/// Parse throws clause from suffix and return typed-throw's type.
///
/// - Returns: if typed-throw is used, returns its concrete type, unless returns nil.
static func extractTypedThrow(
suffix: String
) -> String? {
return suffix
.components(separatedBy: .whitespaces)
.compactMap { clause in
guard let prefixRange = clause.range(of: "\(String.throws)(") else {
return nil
}
let endIndex = clause.dropLast().endIndex
return String(
clause[prefixRange.upperBound..<endIndex]
)
}
.first
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if splitting by .whitespace is suitable or not. but this code works in added test-cases.

As far as I investigated,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throws(any Error) is entirely possible.

The Swift's syntax has become more than complicated enough to look these up from String alone.
More structured information should be received from the arguments.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your information.

throws(any Error) is entirely possible.

I did not know any Error can become throws type. I need to modify the implementation.

The Swift's syntax has become more than complicated enough to look these up from String alone.
More structured information should be received from the arguments.

Absolutely. I felt like suffix variable in MethodModel should not be String but more structured type.


static func toClosureType(with params: [Type], typeParams: [String], suffix: String, returnType: Type, encloser: String) -> Type {

Expand Down Expand Up @@ -558,9 +577,18 @@ public final class `Type` {
displayableReturnType = "(\(displayableReturnType))"
}

let hasThrowsOrRethrows = suffix.hasThrowsOrRethrows
let typedThrowTypeName = extractTypedThrow(suffix: suffix)

let thrownSuffix: String = if let typedThrowTypeName {
"\(String.throws)(\(typedThrowTypeName))"
} else {
String.throws
}

let suffixStr = [
suffix.hasAsync ? String.async + " " : nil,
suffix.hasThrowsOrRethrows ? String.throws + " " : nil,
hasThrowsOrRethrows ? thrownSuffix + " " : nil,
].compactMap { $0 }.joined()

let typeStr = "((\(displayableParamStr)) \(suffixStr)-> \(displayableReturnType))?"
Expand Down
32 changes: 22 additions & 10 deletions Tests/TestFuncs/TestFuncThrow/FixtureFuncThrow.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import MockoloFramework


let funcThrow2 = """
import Foundation

/// \(String.mockAnnotation)
protocol FuncThrow {
func update<T>(arg1: T, arg2: () throws -> T) rethrows -> T
func update<T, U>(arg1: T, arg2: @escaping (U) throws -> ()) throws -> ((T) -> (U))
}
"""

Comment on lines -4 to -13
Copy link
Collaborator Author

@fummicc1 fummicc1 Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI

This test-fixture is not used and is covered by funcThrow test-fixture. So I deleted it.

let funcThrow = """
import Foundation

/// \(String.mockAnnotation)
protocol FuncThrow {
func f1(arg: Int) throws -> String
func f2(arg: Int) throws
func f3(arg: Int) throws(SomeError)
func f4(arg: Int) throws(SomeError) -> String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add other possible patterns?

  • f() throws (MyError)
  • f() async throws(any Error)

func g1(arg: (Int) throws -> ())
throws -> String
func g2(arg: (Int) throws -> ()) throws
Expand Down Expand Up @@ -57,6 +49,26 @@ class FuncThrowMock: FuncThrow {

}

private(set) var f3CallCount = 0
var f3Handler: ((Int) throws(SomeError) -> ())?
func f3(arg: Int) throws(SomeError) {
f3CallCount += 1
if let f3Handler = f3Handler {
try f3Handler(arg)
}

}

private(set) var f4CallCount = 0
var f4Handler: ((Int) throws(SomeError) -> (String))?
func f4(arg: Int) throws(SomeError) -> String {
f4CallCount += 1
if let f4Handler = f4Handler {
return try f4Handler(arg)
}
return ""
}

private(set) var g1CallCount = 0
var g1Handler: (((Int) throws -> ()) throws -> (String))?
func g1(arg: (Int) throws -> ()) throws -> String {
Expand Down
40 changes: 40 additions & 0 deletions Tests/TestInit/FixtureInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,43 @@ class MyProtocolMock: MyProtocol {

}
"""

let throwableInit = """
/// \(String.mockAnnotation)
protocol MyProtocol {
init(param: String) throws
}
"""

let throwableInitMock = """



class MyProtocolMock: MyProtocol {
private var _param: String!
init() { }
required init(param: String = "") throws {
self._param = param
}
}
"""

let typedThrowableInit = """
/// \(String.mockAnnotation)
protocol MyProtocol {
init(param: String) throws(SomeError)
}
"""

let typedThrowableInitMock = """



class MyProtocolMock: MyProtocol {
private var _param: String!
init() { }
required init(param: String = "") throws(SomeError) {
self._param = param
}
}
"""
14 changes: 14 additions & 0 deletions Tests/TestInit/InitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,18 @@ class InitTests: MockoloTestCase {
dstContent: initWithSameParamNameButDifferentTypeMock
)
}

func testThrowableInit() {
verify(
srcContent: throwableInit,
dstContent: throwableInitMock
)
}

func testTypedThrowableInit() {
verify(
srcContent: typedThrowableInit,
dstContent: typedThrowableInitMock
)
}
}