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

Append diagnostic for correct errors for unsupported features #19

Merged
merged 1 commit into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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