Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Sources/Cradle/Cradle.docc/Cradle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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을 읽습니다.
Comment thread
opficdev marked this conversation as resolved.

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는 같은 격리를 따릅니다.
Expand All @@ -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에는 포함되지 않습니다.

Expand All @@ -32,7 +34,7 @@ SwiftPM target에 `CradlePlugin`을 연결하면 build마다 의존성 관계를

### 매크로

- ``DependencyGraph(_:sources:overrides:diagram:)``
- ``DependencyGraph(_:input:sources:overrides:diagram:)``
- ``DependencyGraphLifetime``
- ``DependencyOverride``
- ``External``
Expand Down
29 changes: 24 additions & 5 deletions Sources/Cradle/Cradle.docc/DependencyGraph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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 결과에 보관됩니다.

Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Sources/Cradle/DependencyGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
opficdev marked this conversation as resolved.
sources: [Any.Type] = [],
overrides: Bool = false,
diagram: Bool = true
Expand Down
27 changes: 25 additions & 2 deletions Sources/CradleMacros/DependencyGraphMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,13 +25,29 @@ 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,
context: context
) 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 []
}
Expand All @@ -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 []
Expand Down Expand Up @@ -80,6 +100,7 @@ struct DependencyGraphMacro: MemberMacro {
for: registeredProviders,
graphName: graph.name,
sources: sources,
input: graphInput,
propertyNames: propertyNames,
in: context
)
Expand All @@ -91,6 +112,7 @@ struct DependencyGraphMacro: MemberMacro {

let sourceDeclarations = graph.allowsSources ? sourceGraphDeclarations(
for: sources,
input: graphInput,
accessLevel: graphAccess,
storage: storage
) : []
Expand All @@ -117,6 +139,7 @@ struct DependencyGraphMacro: MemberMacro {
lifetime: lifetime,
providers: providerResult.descriptors,
sources: sources,
input: graphInput,
accessLevel: graphAccess,
propertyNames: propertyNames,
storage: storage,
Expand Down
113 changes: 113 additions & 0 deletions Sources/CradleMacros/GraphInputDescriptor.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions Sources/CradleMacros/ProviderDeclaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand All @@ -30,6 +32,7 @@ func sharedStorage(
graphName: graphName,
providers: sharedProviders,
sources: sources,
input: input,
propertyNames: propertyNames,
in: context
)
Expand Down
Loading