Skip to content

Commit

Permalink
Append diagnostic for correct errors for unsupported features
Browse files Browse the repository at this point in the history
fixes #9
  • Loading branch information
MetalheadSanya committed Oct 19, 2023
1 parent ad3ddea commit feb9efb
Show file tree
Hide file tree
Showing 11 changed files with 850 additions and 42 deletions.
3 changes: 3 additions & 0 deletions Sources/SwiftMockMacros/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions Sources/SwiftMockMacros/Diagnostic/AssociatedTypeDiagnostic.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

129 changes: 129 additions & 0 deletions Sources/SwiftMockMacros/Diagnostic/DiagnosticError.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
36 changes: 36 additions & 0 deletions Sources/SwiftMockMacros/Diagnostic/FunctionDiagnostic.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
51 changes: 51 additions & 0 deletions Sources/SwiftMockMacros/Diagnostic/PropertyDiagnostic.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
84 changes: 84 additions & 0 deletions Sources/SwiftMockMacros/Diagnostic/ProtocolDiagnostic.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading

0 comments on commit feb9efb

Please sign in to comment.