diff --git a/Sources/Cradle/Cradle.docc/Cradle.md b/Sources/Cradle/Cradle.docc/Cradle.md index d502a3d..f35458e 100644 --- a/Sources/Cradle/Cradle.docc/Cradle.md +++ b/Sources/Cradle/Cradle.docc/Cradle.md @@ -12,6 +12,8 @@ Factory가 `any UserRepository`를 반환하면 graph의 `userRepository`도 같 기본 `@Provide`와 `@Provide(.shared)`는 graph 생성 중 한 번 만든 값을 해당 graph가 보유하고 이후 같은 값을 반환합니다. `@Provide(.lazy)`는 생성 프로퍼티를 처음 읽을 때 값을 만들고 해당 graph가 보유합니다. 이 값들은 전역 싱글턴이 아니라 graph 인스턴스마다 분리됩니다. 프로퍼티를 읽을 때마다 Factory를 호출해야 하면 `@Provide(.transient)`를 사용합니다. +`@DependencyGraph(input: Input.self)`는 graph 생성자가 보관할 조립 입력을 선언합니다. 기본 `@Provide`는 input 대입 뒤 graph 생성 중 한 번 실행하고, `.lazy`는 최초 생성 프로퍼티 접근에서 input을 읽습니다. + actor graph의 생성 프로퍼티는 actor 격리를 따릅니다. actor 밖에서는 `await`로 읽으며 반환 값이 actor 경계를 통과할 수 있는지는 Swift 컴파일러가 `Sendable` 규칙으로 검사합니다. `@DependencyGraph(overrides: true)`를 지정하면 등록별 기본값이 `.original`인 static `override`와 `OverrideBuilder.build()`를 사용할 수 있습니다. builder는 교체 선택만 보관하고 `.build()`에서 graph와 shared 등록을 만듭니다. lazy 교체 Factory는 생성 프로퍼티를 처음 읽을 때 실행합니다. actor 교체 Factory는 `@Sendable`이어야 하며, `@MainActor` graph의 builder는 같은 격리를 따릅니다. @@ -20,7 +22,7 @@ actor graph의 생성 프로퍼티는 actor 격리를 따릅니다. actor 밖에 일반 비격리 class graph는 동시 접근을 조정하지 않으므로 단일 소유자로 사용하거나 `@MainActor`처럼 명시한 전역 actor 격리 안에 둡니다. `.shared`와 직접 `Sendable` 준수를 선언한 class graph는 Swift 컴파일러가 검증한 저장 상태만 Task 사이에 전달할 수 있습니다. -`sources`와 `overrides: true`를 모두 지정하지 않은 graph에서는 매크로가 생성자를 추가하지 않으며 사용자가 선언한 생성자와 인스턴스 저장 프로퍼티도 변경하지 않습니다. `sources` 또는 `overrides: true` graph는 생성 경로를 Macro가 소유합니다. +`input`, `sources`, `overrides: true`를 모두 지정하지 않은 graph에서는 매크로가 생성자를 추가하지 않으며 사용자가 선언한 생성자와 인스턴스 저장 프로퍼티도 변경하지 않습니다. `input`, `sources`, `overrides: true` graph는 생성 경로를 Macro가 소유합니다. SwiftPM target에 `CradlePlugin`을 연결하면 build마다 의존성 관계를 Mermaid `.mmd` 개발 산출물로 갱신합니다. 이 산출물은 plugin work directory에만 남으며 library와 app binary에는 포함되지 않습니다. @@ -32,7 +34,7 @@ SwiftPM target에 `CradlePlugin`을 연결하면 build마다 의존성 관계를 ### 매크로 -- ``DependencyGraph(_:sources:overrides:diagram:)`` +- ``DependencyGraph(_:input:sources:overrides:diagram:)`` - ``DependencyGraphLifetime`` - ``DependencyOverride`` - ``External`` diff --git a/Sources/Cradle/Cradle.docc/DependencyGraph.md b/Sources/Cradle/Cradle.docc/DependencyGraph.md index 4e22571..0882c2a 100644 --- a/Sources/Cradle/Cradle.docc/DependencyGraph.md +++ b/Sources/Cradle/Cradle.docc/DependencyGraph.md @@ -29,6 +29,24 @@ let client = graph.httpClient 기본 `@Provide`는 graph 생성 중 한 번 만드는 shared 등록입니다. `graph.httpClient`를 여러 번 읽어도 같은 값을 반환하며 이 값은 전역 싱글턴이 아니라 해당 graph 인스턴스에만 보관됩니다. `@Provide(.lazy)`는 생성 프로퍼티를 처음 읽을 때 graph별 값을 한 번 만들고 보관합니다. 접근할 때마다 새 값을 만들어야 하면 `@Provide(.transient)`를 사용합니다. +## initializer input + +graph를 만들 때 정하는 조립 입력은 `input:`으로 선언합니다. Macro는 `input`을 보관한 뒤 기본 `@Provide`의 shared 결과를 만듭니다. 따라서 기본 provider는 graph 생성 시점에 `input`을 읽고, `.lazy` provider만 최초 접근까지 생성을 미룹니다. + +```swift +@DependencyGraph(input: DomainGraphInput.self) +final class DomainGraph { + @Provide + private func makeUseCase() -> SomeUseCase { + SomeUseCase(repository: input.repository) + } +} + +let graph = DomainGraph(input: input) +``` + +input graph의 initializer는 Macro가 생성하므로 사용자 initializer는 함께 선언할 수 없습니다. actor graph와 `@DependencyGraph(.shared)` graph는 호출자 input을 지원하지 않습니다. 호출 시점마다 받는 값은 initializer input이 아니라 `@Provide(.transient)`의 `@External`을 사용합니다. + | 반환 타입 | 생성 프로퍼티 | | --- | --- | | `TestUseCase` | `testUseCase` | @@ -390,7 +408,9 @@ let second = graph.userRepository shared 수명의 Factory는 다른 shared 등록만 매개변수로 받을 수 있습니다. shared 수명의 Factory가 lazy 또는 transient 등록을 받으면 두 등록의 평가 시점이 shared 결과에 고정되므로, Macro는 해당 매개변수 타입 위치에 오류를 표시합니다. lazy 수명의 Factory는 shared 또는 lazy 등록을 받을 수 있지만 transient 등록은 보관할 수 없습니다. -shared Factory 본문은 사용자가 작성한 initializer 본문보다 먼저 실행됩니다. 따라서 `self`, `super`, class·actor graph 인스턴스 멤버, 다른 Factory를 직접 참조할 수 없습니다. 필요한 shared 의존성은 Factory 매개변수로 선언합니다. actor graph의 shared Factory가 actor 상태를 읽으면 static helper에서 Swift 컴파일러가 오류를 표시합니다. +`input`을 지정하지 않은 graph의 shared Factory 본문은 graph initializer보다 먼저 실행됩니다. 따라서 `self`, `super`, class·actor graph 인스턴스 멤버, 다른 Factory를 직접 참조할 수 없습니다. 필요한 shared 의존성은 Factory 매개변수로 선언합니다. actor graph의 shared Factory가 actor 상태를 읽으면 static helper에서 Swift 컴파일러가 오류를 표시합니다. + +`input` graph의 shared Factory는 Macro가 input을 저장한 뒤 실행합니다. Macro는 Factory 본문의 `input` 참조만 static helper 매개변수로 전달합니다. 따라서 `input`은 읽을 수 있지만 `self`, `super`, input 이외의 graph 인스턴스 멤버와 다른 Factory는 직접 참조할 수 없습니다. source graph 저장 프로퍼티는 shared Factory에서 직접 읽을 수 있습니다. Macro는 이 참조를 생성한 static helper의 매개변수로 바꾸고 source 저장 프로퍼티를 대입한 뒤 helper를 실행합니다. source graph의 transient 값을 읽으면 그 표현식은 조합 graph를 초기화할 때 한 번 평가되어 shared 결과에 보관됩니다. @@ -472,20 +492,19 @@ Factory 반환 타입과 매개변수 타입에는 직접 작성한 Optional을 ## 접근 수준과 초기화 -생성 프로퍼티는 graph와 같은 접근 수준을 가집니다. `overrides: true` graph는 `override`, `OverrideBuilder`, `build()`도 graph와 같은 접근 수준으로 생성합니다. `public` graph에서 교체 Factory의 매개변수·반환 타입과 source graph 인자는 외부 모듈에서 접근할 수 있어야 합니다. +생성 프로퍼티는 graph와 같은 접근 수준을 가집니다. `overrides: true` graph는 `override`, `OverrideBuilder`, `build()`도 graph와 같은 접근 수준으로 생성합니다. `public` graph에서 교체 Factory의 매개변수·반환 타입, initializer input 타입, source graph 인자는 외부 모듈에서 접근할 수 있어야 합니다. -`sources`와 `overrides: true`를 모두 지정하지 않은 graph에서는 Macro가 생성자를 추가하지 않으며 사용자가 선언한 생성자와 인스턴스 저장 프로퍼티를 바꾸지 않습니다. `sources` graph와 `overrides: true` graph는 Macro가 생성 경로를 소유합니다. 이 graph에서는 사용자가 initializer를 직접 선언하거나 Swift가 자동 초기화하지 않는 인스턴스 저장 프로퍼티를 선언하면 오류를 냅니다. Optional `var`와 기본 initializer가 있는 property wrapper 저장 프로퍼티는 Swift의 자동 초기화를 사용합니다. +`input`, `sources`, `overrides: true`를 모두 지정하지 않은 graph에서는 Macro가 생성자를 추가하지 않으며 사용자가 선언한 생성자와 인스턴스 저장 프로퍼티를 바꾸지 않습니다. `input`, `sources`, `overrides: true` graph는 Macro가 생성 경로를 소유합니다. 이 graph에서는 사용자가 initializer를 직접 선언하거나 Swift가 자동 초기화하지 않는 인스턴스 저장 프로퍼티를 선언하면 오류를 냅니다. Optional `var`와 기본 initializer가 있는 property wrapper 저장 프로퍼티는 Swift의 자동 초기화를 사용합니다. `sources` graph가 protocol만 채택하면 생성 initializer가 그대로 protocol 채택을 유지합니다. superclass를 상속한 `sources` graph에는 Macro가 `super.init()`을 생성하지 않습니다. superclass initializer 호출이 필요하면 Swift 컴파일러가 생성 initializer에서 오류를 표시합니다. ## 현재 지원 범위 -현재 `@DependencyGraph`는 동기 Factory의 타입 기반 연결, shared·lazy·transient 수명, 호출 시점 외부 입력 생성 메서드, graph 인스턴스별 Factory 교체를 지원합니다. 다음 기능은 아직 지원하지 않습니다. +현재 `@DependencyGraph`는 동기 Factory의 타입 기반 연결, shared·lazy·transient 수명, initializer input, 호출 시점 외부 입력 생성 메서드, graph 인스턴스별 Factory 교체를 지원합니다. 다음 기능은 아직 지원하지 않습니다. - graph 생성 뒤 등록 교체 - source graph 생성 프로퍼티의 자동 주입 - qualifier와 multibinding -- graph 생성자 입력 - actor graph의 `sources`와 actor source graph 조합 - `async`, `throws`, `rethrows` Factory - `nonisolated`, `nonisolated(unsafe)`, `@unchecked Sendable`, lock diff --git a/Sources/Cradle/DependencyGraph.swift b/Sources/Cradle/DependencyGraph.swift index e5fb9d1..989b7bc 100644 --- a/Sources/Cradle/DependencyGraph.swift +++ b/Sources/Cradle/DependencyGraph.swift @@ -22,18 +22,20 @@ - Parameters: - lifetime: graph 인스턴스의 보유 범위입니다. 기본값은 `.instance`이며, `.shared`는 프로세스 동안 보유하는 `static let shared` graph를 만듭니다. + - input: `final class` graph가 생성자에서 보관할 조립 입력 타입입니다. 기본값은 `nil`입니다. - sources: `final class` 조합 graph가 보관하고 Factory에서 읽을 source graph 타입 배열입니다. 기본값은 빈 배열입니다. - overrides: graph 인스턴스별 Factory 교체를 위한 `override`와 `OverrideBuilder` 생성 여부입니다. 기본값은 `false`입니다. - diagram: `CradlePlugin`의 Mermaid 개발 산출물에 해당 graph를 포함할지 정하는 값입니다. 기본값은 `true`입니다. - - Important: `actor` graph에는 `sources`를 사용할 수 없습니다. `sources` 또는 `overrides: true`를 - 사용한 graph에는 initializer를 직접 선언할 수 없습니다. + - Important: `actor` graph에는 `input` 또는 `sources`를 사용할 수 없습니다. `input`, `sources`, + `overrides: true`를 사용한 graph에는 initializer를 직접 선언할 수 없습니다. */ @attached(member, names: arbitrary) public macro DependencyGraph( _ lifetime: DependencyGraphLifetime = .instance, + input: Any.Type? = nil, sources: [Any.Type] = [], overrides: Bool = false, diagram: Bool = true diff --git a/Sources/CradleMacros/DependencyGraphMacro.swift b/Sources/CradleMacros/DependencyGraphMacro.swift index 4089e7d..7d10ce9 100644 --- a/Sources/CradleMacros/DependencyGraphMacro.swift +++ b/Sources/CradleMacros/DependencyGraphMacro.swift @@ -10,11 +10,11 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length // `@Provide` Factory를 호출하는 반환 타입 기반 생성 프로퍼티 추가 struct DependencyGraphMacro: MemberMacro { // graph 본체의 유효한 Factory별 transient 생성 프로퍼티 생성 - // swiftlint:disable:next function_body_length + // swiftlint:disable:next function_body_length cyclomatic_complexity static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, @@ -25,6 +25,18 @@ struct DependencyGraphMacro: MemberMacro { context.diagnose(Diagnostic(node: node, message: CradleMacroDiagnostic.invalidGraph)) return [] } + let hasInput = graphInputArgument(in: node) != nil + let graphInput = GraphInputDescriptor.from(attribute: node, in: context) + guard !hasInput || graphInput != nil else { + return [] + } + guard let configuredLifetime = dependencyGraphLifetimeConfiguration(from: node, in: context) else { + return [] + } + if graphInput != nil && configuredLifetime.createsSharedGraph { + context.diagnose(Diagnostic(node: node, message: GraphInputDiagnostic.sharedGraphUnsupported)) + return [] + } guard let lifetime = validatedSharedGraphLifetime( for: graph, attribute: node, @@ -32,6 +44,10 @@ struct DependencyGraphMacro: MemberMacro { ) else { return [] } + if graphInput != nil && graph.isActor { + context.diagnose(Diagnostic(node: node, message: GraphInputDiagnostic.actorUnsupported)) + return [] + } guard let overrideConfiguration = typedOverrideConfiguration(from: node, in: context) else { return [] } @@ -42,6 +58,10 @@ struct DependencyGraphMacro: MemberMacro { guard let sources = acceptedSourceDescriptors(for: graph, from: node, result: sourceResult, in: context) else { return [] } + if graphInput != nil, + diagnoseGraphInputInitializationErrors(in: graph.memberBlock.members, context: context) { + return [] + } if overrideConfiguration.isEnabled, diagnoseTypedOverrideInitializationErrors(in: graph.memberBlock.members, context: context) { return [] @@ -80,6 +100,7 @@ struct DependencyGraphMacro: MemberMacro { for: registeredProviders, graphName: graph.name, sources: sources, + input: graphInput, propertyNames: propertyNames, in: context ) @@ -91,6 +112,7 @@ struct DependencyGraphMacro: MemberMacro { let sourceDeclarations = graph.allowsSources ? sourceGraphDeclarations( for: sources, + input: graphInput, accessLevel: graphAccess, storage: storage ) : [] @@ -117,6 +139,7 @@ struct DependencyGraphMacro: MemberMacro { lifetime: lifetime, providers: providerResult.descriptors, sources: sources, + input: graphInput, accessLevel: graphAccess, propertyNames: propertyNames, storage: storage, diff --git a/Sources/CradleMacros/GraphInputDescriptor.swift b/Sources/CradleMacros/GraphInputDescriptor.swift new file mode 100644 index 0000000..c63df93 --- /dev/null +++ b/Sources/CradleMacros/GraphInputDescriptor.swift @@ -0,0 +1,113 @@ +// +// GraphInputDescriptor.swift +// CradleMacros +// +// Created by opfic on 9/7/26. +// + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +// graph 생성자가 보관할 단일 조립 입력 정보 +struct GraphInputDescriptor { + // graph 저장 프로퍼티와 생성자에 사용할 입력 타입 + let type: TypeSyntax + + // graph 입력을 정규화해 수집 + static func from( + attribute: AttributeSyntax, + in context: some MacroExpansionContext + ) -> GraphInputDescriptor? { + guard case let .argumentList(arguments)? = attribute.arguments else { + return nil + } + let inputs = arguments.filter { $0.label?.identifier?.name == "input" } + guard inputs.count <= 1 else { + context.diagnose(Diagnostic(node: arguments, message: GraphInputDiagnostic.invalidInput)) + return nil + } + guard let input = inputs.first else { + return nil + } + guard let member = input.expression.as(MemberAccessExprSyntax.self), + member.declName.baseName.text == "self", + member.declName.argumentNames == nil, + let base = member.base, + !input.expression.hasError else { + context.diagnose(Diagnostic(node: input.expression, message: GraphInputDiagnostic.invalidInput)) + return nil + } + let type = TypeSyntax(stringLiteral: base.trimmedDescription) + guard !type.hasError else { + context.diagnose(Diagnostic(node: input.expression, message: GraphInputDiagnostic.invalidInput)) + return nil + } + return GraphInputDescriptor(type: type) + } +} + +// graph attribute에 직접 작성한 input argument 반환 +func graphInputArgument(in attribute: AttributeSyntax) -> LabeledExprSyntax? { + guard case let .argumentList(arguments)? = attribute.arguments else { + return nil + } + return arguments.first { $0.label?.identifier?.name == "input" } +} + +// graph initializer input 선언 제약 진단 +enum GraphInputDiagnostic: DiagnosticMessage { + case invalidInput + case actorUnsupported + case sharedGraphUnsupported + case initializationConflict + + var diagnosticID: MessageID { + MessageID(domain: "Cradle", id: String(describing: self)) + } + + var message: String { + switch self { + case .invalidInput: + "`input`은 `Input.self` 형식으로 하나만 지정해야 합니다." + case .actorUnsupported: + "initializer input은 `final class` graph에서만 지원합니다." + case .sharedGraphUnsupported: + "`@DependencyGraph(.shared)`는 호출자 initializer input을 받을 수 없습니다." + case .initializationConflict: + "initializer input graph는 initializer 또는 초기화가 필요한 stored property를 직접 선언할 수 없습니다." + } + } + + var severity: DiagnosticSeverity { .error } +} + +// Macro가 소유한 input 생성 경로와 충돌하는 member 진단 +func diagnoseGraphInputInitializationErrors( + in members: MemberBlockItemListSyntax, + context: some MacroExpansionContext +) -> Bool { + var hasError = false + for member in members { + if let initializer = member.decl.as(InitializerDeclSyntax.self) { + context.diagnose(Diagnostic(node: initializer.initKeyword, message: GraphInputDiagnostic.initializationConflict)) + hasError = true + } + guard let variable = member.decl.as(VariableDeclSyntax.self), + !graphInputHasTypeMemberModifier(variable.modifiers) else { + continue + } + for binding in variable.bindings where requiresStoredPropertyInitialization(binding, in: variable) { + context.diagnose(Diagnostic(node: binding.pattern, message: GraphInputDiagnostic.initializationConflict)) + hasError = true + } + } + return hasError +} + +// graph input 저장 프로퍼티 검사에서 제외할 type member 판별 +private func graphInputHasTypeMemberModifier(_ modifiers: DeclModifierListSyntax) -> Bool { + modifiers.contains { modifier in + modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class) + } +} diff --git a/Sources/CradleMacros/ProviderDeclaration.swift b/Sources/CradleMacros/ProviderDeclaration.swift index 46c5853..62a49b3 100644 --- a/Sources/CradleMacros/ProviderDeclaration.swift +++ b/Sources/CradleMacros/ProviderDeclaration.swift @@ -15,10 +15,12 @@ func propertyNames(for providers: [ProviderDescriptor]) -> [RegisteredTypeIdenti } // shared 등록이 있을 때만 graph 전용 저장소 생성 +// swiftlint:disable:next function_parameter_count func sharedStorage( for providers: [ProviderDescriptor], graphName: TokenSyntax, sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, propertyNames: [RegisteredTypeIdentity: String], in context: some MacroExpansionContext ) -> SharedGraphStorage? { @@ -30,6 +32,7 @@ func sharedStorage( graphName: graphName, providers: sharedProviders, sources: sources, + input: input, propertyNames: propertyNames, in: context ) diff --git a/Sources/CradleMacros/SharedGraphStorage.swift b/Sources/CradleMacros/SharedGraphStorage.swift index 1e92060..6aefef8 100644 --- a/Sources/CradleMacros/SharedGraphStorage.swift +++ b/Sources/CradleMacros/SharedGraphStorage.swift @@ -23,6 +23,8 @@ struct SharedGraphStorage { let providers: [ProviderDescriptor] // 조합 graph가 보관하는 source graph let sources: [SourceGraphDescriptor] + // graph 생성자가 보관하는 조립 입력 + let input: GraphInputDescriptor? // 의존성 타입 identity와 생성 프로퍼티 이름 연결 let propertyNames: [RegisteredTypeIdentity: String] // 등록별 복제 static Factory helper 이름 @@ -33,12 +35,15 @@ struct SharedGraphStorage { let providerSources: [RegisteredTypeIdentity: [SourceGraphDescriptor]] // 등록별 source graph helper 매개변수 이름 let helperSourceNames: [RegisteredTypeIdentity: [RegisteredTypeIdentity: TokenSyntax]] + // 등록별 input helper 매개변수 이름 + let helperInputNames: [RegisteredTypeIdentity: TokenSyntax] // 한 graph의 shared Factory와 충돌하지 않는 저장소 식별자 생성 init( graphName: TokenSyntax, providers: [ProviderDescriptor], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor? = nil, propertyNames: [RegisteredTypeIdentity: String], in context: some MacroExpansionContext ) { @@ -48,13 +53,15 @@ struct SharedGraphStorage { builderName = context.makeUniqueName("makeSharedStorage") self.providers = providers self.sources = sources + self.input = input self.propertyNames = propertyNames helperNames = Dictionary(uniqueKeysWithValues: providers.map { provider in (provider.registrationIdentity, context.makeUniqueName("makeShared\(provider.propertyIdentifier)")) }) let sourceNames = Set(sources.map(\.propertyIdentifier)) + let referenceNames = sourceNames.union(input == nil ? [] : ["input"]) let references = Dictionary(uniqueKeysWithValues: providers.map { provider in - (provider.registrationIdentity, sourceGraphReferences(in: provider.factory, sourceNames: sourceNames)) + (provider.registrationIdentity, sourceGraphReferences(in: provider.factory, sourceNames: referenceNames)) }) sourceReferences = references let providerSources = Dictionary(uniqueKeysWithValues: providers.map { provider in @@ -68,6 +75,12 @@ struct SharedGraphStorage { }) return (provider.registrationIdentity, names) }) + helperInputNames = Dictionary(uniqueKeysWithValues: providers.compactMap { provider in + guard references[provider.registrationIdentity]?.sourceNames.contains("input") == true else { + return nil + } + return (provider.registrationIdentity, graphInputHelperParameterName(in: context)) + }) } // 중첩 저장소·정적 helper·즉시 초기화 소유 프로퍼티 선언 생성 @@ -84,7 +97,7 @@ struct SharedGraphStorage { // source 저장 프로퍼티 대입 뒤 shared 저장소를 초기화할지 여부 var requiresSourceInitialization: Bool { - !sources.isEmpty + !sources.isEmpty || input != nil } // source 저장 프로퍼티 대입 뒤 실행할 shared 저장소 초기화문 @@ -92,9 +105,11 @@ struct SharedGraphStorage { guard requiresSourceInitialization else { return nil } - let arguments = builderSources.map { source in + let sourceArguments = builderSources.map { source in "\(source.propertyName): \(source.propertyName)" }.joined(separator: ", ") + let inputArguments = requiresInputParameter ? "input: input" : "" + let arguments = [inputArguments, sourceArguments].filter { !$0.isEmpty }.joined(separator: ", ") return "self.\(storagePropertyName.trimmedDescription) = \(graphName.trimmedDescription).\(builderName.trimmedDescription)(\(arguments))" } @@ -122,9 +137,11 @@ struct SharedGraphStorage { ) else { return nil } - let parameters = provider.factory.signature.parameterClause.parameters.map { parameter in + let factoryParameters = provider.factory.signature.parameterClause.parameters.map { parameter in parameter.with(\.trailingComma, nil).trimmedDescription } + let inputParameters = helperInput(for: provider).map { ["\($0): \(input!.type.trimmedDescription)"] } ?? [] + let parameters = factoryParameters + inputParameters + helperSources(for: provider).compactMap { source in guard let name = helperSourceNames[provider.registrationIdentity]?[source.identity] else { return nil @@ -146,21 +163,24 @@ struct SharedGraphStorage { let providerArguments = provider.parameters.map { parameter in parameter.factoryArgument(propertyName: propertyNames[parameter.typeIdentity]!) } - let sourceArguments = helperSources(for: provider).compactMap { source -> String? in + let inputArguments = helperInput(for: provider).map { ["\($0): input"] } ?? [] + let sourceArguments = helperSources(for: provider).compactMap { source -> String? in guard let name = helperSourceNames[provider.registrationIdentity]?[source.identity] else { return nil } return "\(name.trimmedDescription): \(source.propertyName)" } - let arguments = (providerArguments + sourceArguments).joined(separator: ", ") + let arguments = (providerArguments + inputArguments + sourceArguments).joined(separator: ", ") return "let \(provider.propertyName): \(provider.returnType.trimmedDescription) = \(helper)(\(arguments))" }.joined(separator: "\n") let arguments = providers.map { provider in "\(provider.propertyName): \(provider.propertyName)" }.joined(separator: ", ") - let parameters = builderSources.map { source in + let sourceParameters = builderSources.map { source in "\(source.propertyName): \(source.type.trimmedDescription)" }.joined(separator: ", ") + let inputParameters = requiresInputParameter ? "input: \(input!.type.trimmedDescription)" : "" + let parameters = [inputParameters, sourceParameters].filter { !$0.isEmpty }.joined(separator: ", ") return DeclSyntax( """ private static func \(builderName)(\(raw: parameters)) -> \(storageTypeName) { @@ -194,22 +214,58 @@ struct SharedGraphStorage { } } + // shared builder가 input을 받아야 하는지 여부 + private var requiresInputParameter: Bool { + guard input != nil else { + return false + } + return helperNames.keys.contains { identity in + sourceReferences[identity]?.sourceNames.contains("input") == true + } + } + // 한 shared Factory가 실제로 참조한 source graph private func helperSources(for provider: ProviderDescriptor) -> [SourceGraphDescriptor] { providerSources[provider.registrationIdentity] ?? [] } + // shared helper가 전달받을 input 매개변수 이름 + private func helperInput(for provider: ProviderDescriptor) -> String? { + guard input != nil, + sourceReferences[provider.registrationIdentity]?.sourceNames.contains("input") == true else { + return nil + } + return helperInputNames[provider.registrationIdentity]?.trimmedDescription + } + // Factory source 이름과 helper 매개변수 이름 연결 private func sourceParameterNames(for provider: ProviderDescriptor) -> [String: String] { - Dictionary(uniqueKeysWithValues: helperSources(for: provider).compactMap { source in + var names: [String: String] = Dictionary(uniqueKeysWithValues: helperSources(for: provider).compactMap { source in guard let name = helperSourceNames[provider.registrationIdentity]?[source.identity] else { return nil } return (source.propertyIdentifier, name.trimmedDescription) }) + if let helperInput = helperInput(for: provider) { + names["input"] = helperInput + } + return names } } +// input helper 매개변수로 사용할 유효한 고유 identifier 생성 +private func graphInputHelperParameterName(in context: some MacroExpansionContext) -> TokenSyntax { + let uniqueName = context.makeUniqueName("graphInput").trimmedDescription + let identifier = uniqueName.unicodeScalars.map { scalar in + if scalar == "_" || scalar.properties.isAlphabetic || scalar.properties.numericType != nil { + String(scalar) + } else { + "_" + } + }.joined() + return .identifier(identifier) +} + // helper 매개변수로 사용할 유효한 고유 identifier 생성 private func sourceHelperParameterName( for source: SourceGraphDescriptor, diff --git a/Sources/CradleMacros/SourceGraphDescriptor.swift b/Sources/CradleMacros/SourceGraphDescriptor.swift index c8483fd..3902e16 100644 --- a/Sources/CradleMacros/SourceGraphDescriptor.swift +++ b/Sources/CradleMacros/SourceGraphDescriptor.swift @@ -59,7 +59,7 @@ func sourceGraphResult( guard sourceArguments.count <= 1, arguments.allSatisfy({ argument in let name = argument.label?.identifier?.name - return name == nil || name == "sources" || name == "overrides" || name == "diagram" + return name == nil || name == "input" || name == "sources" || name == "overrides" || name == "diagram" }) else { context.diagnose(Diagnostic(node: arguments, message: SourceGraphDiagnostic.invalidSources)) return SourceGraphResult(descriptors: [], hasError: true) @@ -295,26 +295,35 @@ private func sourceGraphHasTypeMemberModifier(_ modifiers: DeclModifierListSynta // source 저장 프로퍼티와 생성 initializer 선언 생성 func sourceGraphDeclarations( for sources: [SourceGraphDescriptor], + input: GraphInputDescriptor? = nil, accessLevel: AccessLevel, storage: SharedGraphStorage? ) -> [DeclSyntax] { - guard !sources.isEmpty else { + guard !sources.isEmpty || input != nil else { return [] } - let properties = sources.map { source in + let inputProperties: [DeclSyntax] = input.map { input in + [DeclSyntax("private let input: \(raw: input.type.trimmedDescription)")] + } ?? [] + let sourceProperties = sources.map { source in DeclSyntax( """ private let \(raw: source.propertyName): \(raw: source.type.trimmedDescription) """ ) } - let parameters = sources.map { source in + let inputParameters: [String] = input.map { input in + ["input: \(input.type.trimmedDescription)"] + } ?? [] + let sourceParameters = sources.map { source in "\(source.propertyName): \(source.type.trimmedDescription)" - }.joined(separator: ", ") + } + let parameters = (inputParameters + sourceParameters).joined(separator: ", ") + let inputAssignments: [String] = input.map { _ in ["self.input = input"] } ?? [] let sourceAssignments = sources.map { source in "self.\(source.propertyName) = \(source.propertyName)" } - let assignments = (sourceAssignments + [storage?.initializationAssignment()]) + let assignments = (inputAssignments + sourceAssignments + [storage?.initializationAssignment()]) .compactMap { $0 } .joined(separator: "\n") let initializer = DeclSyntax( @@ -324,5 +333,5 @@ func sourceGraphDeclarations( } """ ) - return properties + [initializer] + return inputProperties + sourceProperties + [initializer] } diff --git a/Sources/CradleMacros/TypedOverride.swift b/Sources/CradleMacros/TypedOverride.swift index a974a50..47d0881 100644 --- a/Sources/CradleMacros/TypedOverride.swift +++ b/Sources/CradleMacros/TypedOverride.swift @@ -43,6 +43,7 @@ func typedOverrideDeclarations( lifetime: DependencyGraphLifetimeConfiguration, providers: [ProviderDescriptor], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor? = nil, accessLevel: AccessLevel, propertyNames: [RegisteredTypeIdentity: String], storage: SharedGraphStorage?, @@ -70,6 +71,7 @@ func typedOverrideDeclarations( builderName: builderName, providers: shared, sources: sources, + input: input, propertyNames: propertyNames, in: context ) @@ -80,6 +82,7 @@ func typedOverrideDeclarations( builderName: builderName, providers: overrides, sources: sources, + input: input, accessLevel: accessLevel ) + graphInitializers( @@ -87,6 +90,7 @@ func typedOverrideDeclarations( builderName: builderName, providers: overrides, sources: sources, + input: input, transient: transient, lazy: lazy, storage: sharedStorage, @@ -96,6 +100,7 @@ func typedOverrideDeclarations( + typedOverridePropertyDeclarations( providers: overrides, sources: sources, + input: input, accessLevel: accessLevel, propertyNames: propertyNames, storage: sharedStorage, @@ -119,6 +124,7 @@ private func typedOverrideBuilderDeclarations( builderName: TokenSyntax, providers: [TypedOverrideProvider], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, accessLevel: AccessLevel ) -> [DeclSyntax] { [ @@ -128,6 +134,7 @@ private func typedOverrideBuilderDeclarations( lifetime: lifetime, providers: providers, sources: sources, + input: input, accessLevel: accessLevel ), overrideEntryPoint( @@ -274,6 +281,7 @@ private func builderDeclaration( lifetime: DependencyGraphLifetimeConfiguration, providers: [TypedOverrideProvider], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, accessLevel: AccessLevel ) -> DeclSyntax { let sendable = graph.isActor || ( @@ -289,14 +297,18 @@ private func builderDeclaration( let assignments = providers.map { override in "self.\(override.storageName) = \(override.storageName)" }.joined(separator: "\n") + let inputParameters = input.map { ["input: \($0.type.trimmedDescription)"] } ?? [] let sourceParameters = sources.map { source in "\(source.propertyName): \(source.type.trimmedDescription)" }.joined(separator: ", ") let sourceArguments = sources.map { source in "\(source.propertyName): \(source.propertyName)" }.joined(separator: ", ") - let buildParameters = sourceParameters.isEmpty ? "" : sourceParameters - let buildArguments = (["overrides: self"] + (sourceArguments.isEmpty ? [] : [sourceArguments])).joined(separator: ", ") + let buildParameters = (inputParameters + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", ") + let buildValues = inputParameters.map { _ in "input: input" } + + ["overrides: self"] + + (sourceArguments.isEmpty ? [] : [sourceArguments]) + let buildArguments = buildValues.joined(separator: ", ") return DeclSyntax( """ \(raw: mainActor)\(raw: accessLevel.rawValue) struct \(builderName)\(raw: sendable) { @@ -344,6 +356,7 @@ private func graphInitializers( builderName: TokenSyntax, providers: [TypedOverrideProvider], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, transient: [TypedOverrideProvider], lazy: [TypedOverrideProvider], storage: TypedOverrideSharedStorage, @@ -356,32 +369,34 @@ private func graphInitializers( let lazyAssignments = lazy.map { override in "self.\(override.storageName) = overrides.\(override.storageName)" }.joined(separator: "\n") - let storageAssignment = storage.initializationAssignment(sources: sources) + let storageAssignment = storage.initializationAssignment(sources: sources, input: input) + let inputParameters = input.map { ["input: \($0.type.trimmedDescription)"] } ?? [] let sourceParameters = sources.map { source in "\(source.propertyName): \(source.type.trimmedDescription)" }.joined(separator: ", ") let sourceArguments = sources.map { source in "\(source.propertyName): \(source.propertyName)" }.joined(separator: ", ") - let originalValues = ["overrides: Self.override()"] + let originalValues = inputParameters.map { _ in "input: input" } + ["overrides: Self.override()"] + (sourceArguments.isEmpty ? [] : [sourceArguments]) let originalArguments = originalValues.joined(separator: ", ") let initializerModifier = isActor ? "" : "convenience " let originalInit = DeclSyntax( """ - \(raw: accessLevel.rawValue) \(raw: initializerModifier)init(\(raw: sourceParameters)) { + \(raw: accessLevel.rawValue) \(raw: initializerModifier)init(\(raw: (inputParameters + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", "))) { self.init(\(raw: originalArguments)) } """ ) - let privateParameters = (["overrides: \(builderName)"] + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", ") + let privateParameters = (inputParameters + ["overrides: \(builderName)"] + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", ") + let inputAssignments = input.map { _ in ["self.input = input"] } ?? [] let sourceAssignments = sources.map { source in "self.\(source.propertyName) = \(source.propertyName)" }.joined(separator: "\n") let overrideInit = DeclSyntax( """ private init(\(raw: privateParameters)) { - \(raw: sourceAssignments) + \(raw: (inputAssignments + [sourceAssignments]).filter { !$0.isEmpty }.joined(separator: "\n")) \(raw: transientAssignments) \(raw: lazyAssignments) \(raw: storageAssignment) @@ -396,11 +411,13 @@ private func graphInitializers( private func typedOverridePropertyDeclarations( providers: [TypedOverrideProvider], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, accessLevel: AccessLevel, propertyNames: [RegisteredTypeIdentity: String], storage: TypedOverrideSharedStorage, in context: some MacroExpansionContext ) -> [DeclSyntax] { + let inputStorage: [DeclSyntax] = input.map { [DeclSyntax("private let input: \(raw: $0.type.trimmedDescription)")] } ?? [] let sourceStorage = sources.map { source in DeclSyntax("private let \(raw: source.propertyName): \(raw: source.type.trimmedDescription)") } @@ -426,7 +443,7 @@ private func typedOverridePropertyDeclarations( storage: storage ) } - return sourceStorage + transientStorage + lazyStorage + storage.declarations() + properties + return inputStorage + sourceStorage + transientStorage + lazyStorage + storage.declarations() + properties } // 외부 입력을 호출 시점에 원본 또는 교체 Factory로 전달하는 생성 메서드 @@ -563,6 +580,8 @@ private struct TypedOverrideSharedStorage { let providers: [TypedOverrideProvider] // 조합 graph가 소유하는 source graph let sources: [SourceGraphDescriptor] + // graph 생성자가 보관하는 조립 입력 + let input: GraphInputDescriptor? // 등록 의존성 연결 let propertyNames: [RegisteredTypeIdentity: String] // 원본 shared Factory helper 이름 @@ -573,12 +592,16 @@ private struct TypedOverrideSharedStorage { let providerSources: [RegisteredTypeIdentity: [SourceGraphDescriptor]] // helper source 매개변수 이름 let helperSourceNames: [RegisteredTypeIdentity: [RegisteredTypeIdentity: TokenSyntax]] + // helper input 매개변수 이름 + let helperInputNames: [RegisteredTypeIdentity: TokenSyntax] + // swiftlint:disable:next function_body_length init( graphName: TokenSyntax, builderName: TokenSyntax, providers: [TypedOverrideProvider], sources: [SourceGraphDescriptor], + input: GraphInputDescriptor?, propertyNames: [RegisteredTypeIdentity: String], in context: some MacroExpansionContext ) { @@ -586,6 +609,7 @@ private struct TypedOverrideSharedStorage { overrideBuilderName = builderName self.providers = providers self.sources = sources + self.input = input self.propertyNames = propertyNames guard !providers.isEmpty else { typeName = nil @@ -595,6 +619,7 @@ private struct TypedOverrideSharedStorage { sourceReferences = [:] providerSources = [:] helperSourceNames = [:] + helperInputNames = [:] return } typeName = typedOverrideUniqueName("TypedOverrideSharedStorage", in: context) @@ -604,10 +629,11 @@ private struct TypedOverrideSharedStorage { (override.provider.registrationIdentity, typedOverrideUniqueName("makeTypedOverrideShared", in: context)) }) let sourceNames = Set(sources.map(\.propertyIdentifier)) + let referenceNames = sourceNames.union(input == nil ? [] : ["input"]) let references = Dictionary(uniqueKeysWithValues: providers.map { override in let sourceReferences = sourceGraphReferences( in: override.provider.factory, - sourceNames: sourceNames + sourceNames: referenceNames ) return (override.provider.registrationIdentity, sourceReferences) }) @@ -625,6 +651,12 @@ private struct TypedOverrideSharedStorage { return (override.provider.registrationIdentity, values) }) helperSourceNames = names + helperInputNames = Dictionary(uniqueKeysWithValues: providers.compactMap { override in + guard references[override.provider.registrationIdentity]?.sourceNames.contains("input") == true else { + return nil + } + return (override.provider.registrationIdentity, typedOverrideInputParameterName(in: context)) + }) } // shared 저장소 선언과 helper 선언 생성 @@ -649,10 +681,11 @@ private struct TypedOverrideSharedStorage { let arguments = providers.map { override in "\(override.provider.propertyName): \(override.provider.propertyName)" }.joined(separator: ", ") + let inputParameters = requiresInputParameter ? ["input: \(input!.type.trimmedDescription)"] : [] let sourceParameters = builderSources.map { source in "\(source.propertyName): \(source.type.trimmedDescription)" }.joined(separator: ", ") - let parameters = (["_ overrides: \(overrideBuilderName)"] + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", ") + let parameters = (["_ overrides: \(overrideBuilderName)"] + inputParameters + (sourceParameters.isEmpty ? [] : [sourceParameters])).joined(separator: ", ") let builder = DeclSyntax(""" private static func \(builderName)(\(raw: parameters)) -> \(typeName) { \(raw: constructions) @@ -664,14 +697,15 @@ private struct TypedOverrideSharedStorage { } // graph initializer가 실행할 shared 저장소 대입문 - func initializationAssignment(sources: [SourceGraphDescriptor]) -> String { + func initializationAssignment(sources: [SourceGraphDescriptor], input: GraphInputDescriptor?) -> String { guard let propertyName, let builderName else { return "" } let arguments = builderSources.map { source in "\(source.propertyName): \(source.propertyName)" }.joined(separator: ", ") - let supplied = (["overrides"] + (arguments.isEmpty ? [] : [arguments])).joined(separator: ", ") + let inputArguments = requiresInputParameter && input != nil ? ["input: input"] : [] + let supplied = (["overrides"] + inputArguments + (arguments.isEmpty ? [] : [arguments])).joined(separator: ", ") return "self.\(propertyName) = Self.\(builderName)(\(supplied))" } @@ -693,9 +727,11 @@ private struct TypedOverrideSharedStorage { ) else { return nil } - let parameters = override.provider.factory.signature.parameterClause.parameters.map { parameter in + let factoryParameters = override.provider.factory.signature.parameterClause.parameters.map { parameter in parameter.with(\.trailingComma, nil).trimmedDescription - } + helperSources(for: override).compactMap { source in + } + let inputParameters = helperInput(for: override).map { ["\($0): \(input!.type.trimmedDescription)"] } ?? [] + let parameters = factoryParameters + inputParameters + helperSources(for: override).compactMap { source in guard let name = helperSourceNames[override.provider.registrationIdentity]?[source.identity] else { return nil } @@ -714,13 +750,16 @@ private struct TypedOverrideSharedStorage { let overrideArguments = override.provider.parameters.map { parameter in propertyNames[parameter.typeIdentity]! }.joined(separator: ", ") + let inputArguments = helperInput(for: override).map { ["\($0): input"] } ?? [] let sourceArguments = helperSources(for: override).compactMap { source -> String? in guard let name = helperSourceNames[override.provider.registrationIdentity]?[source.identity] else { return nil } return "\(name): \(source.propertyName)" }.joined(separator: ", ") - let originalCallArguments = ([originalArguments] + (sourceArguments.isEmpty ? [] : [sourceArguments])) + let originalValues = [originalArguments] + inputArguments + + (sourceArguments.isEmpty ? [] : [sourceArguments]) + let originalCallArguments = originalValues .filter { !$0.isEmpty } .joined(separator: ", ") let helper = helperNames[override.provider.registrationIdentity]!.trimmedDescription @@ -744,18 +783,54 @@ private struct TypedOverrideSharedStorage { } } + // shared builder가 input을 받아야 하는지 여부 + private var requiresInputParameter: Bool { + guard input != nil else { + return false + } + return helperNames.keys.contains { identity in + sourceReferences[identity]?.sourceNames.contains("input") == true + } + } + // 한 shared Factory가 읽는 source graph private func helperSources(for override: TypedOverrideProvider) -> [SourceGraphDescriptor] { providerSources[override.provider.registrationIdentity] ?? [] } + // shared helper가 전달받을 input 매개변수 이름 + private func helperInput(for override: TypedOverrideProvider) -> String? { + guard input != nil, + sourceReferences[override.provider.registrationIdentity]?.sourceNames.contains("input") == true else { + return nil + } + return helperInputNames[override.provider.registrationIdentity]?.trimmedDescription + } + // source 이름을 helper 매개변수 이름으로 연결 private func sourceParameterNames(for override: TypedOverrideProvider) -> [String: String] { - Dictionary(uniqueKeysWithValues: helperSources(for: override).compactMap { source in + var names: [String: String] = Dictionary(uniqueKeysWithValues: helperSources(for: override).compactMap { source in guard let name = helperSourceNames[override.provider.registrationIdentity]?[source.identity] else { return nil } return (source.propertyIdentifier, name.trimmedDescription) }) + if let helperInput = helperInput(for: override) { + names["input"] = helperInput + } + return names } } + +// typed override helper input에 사용할 유효한 고유 identifier 생성 +private func typedOverrideInputParameterName(in context: some MacroExpansionContext) -> TokenSyntax { + let uniqueName = context.makeUniqueName("graphInput").trimmedDescription + let identifier = uniqueName.unicodeScalars.map { scalar in + if scalar == "_" || scalar.properties.isAlphabetic || scalar.properties.numericType != nil { + String(scalar) + } else { + "_" + } + }.joined() + return .identifier(identifier) +} diff --git a/Tests/CradleMacrosTests/DependencyGraphMacroDiagnosticsTests.swift b/Tests/CradleMacrosTests/DependencyGraphMacroDiagnosticsTests.swift index 6d0bae3..4bad91c 100644 --- a/Tests/CradleMacrosTests/DependencyGraphMacroDiagnosticsTests.swift +++ b/Tests/CradleMacrosTests/DependencyGraphMacroDiagnosticsTests.swift @@ -1,3 +1,4 @@ +// swiftlint:disable file_length // // DependencyGraphMacroDiagnosticsTests.swift // CradleMacrosTests @@ -8,6 +9,29 @@ import SwiftSyntaxMacrosTestSupport import Testing +// shared graph input이 Sendable 진단보다 먼저 거부되는지 확인 +@Test +func dependencyGraphRejectsSharedInitializerInputBeforeSendableValidation() { + assertMacroExpansion( + """ + @DependencyGraph(.shared, input: Input.self) + final class Graph {} + """, + expandedSource: """ + final class Graph {} + """, + diagnostics: [ + DiagnosticSpec( + id: .init(domain: "Cradle", id: "sharedGraphUnsupported"), + message: "`@DependencyGraph(.shared)`는 호출자 initializer input을 받을 수 없습니다.", + line: 1, + column: 1 + ) + ], + macros: testMacros + ) +} + // struct graph 거부 확인 @Test func dependencyGraphRejectsStruct() { diff --git a/Tests/CradleTests/DependencyGraphRuntimeTests.swift b/Tests/CradleTests/DependencyGraphRuntimeTests.swift index c812bc4..912d456 100644 --- a/Tests/CradleTests/DependencyGraphRuntimeTests.swift +++ b/Tests/CradleTests/DependencyGraphRuntimeTests.swift @@ -64,6 +64,65 @@ final class DefaultSharedValue {} // 기본 shared 해제 확인용 참조 값 final class DefaultSharedReleasedValue {} +// graph initializer가 보관할 repository 입력 +final class GraphInputRepository {} + +// graph initializer가 받은 repository 입력 묶음 +struct GraphInput { + // UseCase 생성에 전달할 repository + let repository: GraphInputRepository + // provider 생성 횟수 기록 + let probe: GraphInputCreationProbe +} + +// input provider 생성 시점을 기록할 probe +final class GraphInputCreationProbe { + // shared Factory 실행 횟수 + var sharedCount = 0 + // lazy Factory 실행 횟수 + var lazyCount = 0 +} + +// input을 읽어 만든 shared 결과 +final class GraphInputUseCase { + // 생성에 사용한 repository + let repository: GraphInputRepository + + // repository 연결 생성 + init(repository: GraphInputRepository) { + self.repository = repository + } +} + +// input을 최초 접근에서 읽을 lazy 결과 +final class GraphInputLazyValue { + // 생성에 사용한 repository + let repository: GraphInputRepository + + // repository 연결 생성 + init(repository: GraphInputRepository) { + self.repository = repository + } +} + +// initializer input과 provider 수명을 함께 검증할 graph +@DependencyGraph(input: GraphInput.self) +final class GraphInputDependencyGraph { + // graph 생성 중 input repository로 만들 shared UseCase + @Provide + private func makeGraphInputUseCase() -> GraphInputUseCase { + input.probe.sharedCount += 1 + return GraphInputUseCase(repository: input.repository) + } + + // 최초 접근에서 input repository로 만들 lazy 값 + @Provide(.lazy) + private func makeGraphInputLazyValue() -> GraphInputLazyValue { + input.probe.lazyCount += 1 + return GraphInputLazyValue(repository: input.repository) + } +} + // 인자 생략 Factory의 graph별 shared 수명 검증용 graph @DependencyGraph final class DefaultSharedGraph { @@ -184,3 +243,54 @@ func defaultProviderReleasesOwnedSharedValue() { } #expect(observed == nil) } + +// 기본 shared provider가 initializer input 대입 뒤 생성되는지 확인 +@Test +func defaultProviderReadsInitializerInputDuringGraphCreation() { + let repository = GraphInputRepository() + let probe = GraphInputCreationProbe() + let graph = GraphInputDependencyGraph(input: GraphInput(repository: repository, probe: probe)) + + #expect(probe.sharedCount == 1) + #expect(probe.lazyCount == 0) + #expect(graph.graphInputUseCase.repository === repository) + #expect(graph.graphInputUseCase === graph.graphInputUseCase) +} + +// lazy provider가 initializer input을 최초 접근까지 읽지 않는지 확인 +@Test +func lazyProviderReadsInitializerInputAtFirstAccess() { + let repository = GraphInputRepository() + let probe = GraphInputCreationProbe() + let graph = GraphInputDependencyGraph(input: GraphInput(repository: repository, probe: probe)) + + #expect(probe.lazyCount == 0) + #expect(graph.graphInputLazyValue.repository === repository) + #expect(probe.lazyCount == 1) + #expect(graph.graphInputLazyValue === graph.graphInputLazyValue) + #expect(probe.lazyCount == 1) +} + +// shared helper가 Factory 지역 이름보다 graph input을 우선하는지 확인 +@Test +func defaultProviderPreservesInputReferenceWhenFactoryDeclaresSameHelperName() { + let repository = GraphInputRepository() + let probe = GraphInputCreationProbe() + let graph = GraphInputShadowingDependencyGraph( + input: GraphInput(repository: repository, probe: probe) + ) + + #expect(graph.graphInputUseCase.repository === repository) +} + +// helper 매개변수와 같은 이름을 가진 지역 선언 회귀 검증용 graph +@DependencyGraph(input: GraphInput.self) +final class GraphInputShadowingDependencyGraph { + // graph input을 읽는 shared UseCase 생성 + @Provide + private func makeGraphInputUseCase() -> GraphInputUseCase { + let graphInput = GraphInput(repository: GraphInputRepository(), probe: input.probe) + _ = graphInput + return GraphInputUseCase(repository: input.repository) + } +} diff --git a/Tests/CradleTests/SourceGraphRuntimeTests.swift b/Tests/CradleTests/SourceGraphRuntimeTests.swift index 49fe3c8..2345028 100644 --- a/Tests/CradleTests/SourceGraphRuntimeTests.swift +++ b/Tests/CradleTests/SourceGraphRuntimeTests.swift @@ -120,6 +120,29 @@ final class SourceGraphSharedFeatureGraph { } } +// initializer input과 source graph를 함께 보관할 조합 graph +@DependencyGraph(input: GraphInput.self, sources: [SourceGraphAppGraph.self]) +final class SourceGraphInputFeatureGraph { + // input과 source graph 값을 함께 읽는 shared 결과 생성 + @Provide + private func makeGraphInputUseCase() -> GraphInputUseCase { + _ = sourceGraphAppGraph.sourceGraphRepository + return GraphInputUseCase(repository: input.repository) + } +} + +// input 대입 뒤 source graph를 보관하고 shared 결과를 생성하는지 확인 +@Test +func sourceGraphCompositionBuildsSharedProviderWithInitializerInput() { + let repository = GraphInputRepository() + let graph = SourceGraphInputFeatureGraph( + input: GraphInput(repository: repository, probe: GraphInputCreationProbe()), + sourceGraphAppGraph: SourceGraphAppGraph() + ) + + #expect(graph.graphInputUseCase.repository === repository) +} + // source graph의 shared 값은 조합 graph 접근마다 같은 identity를 유지하는지 확인 @Test func sourceGraphCompositionUsesSourceSharedValues() { diff --git a/Tests/CradleTests/TypedOverrideClassTests.swift b/Tests/CradleTests/TypedOverrideClassTests.swift index 7c55194..627e592 100644 --- a/Tests/CradleTests/TypedOverrideClassTests.swift +++ b/Tests/CradleTests/TypedOverrideClassTests.swift @@ -161,6 +161,55 @@ final class TypedOverrideClassGraph { } } +// initializer input을 받는 override graph +@DependencyGraph(input: GraphInput.self, overrides: true) +final class TypedOverrideInputGraph { + // input repository를 사용하는 기본 shared 결과 생성 + @Provide + private func makeGraphInputUseCase() -> GraphInputUseCase { + return GraphInputUseCase(repository: input.repository) + } +} + +// input과 source graph를 함께 받는 override graph +@DependencyGraph(input: GraphInput.self, sources: [SourceGraphAppGraph.self], overrides: true) +final class TypedOverrideSourceInputGraph { + // input과 source transient 값을 함께 읽는 shared 결과 생성 + @Provide + private func makeGraphInputUseCase() -> GraphInputUseCase { + _ = sourceGraphAppGraph.sourceGraphRequestIdentifier + return GraphInputUseCase(repository: input.repository) + } +} + +// override builder가 input을 graph 생성 경로로 전달하는지 확인 +@Test +func typedOverrideGraphBuildsSharedProviderWithInitializerInput() { + let repository = GraphInputRepository() + let graph = TypedOverrideInputGraph.override().build( + input: GraphInput(repository: repository, probe: GraphInputCreationProbe()) + ) + + #expect(graph.graphInputUseCase.repository === repository) +} + +// source와 input을 받는 override builder가 build 전에는 Factory를 평가하지 않는지 확인 +@Test +func typedOverrideGraphDefersSourceInputSharedProviderUntilBuild() { + let repository = GraphInputRepository() + let probe = GraphInputCreationProbe() + let source = SourceGraphAppGraph() + let builder = TypedOverrideSourceInputGraph.override() + + #expect(source.requestCount == 0) + let graph = builder.build( + input: GraphInput(repository: repository, probe: probe), + sourceGraphAppGraph: source + ) + #expect(source.requestCount == 1) + #expect(graph.graphInputUseCase.repository === repository) +} + // 교체 shared Factory와 원본 transient 연결 확인 @Test func typedOverrideClassGraphUsesReplacementAndOriginalDependencies() {