diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist index ce20c864fc..bfb66d92d7 100644 --- a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist +++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist @@ -122,6 +122,14 @@ Type PSChildPaneSpecifier + + File + Acknowledgements/Sourcery + Title + Sourcery + Type + PSChildPaneSpecifier + File Acknowledgements/swift-custom-dump diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist new file mode 100644 index 0000000000..51edcbca1b --- /dev/null +++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/Sourcery.plist @@ -0,0 +1,38 @@ + + + + + PreferenceSpecifiers + + + FooterText + MIT License + +Copyright (c) 2016-2021 Krzysztof Zabłocki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/AuthenticatorShared/Sourcery/Generated/.gitignore b/AuthenticatorShared/Sourcery/Generated/.gitignore new file mode 100644 index 0000000000..5a0e94031c --- /dev/null +++ b/AuthenticatorShared/Sourcery/Generated/.gitignore @@ -0,0 +1,2 @@ +# Ignore SwiftGen generated files +*.swift diff --git a/AuthenticatorShared/Sourcery/sourcery.yml b/AuthenticatorShared/Sourcery/sourcery.yml new file mode 100644 index 0000000000..f8d6adb8c3 --- /dev/null +++ b/AuthenticatorShared/Sourcery/sourcery.yml @@ -0,0 +1,18 @@ +sources: + - .. + +templates: + - ../../Sourcery/Templates/AutoMockable.stencil + +output: + Generated + +exclude: + - Generated + - Tests + - TestHelpers + - Fixtures + +args: + autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"] + autoMockableTestableImports: ["AuthenticatorShared"] \ No newline at end of file diff --git a/BitwardenShared/Sourcery/Generated/.gitignore b/BitwardenShared/Sourcery/Generated/.gitignore new file mode 100644 index 0000000000..5a0e94031c --- /dev/null +++ b/BitwardenShared/Sourcery/Generated/.gitignore @@ -0,0 +1,2 @@ +# Ignore SwiftGen generated files +*.swift diff --git a/BitwardenShared/Sourcery/sourcery.yml b/BitwardenShared/Sourcery/sourcery.yml new file mode 100644 index 0000000000..5e4c42c2d1 --- /dev/null +++ b/BitwardenShared/Sourcery/sourcery.yml @@ -0,0 +1,18 @@ +sources: + - .. + +templates: + - ../../Sourcery/Templates/AutoMockable.stencil + +output: + Generated + +exclude: + - Generated + - Tests + - TestHelpers + - Fixtures + +args: + autoMockableImports: ["BitwardenKit", "BitwardenSdk", "Combine"] + autoMockableTestableImports: ["BitwardenShared"] \ No newline at end of file diff --git a/Mintfile b/Mintfile index 3a9056208a..0436c70ebf 100644 --- a/Mintfile +++ b/Mintfile @@ -3,3 +3,4 @@ nicklockwood/SwiftFormat@0.56.4 SwiftGen/SwiftGen@6.6.3 realm/SwiftLint@0.59.1 yonaskolb/xcodegen@2.43.0 +krzysztofzablocki/Sourcery@2.2.7 diff --git a/Sourcery/Templates/AutoMockable.stencil b/Sourcery/Templates/AutoMockable.stencil new file mode 100644 index 0000000000..649205ea99 --- /dev/null +++ b/Sourcery/Templates/AutoMockable.stencil @@ -0,0 +1,136 @@ +// swiftlint:disable line_length +// swiftlint:disable variable_name + +import Foundation + +{% for import in argument.autoMockableImports %} +import {{ import }} +{% endfor %} + +{% for import in argument.autoMockableTestableImports %} +@testable import {{ import }} +{% endfor %} + +{% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %} + +{% macro methodThrowableErrorDeclaration method %} + var {% call swiftifyMethodName method.selectorName %}Error: Error? +{% endmacro %} + +{% macro methodThrowableErrorUsage method %} + if let error = {% call swiftifyMethodName method.selectorName %}Error { + throw error + } +{% endmacro %} + +{% macro methodReceivedParameters method %} + {%if method.parameters.count == 1 %} + {% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% endset %} + {{ receivedVarName }}{% for param in method.parameters %} = {{ param.name }}{% endfor %} + {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!) + {% else %} + {% if not method.parameters.count == 0 %} + {% set receivedVarName %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endset %} + {{ receivedVarName }} = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% if method.isGeneric %} as? ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %} + {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({{ receivedVarName }}!) + {% endif %} + {% endif %} +{% endmacro %} + +{% macro methodReturnType method %}{% if method.returnTypeName.isVoid %}Void{% elif method.annotations["GenericReturn"] %}{{ method.annotations["GenericReturn"] }}{% else %}{{ method.returnTypeName }}{% endif %}{% endmacro %} + +{% macro parameterType param %}{% if param.annotations["Generic"] %}{{ param.annotations["Generic"] }}{% else %}{{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{% endif %}{% endmacro %} +{% macro parameterArguments param %}{% if param.typeAttributes.escaping %}@escaping {% endif %}{% endmacro %} + +{% macro callClosure method %}return {{ 'try ' if method.throws }}{% if method.isGeneric %}({% endif %}{% call methodClosureName method %}.map({ {{ 'try ' if method.throws }}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue{% if method.isGeneric %}) as! {{ method.returnTypeName }}{% endif %}{% endmacro %} +{% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %} + +{% macro methodClosureDeclaration method %} + var {% call methodClosureName method %}: (({% for param in method.parameters %}{% call parameterArguments param %}{% call parameterType param %}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call methodReturnType method %}{% endif %})? +{% endmacro %} + +{% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %} +{% macro methodClosureCallParametersFromArguments method %}{% for param in method.parameters %}{% call receivedArgumentsVarName method %}!{% if method.parameters.count > 1 %}.{{ param.name }}{% if not forloop.last %}, {% endif %}{% endif %}{% endfor %}{% endmacro %} + +{% macro receivedArgumentsVarName method %}{% if method.parameters.count == 1 %}{% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}{% endfor %}{% else %}{% call swiftifyMethodName method.selectorName %}ReceivedArguments{% endif %}{% endmacro %} + +{% macro mockMethod method %} + //MARK: - {{ method.shortName }} + + {% if method.throws %} + {% call methodThrowableErrorDeclaration method %} + {% endif %} + {% if not method.isInitializer %} + var {% call swiftifyMethodName method.selectorName %}CalledCount = 0 + var {% call swiftifyMethodName method.selectorName %}Called: Bool { + return {% call swiftifyMethodName method.selectorName %}CalledCount > 0 + } + {% endif %} + {% if method.parameters.count == 1 %} + var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}?{% endfor %} + var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations{% for param in method.parameters %}: [{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%}?{%endif%}]{% endfor %} = [] + {% elif not method.parameters.count == 0 %} + var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})? + var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {% call parameterType param %}{{ ', ' if not forloop.last }}{% endfor %})] = [] + {% endif %} + {% if not method.returnTypeName.isVoid and not method.isInitializer %} + var {% call swiftifyMethodName method.selectorName %}ReturnValue: {% call methodReturnType method %}{{ '!' if not method.isOptionalReturnType }} + {% endif %} + {% call methodClosureDeclaration method %} + +{% if method.isInitializer %} + required {{ method.name }} { + {% call methodReceivedParameters method %} + {% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %}) + } +{% else %} + func {{ method.name }}{{ ' throws' if method.throws }}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { + {% if method.throws %} + {% call methodThrowableErrorUsage method %} + {% endif %} + {% call swiftifyMethodName method.selectorName %}CalledCount += 1 + {% call methodReceivedParameters method %} + {% if method.returnTypeName.isVoid %} + {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParametersFromArguments method %}) + {% else %} + {% call callClosure method %} + {% endif %} + } + +{% endif %} +{% endmacro %} + +{% macro mockOptionalVariable variable %} + var {% call mockedVariableName variable %}: {{ variable.typeName }} +{% endmacro %} + +{% macro mockNonOptionalArrayOrDictionaryVariable variable %} + var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} +{% endmacro %} + +{% macro mockNonOptionalVariable variable %} + var {% call mockedVariableName variable %}: {{ variable.typeName }} { + get { + return {% call underlyingMockedVariableName variable %} + } + set(value) { + {% call underlyingMockedVariableName variable %} = value + } + } + var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}! +{% endmacro %} + +{% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} +{% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %} + +{% for type in types.protocols where type.based.AutoMockable or type|annotated:"AutoMockable" %}{% if type.name != "AutoMockable" %} +class Mock{{ type.name }}: {{ type.name }} { +{% for variable in type.allVariables|!definedInExtension %} + {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} +{% endfor %} + +{% for method in type.allMethods|!definedInExtension %} + {% call mockMethod method %} +{% endfor %} +} +{% endif %}{% endfor %} \ No newline at end of file diff --git a/project-bwa.yml b/project-bwa.yml index 9c5c3fe6db..5499936f1c 100644 --- a/project-bwa.yml +++ b/project-bwa.yml @@ -174,6 +174,8 @@ targets: - "**/TestHelpers/*" - "**/Fixtures/*" - "**/__Snapshots__/*" + - "**/Sourcery/Generated/*" + - "**/sourcery.yml" - path: AuthenticatorShared includes: - "**/__Snapshots__/*" @@ -184,6 +186,8 @@ targets: optional: true - path: AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift optional: true + - path: AuthenticatorShared/Sourcery/sourcery.yml + buildPhase: none dependencies: - target: BitwardenKit/AuthenticatorBridgeKit - package: BitwardenSdk @@ -210,6 +214,15 @@ targets: $SRCROOT/AuthenticatorShared/Core/Vault/Services/Importers/Support/GoogleAuth.proto outputFiles: - $(SRCROOT)/AuthenticatorShared/Core/Vault/Services/Importers/Support/Generated/GoogleAuth.pb.swift + - name: Sourcery + script: | + if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then + PATH="/opt/homebrew/bin:$PATH" + fi + mint run sourcery --config AuthenticatorShared/Sourcery/sourcery.yml + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift AuthenticatorSharedTests: type: bundle.unit-test platform: iOS @@ -225,6 +238,10 @@ targets: - "**/TestHelpers/*" - "**/Fixtures/*" - path: GlobalTestHelpers-bwa + - path: AuthenticatorShared/Sourcery/Generated + optional: true + - path: AuthenticatorShared/Sourcery/Generated/AutoMockable.generated.swift + optional: true dependencies: - target: Authenticator - target: AuthenticatorShared diff --git a/project-pm.yml b/project-pm.yml index 679bb92690..7bcee9cb5b 100644 --- a/project-pm.yml +++ b/project-pm.yml @@ -362,6 +362,8 @@ targets: - "**/TestHelpers/*" - "**/Fixtures/*" - "**/__Snapshots__/*" + - "**/Sourcery/Generated/*" + - "**/sourcery.yml" - path: BitwardenShared includes: - "**/__Snapshots__/*" @@ -373,6 +375,8 @@ targets: - path: BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift optional: true - path: BitwardenWatchShared + - path: BitwardenShared/Sourcery/sourcery.yml + buildPhase: none dependencies: - package: BitwardenSdk - package: SwiftUIIntrospect @@ -391,6 +395,15 @@ targets: - $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Assets.swift - $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Fonts.swift - $(SRCROOT)/BitwardenShared/UI/Platform/Application/Support/Generated/Localizations.swift + - name: Sourcery + script: | + if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then + PATH="/opt/homebrew/bin:$PATH" + fi + mint run sourcery --config BitwardenShared/Sourcery/sourcery.yml + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift BitwardenSharedTests: type: bundle.unit-test platform: iOS @@ -409,6 +422,10 @@ targets: - "**/TestHelpers/*" - "**/Fixtures/*" - path: GlobalTestHelpers + - path: BitwardenShared/Sourcery/Generated + optional: true + - path: BitwardenShared/Sourcery/Generated/AutoMockable.generated.swift + optional: true dependencies: - target: Bitwarden - target: BitwardenShared