Skip to content

Commit

Permalink
Enforce that 'fulfillingAdditionalTypes' parameter is of a valid type
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Dec 10, 2023
1 parent 5ad4dcf commit 9a55c8e
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import SwiftSyntax

extension AttributeListSyntax {

var instantiableMacro: AttributeSyntax? {
public var instantiableMacro: AttributeSyntax? {
guard let attribute = first(where: { element in
switch element {
case let .attribute(attribute):
Expand Down
37 changes: 37 additions & 0 deletions Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Distributed under the MIT License
//
// 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.

import SwiftSyntax

extension AttributeSyntax {

public var fulfillingAdditionalTypes: ExprSyntax? {
guard
let arguments,
let labeledExpressionList = LabeledExprListSyntax(arguments),
let firstLabeledExpression = labeledExpressionList.first,
firstLabeledExpression.label?.text == "fulfillingAdditionalTypes"
else {
return nil
}

return firstLabeledExpression.expression
}
}
6 changes: 2 additions & 4 deletions Sources/SafeDICore/Visitors/InstantiableVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,11 @@ public final class InstantiableVisitor: SyntaxVisitor {

private func processAttributes(_ attributes: AttributeListSyntax, on node: some ConcreteDeclSyntaxProtocol) {
guard let macro = attributes.instantiableMacro else {
assertionFailure("Constructing macro not found despite processing top-level declaration")
assertionFailure("Instantiable macro not found despite processing top-level declaration")
return
}
guard
let fulfillingAdditionalTypesArgument = macro.arguments,
let fulfillingAdditionalTypesExpressionList = LabeledExprListSyntax(fulfillingAdditionalTypesArgument),
let fulfillingAdditionalTypesExpression = fulfillingAdditionalTypesExpressionList.first?.expression,
let fulfillingAdditionalTypesExpression = macro.fulfillingAdditionalTypes,
let fulfillingAdditionalTypesArray = ArrayExprSyntax(fulfillingAdditionalTypesExpression)
else {
// Nothing to do here.
Expand Down
9 changes: 9 additions & 0 deletions Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public struct InstantiableMacro: MemberMacro {
throw InstantiableError.decoratingIncompatibleType
}

if let fulfillingAdditionalTypesArgument = concreteDeclaration.attributes.instantiableMacro?.fulfillingAdditionalTypes {
if ArrayExprSyntax(fulfillingAdditionalTypesArgument) == nil {
throw InstantiableError.fulfillingAdditionalTypesArgumentInvalid
}
}

let visitor = InstantiableVisitor()
visitor.walk(concreteDeclaration)
for diagnostic in visitor.diagnostics {
Expand Down Expand Up @@ -83,13 +89,16 @@ public struct InstantiableMacro: MemberMacro {
private enum InstantiableError: Error, CustomStringConvertible {
case decoratingIncompatibleType
case tooManyForwardedProperties
case fulfillingAdditionalTypesArgumentInvalid

var description: String {
switch self {
case .decoratingIncompatibleType:
"@\(InstantiableVisitor.macroName) must decorate a class, struct, or actor"
case .tooManyForwardedProperties:
"An @\(InstantiableVisitor.macroName) type must have at most one @\(Dependency.Source.forwarded.rawValue) property"
case .fulfillingAdditionalTypesArgumentInvalid:
"The argument `fulfillingAdditionalTypes` must be an inlined array"
}
}
}
Expand Down
38 changes: 36 additions & 2 deletions Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,40 @@ final class InstantiableMacroTests: XCTestCase {
}
}

func test_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() {
assertMacro {
"""
let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self]
@Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes)
public final class ExampleService {}
"""
} diagnostics: {
"""
let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self]
@Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes)
┬──────────────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array
public final class ExampleService {}
"""
}
}

func test_throwsErrorWhenFulfillingAdditionalTypesIsAClosure() {
assertMacro {
"""
@Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }())
public final class ExampleService {}
"""
} diagnostics: {
"""
@Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }())
┬───────────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array
public final class ExampleService {}
"""
}
}

func test_throwsErrorWhenMoreThanOneForwardedProperty() {
assertMacro {
"""
Expand Down Expand Up @@ -480,7 +514,7 @@ final class InstantiableMacroTests: XCTestCase {
}


func test_fixit_addsFixitMissingRequiredInitializerWhenLazyConstructedDependencyMissingFromInit() {
func test_fixit_addsFixitMissingRequiredInitializerWhenLazyInstantiatedDependencyMissingFromInit() {
assertMacro {
"""
@Instantiable
Expand Down Expand Up @@ -525,7 +559,7 @@ final class InstantiableMacroTests: XCTestCase {
}
}

func test_fixit_addsFixitMissingRequiredInitializerWhenLazyConstructedAndInstantiatorDependencyOfSameTypeMissingFromInit() {
func test_fixit_addsFixitMissingRequiredInitializerWhenLazyInstantiatedAndInstantiatorDependencyOfSameTypeMissingFromInit() {
assertMacro {
"""
@Instantiable
Expand Down
20 changes: 10 additions & 10 deletions Tests/SafeDIPluginTests/SafeDIPluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init() {}
let urlSession: URLSession = .shared
Expand Down Expand Up @@ -100,7 +100,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init() {}
let urlSession: URLSession = .shared
Expand Down Expand Up @@ -157,7 +157,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init() {}
let urlSession: URLSession = .shared
Expand Down Expand Up @@ -214,7 +214,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init() {}
let urlSession: URLSession = .shared
Expand Down Expand Up @@ -1569,7 +1569,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init(urlSession: URLSession) {
self.urlSession = urlSession
Expand Down Expand Up @@ -1614,7 +1614,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init(urlSession: URLSession) {
self.urlSession = urlSession
Expand Down Expand Up @@ -1751,15 +1751,15 @@ final class SafeDIPluginTests: XCTestCase {
"""
import UIKit
@Instantiable(fulfillingAdditionalType: [UIViewController.self])
@Instantiable(fulfillingAdditionalTypes: [UIViewController.self])
public final class RootViewController: UIViewController {
public init() {}
}
""",
"""
import UIKit
@Instantiable(fulfillingAdditionalType: [UIViewController.self])
@Instantiable(fulfillingAdditionalTypes: [UIViewController.self])
public final class SplashViewController: UIViewController {
public init() {}
}
Expand Down Expand Up @@ -1787,7 +1787,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalType: [NetworkService.self])
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init(loggingService: LoggingService) {
self.loggingService = loggingService
Expand All @@ -1802,7 +1802,7 @@ final class SafeDIPluginTests: XCTestCase {
public protocol LoggingService {}
@Instantiable(fulfillingAdditionalType: [LoggingService.self])
@Instantiable(fulfillingAdditionalTypes: [LoggingService.self])
public final class DefaultLoggingService: LoggingService {
public init(networkService: NetworkService) {
self.networkService = networkService
Expand Down

0 comments on commit 9a55c8e

Please sign in to comment.