From feb9efb0101fd82a5aaf3e69cf279609c165e9eb Mon Sep 17 00:00:00 2001 From: Alexandr Zalutskiy Date: Thu, 19 Oct 2023 18:34:43 +0600 Subject: [PATCH] Append diagnostic for correct errors for unsupported features fixes #9 --- Sources/SwiftMockMacros/Constants.swift | 3 + .../Diagnostic/AssociatedTypeDiagnostic.swift | 18 + .../Diagnostic/DiagnosticError.swift | 129 ++++++ .../Diagnostic/FunctionDiagnostic.swift | 36 ++ .../Diagnostic/PropertyDiagnostic.swift | 51 +++ .../Diagnostic/ProtocolDiagnostic.swift | 84 ++++ Sources/SwiftMockMacros/SwiftMockMacro.swift | 83 ++-- .../SwiftSyntaxExtensions/TokenSyntax.swift | 42 ++ .../SwiftSyntaxExtensions/TypeSyntax.swift | 32 ++ .../Macro/MockMacroDiagnosticTests.swift | 408 ++++++++++++++++++ .../SwiftMockTests/Macro/MockMacroTests.swift | 6 +- 11 files changed, 850 insertions(+), 42 deletions(-) create mode 100644 Sources/SwiftMockMacros/Diagnostic/AssociatedTypeDiagnostic.swift create mode 100644 Sources/SwiftMockMacros/Diagnostic/DiagnosticError.swift create mode 100644 Sources/SwiftMockMacros/Diagnostic/FunctionDiagnostic.swift create mode 100644 Sources/SwiftMockMacros/Diagnostic/PropertyDiagnostic.swift create mode 100644 Sources/SwiftMockMacros/Diagnostic/ProtocolDiagnostic.swift create mode 100644 Sources/SwiftMockMacros/SwiftSyntaxExtensions/TokenSyntax.swift create mode 100644 Sources/SwiftMockMacros/SwiftSyntaxExtensions/TypeSyntax.swift create mode 100644 Tests/SwiftMockTests/Macro/MockMacroDiagnosticTests.swift diff --git a/Sources/SwiftMockMacros/Constants.swift b/Sources/SwiftMockMacros/Constants.swift index 5ee5e0d..b5dabe5 100644 --- a/Sources/SwiftMockMacros/Constants.swift +++ b/Sources/SwiftMockMacros/Constants.swift @@ -11,6 +11,9 @@ let emptyArrayExpt = ExprSyntax( let publicModifier = DeclModifierSyntax(name: .keyword(.public)) let privateModifier = DeclModifierSyntax(name: .keyword(.private)) +let fileprivateModifier = DeclModifierSyntax(name: .keyword(.fileprivate)) +let internalModifier = DeclModifierSyntax(name: .keyword(.internal)) + let finalModifier = DeclModifierSyntax(name: .keyword(.final)) let anyFunctionCallExpr = ExprSyntax( diff --git a/Sources/SwiftMockMacros/Diagnostic/AssociatedTypeDiagnostic.swift b/Sources/SwiftMockMacros/Diagnostic/AssociatedTypeDiagnostic.swift new file mode 100644 index 0000000..5a40553 --- /dev/null +++ b/Sources/SwiftMockMacros/Diagnostic/AssociatedTypeDiagnostic.swift @@ -0,0 +1,18 @@ +// +// AssociatedTypeDiagnostic.swift +// +// +// Created by Alexandr Zalutskiy on 19/10/2023. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax + +extension Diagnostic { + static func validateAssociatedTypeDecl(_ declaration: AssociatedTypeDeclSyntax) throws { + let diagnostic = Diagnostic(node: declaration, message: DiagnosticMessage.associatedtypeIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } +} + diff --git a/Sources/SwiftMockMacros/Diagnostic/DiagnosticError.swift b/Sources/SwiftMockMacros/Diagnostic/DiagnosticError.swift new file mode 100644 index 0000000..4fb15b0 --- /dev/null +++ b/Sources/SwiftMockMacros/Diagnostic/DiagnosticError.swift @@ -0,0 +1,129 @@ +// +// DiagnosticMessage.swift +// +// +// Created by Alexandr Zalutskiy on 17/10/2023. +// + +import SwiftDiagnostics + +struct DiagnosticError: Error { + let diagnostic: Diagnostic +} + +enum DiagnosticMessage: String, SwiftDiagnostics.DiagnosticMessage { + case notAProtocol + case `private` + case filePrivate + // FIXME: add support for internal protocols + case notAPublicProtocol + // TODO: support for attributes + case attributesIsNotSupported + // TODO: support primary associated types + case primaryAssociatedTypesIsNotSupported + case inheritanceIsNotSupported + + // TODO: support for generic parameters + case genericParametersIsNotSupported + // TODO: support for rethrows + case rethrowsIsNotSupported + // TODO: support for reasync + case reasyncIsNotSupported + + // TODO: support for associated types + case associatedtypeIsNotSupported + + case propertyAccessorsNotSpecified + case propertyAccessorMustBeGetOrSet + // TODO: support for get async in properties + case asyncPropertiesIsNotSupported + // TODO: support for get throws in properties + case throwsPropertiesIsNotSupported + + case unknownEffectSpecifierInPropertyDeclaration + + var severity: DiagnosticSeverity { + switch self { + case .notAProtocol: + return .error + case .private: + return .error + case .filePrivate: + return .error + case .notAPublicProtocol: + return .error + case .attributesIsNotSupported: + return .error + case .primaryAssociatedTypesIsNotSupported: + return .error + case .inheritanceIsNotSupported: + return .error + + case .genericParametersIsNotSupported: + return .error + case .rethrowsIsNotSupported: + return .error + case .reasyncIsNotSupported: + return .error + + case .associatedtypeIsNotSupported: + return .error + + case .propertyAccessorsNotSpecified: + return .error + case .propertyAccessorMustBeGetOrSet: + return .error + case .asyncPropertiesIsNotSupported: + return .error + case .throwsPropertiesIsNotSupported: + return .error + case .unknownEffectSpecifierInPropertyDeclaration: + return .error + } + } + + var message: String { + switch self { + case .notAProtocol: + return "'@Mock' can only be applied to a 'protocol'" + case .private: + return "'@Mock' cannot be applied to a 'private protocol'" + case .filePrivate: + return "'@Mock' cannot be applied to a 'fileprivate protocol'" + case .notAPublicProtocol: + return "'@Mock' can only be applied to a 'public protocol'" + case .attributesIsNotSupported: + return "'@Mock' doesn't support attributes" + case .primaryAssociatedTypesIsNotSupported: + return "'@Mock' cannot be applied to a 'protocol' with primary associated types" + case .inheritanceIsNotSupported: + return "'@Mock' can only be applied to a non-inherited 'protocol'" + + case .genericParametersIsNotSupported: + return "'@Mock' doesn't support generic parameters" + case .rethrowsIsNotSupported: + return "'@Mock' doesn't support rethrows methods" + case .reasyncIsNotSupported: + return "'@Mock' doesn't support reasync methods" + + case .associatedtypeIsNotSupported: + return "'@Mock' doesn't support associatedtypes" + + case .propertyAccessorsNotSpecified: + return "Property in protocol must have explicit { get } or { get set } specifier" + case .propertyAccessorMustBeGetOrSet: + return "Expected get or set in a protocol property" + + case .asyncPropertiesIsNotSupported: + return "'@Mock' doesn't support async properties" + case .throwsPropertiesIsNotSupported: + return "'@Mock' doesn't support throws properties" + case .unknownEffectSpecifierInPropertyDeclaration: + return "'@Mock' found unknown effect specifier in property declaration, please report in issue in GitHub" + } + } + + var diagnosticID: MessageID { + MessageID(domain: "SwiftMockMacros", id: rawValue) + } +} diff --git a/Sources/SwiftMockMacros/Diagnostic/FunctionDiagnostic.swift b/Sources/SwiftMockMacros/Diagnostic/FunctionDiagnostic.swift new file mode 100644 index 0000000..4658192 --- /dev/null +++ b/Sources/SwiftMockMacros/Diagnostic/FunctionDiagnostic.swift @@ -0,0 +1,36 @@ +// +// FunctionDiagnostic.swift +// +// +// Created by Alexandr Zalutskiy on 18/10/2023. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax + +extension Diagnostic { + static func validateFunctionDecl(_ declaration: FunctionDeclSyntax) throws { + if let attribute = declaration.attributes.first { + let diagnostic = Diagnostic(node: attribute, message: DiagnosticMessage.attributesIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + if let genericParameterClause = declaration.genericParameterClause { + let diagnostic = Diagnostic(node: genericParameterClause, message: DiagnosticMessage.genericParametersIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + if let effectSpecifiers = declaration.signature.effectSpecifiers { + if let asyncSpecifier = effectSpecifiers.asyncSpecifier, asyncSpecifier.isReasync { + let diagnostic = Diagnostic(node: asyncSpecifier, message: DiagnosticMessage.reasyncIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + if let throwsSpecifier = effectSpecifiers.throwsSpecifier, throwsSpecifier.isRethrows { + let diagnostic = Diagnostic(node: throwsSpecifier, message: DiagnosticMessage.rethrowsIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + } + } +} diff --git a/Sources/SwiftMockMacros/Diagnostic/PropertyDiagnostic.swift b/Sources/SwiftMockMacros/Diagnostic/PropertyDiagnostic.swift new file mode 100644 index 0000000..d041379 --- /dev/null +++ b/Sources/SwiftMockMacros/Diagnostic/PropertyDiagnostic.swift @@ -0,0 +1,51 @@ +// +// PropertyDiagnostic.swift +// +// +// Created by Alexandr Zalutskiy on 19/10/2023. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax + +extension Diagnostic { + static func validatePropertyDeclaration(_ declaration: VariableDeclSyntax) throws { + for binding in declaration.bindings { + // Protocols must contains accessor block by language design + guard let accessorBlock = binding.accessorBlock else { + let diagnostic = Diagnostic(node: declaration, message: DiagnosticMessage.propertyAccessorsNotSpecified) + throw DiagnosticError(diagnostic: diagnostic) + } + // Protocols must contains accessor block by language design + guard case let .`accessors`(accessorList) = accessorBlock.accessors else { + let diagnostic = Diagnostic( + node: declaration, + message: DiagnosticMessage.propertyAccessorsNotSpecified + ) + throw DiagnosticError(diagnostic: diagnostic) + } + + for accessorDecl in accessorList { + let accessorSpecifier = accessorDecl.accessorSpecifier + guard accessorSpecifier.isGet || accessorSpecifier.isSet else { + let diagnostic = Diagnostic(node: accessorSpecifier, message: DiagnosticMessage.propertyAccessorMustBeGetOrSet) + throw DiagnosticError(diagnostic: diagnostic) + } + + if let effectSpecifiers = accessorDecl.effectSpecifiers { + if let asyncSpecifier = effectSpecifiers.asyncSpecifier { + let diagnostic = Diagnostic(node: asyncSpecifier, message: DiagnosticMessage.asyncPropertiesIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + if let throwsSpecifier = effectSpecifiers.throwsSpecifier { + let diagnostic = Diagnostic(node: throwsSpecifier, message: DiagnosticMessage.throwsPropertiesIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + let diagnostic = Diagnostic(node: effectSpecifiers, message: DiagnosticMessage.unknownEffectSpecifierInPropertyDeclaration) + throw DiagnosticError(diagnostic: diagnostic) + } + } + } + } +} diff --git a/Sources/SwiftMockMacros/Diagnostic/ProtocolDiagnostic.swift b/Sources/SwiftMockMacros/Diagnostic/ProtocolDiagnostic.swift new file mode 100644 index 0000000..72a90d1 --- /dev/null +++ b/Sources/SwiftMockMacros/Diagnostic/ProtocolDiagnostic.swift @@ -0,0 +1,84 @@ +// +// ProtocolDiagnostic.swift +// +// +// Created by Alexandr Zalutskiy on 18/10/2023. +// + +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax + +extension Diagnostic { + static func extractProtocolDecl(_ declaration: some DeclSyntaxProtocol) throws -> ProtocolDeclSyntax { + guard let declaration = declaration.as(ProtocolDeclSyntax.self) else { + let diagnostic = Diagnostic(node: declaration, message: DiagnosticMessage.notAProtocol) + throw DiagnosticError(diagnostic: diagnostic) + } + + return declaration + } + + static func validateProtocolDecl(_ declaration: ProtocolDeclSyntax) throws { + if let modifier = declaration.modifiers.first(where: { $0.name.text == privateModifier.name.text }) { + let diagnostic = Diagnostic(node: modifier, message: DiagnosticMessage.private) + throw DiagnosticError(diagnostic: diagnostic) + } + if let modifier = declaration.modifiers.first(where: { $0.name.text == fileprivateModifier.name.text }) { + let diagnostic = Diagnostic(node: modifier, message: DiagnosticMessage.filePrivate) + throw DiagnosticError(diagnostic: diagnostic) + } + + // Start + // FIXME: add support for internal protocols + // They can be used using `@testable import` + if !declaration.modifiers.contains(where: { $0.name.text == publicModifier.name.text }) { + if let modifier = declaration.modifiers.first { + let diagnostic = Diagnostic(node: modifier, message: DiagnosticMessage.notAPublicProtocol) + throw DiagnosticError(diagnostic: diagnostic) + } else { + let diagnostic = Diagnostic(node: declaration.protocolKeyword, message: DiagnosticMessage.notAPublicProtocol) + throw DiagnosticError(diagnostic: diagnostic) + } + } + // End + + // TODO: support for attributes + if let attribute = declaration.attributes.first(where: { !$0.isMock }) { + let diagnostic = Diagnostic(node: attribute, message: DiagnosticMessage.attributesIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + // TODO: support for primary associated types + if let primaryAssociatedTypeClause = declaration.primaryAssociatedTypeClause { + let diagnostic = Diagnostic(node: primaryAssociatedTypeClause, message: DiagnosticMessage.primaryAssociatedTypesIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + if let inheritanceClause = declaration.inheritanceClause, inheritanceClause.inheritedTypes.contains(where: { !$0.type.isAnyObject }) { + let diagnostic = Diagnostic(node: inheritanceClause, message: DiagnosticMessage.inheritanceIsNotSupported) + throw DiagnosticError(diagnostic: diagnostic) + } + + for member in declaration.memberBlock.members { + if let functionDecl = member.decl.as(FunctionDeclSyntax.self) { + try Diagnostic.validateFunctionDecl(functionDecl) + } else if let associatedTypeDecl = member.decl.as(AssociatedTypeDeclSyntax.self) { + try Diagnostic.validateAssociatedTypeDecl(associatedTypeDecl) + } else if let variableDeclaration = member.decl.as(VariableDeclSyntax.self) { + try Diagnostic.validatePropertyDeclaration(variableDeclaration) + } + } + } +} + +private extension AttributeListSyntax.Element { + var isMock: Bool { + switch self { + case .attribute(let attribute): + return attribute.attributeName.isMock + case .ifConfigDecl: + return false + } + } +} diff --git a/Sources/SwiftMockMacros/SwiftMockMacro.swift b/Sources/SwiftMockMacros/SwiftMockMacro.swift index 2d00adb..de64e9e 100644 --- a/Sources/SwiftMockMacros/SwiftMockMacro.swift +++ b/Sources/SwiftMockMacros/SwiftMockMacro.swift @@ -1,4 +1,5 @@ import SwiftCompilerPlugin +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros @@ -9,48 +10,52 @@ public struct MockMacro: PeerMacro { providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.DeclSyntax] { - guard let declaration = declaration.as(ProtocolDeclSyntax.self) else { - fatalError("Mock macro can be attached only to a protocol type") - } - guard declaration.modifiers.contains(where: { $0.name.text == TokenSyntax.keyword(.public).text }) else { - fatalError("Mock macro can be attached only to a public type") - } - - let mockTypeToken = TokenSyntax.identifier(declaration.name.text + "Mock") - - return [ - DeclSyntax( - try ClassDeclSyntax( - modifiers: DeclModifierListSyntax { - DeclModifierSyntax(name: .keyword(.public)) - DeclModifierSyntax(name: .keyword(.final)) - }, - name: mockTypeToken, - inheritanceClause: InheritanceClauseSyntax { - InheritedTypeSyntax(type: IdentifierTypeSyntax(name: declaration.name)) - InheritedTypeSyntax(type: IdentifierTypeSyntax(name: "Verifiable")) - } - ) { - try makeVerifyType(declaration) - try makeVerifyCallStorageProperty() - for member in declaration.memberBlock.members { - if let funcDecl = member.decl.as(FunctionDeclSyntax.self) { - makeInvocationContainerProperty(funcDecl: funcDecl) - makeSignatureMethod(from: funcDecl) - funcDecl - .with(\.modifiers, DeclModifierListSyntax { - DeclModifierSyntax(name: .keyword(.public)) - }) - .with(\.body, try makeMockMethodBody(from: funcDecl, type: mockTypeToken)) - } else if let variableDecl = member.decl.as(VariableDeclSyntax.self) { - for decl in try makeVariableMock(from: variableDecl, mockTypeToken: mockTypeToken) { - decl + do { + let declaration = try Diagnostic.extractProtocolDecl(declaration) + try Diagnostic.validateProtocolDecl(declaration) + + let mockTypeToken = TokenSyntax.identifier(declaration.name.text + "Mock") + + return [ + DeclSyntax( + try ClassDeclSyntax( + modifiers: DeclModifierListSyntax { + // FIXME: add support for internal protocols + DeclModifierSyntax(name: .keyword(.public)) + DeclModifierSyntax(name: .keyword(.final)) + }, + name: mockTypeToken, + inheritanceClause: InheritanceClauseSyntax { + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: declaration.name)) + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: "Verifiable")) + } + ) { + try makeVerifyType(declaration) + try makeVerifyCallStorageProperty() + for member in declaration.memberBlock.members { + if let funcDecl = member.decl.as(FunctionDeclSyntax.self) { + makeInvocationContainerProperty(funcDecl: funcDecl) + makeSignatureMethod(from: funcDecl) + funcDecl + .with(\.modifiers, DeclModifierListSyntax { + DeclModifierSyntax(name: .keyword(.public)) + }) + .with(\.body, try makeMockMethodBody(from: funcDecl, type: mockTypeToken)) + } else if let variableDecl = member.decl.as(VariableDeclSyntax.self) { + for decl in try makeVariableMock(from: variableDecl, mockTypeToken: mockTypeToken) { + decl + } } } } - } - ) - ] + ) + ] + } catch let error as DiagnosticError { + context.diagnose(error.diagnostic) + return [] + } catch { + throw error + } } private static func makeInvocationContainerProperty(funcDecl: FunctionDeclSyntax) -> VariableDeclSyntax { diff --git a/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TokenSyntax.swift b/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TokenSyntax.swift new file mode 100644 index 0000000..2521f44 --- /dev/null +++ b/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TokenSyntax.swift @@ -0,0 +1,42 @@ +// +// TokenSyntax.swift +// +// +// Created by Alexandr Zalutskiy on 19/10/2023. +// + +import SwiftSyntax + +extension TokenSyntax { + static var anyObject: TokenSyntax { + .identifier("AnyObject") + } + + static var mock: TokenSyntax { + .identifier("Mock") + } + + var isReasync: Bool { + trimmed.text == TokenSyntax.keyword(.reasync).text + } + + var isRethrows: Bool { + trimmed.text == TokenSyntax.keyword(.rethrows).text + } + + var isGet: Bool { + trimmed.text == TokenSyntax.keyword(.get).text + } + + var isSet: Bool { + trimmed.text == TokenSyntax.keyword(.set).text + } + + var isAnyObject: Bool { + trimmed.text == "AnyObject" + } + + var isMock: Bool { + trimmed.text == "Mock" + } +} diff --git a/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TypeSyntax.swift b/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TypeSyntax.swift new file mode 100644 index 0000000..14c3211 --- /dev/null +++ b/Sources/SwiftMockMacros/SwiftSyntaxExtensions/TypeSyntax.swift @@ -0,0 +1,32 @@ +// +// TypeSyntax.swift +// +// +// Created by Alexandr Zalutskiy on 18/10/2023. +// + +import SwiftSyntax + +extension TypeSyntax { + static var anyObject: IdentifierTypeSyntax { + IdentifierTypeSyntax(name: .anyObject) + } + + static var mock: IdentifierTypeSyntax { + IdentifierTypeSyntax(name: .mock) + } + + var isAnyObject: Bool { + guard let self = self.as(IdentifierTypeSyntax.self) else { + return false + } + return self.name.isAnyObject + } + + var isMock: Bool { + guard let self = self.as(IdentifierTypeSyntax.self) else { + return false + } + return self.name.isMock + } +} diff --git a/Tests/SwiftMockTests/Macro/MockMacroDiagnosticTests.swift b/Tests/SwiftMockTests/Macro/MockMacroDiagnosticTests.swift new file mode 100644 index 0000000..8cfae3f --- /dev/null +++ b/Tests/SwiftMockTests/Macro/MockMacroDiagnosticTests.swift @@ -0,0 +1,408 @@ +// +// MockMacroDiagnosticTests.swift +// +// +// Created by Alexandr Zalutskiy on 18/10/2023. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +#if canImport(SwiftMockMacros) +import SwiftMockMacros + +private let testMacros: [String: Macro.Type] = [ + "Mock": MockMacro.self, +] +#endif + +final class MockMacroDiagnosticTests: XCTestCase { + func testNotAProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public class Test { } + """, + expandedSource: + """ + public class Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' can only be applied to a 'protocol'", line: 1, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPrivateProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + private protocol Test { } + """, + expandedSource: + """ + private protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' cannot be applied to a 'private protocol'", line: 2, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testFilePrivateProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + fileprivate protocol Test { } + """, + expandedSource: + """ + fileprivate protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' cannot be applied to a 'fileprivate protocol'", line: 2, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testNotAPublicProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + protocol Test { } + """, + expandedSource: + """ + protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' can only be applied to a 'public protocol'", line: 2, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testInternalProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + internal protocol Test { } + """, + expandedSource: + """ + internal protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' can only be applied to a 'public protocol'", line: 2, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testProtocolWithPrimaryAssociatedTypesDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { } + """, + expandedSource: + """ + public protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' cannot be applied to a 'protocol' with primary associated types", line: 2, column: 21) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testInheritedProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test: Equitable { } + """, + expandedSource: + """ + public protocol Test: Equitable { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' can only be applied to a non-inherited 'protocol'", line: 2, column: 21) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testAttributedProtocolDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + @available(iOS 14, macOS 12, *) + public protocol Test { } + """, + expandedSource: + """ + @available(iOS 14, macOS 12, *) + public protocol Test { } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support attributes", line: 2, column: 1) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testAttributedMethodDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + @available(iOS 14, macOS 12, *) + func call() + } + """, + expandedSource: + """ + public protocol Test { + @available(iOS 14, macOS 12, *) + func call() + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support attributes", line: 3, column: 2) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMethodWithGenericParametersDiagnostic() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + func call(_: T) + } + """, + expandedSource: + """ + public protocol Test { + func call(_: T) + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support generic parameters", line: 3, column: 11) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testRethrowsMethod() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + func test(_ callback: () throws -> Void) rethrows + } + """, + expandedSource: + """ + public protocol Test { + func test(_ callback: () throws -> Void) rethrows + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support rethrows methods", line: 3, column: 43) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testReasyncMethod() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + func test(_ callback: () async -> Void) reasync + } + """, + expandedSource: + """ + public protocol Test { + func test(_ callback: () async -> Void) reasync + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support reasync methods", line: 3, column: 42) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPropertyWithoutAccessorSpecifier() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + var test: Int + } + """, + expandedSource: + """ + public protocol Test { + var test: Int + } + """, + diagnostics: [ + DiagnosticSpec(message: "Property in protocol must have explicit { get } or { get set } specifier", line: 3, column: 2) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPropertyWithIncorrectAccessorSpecifier() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + var test: Int { didSet } + } + """, + expandedSource: + """ + public protocol Test { + var test: Int { didSet } + } + """, + diagnostics: [ + DiagnosticSpec(message: "Expected get or set in a protocol property", line: 3, column: 18) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPropertyWithAsyncSpecifier() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + var test: Int { get async } + } + """, + expandedSource: + """ + public protocol Test { + var test: Int { get async } + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support async properties", line: 3, column: 22) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPropertyWithThrowsSpecifier() throws { + #if canImport(SwiftMockMacros) + assertMacroExpansion( + """ + @Mock + public protocol Test { + var test: Int { get throws } + } + """, + expandedSource: + """ + public protocol Test { + var test: Int { get throws } + } + """, + diagnostics: [ + DiagnosticSpec(message: "'@Mock' doesn't support throws properties", line: 3, column: 22) + ], + macros: testMacros, + indentationWidth: .tab + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} diff --git a/Tests/SwiftMockTests/Macro/MockMacroTests.swift b/Tests/SwiftMockTests/Macro/MockMacroTests.swift index 08f4dba..3232713 100644 --- a/Tests/SwiftMockTests/Macro/MockMacroTests.swift +++ b/Tests/SwiftMockTests/Macro/MockMacroTests.swift @@ -16,13 +16,13 @@ final class MockMacroTests: XCTestCase { assertMacroExpansion( """ @Mock - public protocol Test { } + public protocol Test: AnyObject { } """, expandedSource: """ - public protocol Test { } + public protocol Test: AnyObject { } - public final class TestMock: Test , Verifiable { + public final class TestMock: Test, Verifiable { public struct Verify: MockVerify { let mock: TestMock let container: CallContainer