diff --git a/Sources/SafeDICore/Builder.swift b/Sources/SafeDICore/Builder.swift new file mode 100644 index 00000000..2f633735 --- /dev/null +++ b/Sources/SafeDICore/Builder.swift @@ -0,0 +1,59 @@ +// 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. + +public struct Builder: 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 + ) + 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 + } +} diff --git a/Sources/SafeDICore/BuilderVisitor.swift b/Sources/SafeDICore/BuilderVisitor.swift index 922bf9c1..b1224487 100644 --- a/Sources/SafeDICore/BuilderVisitor.swift +++ b/Sources/SafeDICore/BuilderVisitor.swift @@ -110,29 +110,51 @@ public final class BuilderVisitor: SyntaxVisitor { } public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { - if node.name.text == DependenciesVisitor.decoratedStructName { + 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 } - return .skipChildren } // MARK: Public - public var dependencies: [Dependency] { - dependenciesVisitor.dependencies - } - public var builtType: String? { - dependenciesVisitor.builtType + 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 decoratedStructName = "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/DependenciesVisitor.swift b/Sources/SafeDICore/DependenciesVisitor.swift index b0ebf724..02cda3f0 100644 --- a/Sources/SafeDICore/DependenciesVisitor.swift +++ b/Sources/SafeDICore/DependenciesVisitor.swift @@ -267,7 +267,7 @@ public final class DependenciesVisitor: SyntaxVisitor { return .skipChildren } - guard node.attributes.isDecoratedWithDependenciesMacro else { + guard node.attributes.dependenciesMacro != nil else { var newAttributes = node.attributes newAttributes.append(.attribute( AttributeSyntax( diff --git a/Sources/SafeDICore/Errors/FixableBuilderError.swift b/Sources/SafeDICore/Errors/FixableBuilderError.swift index fd7e5a78..a71db9cf 100644 --- a/Sources/SafeDICore/Errors/FixableBuilderError.swift +++ b/Sources/SafeDICore/Errors/FixableBuilderError.swift @@ -31,11 +31,11 @@ public enum FixableBuilderError: DiagnosticError { case .missingDependencies: return "Missing nested `@\(DependenciesVisitor.macroName) public struct \(DependenciesVisitor.decoratedStructName)` declaration" case .unexpectedVariableDeclaration: - return "Found unexpected variable declaration in `\(BuilderVisitor.decoratedStructName)`" + return "Found unexpected variable declaration in `@\(BuilderVisitor.macroName)`" case .unexpectedInitializer: - return "Found unexpected initializer in `\(BuilderVisitor.decoratedStructName)`" + return "Found unexpected initializer in `@\(BuilderVisitor.macroName)`" case .unexpectedFuncationDeclaration: - return "Found unexpected function declaration in `\(BuilderVisitor.decoratedStructName)`" + return "Found unexpected function declaration in `@\(BuilderVisitor.macroName)`" } } diff --git a/Sources/SafeDICore/Extensions/ArrayExtensions.swift b/Sources/SafeDICore/Extensions/ArrayExtensions.swift index 10350bca..9744277b 100644 --- a/Sources/SafeDICore/Extensions/ArrayExtensions.swift +++ b/Sources/SafeDICore/Extensions/ArrayExtensions.swift @@ -39,7 +39,7 @@ extension Array where Element == Dependency { public var variantParameterList: FunctionParameterListSyntax { FunctionParameterListSyntax( filter { $0.source == .variant } - .map { "\(raw: $0.property.label): \(raw: $0.property.type)" } + .map { "\(raw: $0.property.asParameterDeclaration)" } .transformUntilLast { var functionPamameterSyntax = $0 functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present) @@ -51,20 +51,20 @@ extension Array where Element == Dependency { public var variantUnlabeledExpressionList: String { filter { $0.isVariant } - .map { "\($0.property.label)" } + .map(\.property.label) .joined(separator: ", ") } public var variantLabeledExpressionList: String { filter { $0.isVariant } - .map { "\($0.property.label): \($0.property.label)" } + .map(\.property.asLabeledParameterExpression) .joined(separator: ", ") } public var invariantParameterList: FunctionParameterListSyntax { FunctionParameterListSyntax( filter { $0.isInvariant } - .map { "\(raw: $0.property.label): \(raw: $0.property.type)" } + .map { "\(raw: $0.property.asParameterDeclaration)" } .transformUntilLast { var functionPamameterSyntax = $0 functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present) @@ -77,7 +77,7 @@ extension Array where Element == Dependency { public var invariantAssignmentExpressionList: String { """ \(filter(\.isInvariant) - .map { "self.\($0.property.label) = \($0.property.label)" } + .map(\.property.asSelfAssignment) .joined(separator: "\n")) """ } diff --git a/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift b/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift index b64927a0..88585eb0 100644 --- a/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift +++ b/Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift @@ -22,15 +22,32 @@ import SwiftSyntax extension AttributeListSyntax { - public var isDecoratedWithDependenciesMacro: Bool { - contains(where: { element in + var dependenciesMacro: 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 + case .ifConfigDecl: + return false + } + }) else { + return nil + } + return AttributeSyntax(attribute) } public var attributedNodes: [(attribute: String, node: AttributeListSyntax.Element)] { diff --git a/Sources/SafeDICore/Extensions/AttributeSyntaxArgumentsExtensions.swift b/Sources/SafeDICore/Extensions/AttributeSyntaxArgumentsExtensions.swift index 14e26032..893945c8 100644 --- a/Sources/SafeDICore/Extensions/AttributeSyntaxArgumentsExtensions.swift +++ b/Sources/SafeDICore/Extensions/AttributeSyntaxArgumentsExtensions.swift @@ -21,7 +21,7 @@ import SwiftSyntax extension AttributeSyntax.Arguments { - public var string: String? { + var firstArgumentString: String? { switch self { case let .argumentList(labeledExprListSyntax): return labeledExprListSyntax diff --git a/Sources/SafeDICore/Property.swift b/Sources/SafeDICore/Property.swift index bd1864ae..3724d51f 100644 --- a/Sources/SafeDICore/Property.swift +++ b/Sources/SafeDICore/Property.swift @@ -25,4 +25,20 @@ public struct Property: Codable, Equatable { public let label: String /// The type to which the property conforms. public let type: String + + public var asPropertyDeclaration: String { + "let \(label): \(type)" + } + + public var asParameterDeclaration: String { + "\(label): \(type)" + } + + public var asLabeledParameterExpression: String { + "\(label): \(label)" + } + + public var asSelfAssignment: String { + "self.\(label) = \(label)" + } } diff --git a/Sources/SafeDIMacros/Macros/BuilderMacro.swift b/Sources/SafeDIMacros/Macros/BuilderMacro.swift index 9bde9b2f..c650d9d9 100644 --- a/Sources/SafeDIMacros/Macros/BuilderMacro.swift +++ b/Sources/SafeDIMacros/Macros/BuilderMacro.swift @@ -44,19 +44,11 @@ public struct BuilderMacro: MemberMacro { } let builderVisitor = BuilderVisitor() - builderVisitor.walk(structDelcaration.memberBlock) + builderVisitor.walk(structDelcaration) for diagnostic in builderVisitor.diagnostics { context.diagnose(diagnostic) } - guard - let builderMacroArguments = node.arguments, - let builtPropertyName = builderMacroArguments.string - else { - // Builder macro is misconfigured. Compiler will highlight the issue – just fail to expand. - return [] - } - guard builderVisitor.didFindDependencies else { var membersWithDependencies = structDelcaration.memberBlock.members membersWithDependencies.append( @@ -79,15 +71,17 @@ public struct BuilderMacro: MemberMacro { return [] } - let variantUnlabeledParameterList = builderVisitor.dependencies.variantUnlabeledParameterList - let variantParameterList = builderVisitor.dependencies.variantParameterList - let variantUnlabeledExpressionList = builderVisitor.dependencies.variantUnlabeledExpressionList - let variantLabeledExpressionList = builderVisitor.dependencies.variantLabeledExpressionList - guard let builtType = builderVisitor.builtType else { + guard let builder = builderVisitor.builder else { + // Builder macro is misconfigured. Compiler will highlight the issue – just fail to expand. return [] } - let builtPropertyDescription = "let \(builtPropertyName): \(builtType)" - let builderPropertyDescription = "let \(builtPropertyName)\(BuilderVisitor.decoratedStructName): \(structDelcaration.name.text)" + + 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 @@ -97,7 +91,7 @@ public struct BuilderMacro: MemberMacro { """, """ // Inject this built product as a dependency by adding `\(raw: builtPropertyDescription)` to your @\(raw: DependenciesVisitor.macroName) type - public func build(\(variantParameterList)) -> \(raw: builtType) { + public func build(\(variantParameterList)) -> \(raw: builder.builtProduct.type) { \(raw: BuilderVisitor.getDependenciesClosureName)(\(raw: variantUnlabeledExpressionList)).build(\(raw: variantLabeledExpressionList)) } """,