diff --git a/Package.swift b/Package.swift index c35c2af0..c35bd4ae 100644 --- a/Package.swift +++ b/Package.swift @@ -44,7 +44,7 @@ let package = Package( "SafeDIMacros", "SafeDICore", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), - .product(name: "MacroTesting", package: "swift-macro-testing"), // TODO: write tests that use this! + .product(name: "MacroTesting", package: "swift-macro-testing"), ] ), .target( diff --git a/Sources/SafeDI/SafeDI.swift b/Sources/SafeDI/SafeDI.swift index 0c1daba9..f0b0c903 100644 --- a/Sources/SafeDI/SafeDI.swift +++ b/Sources/SafeDI/SafeDI.swift @@ -23,22 +23,26 @@ #else -/// Marks a builder utilized by SafeDI. +/// Marks a `class`, `struct`, or `actor` as capable of having properties that conform to this type decorated with `@constructed` or `@singleton`. /// -/// - Parameter propertyName: The name of the property that can be injected into other SafeDI builders. +/// - Parameter fulfillingAdditionalTypes: The types (in addition to the type decorated with this macro) that can be decorated with `@constructed` or `@singleton` and yield a result of this type. The types provided *must* be either superclasses of this type or protocols to which this type conforms. @attached(member, names: arbitrary) -public macro builder(_ propertyName: StaticString) = #externalMacro(module: "SafeDIMacros", type: "BuilderMacro") +public macro constructable(fulfillingAdditionalTypes: [Any.Type] = []) = #externalMacro(module: "SafeDIMacros", type: "ConstructableMacro") -/// Marks a collection of dependencies used by a SafeDI builder. -@attached(member, names: arbitrary) -public macro dependencies() = #externalMacro(module: "SafeDIMacros", type: "DependenciesMacro") +/// Marks a SafeDI dependency that is instantiated when its parent object is instantiated. +@attached(peer) +public macro constructed() = #externalMacro(module: "SafeDIMacros", type: "InjectableMacro") + +/// Marks a SafeDI dependency that is constructed by an object higher up in the dependency tree. +@attached(peer) +public macro provided() = #externalMacro(module: "SafeDIMacros", type: "InjectableMacro") -/// Marks a SafeDI dependency that is instantiated when its associated builder is instantiated. +/// Marks a SafeDI dependency that will only ever have one instance instantiated at a given time. Singleton dependencies may deallocate when all of the objects that use it deallocate. Singleton dependencies can not be marked with @constructed. @attached(peer) -public macro constructed() = #externalMacro(module: "SafeDIMacros", type: "ConstructedMacro") +public macro singleton() = #externalMacro(module: "SafeDIMacros", type: "InjectableMacro") -/// Marks a SafeDI dependency that will only ever have one instance instantiated at a given time. Singleton dependencies may deallocate when the built products that use it deallocate. Singleton dependencies can not be marked with @constructed. +/// Marks a SafeDI dependency that is injected into the parent object's initializer and provided to objects further down in the dependency tree. @attached(peer) -public macro singleton() = #externalMacro(module: "SafeDIMacros", type: "SingletonMacro") +public macro propagated() = #externalMacro(module: "SafeDIMacros", type: "InjectableMacro") #endif diff --git a/Sources/SafeDICore/BuilderVisitor.swift b/Sources/SafeDICore/BuilderVisitor.swift deleted file mode 100644 index b1224487..00000000 --- a/Sources/SafeDICore/BuilderVisitor.swift +++ /dev/null @@ -1,160 +0,0 @@ -// 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 SwiftDiagnostics -import SwiftSyntax - -public final class BuilderVisitor: SyntaxVisitor { - - // MARK: Initialization - - public init() { - super.init(viewMode: .sourceAccurate) - } - - // MARK: SyntaxVisitor - - public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - guard - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - else { - return .skipChildren - } - - modifiedGreatGrandparent.members.remove(at: index) - - diagnostics.append(Diagnostic( - node: node, - error: FixableBuilderError.unexpectedVariableDeclaration, - changes: [ - .replace( - oldNode: greatGrandparent, - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - return .skipChildren - } - - public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - guard - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - else { - return .skipChildren - } - - modifiedGreatGrandparent.members.remove(at: index) - - diagnostics.append(Diagnostic( - node: node, - error: FixableBuilderError.unexpectedInitializer, - changes: [ - .replace( - oldNode: greatGrandparent, - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - return .skipChildren - } - - public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - guard - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - else { - return .skipChildren - } - - modifiedGreatGrandparent.members.remove(at: index) - - diagnostics.append(Diagnostic( - node: node, - error: FixableBuilderError.unexpectedFuncationDeclaration, - changes: [ - .replace( - oldNode: greatGrandparent, - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - return .skipChildren - } - - public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - if - let builderMacro = node.attributes.builderMacro, - let propertyName = builderMacro.arguments?.firstArgumentString - { - builderTypeName = node.name.text - builtPropertyName = propertyName - return .visitChildren - - } else if node.attributes.dependenciesMacro != nil { - didFindDependencies = true - dependenciesVisitor.walk(node) - return .skipChildren - - } else { - return .skipChildren - } - } - - // MARK: Public - - public var builder: Builder? { - guard - let builtPropertyName, - let builtType = dependenciesVisitor.builtType, - let builderTypeName - else { - return nil - } - return Builder( - typeName: builderTypeName, - builtPropertyName: builtPropertyName, - builtType: builtType, - dependencies: dependenciesVisitor.dependencies - ) - } - - public private(set) var didFindDependencies = false - public private(set) var diagnostics = [Diagnostic]() - - public static let macroName = "builder" - public static let getDependenciesClosureName = "getDependencies" - - // MARK: Private - - private let dependenciesVisitor = DependenciesVisitor() - private var builderTypeName: String? - private var builtPropertyName: String? -} diff --git a/Sources/SafeDICore/Property.swift b/Sources/SafeDICore/ConcreteDeclSyntaxProtocol.swift similarity index 64% rename from Sources/SafeDICore/Property.swift rename to Sources/SafeDICore/ConcreteDeclSyntaxProtocol.swift index 3724d51f..405eddca 100644 --- a/Sources/SafeDICore/Property.swift +++ b/Sources/SafeDICore/ConcreteDeclSyntaxProtocol.swift @@ -18,27 +18,22 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -/// A representation of a property. -/// e.g. `let myDependency: MyDependency` -public struct Property: Codable, Equatable { - /// The label by which the property is referenced. - public let label: String - /// The type to which the property conforms. - public let type: String +import SwiftSyntax - public var asPropertyDeclaration: String { - "let \(label): \(type)" - } - - public var asParameterDeclaration: String { - "\(label): \(type)" - } - - public var asLabeledParameterExpression: String { - "\(label): \(label)" - } +public protocol ConcreteDeclSyntaxProtocol: SyntaxProtocol { + var attributes: AttributeListSyntax { get set } + var modifiers: DeclModifierListSyntax { get set } + var inheritanceClause: InheritanceClauseSyntax? { get set } + var name: TokenSyntax { get set } + var isClass: Bool { get } +} - public var asSelfAssignment: String { - "self.\(label) = \(label)" - } +extension ActorDeclSyntax: ConcreteDeclSyntaxProtocol { + public var isClass: Bool { false } +} +extension ClassDeclSyntax: ConcreteDeclSyntaxProtocol { + public var isClass: Bool { true } +} +extension StructDeclSyntax: ConcreteDeclSyntaxProtocol { + public var isClass: Bool { false } } diff --git a/Sources/SafeDICore/ConstructableVisitor.swift b/Sources/SafeDICore/ConstructableVisitor.swift new file mode 100644 index 00000000..1d2d419e --- /dev/null +++ b/Sources/SafeDICore/ConstructableVisitor.swift @@ -0,0 +1,193 @@ +// 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 SwiftDiagnostics +import SwiftSyntax + +public final class ConstructableVisitor: SyntaxVisitor { + + // MARK: Initialization + + public init() { + super.init(viewMode: .sourceAccurate) + } + + // MARK: SyntaxVisitor + + public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + // Check attributes and extract dependency source. + let dependencySources = node.attributes.dependencySources + guard dependencySources.isEmpty || dependencySources.count == 1 else { + diagnostics.append(Diagnostic( + node: node.attributes, + error: FixableConstructableError.dependencyHasTooManyAttributes, + changes: [ + .replace( + oldNode: Syntax(node.attributes), + newNode: Syntax(dependencySources[0].node) + ) + ] + )) + return .skipChildren + } + guard let dependencySource = dependencySources.first?.source else { + // This dependency is not part of the DI system. + return .skipChildren + } + + // Check the bindings. + for binding in node.bindings { + // Check that each variable has no initializer. + if binding.initializer != nil { + var bindingWithoutInitializer = binding + bindingWithoutInitializer.initializer = nil + diagnostics.append(Diagnostic( + node: node, + error: FixableConstructableError.dependencyHasInitializer, + changes: [ + .replace( + oldNode: Syntax(binding), + newNode: Syntax(bindingWithoutInitializer) + ) + ] + )) + } + + if + let label = IdentifierPatternSyntax(binding.pattern)?.identifier.text, + let type = binding.typeAnnotation?.type + { + dependencies.append( + Dependency( + property: Property( + label: label, + type: type.description + ), + source: dependencySource + ) + ) + } + } + + return .skipChildren + } + + public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { + initializers.append(Initializer(node)) + return .skipChildren + } + + public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { + .skipChildren + } + + public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + visitDecl(node) + } + + public override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + visitDecl(node) + } + + public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + visitDecl(node) + } + + // MARK: Public + + public private(set) var dependencies = [Dependency]() + public private(set) var initializers = [Initializer]() + public private(set) var constructableType: String? + public private(set) var additionalConstructableTypes: [String]? + public private(set) var diagnostics = [Diagnostic]() + public var constructable: Constructable? { + guard let constructableType else { return nil } + return Constructable( + constructableType: constructableType, + additionalConstructableTypes: additionalConstructableTypes, + dependencies: dependencies) + } + + public static let macroName = "constructable" + + // MARK: Private + + private var isInTopLevelDeclaration = false + + private func visitDecl(_ node: some ConcreteDeclSyntaxProtocol) -> SyntaxVisitorContinueKind { + guard !isInTopLevelDeclaration else { + return .skipChildren + } + isInTopLevelDeclaration = true + + constructableType = node.name.text + processAttributes(node.attributes, on: node) + processModifiers(node.modifiers, on: node) + + return .visitChildren + } + + private func processAttributes(_ attributes: AttributeListSyntax, on node: some ConcreteDeclSyntaxProtocol) { + guard let macro = attributes.constructingMacro else { + assertionFailure("Constructing macro not found despite processing top-level declaration") + return + } + guard + let fulfillingAdditionalTypesArgument = macro.arguments, + let fulfillingAdditionalTypesExpressionList = LabeledExprListSyntax(fulfillingAdditionalTypesArgument), + let fulfillingAdditionalTypesExpression = fulfillingAdditionalTypesExpressionList.first?.expression, + let fulfillingAdditionalTypesArray = ArrayExprSyntax(fulfillingAdditionalTypesExpression) + else { + // Nothing to do here. + return + } + + additionalConstructableTypes = fulfillingAdditionalTypesArray + .elements + .map { $0.expression } + .compactMap { MemberAccessExprSyntax($0)?.base } + .compactMap { DeclReferenceExprSyntax($0)?.baseName.text } + } + + private func processModifiers(_ modifiers: DeclModifierListSyntax, on node: some ConcreteDeclSyntaxProtocol) { + if !node.modifiers.containsPublicOrOpen { + diagnostics.append(Diagnostic( + node: node, + error: FixableConstructableError.missingPublicOrOpenAttribute, + changes: [ + .replace( + oldNode: Syntax(node.modifiers), + newNode: Syntax(DeclModifierListSyntax( + arrayLiteral: + DeclModifierSyntax( + name: TokenSyntax( + TokenKind.keyword(.public), + leadingTrivia: .newline, + trailingTrivia: .space, + presence: .present + ) + ) + )) + ) + ] + )) + } + } +} diff --git a/Sources/SafeDICore/DependenciesVisitor.swift b/Sources/SafeDICore/DependenciesVisitor.swift deleted file mode 100644 index 02cda3f0..00000000 --- a/Sources/SafeDICore/DependenciesVisitor.swift +++ /dev/null @@ -1,360 +0,0 @@ -// 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 SwiftDiagnostics -import SwiftSyntax - -public final class DependenciesVisitor: SyntaxVisitor { - - // MARK: Initialization - - public init() { - super.init(viewMode: .sourceAccurate) - } - - // MARK: SyntaxVisitor - - public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { - // Check attributes and extract dependency source. - let dependencySources = node.attributes.dependencySources - guard dependencySources.isEmpty || dependencySources.count == 1 else { - let replacementNode: Syntax - if let firstDependencySourceNode = dependencySources.first?.node { - replacementNode = Syntax(firstDependencySourceNode) - } else { - replacementNode = Syntax(AttributeSyntax( - attributeName: IdentifierTypeSyntax( - name: TokenSyntax( - TokenKind.identifier(Dependency.Source.constructedAttributeName), - presence: .present - ) - ) - )) - } - diagnostics.append(Diagnostic( - node: node.attributes, - error: FixableDependenciesError.dependencyHasTooManyAttributes, - changes: [ - .replace( - oldNode: Syntax(node.attributes), - newNode: replacementNode - ) - ] - )) - return .skipChildren - } - let dependencySource = dependencySources.first?.source ?? .providedInvariant - - // Check modifiers. - if node.modifiers.staticModifier != nil { - var mutatedNode = node - mutatedNode.modifiers = mutatedNode.modifiers.filter { - $0.name.text != "static" - } - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.dependencyIsStatic, - changes: [ - .replace( - oldNode: Syntax(node), - newNode: Syntax(mutatedNode) - ) - ] - )) - } - - if node.modifiers.count != 1, - node.modifiers.first?.name.text != "private" - { - let replacedModifiers = DeclModifierListSyntax( - arrayLiteral: DeclModifierSyntax( - name: TokenSyntax( - TokenKind.identifier("private"), - presence: .present - ) - ) - ) - var modifiedNode = node - modifiedNode.modifiers = replacedModifiers - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.dependencyIsNotPrivate, - changes: [ - .replace( - oldNode: Syntax(node), - newNode: Syntax(modifiedNode) - ) - ] - )) - return .skipChildren - } - - // Check the binding specifier. - if node.bindingSpecifier.text == "var" { - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.dependencyIsMutable, - changes: [ - .replace( - oldNode: Syntax(node.bindingSpecifier), - newNode: Syntax(TokenSyntax(TokenKind.keyword(.let), presence: .present)) - ) - ] - )) - } - - for binding in node.bindings { - // Check that each variable has no initializer. - if binding.initializer != nil { - var bindingWithoutInitializer = binding - bindingWithoutInitializer.initializer = nil - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.unexpectedInitializer, - changes: [ - .replace( - oldNode: Syntax(binding), - newNode: Syntax(bindingWithoutInitializer) - ) - ] - )) - } - - if - let label = IdentifierPatternSyntax(binding.pattern)?.identifier.text, - let type = binding.typeAnnotation?.type - { - addDependency( - Dependency( - property: Property( - label: label, - type: type.description - ), - source: dependencySource - ), - derivedFrom: Syntax(node) - ) - } - } - - return .skipChildren - } - - public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { - guard - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - else { - return .skipChildren - } - - modifiedGreatGrandparent.members.remove(at: index) - - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.unexpectedInitializer, - changes: [ - .replace( - oldNode: greatGrandparent, - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - return .skipChildren - } - - public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { - if node.name.text == DependenciesVisitor.buildMethodName { - if didFindBuildMethod { - // We've already found a `build` method! - if - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - { - modifiedGreatGrandparent.members.remove(at: index) - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.multipleBuildMethods, - changes: [ - .replace( - oldNode: Syntax(greatGrandparent), - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - } else { - assertionFailure("Found duplicate build method with unexpected properties \(node)") - } - - } else { - didFindBuildMethod = true - for parameter in node.signature.parameterClause.parameters { - addDependency( - Dependency( - property: Property( - label: parameter.secondName?.text ?? parameter.firstName.text, - type: parameter.type.trimmedDescription - ), - source: .variant - ), - derivedFrom: Syntax(parameter) - ) - } - - if let returnClause = node.signature.returnClause { - builtType = returnClause.type.trimmedDescription - } else { - var signatureWithReturnClause = node.signature - signatureWithReturnClause.returnClause = FunctionDeclSyntax.returnClauseTemplate - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.missingBuildMethodReturnClause, - changes: [ - .replace( - oldNode: Syntax(node.signature), - newNode: Syntax(signatureWithReturnClause) - ) - ] - )) - } - } - } - - return .skipChildren - } - - public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - if node.name.text == DependenciesVisitor.decoratedStructName { - guard node.modifiers.containsPublic else { - diagnostics.append(Diagnostic( - node: node.attributes, - error: FixableDependenciesError.missingPublicAttributeOnDependencies, - changes: [ - .replace( - oldNode: Syntax(node.modifiers), - newNode: Syntax(DeclModifierSyntax( - name: TokenSyntax( - TokenKind.keyword(.public), - presence: .present - ) - )) - ) - ] - )) - return .skipChildren - } - - guard node.attributes.dependenciesMacro != nil else { - var newAttributes = node.attributes - newAttributes.append(.attribute( - AttributeSyntax( - attributeName: IdentifierTypeSyntax( - name: .identifier(DependenciesVisitor.decoratedStructName) - ) - ) - )) - - diagnostics.append(Diagnostic( - node: node.attributes, - error: FixableDependenciesError.missingDependenciesAttribute, - changes: [ - .replace( - oldNode: Syntax(node.attributes), - newNode: Syntax(newAttributes) - ) - ] - )) - return .skipChildren - } - - return .visitChildren - } else { - return .skipChildren - } - } - - // MARK: Public - - public private(set) var didFindBuildMethod = false - public private(set) var dependencies = [Dependency]() - public private(set) var builtType: String? - public private(set) var diagnostics = [Diagnostic]() - - public static let macroName = "dependencies" - public static let decoratedStructName = "Dependencies" - public static let buildMethodName = "build" - - // MARK: Private - - private var dependencyVariableNames = Set() - - private func addDependency(_ dependency: Dependency, derivedFrom node: Syntax) { - guard !dependencyVariableNames.contains(dependency.property.label) else { - if - let typedNode = FunctionParameterSyntax(node), - let parent = node.parent, - let typedParent = FunctionParameterListSyntax(parent), - let index = typedParent.index(of: typedNode) - { - var modifiedParent = typedParent - modifiedParent.remove(at: index) - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.duplicateDependency, - changes: [ - .replace( - oldNode: Syntax(typedParent), - newNode: Syntax(modifiedParent) - ) - ] - )) - } else if - let parent = node.parent, - let typedParent = MemberBlockItemSyntax(parent), - let greatGrandparent = parent.parent?.parent, - var modifiedGreatGrandparent = MemberBlockSyntax(greatGrandparent), - let index = modifiedGreatGrandparent.members.index(of: typedParent) - { - modifiedGreatGrandparent.members.remove(at: index) - diagnostics.append(Diagnostic( - node: node, - error: FixableDependenciesError.duplicateDependency, - changes: [ - .replace( - oldNode: Syntax(greatGrandparent), - newNode: Syntax(modifiedGreatGrandparent) - ) - ] - )) - } else { - assertionFailure("Unexpected node with duplicate dependency \(node)") - } - return - } - dependencyVariableNames.insert(dependency.property.label) - dependencies.append(dependency) - } -} diff --git a/Sources/SafeDICore/Errors/FixableBuilderError.swift b/Sources/SafeDICore/Errors/FixableBuilderError.swift deleted file mode 100644 index a71db9cf..00000000 --- a/Sources/SafeDICore/Errors/FixableBuilderError.swift +++ /dev/null @@ -1,97 +0,0 @@ -// 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 SwiftDiagnostics - -public enum FixableBuilderError: DiagnosticError { - case missingDependencies - case unexpectedVariableDeclaration - case unexpectedInitializer - case unexpectedFuncationDeclaration - - public var description: String { - switch self { - case .missingDependencies: - return "Missing nested `@\(DependenciesVisitor.macroName) public struct \(DependenciesVisitor.decoratedStructName)` declaration" - case .unexpectedVariableDeclaration: - return "Found unexpected variable declaration in `@\(BuilderVisitor.macroName)`" - case .unexpectedInitializer: - return "Found unexpected initializer in `@\(BuilderVisitor.macroName)`" - case .unexpectedFuncationDeclaration: - return "Found unexpected function declaration in `@\(BuilderVisitor.macroName)`" - } - } - - public var diagnostic: SwiftDiagnostics.DiagnosticMessage { - BuilderDiagnosticMessage(error: self) - } - - public var fixIt: SwiftDiagnostics.FixItMessage { - BuilderFixItMessage(error: self) - } - - // MARK: - BuilderDiagnosticMessage - - private struct BuilderDiagnosticMessage: DiagnosticMessage { - var diagnosticID: MessageID { - MessageID(domain: "FixableBuilderError.DiagnosticMessage", id: error.description) - } - - var severity: DiagnosticSeverity { - switch error { - case .missingDependencies, - .unexpectedVariableDeclaration, - .unexpectedInitializer, - .unexpectedFuncationDeclaration: - return .error - } - } - - var message: String { - error.description - } - - let error: FixableBuilderError - } - - // MARK: - BuilderFixItMessage - - private struct BuilderFixItMessage: FixItMessage { - var message: String { - switch error { - case .missingDependencies: - return "Create nested `@\(DependenciesVisitor.macroName) struct \(DependenciesVisitor.decoratedStructName)`" - case .unexpectedVariableDeclaration: - return "Delete variable declaration" - case .unexpectedInitializer: - return "Delete initializer" - case .unexpectedFuncationDeclaration: - return "Delete function declaration" - } - } - - var fixItID: SwiftDiagnostics.MessageID { - MessageID(domain: "FixableBuilderError.FixItMessage", id: error.description) - } - - - let error: FixableBuilderError - } -} diff --git a/Sources/SafeDICore/Errors/FixableConstructableError.swift b/Sources/SafeDICore/Errors/FixableConstructableError.swift new file mode 100644 index 00000000..8a588c91 --- /dev/null +++ b/Sources/SafeDICore/Errors/FixableConstructableError.swift @@ -0,0 +1,96 @@ +// 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 SwiftDiagnostics + +public enum FixableConstructableError: DiagnosticError { + case dependencyHasTooManyAttributes + case dependencyHasInitializer + case missingPublicOrOpenAttribute + case missingRequiredInitializer + + public var description: String { + switch self { + case .dependencyHasTooManyAttributes: + return "Dependency can have at most one of @\(Dependency.Source.constructedInvariant), @\(Dependency.Source.providedInvariant), @\(Dependency.Source.singletonInvariant), or @\(Dependency.Source.propagatedVariant) attached macro" + case .dependencyHasInitializer: + return "Dependency must not have hand-written initializer" + case .missingPublicOrOpenAttribute: + return "@\(ConstructableVisitor.macroName)-decorated type must be `public` or `open`" + case .missingRequiredInitializer: + return "@\(ConstructableVisitor.macroName)-decorated type must have initializer for all injected parameters" + } + } + + public var diagnostic: DiagnosticMessage { + ConstructableDiagnosticMessage(error: self) + } + + public var fixIt: FixItMessage { + ConstructableFixItMessage(error: self) + } + + // MARK: - ConstructableDiagnosticMessage + + private struct ConstructableDiagnosticMessage: DiagnosticMessage { + var diagnosticID: MessageID { + MessageID(domain: "\(Self.self)", id: error.description) + } + + var severity: DiagnosticSeverity { + switch error { + case .dependencyHasTooManyAttributes, + .dependencyHasInitializer, + .missingPublicOrOpenAttribute, + .missingRequiredInitializer: + return .error + } + } + + var message: String { + error.description + } + + let error: FixableConstructableError + } + + // MARK: - ConstructableFixItMessage + + private struct ConstructableFixItMessage: FixItMessage { + var message: String { + switch error { + case .dependencyHasTooManyAttributes: + return "Remove excessive attached macros" + case .dependencyHasInitializer: + return "Remove initializer" + case .missingPublicOrOpenAttribute: + return "Add `public` modifier" + case .missingRequiredInitializer: + return "Add required initializer" + } + } + + var fixItID: SwiftDiagnostics.MessageID { + MessageID(domain: "\(Self.self)", id: error.description) + } + + let error: FixableConstructableError + } +} diff --git a/Sources/SafeDICore/Errors/FixableDependenciesError.swift b/Sources/SafeDICore/Errors/FixableDependenciesError.swift deleted file mode 100644 index be56f7c2..00000000 --- a/Sources/SafeDICore/Errors/FixableDependenciesError.swift +++ /dev/null @@ -1,140 +0,0 @@ -// 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 SwiftDiagnostics - -public enum FixableDependenciesError: DiagnosticError { - case missingDependenciesAttribute - case missingPublicAttributeOnDependencies - case dependencyHasTooManyAttributes - case dependencyIsStatic - case dependencyIsNotPrivate - case dependencyIsMutable - case unexpectedInitializer - case missingBuildMethod - case missingBuildMethodReturnClause - case multipleBuildMethods - case duplicateDependency - - public var description: String { - switch self { - case .missingDependenciesAttribute: - return "Missing `@\(DependenciesVisitor.macroName)` attached macro on `public struct Dependencies`" - case .missingPublicAttributeOnDependencies: - return "Missing `public` modifier on `struct Dependencies`" - case .dependencyHasTooManyAttributes: - return "Dependency can have at most one `@\(Dependency.Source.constructedAttributeName)` or `@\(Dependency.Source.singletonAttributeName)` attached macro" - case .dependencyIsStatic: - return "Dependency must not be `static`" - case .dependencyIsNotPrivate: - return "Dependency property must be `private`" - case .dependencyIsMutable: - return "Dependency must be immutable" - case .unexpectedInitializer: - return "Dependency must not have hand-written initializer" - case .missingBuildMethod: - return "@\(DependenciesVisitor.macroName)-decorated type must have `func build(...) -> BuiltProduct` method" - case .missingBuildMethodReturnClause: - return "@\(DependenciesVisitor.macroName)-decorated type's `func build(...)` method must return a type" - case .multipleBuildMethods: - return "@\(DependenciesVisitor.macroName)-decorated type must have a single `func build(...) -> BuiltProduct` method" - case .duplicateDependency: - return "Every declared dependency must have a unique name" - } - } - - public var diagnostic: DiagnosticMessage { - DependenciesDiagnosticMessage(error: self) - } - - public var fixIt: FixItMessage { - DependenciesFixItMessage(error: self) - } - - // MARK: - DependenciesDiagnosticMessage - - private struct DependenciesDiagnosticMessage: DiagnosticMessage { - - var diagnosticID: MessageID { - MessageID(domain: "FixableDependenciesError.DiagnosticMessage", id: error.description) - } - - var severity: DiagnosticSeverity { - switch error { - case .missingDependenciesAttribute, - .missingPublicAttributeOnDependencies, - .dependencyHasTooManyAttributes, - .dependencyIsStatic, - .dependencyIsNotPrivate, - .dependencyIsMutable, - .unexpectedInitializer, - .missingBuildMethod, - .missingBuildMethodReturnClause, - .multipleBuildMethods, - .duplicateDependency: - return .error - } - } - - var message: String { - error.description - } - - let error: FixableDependenciesError - } - - // MARK: - DependenciesFixItMessage - - struct DependenciesFixItMessage: SwiftDiagnostics.FixItMessage { - var message: String { - switch error { - case .missingDependenciesAttribute: - return "Attach `@\(DependenciesVisitor.macroName)` macro" - case .missingPublicAttributeOnDependencies: - return "Make `struct \(DependenciesVisitor.decoratedStructName)` have an access level of `public`" - case .dependencyHasTooManyAttributes: - return "Remove all but first `@\(Dependency.Source.constructedAttributeName)` or `@\(Dependency.Source.singletonAttributeName)` attached macro" - case .dependencyIsStatic: - return "Remove `static` from property" - case .dependencyIsNotPrivate: - return "Make property `private`" - case .dependencyIsMutable: - return "Make property immutable" - case .unexpectedInitializer: - return "Remove initializer" - case .missingBuildMethod: - return "Add `func build(...) -> BuiltProduct` template" - case .missingBuildMethodReturnClause: - return "Add return clause to `func build(...)`" - case .multipleBuildMethods: - return "Remove duplicate `func build(...)` method" - case .duplicateDependency: - return "Delete duplicated dependency" - } - } - - var fixItID: SwiftDiagnostics.MessageID { - MessageID(domain: "FixableDependenciesError.FixItMessage", id: error.description) - } - - - let error: FixableDependenciesError - } -} diff --git a/Sources/SafeDICore/Errors/FixableInjectableError.swift b/Sources/SafeDICore/Errors/FixableInjectableError.swift new file mode 100644 index 00000000..9fd7a27f --- /dev/null +++ b/Sources/SafeDICore/Errors/FixableInjectableError.swift @@ -0,0 +1,78 @@ +// 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 SwiftDiagnostics + +public enum FixableInjectableError: DiagnosticError { + case unexpectedMutable + + public var description: String { + switch self { + case .unexpectedMutable: + return "Dependency can not be mutable" + } + } + + public var diagnostic: DiagnosticMessage { + InjectableDiagnosticMessage(error: self) + } + + public var fixIt: FixItMessage { + InjectableFixItMessage(error: self) + } + + // MARK: - InjectableDiagnosticMessage + + private struct InjectableDiagnosticMessage: DiagnosticMessage { + var diagnosticID: MessageID { + MessageID(domain: "\(Self.self)", id: error.description) + } + + var severity: DiagnosticSeverity { + switch error { + case .unexpectedMutable: + return .error + } + } + + var message: String { + error.description + } + + let error: FixableInjectableError + } + + // MARK: - InjectableFixItMessage + + private struct InjectableFixItMessage: FixItMessage { + var message: String { + switch error { + case .unexpectedMutable: + return "Replace `var` with `let`" + } + } + + var fixItID: SwiftDiagnostics.MessageID { + MessageID(domain: "\(Self.self)", id: error.description) + } + + let error: FixableInjectableError + } +} diff --git a/Sources/SafeDICore/Extensions/ArrayExtensions.swift b/Sources/SafeDICore/Extensions/ArrayExtensions.swift index 9744277b..b10ee453 100644 --- a/Sources/SafeDICore/Extensions/ArrayExtensions.swift +++ b/Sources/SafeDICore/Extensions/ArrayExtensions.swift @@ -23,72 +23,128 @@ import SwiftSyntaxBuilder extension Array where Element == Dependency { - public var variantUnlabeledParameterList: FunctionParameterListSyntax { - FunctionParameterListSyntax( - filter { $0.source == .variant } - .map { "\(raw: $0.property.type)" } - .transformUntilLast { - var functionPamameterSyntax = $0 - functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present) - functionPamameterSyntax.trailingTrivia = .space - return functionPamameterSyntax - } + var buildDependenciesFunctionParameter: FunctionParameterSyntax { + FunctionParameterSyntax( + firstName: Initializer.Argument.dependenciesArgumentName, + colon: .colonToken(trailingTrivia: .space), + type: buildDependenciesFunctionSignature, + trailingComma: filter { $0.isVariant }.isEmpty ? nil : .commaToken(trailingTrivia: .space) ) } - public var variantParameterList: FunctionParameterListSyntax { - FunctionParameterListSyntax( - filter { $0.source == .variant } - .map { "\(raw: $0.property.asParameterDeclaration)" } - .transformUntilLast { - var functionPamameterSyntax = $0 - functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present) - functionPamameterSyntax.trailingTrivia = .space - return functionPamameterSyntax - } + var buildDependenciesFunctionSignature: FunctionTypeSyntax { + FunctionTypeSyntax( + parameters: buildDependenciesClosureArguments, + returnClause: ReturnClauseSyntax( + leadingTrivia: .space, + type: TupleTypeSyntax( + leadingTrivia: .space, + elements: buildDependenciesClosureReturnType + ) + ) ) } - public var variantUnlabeledExpressionList: String { - filter { $0.isVariant } - .map(\.property.label) - .joined(separator: ", ") + var buildDependenciesClosureArguments: TupleTypeElementListSyntax { + TupleTypeElementListSyntax { + for variantUnamedTuple in variantUnamedTuples { + variantUnamedTuple + } + } + } + + var buildDependenciesClosureReturnType: TupleTypeElementListSyntax { + TupleTypeElementListSyntax { + for invariantNamedTuple in namedTuples { + invariantNamedTuple + } + } + } + + var namedTuples: [TupleTypeElementSyntax] { + map { + if count > 1 { + return $0.property.asNamedTupleTypeElement + } else { + return $0.property.asUnnamedTupleTypeElement + } + } + .transformUntilLast { + var node = $0 + node.trailingComma = .commaToken(trailingTrivia: .space) + return node + } } - public var variantLabeledExpressionList: String { + var variantUnamedTuples: [TupleTypeElementSyntax] { filter { $0.isVariant } - .map(\.property.asLabeledParameterExpression) - .joined(separator: ", ") + .map(\.property.asUnnamedTupleTypeElement) + .transformUntilLast { + var node = $0 + node.trailingComma = .commaToken(trailingTrivia: .space) + return node + } } - public var invariantParameterList: FunctionParameterListSyntax { - FunctionParameterListSyntax( - filter { $0.isInvariant } - .map { "\(raw: $0.property.asParameterDeclaration)" } - .transformUntilLast { - var functionPamameterSyntax = $0 - functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present) - functionPamameterSyntax.trailingTrivia = .space - return functionPamameterSyntax - } - ) + var functionParameters: [FunctionParameterSyntax] { + map { $0.property.asFunctionParamter } + .transformUntilLast { + var node = $0 + node.trailingComma = .commaToken(trailingTrivia: .space) + return node + } + } + + var propagatedVariantsFunctionParameters: [FunctionParameterSyntax] { + filter { $0.isVariant } + .map { $0.property.asFunctionParamter } + .transformUntilLast { + var node = $0 + node.trailingComma = .commaToken(trailingTrivia: .space) + return node + } } - public var invariantAssignmentExpressionList: String { - """ - \(filter(\.isInvariant) - .map(\.property.asSelfAssignment) - .joined(separator: "\n")) - """ + var propagatedVariantsLabeledExpressions: [LabeledExprSyntax] { + filter { $0.isVariant } + .map { $0.property.asUnnamedLabeledExpr } + .transformUntilLast { + var node = $0 + node.trailingComma = .commaToken(trailingTrivia: .space) + return node + } } + var dependenciesDeclaration: VariableDeclSyntax { + VariableDeclSyntax( + leadingTrivia: .spaces(4), + .let, + name: PatternSyntax( + IdentifierPatternSyntax( + leadingTrivia: .space, + identifier: isEmpty ? .identifier("_") : Initializer.dependenciesToken) + ), + initializer: InitializerClauseSyntax( + leadingTrivia: .space, + equal: .equalToken(trailingTrivia: .space), + value: FunctionCallExprSyntax( + calledExpression: DeclReferenceExprSyntax( + baseName: Initializer.Argument.dependenciesArgumentName), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + for propagatedVariantsLabeledExpression in propagatedVariantsLabeledExpressions { + propagatedVariantsLabeledExpression + } + }, + rightParen: .rightParenToken() + ), + trailingTrivia: .newline + ) + ) + } } extension Array { - - /// Returns an array with all of the items in the array except for the last transformed. - /// - Parameter transform: A transforming closure. `transform` accepts an element of this sequence as its parameter and returns a transformed value of the same type. - /// - Returns: An array containing the transformed elements of this sequence, plus the untransfomred last element. fileprivate func transformUntilLast(_ transform: (Element) throws -> Element) rethrows -> [Element] { var arrayToTransform = self guard let lastItem = arrayToTransform.popLast() else { diff --git a/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift b/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift index 88585eb0..d55d6016 100644 --- a/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift +++ b/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift @@ -22,25 +22,11 @@ import SwiftSyntax extension AttributeListSyntax { - var dependenciesMacro: AttributeSyntax? { + var constructingMacro: AttributeSyntax? { guard let attribute = first(where: { element in switch element { case let .attribute(attribute): - return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == DependenciesVisitor.macroName - case .ifConfigDecl: - return false - } - }) else { - return nil - } - return AttributeSyntax(attribute) - } - - var builderMacro: AttributeSyntax? { - guard let attribute = first(where: { element in - switch element { - case let .attribute(attribute): - return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == BuilderVisitor.macroName + return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == ConstructableVisitor.macroName case .ifConfigDecl: return false } @@ -66,7 +52,7 @@ extension AttributeListSyntax { public var dependencySources: [(source: Dependency.Source, node: AttributeListSyntax.Element)] { attributedNodes.compactMap { - guard let source = Dependency.Source.init($0.attribute) else { + guard let source = Dependency.Source.init(rawValue: $0.attribute) else { return nil } return (source: source, node: $0.node) diff --git a/Sources/SafeDICore/Extensions/DeclModifierListSyntaxExtensions.swift b/Sources/SafeDICore/Extensions/DeclModifierListSyntaxExtensions.swift index 883d306c..f9d136a3 100644 --- a/Sources/SafeDICore/Extensions/DeclModifierListSyntaxExtensions.swift +++ b/Sources/SafeDICore/Extensions/DeclModifierListSyntaxExtensions.swift @@ -22,9 +22,10 @@ import SwiftSyntax extension DeclModifierListSyntax { - public var containsPublic: Bool { + public var containsPublicOrOpen: Bool { contains(where: { modifier in modifier.name.text == "public" + || modifier.name.text == "open" }) } diff --git a/Sources/SafeDICore/Builder.swift b/Sources/SafeDICore/Models/Constructable.swift similarity index 57% rename from Sources/SafeDICore/Builder.swift rename to Sources/SafeDICore/Models/Constructable.swift index 2f633735..24e429a5 100644 --- a/Sources/SafeDICore/Builder.swift +++ b/Sources/SafeDICore/Models/Constructable.swift @@ -18,42 +18,27 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -public struct Builder: Codable, Equatable { +public struct Constructable: Codable, Equatable { // MARK: Initialization - init( - typeName: String, - builtPropertyName: String, - builtType: String, - dependencies: [Dependency] - ) { - builtProduct = Property( - label: builtPropertyName, - type: builtType - ) - builder = Property( - label: Self.propertyNameSuffix(forProperty: builtPropertyName), - type: typeName - ) + public init?( + constructableType: String, + additionalConstructableTypes: [String]?, + dependencies: [Dependency]) + { + self.constructableTypes = [constructableType] + (additionalConstructableTypes ?? []) self.dependencies = dependencies } // MARK: Public - /// The injectable built product created by this builder. - public let builtProduct: Property - /// The injectable builder property that represents this builder. - public let builder: Property - /// This builder's dependencies. - public let dependencies: [Dependency] - - // MARK: Private - - /// The label suffix on all builder properties. - private static let propertyNameSuffix = "Builder" - - private static func propertyNameSuffix(forProperty label: String) -> String { - label + Self.propertyNameSuffix + /// The types that can be fulfilled with this Constructable. + public let constructableTypes: [String] + /// The concrete type that fulfills `constructableTypes`. + public var concreteConstructableType: String { + constructableTypes[0] } + /// The ordered dependencies of this Constructable. + public let dependencies: [Dependency] } diff --git a/Sources/SafeDICore/Dependency.swift b/Sources/SafeDICore/Models/Dependency.swift similarity index 70% rename from Sources/SafeDICore/Dependency.swift rename to Sources/SafeDICore/Models/Dependency.swift index 2be44c76..a5c500ab 100644 --- a/Sources/SafeDICore/Dependency.swift +++ b/Sources/SafeDICore/Models/Dependency.swift @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +/// A representation of a dependency. +/// e.g. `@singleton let mySingleton: MySingleton` public struct Dependency: Codable, Equatable { public let property: Property public let source: Source @@ -26,7 +28,7 @@ public struct Dependency: Codable, Equatable { switch source { case .constructedInvariant, .providedInvariant, .singletonInvariant: return false - case .variant: + case .propagatedVariant: return true } } @@ -35,28 +37,15 @@ public struct Dependency: Codable, Equatable { switch source { case .constructedInvariant, .providedInvariant, .singletonInvariant: return true - case .variant: + case .propagatedVariant: return false } } - public enum Source: Codable, Equatable { - case constructedInvariant - case providedInvariant - case singletonInvariant - case variant - - public static let constructedAttributeName = "constructed" - public static let singletonAttributeName = "singleton" - - public init?(_ attributeText: String) { - if attributeText == Self.constructedAttributeName { - self = .constructedInvariant - } else if attributeText == Self.singletonAttributeName { - self = .singletonInvariant - } else { - return nil - } - } + public enum Source: String, RawRepresentable, Codable, Equatable { + case constructedInvariant = "constructed" + case providedInvariant = "provided" + case singletonInvariant = "singleton" + case propagatedVariant = "propagated" } } diff --git a/Sources/SafeDICore/Models/Initializer.swift b/Sources/SafeDICore/Models/Initializer.swift new file mode 100644 index 00000000..539f2cb3 --- /dev/null +++ b/Sources/SafeDICore/Models/Initializer.swift @@ -0,0 +1,257 @@ +// 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 +import SwiftSyntaxBuilder + +public struct Initializer: Codable, Equatable { + + // MARK: Initialization + + init(_ node: InitializerDeclSyntax) { + isOptional = node.optionalMark != nil + hasGenericParameter = node.genericParameterClause != nil + hasGenericWhereClause = node.genericWhereClause != nil + arguments = node + .signature + .parameterClause + .parameters + .map(Argument.init) + } + + public init( + isOptional: Bool, + hasGenericParameter: Bool, + hasGenericWhereClause: Bool, + arguments: [Initializer.Argument]) + { + self.isOptional = isOptional + self.hasGenericParameter = hasGenericParameter + self.hasGenericWhereClause = hasGenericWhereClause + self.arguments = arguments + } + + // MARK: Public + + public let isOptional: Bool + public let hasGenericParameter: Bool + public let hasGenericWhereClause: Bool + public let arguments: [Argument] + + public func generateSafeDIInitializer(fulfilling dependencies: [Dependency], typeIsClass: Bool, trailingNewline: Bool = false) throws -> InitializerDeclSyntax { + guard !isOptional else { + throw GenerationError.optionalInitializer + } + guard !hasGenericParameter else { + throw GenerationError.genericParameterInInitializer + } + guard !hasGenericWhereClause else { + throw GenerationError.whereClauseOnInitializer + } + + let propertyLabels = Set(dependencies.map(\.property.label)) + let argumentLabels = Set(arguments.map(\.innerLabel)) + let extraArguments = argumentLabels.subtracting(propertyLabels) + guard extraArguments.isEmpty else { + throw GenerationError.tooManyArguments(labels: extraArguments) + } + let missingArguments = propertyLabels.subtracting(argumentLabels) + guard missingArguments.isEmpty else { + throw GenerationError.missingArguments(labels: missingArguments) + } + guard !dependencies.isEmpty else { + throw GenerationError.noDependencies + } + + let modifiers: DeclModifierListSyntax + let publicModifier = DeclModifierSyntax( + name: TokenSyntax( + TokenKind.identifier("public"), + presence: .present + ), + trailingTrivia: .space + ) + if typeIsClass { + modifiers = DeclModifierListSyntax( + arrayLiteral: publicModifier, + DeclModifierSyntax( + name: TokenSyntax( + TokenKind.identifier("convenience"), + presence: .present + ), + trailingTrivia: .space + ) + ) + } else { + modifiers = DeclModifierListSyntax(arrayLiteral: publicModifier) + } + + let initFunctionCall = FunctionCallExprSyntax( + leadingTrivia: .spaces(4), + calledExpression: MemberAccessExprSyntax( + base: DeclReferenceExprSyntax( + baseName: TokenSyntax.keyword(.`self`) + ), + name: TokenSyntax.keyword(.`init`)), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax { + for (index, argument) in arguments.enumerated() { + if dependencies.count > 1 { + LabeledExprSyntax( + leadingTrivia: index == 0 ? nil : .space, + label: .identifier(argument.label), + colon: .colonToken(trailingTrivia: .space), + expression: + MemberAccessExprSyntax( + base: DeclReferenceExprSyntax(baseName: Self.dependenciesToken), + name: .identifier(argument.innerLabel) + ) + ) + } else { + LabeledExprSyntax( + leadingTrivia: index == 0 ? nil : .space, + label: .identifier(argument.label), + colon: .colonToken(trailingTrivia: .space), + expression: DeclReferenceExprSyntax(baseName: Self.dependenciesToken) + ) + } + } + }, + rightParen: .rightParenToken() + ) + return InitializerDeclSyntax( + modifiers: modifiers, + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + parameters: FunctionParameterListSyntax(itemsBuilder: { + dependencies.buildDependenciesFunctionParameter + for propagatedVariantsFunctionParameter in dependencies.propagatedVariantsFunctionParameters { + propagatedVariantsFunctionParameter + } + }) + ), + trailingTrivia: .space + ), + bodyBuilder: { + CodeBlockItemSyntax( + leadingTrivia: .newline, + item: .decl(DeclSyntax(dependencies.dependenciesDeclaration)) + ) + CodeBlockItemSyntax( + item: .expr(ExprSyntax(initFunctionCall)), + trailingTrivia: trailingNewline ? .newline : nil + ) + } + ) + } + + public static func generateRequiredInitializer(for dependencies: [Dependency]) -> InitializerDeclSyntax { + return InitializerDeclSyntax( + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax( + parameters: FunctionParameterListSyntax(itemsBuilder: { + for functionParameter in dependencies.functionParameters { + functionParameter + } + }) + ), + trailingTrivia: .space + ), + bodyBuilder: { + for dependency in dependencies { + CodeBlockItemSyntax( + item: .expr(ExprSyntax(InfixOperatorExprSyntax( + leadingTrivia: .newline, + leftOperand: MemberAccessExprSyntax( + base: DeclReferenceExprSyntax(baseName: TokenSyntax.keyword(.`self`)), + name: TokenSyntax.identifier(dependency.property.label)), + operator: AssignmentExprSyntax( + leadingTrivia: .space, + trailingTrivia: .space), + rightOperand: DeclReferenceExprSyntax(baseName: TokenSyntax.identifier(dependency.property.label)), + trailingTrivia: .newline + ))) + ) + } + } + ) + } + // MARK: - GenerationError + + public enum GenerationError: Error, Equatable { + case noDependencies + case optionalInitializer + case genericParameterInInitializer + case whereClauseOnInitializer + /// The initializer is missing arguments for injected properties. + case missingArguments(labels: Set) + /// The initializer has arguments that don't map to any injected properties. + case tooManyArguments(labels: Set) + } + + // MARK: - Argument + + public struct Argument: Codable, Equatable { + /// The outer label, if one exists, by which the argument is referenced at the call site. + public let outerLabel: String? + /// The label by which the argument is referenced. + public let innerLabel: String + /// The type to which the property conforms. + public let type: String + /// The label by which this argument is referenced at the call site. + public var label: String { + outerLabel ?? innerLabel + } + + public var asProperty: Property { + Property( + label: innerLabel, + type: type + ) + } + + public init(property: Property) { + outerLabel = nil + innerLabel = property.label + type = property.type + } + + init(_ node: FunctionParameterSyntax) { + if let secondName = node.secondName { + outerLabel = node.firstName.text + innerLabel = secondName.text + } else { + outerLabel = nil + innerLabel = node.firstName.text + } + type = node.type.trimmedDescription + } + + init(outerLabel: String? = nil, innerLabel: String, type: String) { + self.outerLabel = outerLabel + self.innerLabel = innerLabel + self.type = type + } + + static let dependenciesArgumentName: TokenSyntax = .identifier("buildSafeDIDependencies") + } + + static let dependenciesToken: TokenSyntax = .identifier("dependencies") +} diff --git a/Sources/SafeDICore/Models/Property.swift b/Sources/SafeDICore/Models/Property.swift new file mode 100644 index 00000000..3d2344e6 --- /dev/null +++ b/Sources/SafeDICore/Models/Property.swift @@ -0,0 +1,71 @@ +// 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 + +/// A representation of a property. +/// e.g. `let myProperty: MyProperty` +public struct Property: Codable, Equatable { + // MARK: Public + + /// The label by which the property is referenced. + public let label: String + /// The type to which the property conforms. + public let type: String + + // MARK: Internal + + var asFunctionParamter: FunctionParameterSyntax { + FunctionParameterSyntax( + firstName: .identifier(label), + colon: .colonToken(trailingTrivia: .space), + type: IdentifierTypeSyntax( + name: .identifier(type) + ) + ) + } + + var asNamedTupleTypeElement: TupleTypeElementSyntax { + TupleTypeElementSyntax( + firstName: .identifier(label), + colon: .colonToken(trailingTrivia: .space), + type: IdentifierTypeSyntax( + name: .identifier(type) + ) + ) + } + + var asUnnamedTupleTypeElement: TupleTypeElementSyntax { + TupleTypeElementSyntax( + type: IdentifierTypeSyntax( + name: .identifier(type) + ) + ) + } + + var asUnnamedLabeledExpr: LabeledExprSyntax { + LabeledExprSyntax( + expression: DeclReferenceExprSyntax( + baseName: .identifier(label) + ) + ) + } + +} diff --git a/Sources/SafeDIMacros/Macros/BuilderMacro.swift b/Sources/SafeDIMacros/Macros/BuilderMacro.swift deleted file mode 100644 index 7a7767a4..00000000 --- a/Sources/SafeDIMacros/Macros/BuilderMacro.swift +++ /dev/null @@ -1,115 +0,0 @@ -// 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 SafeDICore -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -public struct BuilderMacro: MemberMacro { - public static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclGroupSyntax, - in context: some MacroExpansionContext) - throws -> [DeclSyntax] - { - guard declaration.modifiers.containsPublic else { - throw BuilderError.notPublic // TODO: add fixit instead. - } - - guard let structDelcaration = StructDeclSyntax(declaration) else { - throw BuilderError.notStruct // TODO: add fixit instead - } - - let builderVisitor = BuilderVisitor() - builderVisitor.walk(structDelcaration) - for diagnostic in builderVisitor.diagnostics { - context.diagnose(diagnostic) - } - - guard builderVisitor.didFindDependencies else { - var membersWithDependencies = structDelcaration.memberBlock.members - membersWithDependencies.append( - MemberBlockItemSyntax( - leadingTrivia: .newline, - decl: StructDeclSyntax.dependenciesTemplate, - trailingTrivia: .newline - ) - ) - context.diagnose(Diagnostic( - node: structDelcaration, - error: FixableBuilderError.missingDependencies, - changes: [ - .replace( - oldNode: Syntax(structDelcaration.memberBlock.members), - newNode: Syntax(membersWithDependencies) - ) - ] - )) - return [] - } - - guard let builder = builderVisitor.builder else { - // Builder macro is misconfigured. Compiler will highlight the issue – just fail to expand. - return [] - } - - let variantUnlabeledParameterList = builder.dependencies.variantUnlabeledParameterList - let variantParameterList = builder.dependencies.variantParameterList - let variantUnlabeledExpressionList = builder.dependencies.variantUnlabeledExpressionList - let variantLabeledExpressionList = builder.dependencies.variantLabeledExpressionList - let builtPropertyDescription = builder.builtProduct.asPropertyDeclaration - let builderPropertyDescription = builder.builder.asPropertyDeclaration - return [ - """ - // Inject this builder as a dependency by adding `\(raw: builderPropertyDescription)` to your @\(raw: DependenciesVisitor.macroName) type - public init(\(raw: BuilderVisitor.getDependenciesClosureName): @escaping (\(variantUnlabeledParameterList)) -> \(raw: DependenciesVisitor.decoratedStructName)) { - self.\(raw: BuilderVisitor.getDependenciesClosureName) = \(raw: BuilderVisitor.getDependenciesClosureName) - } - """, - """ - // Inject this built product as a dependency by adding `\(raw: builtPropertyDescription)` to your @\(raw: DependenciesVisitor.macroName) type - public func build(\(variantParameterList)) -> \(raw: builder.builtProduct.type) { - \(raw: BuilderVisitor.getDependenciesClosureName)(\(raw: variantUnlabeledExpressionList)).build(\(raw: variantLabeledExpressionList)) - } - """, - """ - private let \(raw: BuilderVisitor.getDependenciesClosureName): (\(variantUnlabeledParameterList)) -> \(raw: DependenciesVisitor.decoratedStructName) - """, - ] - } - - // MARK: - BuilderError - - private enum BuilderError: Error, CustomStringConvertible { - case notPublic - case notStruct - - var description: String { - switch self { - case .notPublic: - return "@\(BuilderVisitor.macroName) struct must be `public`" - case .notStruct: - return "@\(BuilderVisitor.macroName) must decorate a `struct`" - } - } - } -} diff --git a/Sources/SafeDIMacros/Macros/ConstructableMacro.swift b/Sources/SafeDIMacros/Macros/ConstructableMacro.swift new file mode 100644 index 00000000..9dda7de5 --- /dev/null +++ b/Sources/SafeDIMacros/Macros/ConstructableMacro.swift @@ -0,0 +1,102 @@ +// 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 SafeDICore +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct ConstructableMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext) + throws -> [DeclSyntax] + { + guard + let concreteDeclaration: ConcreteDeclSyntaxProtocol + = ActorDeclSyntax(declaration) + ?? ClassDeclSyntax(declaration) + ?? StructDeclSyntax(declaration) else { + throw ConstructableError.decoratingIncompatibleType + } + + let visitor = ConstructableVisitor() + visitor.walk(concreteDeclaration) + for diagnostic in visitor.diagnostics { + context.diagnose(diagnostic) + } + + let initializerAndResultPairs = visitor.initializers.map { initializer in + (initializer: initializer, result: Result { + try initializer.generateSafeDIInitializer( + fulfilling: visitor.dependencies, + typeIsClass: concreteDeclaration.isClass + ) + }) + } + + guard + let generatedInitializer = initializerAndResultPairs + .compactMap({ try? $0.result.get() }) + .first + else { + if initializerAndResultPairs.isEmpty { + var membersWithInitializer = declaration.memberBlock.members + membersWithInitializer.insert( + MemberBlockItemSyntax( + leadingTrivia: .newline, + decl: Initializer.generateRequiredInitializer(for: visitor.dependencies), + trailingTrivia: .newline + ), + at: membersWithInitializer.startIndex + ) + context.diagnose(Diagnostic( + node: Syntax(declaration.memberBlock), + error: FixableConstructableError.missingRequiredInitializer, + changes: [ + .replace( + oldNode: Syntax(declaration.memberBlock.members), + newNode: Syntax(membersWithInitializer)) + ])) + } + return [] + } + + return [ + DeclSyntax(generatedInitializer) + ] + } + + // MARK: - BuilderError + + private enum ConstructableError: Error, CustomStringConvertible { + case decoratingIncompatibleType + + var description: String { + switch self { + case .decoratingIncompatibleType: + return "@\(ConstructableVisitor.macroName) must decorate a class, struct, or actor" + } + } + } + +} diff --git a/Sources/SafeDIMacros/Macros/ConstructedMacro.swift b/Sources/SafeDIMacros/Macros/ConstructedMacro.swift deleted file mode 100644 index 221de1b0..00000000 --- a/Sources/SafeDIMacros/Macros/ConstructedMacro.swift +++ /dev/null @@ -1,54 +0,0 @@ -// 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 SafeDICore -import SwiftSyntax -import SwiftSyntaxMacros - -public struct ConstructedMacro: PeerMacro { - public static func expansion( - of node: AttributeSyntax, - providingPeersOf declaration: some DeclSyntaxProtocol, - in context: some MacroExpansionContext) - throws -> [DeclSyntax] - { - guard VariableDeclSyntax(declaration) != nil else { - throw ConstructedError.notDecoratingBinding - } - - // This macro purposefully does not expand. - // This macro serves as a decorator, nothing more. - return [] - } - - // MARK: - ConstructedError - - private enum ConstructedError: Error, CustomStringConvertible { - case notDecoratingBinding - - var description: String { - switch self { - case .notDecoratingBinding: - return "@\(Dependency.Source.constructedAttributeName) must decorate a instance variable" - } - } - } -} - diff --git a/Sources/SafeDIMacros/Macros/DependenciesMacro.swift b/Sources/SafeDIMacros/Macros/DependenciesMacro.swift deleted file mode 100644 index 7f44d6bb..00000000 --- a/Sources/SafeDIMacros/Macros/DependenciesMacro.swift +++ /dev/null @@ -1,101 +0,0 @@ -// 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 SafeDICore -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -public struct DependenciesMacro: MemberMacro { - public static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: some DeclGroupSyntax, - in context: some MacroExpansionContext) - throws -> [DeclSyntax] - { - guard declaration.modifiers.containsPublic else { - throw DependenciesError.notPublic // TODO: add fixit instead. - } - - guard let structDelcaration = StructDeclSyntax(declaration) else { - throw DependenciesError.notStruct // TODO: add fixit instead - } - - guard structDelcaration.name.text == DependenciesVisitor.decoratedStructName else { - throw DependenciesError.notNamedDependencies // TODO: add fixit instead - } - - let dependenciesVisitor = DependenciesVisitor() - dependenciesVisitor.walk(structDelcaration) - for diagnostic in dependenciesVisitor.diagnostics { - context.diagnose(diagnostic) - } - - guard dependenciesVisitor.didFindBuildMethod else { - var memberWithDependencies = structDelcaration.memberBlock.members - memberWithDependencies.append( - MemberBlockItemSyntax( - leadingTrivia: .newline, - decl: FunctionDeclSyntax.buildTemplate - ) - ) - context.diagnose(Diagnostic( - node: structDelcaration, - error: FixableDependenciesError.missingBuildMethod, - changes: [ - .replace( - oldNode: Syntax(structDelcaration.memberBlock.members), - newNode: Syntax(memberWithDependencies) - ) - ] - )) - - return [] - } - - return [ - """ - public init(\(dependenciesVisitor.dependencies.invariantParameterList)) { - \(raw: dependenciesVisitor.dependencies.invariantAssignmentExpressionList) - } - """ - ] - } - - // MARK: - DependenciesError - - private enum DependenciesError: Error, CustomStringConvertible { - case notPublic - case notStruct - case notNamedDependencies - - var description: String { - switch self { - case .notPublic: - return "@\(DependenciesVisitor.macroName) struct must be `public`" - case .notStruct: - return "@\(DependenciesVisitor.macroName) must decorate a `struct`" - case .notNamedDependencies: - return "@\(DependenciesVisitor.macroName) must decorate a `struct` with the name `Dependencies`" - } - } - } -} diff --git a/Sources/SafeDIMacros/Macros/SingletonMacro.swift b/Sources/SafeDIMacros/Macros/InjectableMacro.swift similarity index 57% rename from Sources/SafeDIMacros/Macros/SingletonMacro.swift rename to Sources/SafeDIMacros/Macros/InjectableMacro.swift index 3e8e236f..dce4a227 100644 --- a/Sources/SafeDIMacros/Macros/SingletonMacro.swift +++ b/Sources/SafeDIMacros/Macros/InjectableMacro.swift @@ -19,18 +19,40 @@ // SOFTWARE. import SafeDICore +import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct SingletonMacro: PeerMacro { +public struct InjectableMacro: PeerMacro { public static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { - guard VariableDeclSyntax(declaration) != nil else { - throw SingletonError.notDecoratingBinding + guard let variableDecl = VariableDeclSyntax(declaration) else { + throw InjectableError.notDecoratingBinding + } + + guard variableDecl.modifiers.staticModifier == nil else { + throw InjectableError.decoratingStatic + } + + if variableDecl.bindingSpecifier.text != TokenSyntax.keyword(.let).text { + context.diagnose(Diagnostic( + node: variableDecl.bindingSpecifier, + error: FixableInjectableError.unexpectedMutable, + changes: [ + .replace( + oldNode: Syntax(variableDecl.bindingSpecifier), + newNode: Syntax(TokenSyntax.keyword( + .let, + leadingTrivia: .space, + trailingTrivia: .space + )) + ) + ] + )) } // This macro purposefully does not expand. @@ -38,15 +60,18 @@ public struct SingletonMacro: PeerMacro { return [] } - // MARK: - SingletonError + // MARK: - InjectableError - private enum SingletonError: Error, CustomStringConvertible { + private enum InjectableError: Error, CustomStringConvertible { case notDecoratingBinding + case decoratingStatic var description: String { switch self { case .notDecoratingBinding: - return "@\(Dependency.Source.singletonAttributeName) must decorate a instance variable" + return "This macro must decorate a instance variable" + case .decoratingStatic: + return "This macro can not decorate `static` variables" } } } diff --git a/Sources/SafeDIMacros/SafeDIPlugin.swift b/Sources/SafeDIMacros/SafeDIPlugin.swift index 5739fc82..0ce0be2d 100644 --- a/Sources/SafeDIMacros/SafeDIPlugin.swift +++ b/Sources/SafeDIMacros/SafeDIPlugin.swift @@ -24,9 +24,7 @@ import SwiftSyntaxMacros @main struct SafeDIPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - BuilderMacro.self, - DependenciesMacro.self, - ConstructedMacro.self, - SingletonMacro.self, + ConstructableMacro.self, + InjectableMacro.self, ] } diff --git a/Tests/SafeDICoreTests/ArrayExtensionsTests.swift b/Tests/SafeDICoreTests/ArrayExtensionsTests.swift deleted file mode 100644 index 24dfea0b..00000000 --- a/Tests/SafeDICoreTests/ArrayExtensionsTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -// 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 XCTest - -@testable import SafeDICore - -final class ArrayExtensionsTests: XCTestCase { - - func test_variantUnlabeledParameterList_withSingleVariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .variant)] - XCTAssertEqual( - dependencies.variantUnlabeledParameterList.description, - "Int" - ) - } - - func test_variantUnlabeledParameterList_withMultipleVariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .variant), - Dependency(property: Property(label: "string", type: "String"), source: .variant), - Dependency(property: Property(label: "double", type: "Double"), source: .variant), - Dependency(property: Property(label: "invariant", type: "Invariant"), source: .providedInvariant) - ] - XCTAssertEqual( - dependencies.variantUnlabeledParameterList.description, - "Int, String, Double" - ) - } - - func test_variantParameterList_withSingleVariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .variant)] - XCTAssertEqual( - dependencies.variantParameterList.description, - "int: Int" - ) - } - - func test_variantParameterList_withMultipleVariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .variant), - Dependency(property: Property(label: "string", type: "String"), source: .variant), - Dependency(property: Property(label: "double", type: "Double"), source: .variant), - Dependency(property: Property(label: "invariant", type: "Invariant"), source: .providedInvariant) - ] - XCTAssertEqual( - dependencies.variantParameterList.description, - "int: Int, string: String, double: Double" - ) - } - - func test_variantUnlabeledExpressionList_withSingleVariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .variant)] - XCTAssertEqual( - dependencies.variantUnlabeledExpressionList, - "int" - ) - } - - func test_variantUnlabeledExpressionList_withMultipleVariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .variant), - Dependency(property: Property(label: "string", type: "String"), source: .variant), - Dependency(property: Property(label: "double", type: "Double"), source: .variant), - Dependency(property: Property(label: "invariant", type: "Invariant"), source: .providedInvariant) - ] - XCTAssertEqual( - dependencies.variantUnlabeledExpressionList, - "int, string, double" - ) - } - - - func test_variantLabeledExpressionList_withSingleVariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .variant)] - XCTAssertEqual( - dependencies.variantLabeledExpressionList, - "int: int" - ) - } - - func test_variantLabeledExpressionList_withMultipleVariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .variant), - Dependency(property: Property(label: "string", type: "String"), source: .variant), - Dependency(property: Property(label: "double", type: "Double"), source: .variant), - Dependency(property: Property(label: "invariant", type: "Invariant"), source: .providedInvariant) - ] - XCTAssertEqual( - dependencies.variantLabeledExpressionList, - "int: int, string: string, double: double" - ) - } - - func test_invariantParameterList_withSingleInvariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .providedInvariant)] - XCTAssertEqual( - dependencies.invariantParameterList.description, - "int: Int" - ) - } - - func test_invariantParameterList_withMultipleInvariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .singletonInvariant), - Dependency(property: Property(label: "string", type: "String"), source: .constructedInvariant), - Dependency(property: Property(label: "double", type: "Double"), source: .providedInvariant), - Dependency(property: Property(label: "variant", type: "Variant"), source: .variant) - ] - XCTAssertEqual( - dependencies.invariantParameterList.description, - "int: Int, string: String, double: Double" - ) - } - - func test_invariantAssignmentExpressionList_withSingleInvariant() throws { - let dependencies = [Dependency(property: Property(label: "int", type: "Int"), source: .providedInvariant)] - XCTAssertEqual( - dependencies.invariantAssignmentExpressionList, - "self.int = int" - ) - } - - func test_invariantAssignmentExpressionList_withMultipleInvariants() throws { - let dependencies = [ - Dependency(property: Property(label: "int", type: "Int"), source: .singletonInvariant), - Dependency(property: Property(label: "string", type: "String"), source: .constructedInvariant), - Dependency(property: Property(label: "double", type: "Double"), source: .providedInvariant), - Dependency(property: Property(label: "variant", type: "Variant"), source: .variant) - ] - XCTAssertEqual( - dependencies.invariantAssignmentExpressionList, - """ - self.int = int - self.string = string - self.double = double - """ - ) - } -} diff --git a/Tests/SafeDICoreTests/InitializerTests.swift b/Tests/SafeDICoreTests/InitializerTests.swift new file mode 100644 index 00000000..06cff99a --- /dev/null +++ b/Tests/SafeDICoreTests/InitializerTests.swift @@ -0,0 +1,612 @@ +// 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 XCTest + +@testable import SafeDICore + +final class InitializerTests: XCTestCase { + + func test_generateSafeDIInitializer_withNoArguments() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .noDependencies) + } + } + + func test_generateSafeDIInitializer_throwsWhenInitializerIsOptional() throws { + let initializer = Initializer( + isOptional: true, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .optionalInitializer) + } + } + + func test_generateSafeDIInitializer_throwsWhenInitializerHasGenericParameters() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: true, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "variant", + type: "Variant" + ) + ] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .genericParameterInInitializer) + } + } + + func test_generateSafeDIInitializer_throwsWhenInitializerHasGenericWhereClause() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: true, + arguments: [ + .init( + innerLabel: "variant", + type: "Variant" + ) + ] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .whereClauseOnInitializer) + } + } + + func test_generateSafeDIInitializer_throwsWhenInitializerHasUnexpectedArgument() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "variant", + type: "Variant" + ) + ] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .tooManyArguments(labels: ["variant"])) + } + } + + func test_generateSafeDIInitializer_throwsWhenInitializerIsMissingArgument() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [] + ) + + XCTAssertThrowsError( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description + ) { error in + XCTAssertEqual(error as? Initializer.GenerationError, .missingArguments(labels: ["variant"])) + } + } + + func test_generateSafeDIInitializer_withSingleVariantWithoutOuterLabel() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "variant", + type: "Variant" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: (Variant) -> (Variant), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(variant: dependencies) + } + """ + ) + } + + func test_generateSafeDIInitializer_withSingleVariantWithOuterLabel() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + outerLabel: "with", + innerLabel: "variant", + type: "Variant" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: (Variant) -> (Variant), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(with: dependencies) + } + """ + ) + } + + func test_generateSafeDIInitializer_withMultipleVariants() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + outerLabel: "with", + innerLabel: "variantA", + type: "VariantA" + ), + .init( + innerLabel: "variantB", + type: "VariantB" + ), + .init( + innerLabel: "variantC", + type: "VariantC" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variantA", + type: "VariantA" + ), + source: .propagatedVariant), + .init( + property: .init( + label: "variantB", + type: "VariantB" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "variantC", + type: "VariantC" + ), + source: .propagatedVariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: (VariantA, VariantB, VariantC) -> (variantA: VariantA, variantB: VariantB, variantC: VariantC), variantA: VariantA, variantB: VariantB, variantC: VariantC) { + let dependencies = buildSafeDIDependencies(variantA, variantB, variantC) + self.init(with: dependencies.variantA, variantB: dependencies.variantB, variantC: dependencies.variantC) + } + """ + ) + } + + func test_generateSafeDIInitializer_withSingleInvariantWithoutOuterLabel() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "invariant", + type: "Invariant" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "invariant", + type: "Invariant" + ), + source: .providedInvariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: () -> (Invariant)) { + let dependencies = buildSafeDIDependencies() + self.init(invariant: dependencies) + } + """ + ) + } + + func test_generateSafeDIInitializer_withSingleInvariantWithOuterLabel() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + outerLabel: "with", + innerLabel: "invariant", + type: "Invariant" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "invariant", + type: "Invariant" + ), + source: .providedInvariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: () -> (Invariant)) { + let dependencies = buildSafeDIDependencies() + self.init(with: dependencies) + } + """ + ) + } + + func test_generateSafeDIInitializer_withMultipleInvariants() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "invariantA", + type: "InvariantA" + ), + .init( + outerLabel: "with", + innerLabel: "invariantB", + type: "InvariantB" + ), + .init( + innerLabel: "invariantC", + type: "InvariantC" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "invariantA", + type: "InvariantA" + ), + source: .providedInvariant), + .init( + property: .init( + label: "invariantB", + type: "InvariantB" + ), + source: .constructedInvariant + ), + .init( + property: .init( + label: "invariantC", + type: "InvariantC" + ), + source: .singletonInvariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: () -> (invariantA: InvariantA, invariantB: InvariantB, invariantC: InvariantC)) { + let dependencies = buildSafeDIDependencies() + self.init(invariantA: dependencies.invariantA, with: dependencies.invariantB, invariantC: dependencies.invariantC) + } + """ + ) + } + + func test_generateSafeDIInitializer_withSingleVariantAndInvariant() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "variant", + type: "Variant" + ), + .init( + innerLabel: "invariant", + type: "Invariant" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variant", + type: "Variant" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "invariant", + type: "Invariant" + ), + source: .constructedInvariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: (Variant) -> (variant: Variant, invariant: Invariant), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(variant: dependencies.variant, invariant: dependencies.invariant) + } + """ + ) + } + + func test_generateSafeDIInitializer_withMultileVariantsAndInvariants() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "invariantA", + type: "InvariantA" + ), + .init( + innerLabel: "variantA", + type: "VariantA" + ), + .init( + innerLabel: "invariantB", + type: "InvariantB" + ), + .init( + innerLabel: "variantB", + type: "VariantB" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variantA", + type: "VariantA" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "variantB", + type: "VariantB" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "invariantA", + type: "InvariantA" + ), + source: .constructedInvariant + ), + .init( + property: .init( + label: "invariantB", + type: "InvariantB" + ), + source: .constructedInvariant + ) + ], + typeIsClass: false, + trailingNewline: true).description, + """ + public init(buildSafeDIDependencies: (VariantA, VariantB) -> (variantA: VariantA, variantB: VariantB, invariantA: InvariantA, invariantB: InvariantB), variantA: VariantA, variantB: VariantB) { + let dependencies = buildSafeDIDependencies(variantA, variantB) + self.init(invariantA: dependencies.invariantA, variantA: dependencies.variantA, invariantB: dependencies.invariantB, variantB: dependencies.variantB) + } + """ + ) + } + + func test_generateSafeDIInitializer_onClassWithMultileVariantsAndInvariants() throws { + let initializer = Initializer( + isOptional: false, + hasGenericParameter: false, + hasGenericWhereClause: false, + arguments: [ + .init( + innerLabel: "invariantA", + type: "InvariantA" + ), + .init( + innerLabel: "variantA", + type: "VariantA" + ), + .init( + innerLabel: "invariantB", + type: "InvariantB" + ), + .init( + innerLabel: "variantB", + type: "VariantB" + ) + ] + ) + + XCTAssertEqual( + try initializer.generateSafeDIInitializer( + fulfilling: [ + .init( + property: .init( + label: "variantA", + type: "VariantA" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "variantB", + type: "VariantB" + ), + source: .propagatedVariant + ), + .init( + property: .init( + label: "invariantA", + type: "InvariantA" + ), + source: .constructedInvariant + ), + .init( + property: .init( + label: "invariantB", + type: "InvariantB" + ), + source: .constructedInvariant + ) + ], + typeIsClass: true, + trailingNewline: true).description, + """ + public convenience init(buildSafeDIDependencies: (VariantA, VariantB) -> (variantA: VariantA, variantB: VariantB, invariantA: InvariantA, invariantB: InvariantB), variantA: VariantA, variantB: VariantB) { + let dependencies = buildSafeDIDependencies(variantA, variantB) + self.init(invariantA: dependencies.invariantA, variantA: dependencies.variantA, invariantB: dependencies.invariantB, variantB: dependencies.variantB) + } + """ + ) + } + +} diff --git a/Tests/SafeDIMacrosTests/MacroTests.swift b/Tests/SafeDIMacrosTests/MacroTests.swift index 37a4e9ea..521dbced 100644 --- a/Tests/SafeDIMacrosTests/MacroTests.swift +++ b/Tests/SafeDIMacrosTests/MacroTests.swift @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import MacroTesting import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport import XCTest @@ -28,481 +29,714 @@ import SafeDICore @testable import SafeDIMacros let testMacros: [String: Macro.Type] = [ - BuilderVisitor.macroName: BuilderMacro.self, - DependenciesVisitor.macroName: DependenciesMacro.self, - Dependency.Source.constructedAttributeName: ConstructedMacro.self, - Dependency.Source.singletonAttributeName: SingletonMacro.self, + ConstructableVisitor.macroName: ConstructableMacro.self, + Dependency.Source.constructedInvariant.rawValue: InjectableMacro.self, + Dependency.Source.providedInvariant.rawValue: InjectableMacro.self, + Dependency.Source.singletonInvariant.rawValue: InjectableMacro.self, + Dependency.Source.propagatedVariant.rawValue: InjectableMacro.self, ] final class MacroTests: XCTestCase { + // MARK: XCTestCase + + override func invokeTest() { + withMacroTesting(macros: testMacros) { + super.invokeTest() + } + } + // MARK: Expansion tests - func test_builderAndDependenciesMacros_withNoInvariantsOrVariants() throws { + func test_constructableAndInjectableMacros_withNoInvariantsOrVariants() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build() -> MyExample { - MyExample() - } - } + @constructable + public class ExampleService { + init() {} } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build() -> MyExample { - MyExample() - } - - public init() { + public class ExampleService { + init() {} + } + """, + macros: testMacros + ) + } - } + func test_constructableAndInjectableMacros_withSingleInvariantAndNoVariants() throws { + assertMacroExpansion( + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping () -> Dependencies) { - self.getDependencies = getDependencies + @constructed + private let invariantA: InvariantA + } + """, + expandedSource: """ + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } + private let invariantA: InvariantA - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build() -> MyExample { - getDependencies().build() + public init(buildSafeDIDependencies: () -> (InvariantA)) { + let dependencies = buildSafeDIDependencies() + self.init(invariantA: dependencies) } - - private let getDependencies: () -> Dependencies } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withSingleInvariantAndNoVariants() throws { + func test_constructableAndInjectableMacros_withMultipleInvariantsAndNoVariants() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build() -> MyExample { - MyExample(invariantA: invariantA) - } - - @constructed - private let invariantA: InvariantA - } + @constructable(fulfillingAdditionalTypes: [ExampleService.self]) + public struct DefaultExampleService: ExampleService { + init( + invariantA: invariantA, + invariantB: invariantB, + invariantC: invariantC) + { + self.invariantA = invariantA + self.invariantB = invariantB + self.invariantC = invariantC + } + + @constructed + public let invariantA: InvariantA + @provided + let invariantB: InvariantB + @singleton + private let invariantC: InvariantC } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build() -> MyExample { - MyExample(invariantA: invariantA) - } - private let invariantA: InvariantA - - public init(invariantA: InvariantA) { - self.invariantA = invariantA - } - } - - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping () -> Dependencies) { - self.getDependencies = getDependencies - } - - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build() -> MyExample { - getDependencies().build() + public struct DefaultExampleService: ExampleService { + init( + invariantA: invariantA, + invariantB: invariantB, + invariantC: invariantC) + { + self.invariantA = invariantA + self.invariantB = invariantB + self.invariantC = invariantC + } + public let invariantA: InvariantA + let invariantB: InvariantB + private let invariantC: InvariantC + + public init(buildSafeDIDependencies: () -> (invariantA: InvariantA, invariantB: InvariantB, invariantC: InvariantC)) { + let dependencies = buildSafeDIDependencies() + self.init(invariantA: dependencies.invariantA, invariantB: dependencies.invariantB, invariantC: dependencies.invariantC) } - - private let getDependencies: () -> Dependencies } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withMultipleInvariantsAndNoVariants() throws { + func test_constructableAndInjectableMacros_withNoInvariantsAndSingleVariant() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build() -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC - ) - } - - @constructed - private let invariantA: InvariantA - private let invariantB: InvariantB - @singleton - private let invariantC: InvariantC + @constructable + public struct ExampleService { + init(with variant: Variant) { + self.variant = variant } + + @propagated + public let variant: Variant } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build() -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC - ) - } - private let invariantA: InvariantA - private let invariantB: InvariantB - private let invariantC: InvariantC - - public init(invariantA: InvariantA, invariantB: InvariantB, invariantC: InvariantC) { - self.invariantA = invariantA - self.invariantB = invariantB - self.invariantC = invariantC - } + public struct ExampleService { + init(with variant: Variant) { + self.variant = variant } + public let variant: Variant - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping () -> Dependencies) { - self.getDependencies = getDependencies + public init(buildSafeDIDependencies: (Variant) -> (Variant), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(with: dependencies) } - - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build() -> MyExample { - getDependencies().build() - } - - private let getDependencies: () -> Dependencies } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withNoInvariantsAndSingleVariant() throws { + func test_constructableAndInjectableMacros_withSingleInvariantAndVariant() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample(variant: variant) - } + @constructable + public struct ExampleService { + init(with variant: Variant, invariant: Invariant) { + self.variant = variant + self.invariant = invariant } + + @propagated + public let variant: Variant + @constructed + private let invariant: Invariant } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample(variant: variant) - } - - public init() { - - } - } - - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (Variant) -> Dependencies) { - self.getDependencies = getDependencies + public struct ExampleService { + init(with variant: Variant, invariant: Invariant) { + self.variant = variant + self.invariant = invariant } + public let variant: Variant + private let invariant: Invariant - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variant: Variant) -> MyExample { - getDependencies(variant).build(variant: variant) + public init(buildSafeDIDependencies: (Variant) -> (variant: Variant, invariant: Invariant), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(with: dependencies.variant, invariant: dependencies.invariant) } - - private let getDependencies: (Variant) -> Dependencies } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withSingleInvariantAndVariant() throws { + func test_constructableAndInjectableMacros_withMultipleInvariantsAndSingleVariant() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample( - invariantA: invariantA, - variant: variant - ) - } - - @constructed - private let invariantA: InvariantA - } + @constructable + public struct ExampleService { + init( + with variant: Variant, + invariantA: invariantA, + invariantB: InvariantB, + invariantC: InvariantC) + { + self.variant = variant + self.invariantA = invariantA + self.invariantB = invariantB + self.invariantC = invariantC + } + + @propagated + public let variant: Variant + @constructed + private let invariantA: invariantA + @provided + private let invariantB: InvariantB + @singleton + private let invariantC: InvariantC } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample( - invariantA: invariantA, - variant: variant - ) - } - private let invariantA: InvariantA - - public init(invariantA: InvariantA) { - self.invariantA = invariantA - } + public struct ExampleService { + init( + with variant: Variant, + invariantA: invariantA, + invariantB: InvariantB, + invariantC: InvariantC) + { + self.variant = variant + self.invariantA = invariantA + self.invariantB = invariantB + self.invariantC = invariantC + } + public let variant: Variant + private let invariantA: invariantA + private let invariantB: InvariantB + private let invariantC: InvariantC + + public init(buildSafeDIDependencies: (Variant) -> (variant: Variant, invariantA: invariantA, invariantB: InvariantB, invariantC: InvariantC), variant: Variant) { + let dependencies = buildSafeDIDependencies(variant) + self.init(with: dependencies.variant, invariantA: dependencies.invariantA, invariantB: dependencies.invariantB, invariantC: dependencies.invariantC) } + } + """, + macros: testMacros + ) + } - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (Variant) -> Dependencies) { - self.getDependencies = getDependencies + func test_constructableAndInjectableMacros_withNoInvariantsAndMultipleVariant() throws { + assertMacroExpansion( + """ + @constructable + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB) { + self.variantA = variantA + self.variantB = variantB } - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variant: Variant) -> MyExample { - getDependencies(variant).build(variant: variant) + @propagated + let variantA: VariantA + @propagated + let variantB: VariantB + } + """, + expandedSource: """ + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB) { + self.variantA = variantA + self.variantB = variantB } + let variantA: VariantA + let variantB: VariantB - private let getDependencies: (Variant) -> Dependencies + public init(buildSafeDIDependencies: (VariantA, VariantB) -> (variantA: VariantA, variantB: VariantB), variantA: VariantA, variantB: VariantB) { + let dependencies = buildSafeDIDependencies(variantA, variantB) + self.init(variantA: dependencies.variantA, variantB: dependencies.variantB) + } } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withMultipleInvariantsAndSingleVariant() throws { + func test_constructableAndInjectableMacros_withSingleInvariantAndMultipleVariants() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC, - variant: variant - ) - } - - @constructed - private let invariantA: InvariantA - private let invariantB: InvariantB - @singleton - private let invariantC: InvariantC - } + @constructable + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB, invariantA: InvariantA) { + self.variantA = variantA + self.variantB = variantB + self.invariantA = invariantA + } + + @propagated + let variantA: VariantA + @propagated + let variantB: VariantB + @constructed + private let invariantA: InvariantA } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variant: Variant) -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC, - variant: variant - ) - } - private let invariantA: InvariantA - private let invariantB: InvariantB - private let invariantC: InvariantC - - public init(invariantA: InvariantA, invariantB: InvariantB, invariantC: InvariantC) { - self.invariantA = invariantA - self.invariantB = invariantB - self.invariantC = invariantC - } + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB, invariantA: InvariantA) { + self.variantA = variantA + self.variantB = variantB + self.invariantA = invariantA } + let variantA: VariantA + let variantB: VariantB + private let invariantA: InvariantA - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (Variant) -> Dependencies) { - self.getDependencies = getDependencies + public init(buildSafeDIDependencies: (VariantA, VariantB) -> (variantA: VariantA, variantB: VariantB, invariantA: InvariantA), variantA: VariantA, variantB: VariantB) { + let dependencies = buildSafeDIDependencies(variantA, variantB) + self.init(variantA: dependencies.variantA, variantB: dependencies.variantB, invariantA: dependencies.invariantA) } - - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variant: Variant) -> MyExample { - getDependencies(variant).build(variant: variant) - } - - private let getDependencies: (Variant) -> Dependencies } """, macros: testMacros ) } - func test_builderAndDependenciesMacros_withNoInvariantsAndMultipleVariant() throws { + func test_constructableAndInjectableMacros_withMultipleInvariantsAndMultipleVariants() throws { assertMacroExpansion( """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample(variantA: variantA, variantB: VariantB) - } - } + @constructable + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB, invariantA: InvariantA, invariantB: InvariantB) { + self.variantA = variantA + self.variantB = variantB + self.invariantA = invariantA + self.invariantB = invariantB + } + + @propagated + let variantA: VariantA + @propagated + let variantB: VariantB + @constructed + private let invariantA: InvariantA + @singleton + public let invariantB: InvariantB } """, expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample(variantA: variantA, variantB: VariantB) - } + public struct ExampleService { + init(variantA: VariantA, variantB: VariantB, invariantA: InvariantA, invariantB: InvariantB) { + self.variantA = variantA + self.variantB = variantB + self.invariantA = invariantA + self.invariantB = invariantB + } + let variantA: VariantA + let variantB: VariantB + private let invariantA: InvariantA + public let invariantB: InvariantB + + public init(buildSafeDIDependencies: (VariantA, VariantB) -> (variantA: VariantA, variantB: VariantB, invariantA: InvariantA, invariantB: InvariantB), variantA: VariantA, variantB: VariantB) { + let dependencies = buildSafeDIDependencies(variantA, variantB) + self.init(variantA: dependencies.variantA, variantB: dependencies.variantB, invariantA: dependencies.invariantA, invariantB: dependencies.invariantB) + } + } + """, + macros: testMacros + ) + } - public init() { + // MARK: Error tests - } - } + func test_constructableMacro_throwsErrorWhenOnProtocol() { + assertMacro { + """ + @constructable + protocol ExampleService {} + """ + } diagnostics: { + """ + @constructable + ┬───────────── + ╰─ 🛑 @constructable must decorate a class, struct, or actor + protocol ExampleService {} + """ + } + } - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (VariantA, VariantB) -> Dependencies) { - self.getDependencies = getDependencies + func test_injectableMacro_throwsErrorWhenOnProtocol() { + assertMacro { + """ + @constructed + protocol ExampleService {} + """ + } diagnostics: { + """ + @constructed + ┬─────────── + ╰─ 🛑 This macro must decorate a instance variable + protocol ExampleService {} + """ + } + } + + func test_constructableMacro_throwsErrorWhenInjectableMacroAttachedtoStaticProperty() { + assertMacro { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variantA: VariantA, variantB: VariantB) -> MyExample { - getDependencies(variantA, variantB).build(variantA: variantA, variantB: variantB) + @provided + static let invariantA: InvariantA + } + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - private let getDependencies: (VariantA, VariantB) -> Dependencies + @provided + ┬──────── + ╰─ 🛑 This macro can not decorate `static` variables + static let invariantA: InvariantA } - """, - macros: testMacros - ) + """ + } } - func test_builderAndDependenciesMacros_withSingleInvariantAndMultipleVariants() throws { - assertMacroExpansion( + // MARK: FixIt tests + + func test_constructableMacro_addsFixitWhenMultipleInjectableMacrosOntopOfSingleProperty() { + assertMacro { """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample( - invariantA: invariantA, - variantA: VariantA, - variantB: VariantB - ) - } + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } - @constructed - private let invariantA: InvariantA + @provided + @constructed + let invariantA: InvariantA + } + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } + + @provided + ╰─ 🛑 Dependency can have at most one of @constructedInvariant, @providedInvariant, @singletonInvariant, or @propagatedVariant attached macro + ✏️ Remove excessive attached macros + @constructed + let invariantA: InvariantA } - """, - expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample( - invariantA: invariantA, - variantA: VariantA, - variantB: VariantB - ) - } - private let invariantA: InvariantA + """ + } fixes: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + + @provided + } + """ // fixes is quite wrong here. In Xcode this removes all but the first macro. + } + } - public init(invariantA: InvariantA) { - self.invariantA = invariantA - } + func test_constructableAndInjectableMacros_addsFixitWhenInjectableParameterHasInitializer() { + assertMacro { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (VariantA, VariantB) -> Dependencies) { - self.getDependencies = getDependencies + @constructed + let invariantA: InvariantA = .init() + } + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variantA: VariantA, variantB: VariantB) -> MyExample { - getDependencies(variantA, variantB).build(variantA: variantA, variantB: variantB) + @constructed + ╰─ 🛑 Dependency must not have hand-written initializer + ✏️ Remove initializer + let invariantA: InvariantA = .init() + } + """ + } fixes: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - private let getDependencies: (VariantA, VariantB) -> Dependencies + @constructed + let invariantA: InvariantA } - """, - macros: testMacros - ) + """ + } expansion: { + """ + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + let invariantA: InvariantA + + public init(buildSafeDIDependencies: () -> (InvariantA )) { + let dependencies = buildSafeDIDependencies() + self.init(invariantA: dependencies) + } + } + """ + } } - func test_builderAndDependenciesMacros_withMultipleInvariantsAndMultipleVariants() throws { - assertMacroExpansion( + func test_constructableMacro_addsFixitWhenInjectableTypeIsNotPublicOrOpen() { + assertMacro { """ - @builder("myExample") - public struct MyExampleBuilder { - @dependencies - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC, - variantA: variantA, - variantB: variantB - ) - } - - @constructed - private let invariantA: InvariantA - private let invariantB: InvariantB - @singleton - private let invariantC: InvariantC + @constructable + struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } + + @constructed + let invariantA: InvariantA } - """, - expandedSource: """ - public struct MyExampleBuilder { - public struct Dependencies { - func build(variantA: VariantA, variantB: VariantB) -> MyExample { - MyExample( - invariantA: invariantA, - invariantB: invariantB, - invariantC: invariantC, - variantA: variantA, - variantB: variantB - ) - } - private let invariantA: InvariantA - private let invariantB: InvariantB - private let invariantC: InvariantC + """ + } diagnostics: { + """ + @constructable + ╰─ 🛑 @constructable-decorated type must be `public` or `open` + ✏️ Add `public` modifier + struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } - public init(invariantA: InvariantA, invariantB: InvariantB, invariantC: InvariantC) { - self.invariantA = invariantA - self.invariantB = invariantB - self.invariantC = invariantC - } + @constructed + let invariantA: InvariantA + } + """ + } fixes: { + """ + @constructable + public + public ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this builder as a dependency by adding `let myExampleBuilder: MyExampleBuilder` to your @dependencies type - public init(getDependencies: @escaping (VariantA, VariantB) -> Dependencies) { - self.getDependencies = getDependencies + @constructed + let invariantA: InvariantA + } + """ // this fixes are wrong (we aren't deleting 'struct'), but also the whitespace is wrong in Xcode. + // TODO: fix Xcode spacing of this replacement. + } expansion: { + """ + @constructable + public + public ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - // Inject this built product as a dependency by adding `let myExample: MyExample` to your @dependencies type - public func build(variantA: VariantA, variantB: VariantB) -> MyExample { - getDependencies(variantA, variantB).build(variantA: variantA, variantB: variantB) + @constructed + let invariantA: InvariantA + } + """ + } + } + + func test_constructableAndInjectableMacros_addsFixitWhenInjectableParameterIsMutable() { + assertMacro { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA } - private let getDependencies: (VariantA, VariantB) -> Dependencies + @constructed + var invariantA: InvariantA } - """, - macros: testMacros - ) + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + + @constructed + var invariantA: InvariantA + ┬── + ╰─ 🛑 Dependency can not be mutable + ✏️ Replace `var` with `let` + } + """ + } fixes: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + + @constructed let let invariantA: InvariantA + } + """ // fixes are wrong! It's duplicating the correction. not sure why. + } expansion: { + """ + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + + let let invariantA: InvariantA + } + """ // expansion is wrong! It's duplicating the correction. not sure why. + } + } + + func test_constructableMacro_addsFixitMissingRequiredInitializerWithoutAnyDependencies() { + assertMacro { + """ + @constructable + public struct ExampleService { + } + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + ╰─ 🛑 @constructable-decorated type must have initializer for all injected parameters + ✏️ Add required initializer + } + """ + } fixes: { + """ + @constructable + public struct ExampleService { + init() {} + + init() {} + """ // fixes are wrong! It's duplicating the correction. not sure why. + } expansion: { + """ + public struct ExampleService { + init() {} + + init() {} + """ // expansion is wrong! It's duplicating the correction. not sure why. + } + } + + func test_constructableAndInjectableMacros_addsFixitMissingRequiredInitializerWithDependencies() { + assertMacro { + """ + @constructable + public struct ExampleService { + @constructed + let invariantA: InvariantA + } + """ + } diagnostics: { + """ + @constructable + public struct ExampleService { + ╰─ 🛑 @constructable-decorated type must have initializer for all injected parameters + ✏️ Add required initializer + @constructed + let invariantA: InvariantA + } + """ + } fixes: { + """ + @constructable + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + + @constructed + let invariantA: InvariantA + } + """ // Whitespace is correct in Xcode, but not here. + } expansion: { + """ + public struct ExampleService { + init(invariantA: InvariantA) { + self.invariantA = invariantA + } + let invariantA: InvariantA + + public init(buildSafeDIDependencies: () -> (InvariantA)) { + let dependencies = buildSafeDIDependencies() + self.init(invariantA: dependencies) + } + } + """ + } } + } #endif