Skip to content

Commit

Permalink
Add async and throwing property support (#267)
Browse files Browse the repository at this point in the history
* Read accessor and omit setCallCount if protocol has no setter

* Remove unnecessary static var handling

* Support get async and get throws property accessor

* small rename and remove unused code

* generate initialize for computed var

* computed getter handles with the same way of method

* remove deprecated method

* Add test for throwing never

* small formatting

* Update Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift

Co-authored-by: Fumiya Tanaka <[email protected]>

* split syntax representation

---------

Co-authored-by: Fumiya Tanaka <[email protected]>
  • Loading branch information
sidepelican and fummicc1 authored Oct 26, 2024
1 parent 0ab3723 commit 8f458e2
Show file tree
Hide file tree
Showing 26 changed files with 428 additions and 135 deletions.
4 changes: 1 addition & 3 deletions Sources/MockoloFramework/Models/ArgumentsHistoryModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ final class ArgumentsHistoryModel: Model {
var name: String
var type: SwiftType
var offset: Int64 = .max
let suffix: String
let capturableParamNames: [String]
let capturableParamTypes: [SwiftType]
let isHistoryAnnotated: Bool
Expand All @@ -13,15 +12,14 @@ final class ArgumentsHistoryModel: Model {
return .argumentsHistory
}

init?(name: String, genericTypeParams: [ParamModel], params: [ParamModel], isHistoryAnnotated: Bool, suffix: String) {
init?(name: String, genericTypeParams: [ParamModel], params: [ParamModel], isHistoryAnnotated: Bool) {
// Value contains closure is not supported.
let capturables = params.filter { !$0.type.hasClosure && !$0.type.isEscaping && !$0.type.isAutoclosure }
guard !capturables.isEmpty else {
return nil
}

self.name = name + .argsHistorySuffix
self.suffix = suffix
self.isHistoryAnnotated = isHistoryAnnotated

self.capturableParamNames = capturables.map(\.name.safeName)
Expand Down
21 changes: 13 additions & 8 deletions Sources/MockoloFramework/Models/ClosureModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,36 @@ final class ClosureModel: Model {
let genericTypeNames: [String]
let paramNames: [String]
let paramTypes: [SwiftType]
let suffix: String
let isAsync: Bool
let throwing: ThrowingKind

var modelType: ModelType {
return .closure
}


init(name: String, genericTypeParams: [ParamModel], paramNames: [String], paramTypes: [SwiftType], suffix: String, returnType: SwiftType, encloser: String) {
init(name: String, genericTypeParams: [ParamModel], paramNames: [String], paramTypes: [SwiftType], isAsync: Bool, throwing: ThrowingKind, returnType: SwiftType, encloser: String) {
self.name = name + .handlerSuffix
self.suffix = suffix
self.isAsync = isAsync
self.throwing = throwing
let genericTypeNameList = genericTypeParams.map(\.name)
self.genericTypeNames = genericTypeNameList
self.paramNames = paramNames
self.paramTypes = paramTypes
self.funcReturnType = returnType
self.type = SwiftType.toClosureType(with: paramTypes, typeParams: genericTypeNameList, suffix: suffix, returnType: returnType, encloser: encloser)
self.type = SwiftType.toClosureType(
params: paramTypes,
typeParams: genericTypeNameList,
isAsync: isAsync,
throwing: throwing,
returnType: returnType,
encloser: encloser
)
}

func render(with identifier: String, encloser: String, useTemplateFunc: Bool = false, useMockObservable: Bool = false, allowSetCallCount: Bool = false, mockFinal: Bool = false, enableFuncArgsHistory: Bool = false, disableCombineDefaultValues: Bool = false) -> String? {
return applyClosureTemplate(name: identifier + .handlerSuffix,
type: type,
genericTypeNames: genericTypeNames,
paramVals: paramNames,
paramTypes: paramTypes,
suffix: suffix,
returnDefaultType: funcReturnType)
}
}
Expand Down
17 changes: 9 additions & 8 deletions Sources/MockoloFramework/Models/MethodModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ final class MethodModel: Model {
var modelDescription: String? = nil
var isStatic: Bool
let shouldOverride: Bool
let suffix: String
let isAsync: Bool
let throwing: ThrowingKind
let kind: MethodKind
let funcsWithArgsHistory: [String]
let customModifiers: [String : Modifier]
Expand Down Expand Up @@ -134,8 +135,7 @@ final class MethodModel: Model {
let ret = ArgumentsHistoryModel(name: name,
genericTypeParams: genericTypeParams,
params: params,
isHistoryAnnotated: funcsWithArgsHistory.contains(name),
suffix: suffix)
isHistoryAnnotated: funcsWithArgsHistory.contains(name))

return ret
}()
Expand All @@ -151,7 +151,8 @@ final class MethodModel: Model {
genericTypeParams: genericTypeParams,
paramNames: paramNames,
paramTypes: paramTypes,
suffix: suffix,
isAsync: isAsync,
throwing: throwing,
returnType: type,
encloser: encloser)

Expand All @@ -167,8 +168,8 @@ final class MethodModel: Model {
genericTypeParams: [ParamModel],
genericWhereClause: String?,
params: [ParamModel],
throwsOrRethrows: String?,
asyncOrReasync: String?,
isAsync: Bool,
throwing: ThrowingKind,
isStatic: Bool,
offset: Int64,
length: Int64,
Expand All @@ -178,7 +179,8 @@ final class MethodModel: Model {
processed: Bool) {
self.name = name.trimmingCharacters(in: .whitespaces)
self.type = SwiftType(typeName.trimmingCharacters(in: .whitespaces))
self.suffix = [asyncOrReasync, throwsOrRethrows].compactMap { $0 }.joined(separator: " ")
self.isAsync = isAsync
self.throwing = throwing
self.offset = offset
self.length = length
self.kind = kind
Expand Down Expand Up @@ -237,7 +239,6 @@ final class MethodModel: Model {
params: params,
returnType: type,
accessLevel: accessLevel,
suffix: suffix,
argsHistory: argsHistory,
handler: handler(encloser: encloser))
return result
Expand Down
35 changes: 35 additions & 0 deletions Sources/MockoloFramework/Models/ThrowingKind.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

enum ThrowingKind: Equatable {
case none
case any
case `rethrows`
case typed(errorType: String)

var hasError: Bool {
switch self {
case .none:
return false
case .any:
return true
case .rethrows:
return true
case .typed(let errorType):
return errorType != .neverType && errorType != "Swift.\(String.neverType)"
}
}
}
16 changes: 15 additions & 1 deletion Sources/MockoloFramework/Models/VariableModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import Foundation

final class VariableModel: Model {
struct GetterEffects: Equatable {
var isAsync: Bool
var throwing: ThrowingKind
static let empty: GetterEffects = .init(isAsync: false, throwing: .none)
}

enum MockStorageType {
case stored(needsSetCount: Bool)
case computed(GetterEffects)
}

var name: String
var type: SwiftType
var offset: Int64
Expand All @@ -13,6 +24,7 @@ final class VariableModel: Model {
var filePath: String = ""
var isStatic = false
var shouldOverride = false
let storageType: MockStorageType
var rxTypes: [String: String]?
var customModifiers: [String: Modifier]?
var modelDescription: String? = nil
Expand All @@ -29,7 +41,7 @@ final class VariableModel: Model {
}

var underlyingName: String {
if isStatic || type.defaultVal() == nil {
if type.defaultVal() == nil {
return "_\(name)"
}
return name
Expand All @@ -40,6 +52,7 @@ final class VariableModel: Model {
acl: String?,
encloserType: DeclType,
isStatic: Bool,
storageType: MockStorageType,
canBeInitParam: Bool,
offset: Int64,
rxTypes: [String: String]?,
Expand All @@ -51,6 +64,7 @@ final class VariableModel: Model {
self.type = SwiftType(typeName.trimmingCharacters(in: .whitespaces))
self.offset = offset
self.isStatic = isStatic
self.storageType = storageType
self.shouldOverride = encloserType == .classType
self.canBeInitParam = canBeInitParam
self.processed = processed
Expand Down
63 changes: 57 additions & 6 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,44 @@ extension VariableDeclSyntax {
typeName = vtype
}

let storageType: VariableModel.MockStorageType
switch v.accessorBlock?.accessors {
case .accessors(let accessorDecls):
if accessorDecls.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.set) }) {
storageType = .stored(needsSetCount: true)
} else if let getterDecl = accessorDecls.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) {
if getterDecl.body == nil { // is protoccol
var getterEffects = VariableModel.GetterEffects.empty
if getterDecl.effectSpecifiers?.asyncSpecifier != nil {
getterEffects.isAsync = true
}
if let `throws` = getterDecl.effectSpecifiers?.throwsClause {
getterEffects.throwing = .init(`throws`)
}
if getterEffects == .empty {
storageType = .stored(needsSetCount: false)
} else {
storageType = .computed(getterEffects)
}
} else { // is class
storageType = .computed(.empty)
}
} else {
// will never happens
storageType = .stored(needsSetCount: false) // fallback
}
case .getter:
storageType = .computed(.empty)
case nil:
storageType = .stored(needsSetCount: true)
}

let varmodel = VariableModel(name: name,
typeName: typeName,
acl: acl,
encloserType: declType,
isStatic: isStatic,
storageType: storageType,
canBeInitParam: potentialInitParam,
offset: v.offset,
rxTypes: metadata?.varTypes,
Expand Down Expand Up @@ -402,8 +435,8 @@ extension SubscriptDeclSyntax {
genericTypeParams: genericTypeParams,
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: nil,
asyncOrReasync: nil,
isAsync: false,
throwing: .none,
isStatic: isStatic,
offset: self.offset,
length: self.length,
Expand Down Expand Up @@ -432,8 +465,8 @@ extension FunctionDeclSyntax {
genericTypeParams: genericTypeParams,
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsClause?.throwsSpecifier.text,
asyncOrReasync: self.signature.effectSpecifiers?.asyncSpecifier?.text,
isAsync: self.signature.effectSpecifiers?.asyncSpecifier != nil,
throwing: .init(self.signature.effectSpecifiers?.throwsClause),
isStatic: isStatic,
offset: self.offset,
length: self.length,
Expand Down Expand Up @@ -473,8 +506,8 @@ extension InitializerDeclSyntax {
genericTypeParams: genericTypeParams,
genericWhereClause: genericWhereClause,
params: params,
throwsOrRethrows: self.signature.effectSpecifiers?.throwsClause?.throwsSpecifier.text,
asyncOrReasync: self.signature.effectSpecifiers?.asyncSpecifier?.text,
isAsync: self.signature.effectSpecifiers?.asyncSpecifier != nil,
throwing: .init(self.signature.effectSpecifiers?.throwsClause),
isStatic: false,
offset: self.offset,
length: self.length,
Expand Down Expand Up @@ -796,3 +829,21 @@ extension Trivia {
return nil
}
}

extension ThrowingKind {
fileprivate init(_ syntax: ThrowsClauseSyntax?) {
guard let syntax else {
self = .none
return
}
if syntax.throwsSpecifier.tokenKind == .keyword(.rethrows) {
self = .rethrows
} else {
if let type = syntax.type {
self = .typed(errorType: type.trimmedDescription)
} else {
self = .any
}
}
}
}
7 changes: 2 additions & 5 deletions Sources/MockoloFramework/Templates/ClosureTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ import Foundation

extension ClosureModel {
func applyClosureTemplate(name: String,
type: SwiftType,
genericTypeNames: [String],
paramVals: [String]?,
paramTypes: [SwiftType]?,
suffix: String,
returnDefaultType: SwiftType) -> String {

var handlerParamValsStr = ""
Expand All @@ -46,8 +43,8 @@ extension ClosureModel {
let handlerReturnDefault = renderReturnDefaultStatement(name: name, type: returnDefaultType)

let prefix = [
suffix.hasThrowsOrRethrows ? String.try + " " : nil,
suffix.hasAsync ? String.await + " " : nil,
throwing.hasError ? String.try + " " : nil,
isAsync ? String.await + " " : nil,
].compactMap { $0 }.joined()

let returnStr = returnDefaultType.typeName.isEmpty ? "" : "return "
Expand Down
8 changes: 5 additions & 3 deletions Sources/MockoloFramework/Templates/MethodTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ extension MethodModel {
params: [ParamModel],
returnType: SwiftType,
accessLevel: String,
suffix: String,
argsHistory: ArgumentsHistoryModel?,
handler: ClosureModel?) -> String {
var template = ""
Expand Down Expand Up @@ -59,14 +58,17 @@ extension MethodModel {
let handlerVarType = handler.type.typeName // ?? "Any"
let handlerReturn = handler.render(with: identifier, encloser: "") ?? ""

let suffixStr = suffix.isEmpty ? "" : "\(suffix) "
let suffixStr = [
isAsync ? String.async : nil,
throwing.applyThrowingTemplate(),
].compactMap { $0 }.joined(separator: " ") + " "
let returnStr = returnTypeName.isEmpty ? "" : "-> \(returnTypeName)"
let staticStr = isStatic ? String.static + " " : ""
let keyword = isSubscript ? "" : "func "
var body = ""

if useTemplateFunc {
let callMockFunc = !suffix.hasThrowsOrRethrows && (handler.type.cast?.isEmpty ?? false)
let callMockFunc = !throwing.hasError && (handler.type.cast?.isEmpty ?? false)
if callMockFunc {
let handlerParamValsStr = params.map { (arg) -> String in
if arg.type.typeName.hasPrefix(String.autoclosure) {
Expand Down
Loading

0 comments on commit 8f458e2

Please sign in to comment.