diff --git a/Sources/SwordCommand/SwordCommand.swift b/Sources/SwordCommand/SwordCommand.swift index eb283c6..295ea9c 100644 --- a/Sources/SwordCommand/SwordCommand.swift +++ b/Sources/SwordCommand/SwordCommand.swift @@ -6,7 +6,7 @@ import SwordGenerator import Yams @main -struct SwordCommand: ParsableCommand { +struct SwordCommand: AsyncParsableCommand { @Option(parsing: .upToNextOption) var targets: [String] = [] @Option(parsing: .upToNextOption) @@ -14,7 +14,7 @@ struct SwordCommand: ParsableCommand { @Option var output: String - mutating func run() throws { + mutating func run() async throws { try loadLocalPackagesIfNeeded() // Parse files in current working directory if no inputs were specified. @@ -29,15 +29,17 @@ struct SwordCommand: ParsableCommand { tree: Parser.parse(source: source) ) } + + let parser = SwordParser() let reporter = SwordReporter(fileUpdater: .standardOutput) - let parser = SwordParser(reporter: reporter) let renderer = SwordRenderer() let exporter = SwordExporter(renderer: renderer) let generator = SwordGenerator( parser: parser, + reporter: reporter, exporter: exporter ) - try generator.generate( + try await generator.generate( sourceFiles: sourceFiles, targets: targets, output: output diff --git a/Sources/SwordFoundation/Scope.swift b/Sources/SwordFoundation/Scope.swift index 3d37ede..51bd8e7 100644 --- a/Sources/SwordFoundation/Scope.swift +++ b/Sources/SwordFoundation/Scope.swift @@ -1,4 +1,4 @@ -public enum Scope: String, Codable { +public enum Scope: String, Codable, Sendable { case single case weakReference } diff --git a/Sources/SwordGenerator/Parser/Descriptor/DependencyDescriptor.swift b/Sources/SwordGenerator/Descriptor/DependencyDescriptor.swift similarity index 89% rename from Sources/SwordGenerator/Parser/Descriptor/DependencyDescriptor.swift rename to Sources/SwordGenerator/Descriptor/DependencyDescriptor.swift index b55caf7..f9efbc9 100644 --- a/Sources/SwordGenerator/Parser/Descriptor/DependencyDescriptor.swift +++ b/Sources/SwordGenerator/Descriptor/DependencyDescriptor.swift @@ -3,6 +3,7 @@ import SwiftSyntax import SwordFoundation struct DependencyDescriptor { + let componentName: ComponentName let type: Type let interface: Interface? let injectedInitializers: [Initializer] diff --git a/Sources/SwordGenerator/Parser/Descriptor/ModuleDescriptor.swift b/Sources/SwordGenerator/Descriptor/ModuleDescriptor.swift similarity index 52% rename from Sources/SwordGenerator/Parser/Descriptor/ModuleDescriptor.swift rename to Sources/SwordGenerator/Descriptor/ModuleDescriptor.swift index b06181c..67ab56f 100644 --- a/Sources/SwordGenerator/Parser/Descriptor/ModuleDescriptor.swift +++ b/Sources/SwordGenerator/Descriptor/ModuleDescriptor.swift @@ -1,6 +1,7 @@ import Foundation -struct ModuleDescriptor { +struct ModuleDescriptor: Hashable { let name: String + let componentName: ComponentName let providers: [ProviderDescriptor] } diff --git a/Sources/SwordGenerator/Parser/Descriptor/ProviderDescriptor.swift b/Sources/SwordGenerator/Descriptor/ProviderDescriptor.swift similarity index 86% rename from Sources/SwordGenerator/Parser/Descriptor/ProviderDescriptor.swift rename to Sources/SwordGenerator/Descriptor/ProviderDescriptor.swift index ded8866..2bf1efa 100644 --- a/Sources/SwordGenerator/Parser/Descriptor/ProviderDescriptor.swift +++ b/Sources/SwordGenerator/Descriptor/ProviderDescriptor.swift @@ -2,7 +2,7 @@ import Foundation import SwiftSyntax import SwordFoundation -struct ProviderDescriptor { +struct ProviderDescriptor: Hashable { let name: String let isStaticFunction: Bool let returnType: Type? diff --git a/Sources/SwordGenerator/Parser/Descriptor/RootComponentDescriptor.swift b/Sources/SwordGenerator/Descriptor/RootComponentDescriptor.swift similarity index 100% rename from Sources/SwordGenerator/Parser/Descriptor/RootComponentDescriptor.swift rename to Sources/SwordGenerator/Descriptor/RootComponentDescriptor.swift diff --git a/Sources/SwordGenerator/Parser/Descriptor/SubcomponentDescriptor.swift b/Sources/SwordGenerator/Descriptor/SubcomponentDescriptor.swift similarity index 100% rename from Sources/SwordGenerator/Parser/Descriptor/SubcomponentDescriptor.swift rename to Sources/SwordGenerator/Descriptor/SubcomponentDescriptor.swift diff --git a/Sources/SwordGenerator/Renderer/SwordRenderer.swift b/Sources/SwordGenerator/Exporter/Renderer/SwordRenderer.swift similarity index 93% rename from Sources/SwordGenerator/Renderer/SwordRenderer.swift rename to Sources/SwordGenerator/Exporter/Renderer/SwordRenderer.swift index 80ece9a..594066f 100644 --- a/Sources/SwordGenerator/Renderer/SwordRenderer.swift +++ b/Sources/SwordGenerator/Exporter/Renderer/SwordRenderer.swift @@ -7,8 +7,9 @@ package struct SwordRenderer { bindingGraph: BindingGraph, imports: [Import] ) -> String { + let sortedImports = imports.sorted { $0.path < $1.path } let output = SourceFileSyntax { - for `import` in imports { + for `import` in sortedImports { ImportDeclSyntax( importKindSpecifier: `import`.kind.map { .identifier($0) }, path: ImportPathComponentListSyntax { @@ -24,14 +25,20 @@ package struct SwordRenderer { } private func render(_ bindingGraph: BindingGraph, for component: Component) -> CodeBlockItemListSyntax { - CodeBlockItemListSyntax { + let sortedBindings = bindingGraph.bindings(for: component).sorted { + $0.key.value < $1.key.value + } + let sortedSubcomponents = bindingGraph.subcomponents(for: component).sorted { + $0.name.value < $1.name.value + } + return CodeBlockItemListSyntax { ExtensionDeclSyntax( leadingTrivia: .newlines(2), extendedType: IdentifierTypeSyntax( name: .identifier(component.name.value) ) ) { - for binding in bindingGraph.bindings(for: component) { + for binding in sortedBindings { if case .registration( let parameters, let calledExpression, @@ -139,7 +146,7 @@ package struct SwordRenderer { } } - for (index, subcomponent) in bindingGraph.subcomponents(for: component).enumerated() { + for (index, subcomponent) in sortedSubcomponents.enumerated() { FunctionDeclSyntax( leadingTrivia: index == 0 ? [ @@ -195,7 +202,7 @@ package struct SwordRenderer { } } } - for subcomponent in bindingGraph.subcomponents(for: component) { + for subcomponent in sortedSubcomponents { render(bindingGraph, for: subcomponent) } } diff --git a/Sources/SwordGenerator/Factory/BindingGraphFactory.swift b/Sources/SwordGenerator/Factory/BindingGraphFactory.swift new file mode 100644 index 0000000..a742135 --- /dev/null +++ b/Sources/SwordGenerator/Factory/BindingGraphFactory.swift @@ -0,0 +1,355 @@ +import Foundation +import SwiftGraph +import SwiftSyntax +import SwordFoundation + +struct BindingGraphFactory: Factory { + private struct ResolvedBinding { + enum DependencyRequestResult { + case missing(dependencyRequest: DependencyRequest) + case resolved(index: Int) + } + + let bindingIndex: Int + let dependencyRequestResults: [DependencyRequestResult] + } + + let componentTree: ComponentTree + let dependenciesByComponentName: [ComponentName: [Dependency]] + let modulesByComponentName: [ComponentName: [Module]] + + init( + componentTree: ComponentTree, + dependencies: [Dependency], + modules: [Module] + ) { + self.componentTree = componentTree + var dependenciesByComponentName = [ComponentName: [Dependency]]() + for dependency in dependencies { + dependenciesByComponentName[ + dependency.componentName, + default: [] + ].append(dependency) + } + self.dependenciesByComponentName = dependenciesByComponentName + var modulesByComponentName = [ComponentName: [Module]]() + for module in modules { + modulesByComponentName[ + module.componentName, + default: [] + ].append(module) + } + self.modulesByComponentName = modulesByComponentName + } + + func make() async -> FactoryResult { + let network = UnweightedGraph() + await visit(component: componentTree.rootComponent, network: network) + + let bindingGraph = BindingGraph( + rootComponent: componentTree.rootComponent, + network: network + ) + let reports = await validate(bindingGraph) + return if reports.isEmpty { + .success(bindingGraph) + } else { + .failure(reports) + } + } + + private func visit( + component: Component, + network: UnweightedGraph, + parentBindingsByKey: [Key: [Binding]]? = nil + ) async { + var bindingsByKey = [Key: [Binding]]() + for componentArgument in component.arguments { + bindingsByKey[componentArgument.key, default: []].append(Binding(componentArgument: componentArgument)) + } + let dependencies = dependenciesByComponentName[component.name] ?? [] + for dependency in dependencies { + bindingsByKey[dependency.key, default: []].append(Binding(dependency: dependency)) + } + let providers = (modulesByComponentName[component.name] ?? []).flatMap(\.providers) + for provider in providers { + bindingsByKey[provider.key, default: []].append(Binding(provider: provider)) + } + + let bindings = bindingsByKey.values.flatMap { $0 } + let componentIndex = network.addVertex(.component(component)) + for binding in bindings { + let bindingNode: BindingGraph.Node = .binding(binding) + let bindingIndex = network.addVertex(bindingNode) + network.addEdge(fromIndex: componentIndex, toIndex: bindingIndex, directed: true) + } + + let reachableBindingsByKey = bindingsByKey.merging(parentBindingsByKey ?? [Key: [Binding]](), uniquingKeysWith: +) + let resolvedBindings = await withTaskGroup(of: ResolvedBinding?.self) { group in + for binding in bindings { + group.addTask { + resolveBinding( + binding, + reachableBindingsByKey: reachableBindingsByKey, + network: network + ) + } + } + var results = [ResolvedBinding]() + for await result in group { + if let result { + results.append(result) + } + } + return results + } + for resolvedBinding in resolvedBindings { + for dependencyRequestResult in resolvedBinding.dependencyRequestResults { + switch dependencyRequestResult { + case .missing(let dependencyRequest): + let missingBindingIndex = network.addVertex(.missingBinding(dependencyRequest)) + network.addEdgeIfNotExist( + fromIndex: resolvedBinding.bindingIndex, + toIndex: missingBindingIndex, + directed: true + ) + case .resolved(let index): + network.addEdgeIfNotExist(fromIndex: resolvedBinding.bindingIndex, toIndex: index, directed: true) + } + } + } + for subcomponent in (componentTree.subcomponentsByParent[component.name] ?? []) { + await visit( + component: subcomponent, + network: network, + parentBindingsByKey: reachableBindingsByKey + ) + if let subcomponentIndex = network.indexOfVertex(.component(subcomponent)) { + network.addEdgeIfNotExist(fromIndex: componentIndex, toIndex: subcomponentIndex, directed: true) + } + } + } + + private func resolveBinding( + _ binding: Binding, + reachableBindingsByKey: [Key: [Binding]], + network: UnweightedGraph + ) -> ResolvedBinding? { + guard let bindingIndex = network.indexOfVertex(.binding(binding)) else { return nil } + + let dependencyRequestResults = binding.dependencyRequests.flatMap { + dependencyRequest -> [ResolvedBinding.DependencyRequestResult] in + let resolvedBindings = reachableBindingsByKey[dependencyRequest.key] ?? [] + if resolvedBindings.isEmpty { + return [.missing(dependencyRequest: dependencyRequest)] + } else { + return resolvedBindings.compactMap { resolvedBinding in + let resolvedBindingNode = BindingGraph.Node.binding(resolvedBinding) + guard let index = network.indexOfVertex(resolvedBindingNode) else { return nil } + + return .resolved(index: index) + } + } + } + return ResolvedBinding( + bindingIndex: bindingIndex, + dependencyRequestResults: dependencyRequestResults + ) + } + + private func validate(_ bindingGraph: BindingGraph) async -> [Report] { + let (bindingsByKey, missingDependencyRequests) = bindingGraph.nodes.reduce( + into: ([Key: [Binding]](), [DependencyRequest]()) + ) { result, node in + switch node { + case .component: + break + case .binding(let binding): + result.0[binding.key, default: []].append(binding) + case .missingBinding(let dependencyRequest): + result.1.append(dependencyRequest) + } + } + let bindingsList = bindingsByKey.values + let bindings = bindingsList.flatMap { $0 } + + return await withTaskGroup(of: [Report].self) { group in + group.addTask { + var reports = [Report]() + for bindings in bindingsList { + if bindings.count > 1 { + for binding in bindings { + reports.append( + Report( + message: "\(binding.type.value) is duplicate", + severity: .error, + location: binding.location + ) + ) + } + } + } + return reports + } + group.addTask { + var reports = [Report]() + + let requiredBindingsByBinding = await withTaskGroup(of: [Binding: [Binding]].self) { group in + for binding in bindings { + group.addTask { + let requiredBindings = bindingGraph.requiredBindings(for: binding) + return [binding: requiredBindings] + } + } + var results: [Binding: [Binding]] = [:] + for await result in group { + results.merge(result, uniquingKeysWith: +) + } + + return results + } + + for (binding, requiredBindings) in requiredBindingsByBinding { + for requiredBinding in requiredBindings { + switch requiredBinding.kind { + case .registration(let parameters, _, _, _): + if parameters.contains(where: \.isAssisted) { + reports.append( + Report( + message: + "Sword does not support the injection of dependencies with @Assisted parameters, \(requiredBinding.type.value)", + severity: .error, + location: binding.location + ) + ) + } + case .componentArgument: + break + } + } + } + + return reports + } + group.addTask { + var reports = [Report]() + for missingDependencyRequest in missingDependencyRequests { + reports.append( + Report( + message: "\(missingDependencyRequest.type.value) is missing", + severity: .error, + location: missingDependencyRequest.location + ) + ) + } + return reports + } + + if bindingGraph.isDAG { + group.addTask { + var reports = [Report]() + for binding in bindings { + switch binding.kind { + case .registration(_, _, _, let scope): + guard let scope else { break } + + switch scope { + case .single: + detectCaptiveDependency( + bindingGraph: bindingGraph, + binding: binding, + originalBinding: binding, + reports: &reports + ) + case .weakReference: + break + } + case .componentArgument: + break + } + } + return reports + } + } else { + group.addTask { + var reports = [Report]() + for cycle in bindingGraph.cycles { + for node in cycle { + switch node { + case .component(let component): + reports.append( + Report( + message: "A component cycle is found", + severity: .error, + location: component.location + ) + ) + case .binding(let binding): + reports.append( + Report( + message: "A dependency cycle is found", + severity: .error, + location: binding.location + ) + ) + case .missingBinding: + break + } + } + } + return reports + } + } + + var results = [Report]() + for await result in group { + results.append(contentsOf: result) + } + return results + } + } + + private func detectCaptiveDependency( + bindingGraph: BindingGraph, + binding: Binding, + originalBinding: Binding, + reports: inout [Report] + ) { + let requiredBindings = bindingGraph.requiredBindings(for: binding) + for requiredBinding in requiredBindings { + switch requiredBinding.kind { + case .registration(_, _, _, let scope): + if let scope { + switch scope { + case .single: break + case .weakReference: + reports.append( + Report( + message: "A captive dependency found, \(requiredBinding.type.value)", + severity: .error, + location: originalBinding.location + ) + ) + } + } + case .componentArgument: break + } + + detectCaptiveDependency( + bindingGraph: bindingGraph, + binding: requiredBinding, + originalBinding: originalBinding, + reports: &reports + ) + } + } +} + +private extension UnweightedGraph { + func addEdgeIfNotExist(fromIndex: Int, toIndex: Int, directed: Bool) { + let edge = UnweightedEdge(u: fromIndex, v: toIndex, directed: directed) + if !edgeExists(edge) { + addEdge(edge, directed: true) + } + } +} diff --git a/Sources/SwordGenerator/Factory/ComponentTreeFactory.swift b/Sources/SwordGenerator/Factory/ComponentTreeFactory.swift new file mode 100644 index 0000000..ae2780c --- /dev/null +++ b/Sources/SwordGenerator/Factory/ComponentTreeFactory.swift @@ -0,0 +1,48 @@ +struct ComponentTreeFactory: Factory { + let rootComponentDescriptors: [RootComponentDescriptor] + let subcomponentDescriptors: [SubcomponentDescriptor] + + func make() async -> FactoryResult { + guard let rootComponentDescriptor = rootComponentDescriptors.first else { + return .failure([ + Report( + message: "'@Component' must be declared", + severity: .error + ) + ]) + } + + if rootComponentDescriptors.count > 1 { + return .failure([ + Report( + message: "'@Component' must be just one", + severity: .error + ) + ]) + } + + var subcomponentsByParent = [ComponentName: [Component]]() + for subcomponentDescriptor in subcomponentDescriptors { + subcomponentsByParent[ComponentName(value: subcomponentDescriptor.parentName), default: []].append( + Component( + name: subcomponentDescriptor.name, + arguments: subcomponentDescriptor.arguments, + parentComponentName: ComponentName(value: subcomponentDescriptor.parentName), + location: subcomponentDescriptor.location + ) + ) + } + + return .success( + ComponentTree( + rootComponent: Component( + name: rootComponentDescriptor.name, + arguments: rootComponentDescriptor.arguments, + parentComponentName: nil, + location: rootComponentDescriptor.location + ), + subcomponentsByParent: subcomponentsByParent + ) + ) + } +} diff --git a/Sources/SwordGenerator/Factory/DependenciesFactory.swift b/Sources/SwordGenerator/Factory/DependenciesFactory.swift new file mode 100644 index 0000000..91f0f6e --- /dev/null +++ b/Sources/SwordGenerator/Factory/DependenciesFactory.swift @@ -0,0 +1,90 @@ +import Foundation + +struct DependenciesFactory: Factory { + let dependencyDescriptors: [DependencyDescriptor] + + func make() async -> FactoryResult<[Dependency]> { + let dependencyFactoryResults = await withTaskGroup(of: FactoryResult.self) { group in + for dependencyDescriptor in dependencyDescriptors { + group.addTask { + var reports = [Report]() + if !dependencyDescriptor.isReferenceType, dependencyDescriptor.scope != nil { + reports.append( + Report( + message: "Scoped '@Dependency' must be reference type", + severity: .error, + location: dependencyDescriptor.location + ) + ) + } + + if dependencyDescriptor.injectedInitializers.count > 1 { + reports.append( + Report( + message: "'@Dependency' must have just one '@Injected' initializer", + severity: .error, + location: dependencyDescriptor.location + ) + ) + } + + guard let injectedInitializer = dependencyDescriptor.injectedInitializers.first else { + reports.append( + Report( + message: "'@Dependency' requires an '@Injected' initializer", + severity: .error, + location: dependencyDescriptor.location + ) + ) + return .failure(reports) + } + + if injectedInitializer.parameters.contains(where: { $0.isAssisted }), dependencyDescriptor.scope != nil { + reports.append( + Report( + message: "'@Dependency' must not set scope when having '@Assisted' parameter", + severity: .error, + location: dependencyDescriptor.location + ) + ) + } + + return if reports.isEmpty { + .success( + Dependency( + type: dependencyDescriptor.type, + componentName: dependencyDescriptor.componentName, + interface: dependencyDescriptor.interface, + initializer: injectedInitializer, + hasMainActor: dependencyDescriptor.hasMainActor, + scope: dependencyDescriptor.scope, + location: dependencyDescriptor.location + ) + ) + } else { + .failure(reports) + } + } + } + var results = [FactoryResult]() + for await result in group { + results.append(result) + } + return results + } + + let (dependencies, reports) = dependencyFactoryResults.reduce( + into: ([Dependency](), [Report]()) + ) { result, dependencyFactoryResult in + switch dependencyFactoryResult { + case .success(let dependency): result.0.append(dependency) + case .failure(let reports): result.1.append(contentsOf: reports) + } + } + return if reports.isEmpty { + .success(dependencies) + } else { + .failure(reports) + } + } +} diff --git a/Sources/SwordGenerator/Factory/Factory.swift b/Sources/SwordGenerator/Factory/Factory.swift new file mode 100644 index 0000000..c8bf062 --- /dev/null +++ b/Sources/SwordGenerator/Factory/Factory.swift @@ -0,0 +1,16 @@ +protocol Factory { + associatedtype T + func make() async -> FactoryResult +} + +enum FactoryResult: Sendable { + case success(Success) + case failure([Report]) + + var reports: [Report] { + switch self { + case .success: [] + case .failure(let reports): reports + } + } +} diff --git a/Sources/SwordGenerator/Factory/ModulesFactory.swift b/Sources/SwordGenerator/Factory/ModulesFactory.swift new file mode 100644 index 0000000..20463f1 --- /dev/null +++ b/Sources/SwordGenerator/Factory/ModulesFactory.swift @@ -0,0 +1,91 @@ +struct ModulesFactory: Factory { + let moduleDescriptors: [ModuleDescriptor] + + func make() async -> FactoryResult<[Module]> { + var providerFactoryResultsByModuleDescriptor = [ModuleDescriptor: [FactoryResult]]() + for moduleDescriptor in moduleDescriptors { + let providerFactoryResults = await withTaskGroup(of: FactoryResult.self) { group in + for provider in moduleDescriptor.providers { + group.addTask { + var reports = [Report]() + if !provider.isStaticFunction { + reports.append( + Report( + message: "'@Provider' must be static function", + severity: .error, + location: provider.location + ) + ) + } + + if provider.parameters.contains(where: { $0.isAssisted }), provider.scope != nil { + reports.append( + Report( + message: "'@Provider' must not set scope when having '@Assisted' parameter", + severity: .error, + location: provider.location + ) + ) + } + + guard let returnType = provider.returnType else { + reports.append( + Report( + message: "'@Provider' must have return type", + severity: .error, + location: provider.location + ) + ) + return .failure(reports) + } + + return if reports.isEmpty { + .success( + Provider( + moduleName: moduleDescriptor.name, + name: provider.name, + type: returnType, + parameters: provider.parameters, + hasMainActor: provider.hasMainActor, + scope: provider.scope, + location: provider.location + ) + ) + } else { + .failure(reports) + } + } + } + var results = [FactoryResult]() + for await result in group { + results.append(result) + } + return results + } + providerFactoryResultsByModuleDescriptor[moduleDescriptor] = providerFactoryResults + } + let (providersByModuleDescriptor, reports) = providerFactoryResultsByModuleDescriptor.reduce( + into: ([ModuleDescriptor: [Provider]](), [Report]()) + ) { result, providerFactoryResults in + for providerFactoryResult in providerFactoryResults.value { + switch providerFactoryResult { + case .success(let provider): result.0[providerFactoryResults.key, default: []].append(provider) + case .failure(let reports): result.1.append(contentsOf: reports) + } + } + } + return if reports.isEmpty { + .success( + providersByModuleDescriptor.map { moduleDescriptor, providers in + Module( + name: moduleDescriptor.name, + componentName: moduleDescriptor.componentName, + providers: providers + ) + } + ) + } else { + .failure(reports) + } + } +} diff --git a/Sources/SwordGenerator/Logging/Logging.swift b/Sources/SwordGenerator/Logging/Logging.swift index 3fc3312..c52dd9c 100644 --- a/Sources/SwordGenerator/Logging/Logging.swift +++ b/Sources/SwordGenerator/Logging/Logging.swift @@ -11,4 +11,12 @@ enum Logging { signposter.endInterval(name, state) return result } + + static func recordInterval(name: StaticString, body: () async throws -> R) async rethrows -> R { + let signpostID = signposter.makeSignpostID() + let state = signposter.beginInterval(name, id: signpostID) + let result = try await body() + signposter.endInterval(name, state) + return result + } } diff --git a/Sources/SwordGenerator/Model/Binding.swift b/Sources/SwordGenerator/Model/Binding.swift index 3f7ead4..f66debc 100644 --- a/Sources/SwordGenerator/Model/Binding.swift +++ b/Sources/SwordGenerator/Model/Binding.swift @@ -3,8 +3,8 @@ import SwiftSyntax import SwordComponentArgument import SwordFoundation -struct Binding: Codable { - enum Kind: Codable { +struct Binding: Hashable, Codable { + enum Kind: Hashable, Codable { case registration( parameters: [Parameter], calledExpression: String, diff --git a/Sources/SwordGenerator/Model/BindingGraph.swift b/Sources/SwordGenerator/Model/BindingGraph.swift index 925b4d8..9a2da08 100644 --- a/Sources/SwordGenerator/Model/BindingGraph.swift +++ b/Sources/SwordGenerator/Model/BindingGraph.swift @@ -2,7 +2,7 @@ import SwiftGraph import SwiftSyntax import SwordFoundation -final class BindingGraph { +final class BindingGraph: Sendable { enum Node: Equatable, Codable { case component(Component) case binding(Binding) @@ -93,3 +93,5 @@ final class BindingGraph { } } } + +extension UnweightedGraph: @retroactive @unchecked Sendable {} diff --git a/Sources/SwordGenerator/Model/ComponentTree.swift b/Sources/SwordGenerator/Model/ComponentTree.swift new file mode 100644 index 0000000..0083d30 --- /dev/null +++ b/Sources/SwordGenerator/Model/ComponentTree.swift @@ -0,0 +1,4 @@ +struct ComponentTree { + let rootComponent: Component + let subcomponentsByParent: [ComponentName: [Component]] +} diff --git a/Sources/SwordGenerator/Model/Dependency.swift b/Sources/SwordGenerator/Model/Dependency.swift index be92503..eb71a71 100644 --- a/Sources/SwordGenerator/Model/Dependency.swift +++ b/Sources/SwordGenerator/Model/Dependency.swift @@ -4,6 +4,7 @@ import SwordFoundation struct Dependency { let key: Key let type: Type + let componentName: ComponentName let interface: Interface? let initializer: Initializer let hasMainActor: Bool @@ -12,6 +13,7 @@ struct Dependency { init( type: Type, + componentName: ComponentName, interface: Interface?, initializer: Initializer, hasMainActor: Bool, @@ -20,6 +22,7 @@ struct Dependency { ) { self.key = Key(type: interface?.asType() ?? type) self.type = type + self.componentName = componentName self.interface = interface self.initializer = initializer self.hasMainActor = hasMainActor diff --git a/Sources/SwordGenerator/Model/Module.swift b/Sources/SwordGenerator/Model/Module.swift index b39408a..a3e6bb9 100644 --- a/Sources/SwordGenerator/Model/Module.swift +++ b/Sources/SwordGenerator/Model/Module.swift @@ -2,5 +2,6 @@ import Foundation struct Module { let name: String + let componentName: ComponentName let providers: [Provider] } diff --git a/Sources/SwordGenerator/Model/Parameter.swift b/Sources/SwordGenerator/Model/Parameter.swift index 5909307..72f5ccc 100644 --- a/Sources/SwordGenerator/Model/Parameter.swift +++ b/Sources/SwordGenerator/Model/Parameter.swift @@ -2,7 +2,7 @@ import Foundation import SwiftSyntax import SwordFoundation -struct Parameter: Codable { +struct Parameter: Hashable, Codable { let key: Key let type: Type let name: String diff --git a/Sources/SwordGenerator/Parser/Factory/BindingGraphFactory.swift b/Sources/SwordGenerator/Parser/Factory/BindingGraphFactory.swift deleted file mode 100644 index 18c2ee9..0000000 --- a/Sources/SwordGenerator/Parser/Factory/BindingGraphFactory.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftGraph -import SwordFoundation - -struct BindingGraphFactory { - private let subcomponentsByParent: [ComponentName: [Component]] - private let dependenciesByComponentName: [ComponentName: [Dependency]] - private let modulesByComponentName: [ComponentName: [Module]] - - init( - subcomponentsByParent: [ComponentName: [Component]], - dependenciesByComponentName: [ComponentName: [Dependency]], - modulesByComponentName: [ComponentName: [Module]] - ) { - self.subcomponentsByParent = subcomponentsByParent - self.dependenciesByComponentName = dependenciesByComponentName - self.modulesByComponentName = modulesByComponentName - } - - func makeBindingGraph(rootComponent: Component) -> BindingGraph { - let network = UnweightedGraph() - visit(component: rootComponent, network: network) - return BindingGraph(rootComponent: rootComponent, network: network) - } - - private func visit( - component: Component, - network: UnweightedGraph, - parentBindingsByKey: [Key: [Binding]]? = nil - ) { - var bindingsByKey = [Key: [Binding]]() - - for componentArgument in component.arguments { - bindingsByKey[componentArgument.key, default: []].append(Binding(componentArgument: componentArgument)) - } - - let dependencies = dependenciesByComponentName[component.name] ?? [] - for dependency in dependencies { - bindingsByKey[dependency.key, default: []].append(Binding(dependency: dependency)) - } - - let providers = (modulesByComponentName[component.name] ?? []).flatMap(\.providers) - for provider in providers { - bindingsByKey[provider.key, default: []].append(Binding(provider: provider)) - } - - let bindings = bindingsByKey.values.flatMap { $0 } - let componentIndex = network.addVertex(.component(component)) - addBindings( - bindings, - componentIndex: componentIndex, - network: network - ) - - let reachableBindingsByKey = bindingsByKey.merging(parentBindingsByKey ?? [Key: [Binding]](), uniquingKeysWith: +) - resolveBindings( - bindings, - network: network, - reachableBindingsByKey: reachableBindingsByKey - ) - - for subcomponent in (subcomponentsByParent[component.name] ?? []) { - visit( - component: subcomponent, - network: network, - parentBindingsByKey: reachableBindingsByKey - ) - if let subcomponentIndex = network.indexOfVertex(.component(subcomponent)) { - network.addEdgeIfNotExist(fromIndex: componentIndex, toIndex: subcomponentIndex, directed: true) - } - } - } - - private func addBindings( - _ bindings: [Binding], - componentIndex: Int, - network: UnweightedGraph - ) { - for binding in bindings { - let bindingNode: BindingGraph.Node = .binding(binding) - let bindingIndex = network.addVertex(bindingNode) - network.addEdge(fromIndex: componentIndex, toIndex: bindingIndex, directed: true) - } - } - - private func resolveBindings( - _ bindings: [Binding], - network: UnweightedGraph, - reachableBindingsByKey: [Key: [Binding]] - ) { - for binding in bindings { - guard let bindingIndex = network.indexOfVertex(.binding(binding)) else { continue } - - for dependencyRequest in binding.dependencyRequests { - let resolvedBindings = reachableBindingsByKey[dependencyRequest.key] ?? [] - if resolvedBindings.isEmpty { - let missingBindingNode: BindingGraph.Node = .missingBinding(dependencyRequest) - let missingBindingIndex = network.addVertex(missingBindingNode) - network.addEdgeIfNotExist(fromIndex: bindingIndex, toIndex: missingBindingIndex, directed: true) - } else { - for resolvedBinding in resolvedBindings { - let resolvedBindingNode: BindingGraph.Node = .binding(resolvedBinding) - if let resolvedBindingIndex = network.indexOfVertex(resolvedBindingNode) { - network.addEdgeIfNotExist(fromIndex: bindingIndex, toIndex: resolvedBindingIndex, directed: true) - } - } - } - } - } - } -} - -private extension UnweightedGraph { - func addEdgeIfNotExist(fromIndex: Int, toIndex: Int, directed: Bool) { - let edge = UnweightedEdge(u: fromIndex, v: toIndex, directed: directed) - if !edgeExists(edge) { - addEdge(edge, directed: true) - } - } -} diff --git a/Sources/SwordGenerator/Parser/Registry/ComponentRegistry.swift b/Sources/SwordGenerator/Parser/Registry/ComponentRegistry.swift deleted file mode 100644 index 7ba7839..0000000 --- a/Sources/SwordGenerator/Parser/Registry/ComponentRegistry.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -final class ComponentRegistry { - private(set) var components = [RootComponentDescriptor]() - private(set) var subcomponentsByParent = [ComponentName: [SubcomponentDescriptor]]() - - func register(_ rootComponent: RootComponentDescriptor) { - components.append(rootComponent) - } - - func register(_ subcomponent: SubcomponentDescriptor, by parent: ComponentName) { - subcomponentsByParent[parent, default: []].append(subcomponent) - } -} diff --git a/Sources/SwordGenerator/Parser/Registry/DependencyRegistry.swift b/Sources/SwordGenerator/Parser/Registry/DependencyRegistry.swift deleted file mode 100644 index 2add160..0000000 --- a/Sources/SwordGenerator/Parser/Registry/DependencyRegistry.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -final class DependencyRegistry { - private(set) var descriptorsByComponentName = [ComponentName: [DependencyDescriptor]]() - - func descriptors(for componentName: ComponentName) -> [DependencyDescriptor] { - descriptorsByComponentName[componentName] ?? [] - } - - func register(_ descriptor: DependencyDescriptor, by componentName: ComponentName) { - descriptorsByComponentName[componentName, default: []].append(descriptor) - } -} diff --git a/Sources/SwordGenerator/Parser/Registry/ImportRegistry.swift b/Sources/SwordGenerator/Parser/Registry/ImportRegistry.swift deleted file mode 100644 index f625531..0000000 --- a/Sources/SwordGenerator/Parser/Registry/ImportRegistry.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import SwiftSyntax - -final class ImportRegistry { - private var _imports = Set() - var imports: [Import] { - _imports.sorted { $0.path < $1.path } - } - - func register(_ declaration: ImportDeclSyntax) { - _imports.insert( - Import( - path: "\(declaration.path)", - kind: declaration.importKindSpecifier?.text - ) - ) - } - - func register(_ target: String) { - _imports.insert(Import(path: target)) - } -} diff --git a/Sources/SwordGenerator/Parser/Registry/ModuleRegistry.swift b/Sources/SwordGenerator/Parser/Registry/ModuleRegistry.swift deleted file mode 100644 index 5c62431..0000000 --- a/Sources/SwordGenerator/Parser/Registry/ModuleRegistry.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -final class ModuleRegistry { - private(set) var descriptorsByComponentName = [ComponentName: [ModuleDescriptor]]() - - func modules(for componentName: ComponentName) -> [ModuleDescriptor] { - descriptorsByComponentName[componentName] ?? [] - } - - func register(_ descriptor: ModuleDescriptor, by componentName: ComponentName) { - descriptorsByComponentName[componentName, default: []].append(descriptor) - } -} diff --git a/Sources/SwordGenerator/Parser/SwordParser.swift b/Sources/SwordGenerator/Parser/SwordParser.swift index e94e839..dff4cdb 100644 --- a/Sources/SwordGenerator/Parser/SwordParser.swift +++ b/Sources/SwordGenerator/Parser/SwordParser.swift @@ -4,104 +4,47 @@ import SwiftSyntax import SwordFoundation package struct SwordParser { - private let reporter: SwordReporter - - package init(reporter: SwordReporter) { - self.reporter = reporter + struct Result { + var rootComponentDescriptors = [RootComponentDescriptor]() + var subcomponentDescriptors = [SubcomponentDescriptor]() + var dependencyDescriptors = [DependencyDescriptor]() + var moduleDescriptors = [ModuleDescriptor]() + var imports = [Import]() + + mutating func merge(_ result: Result) { + rootComponentDescriptors += result.rootComponentDescriptors + subcomponentDescriptors += result.subcomponentDescriptors + dependencyDescriptors += result.dependencyDescriptors + moduleDescriptors += result.moduleDescriptors + imports += result.imports + } } - func parse( - sourceFiles: [SourceFile], - targets: [String] - ) throws -> (BindingGraph, [Import]) { - let componentRegistry = ComponentRegistry() - let dependencyRegistry = DependencyRegistry() - let moduleRegistry = ModuleRegistry() - let importRegistry = ImportRegistry() + package init() { + } - Logging.recordInterval(name: "parseSourceFiles") { + func parse(_ sourceFiles: [SourceFile]) async -> Result { + await withTaskGroup(of: Result.self) { group in for sourceFile in sourceFiles { - let visitors: [SyntaxVisitor] = [ - ComponentVisitor( - componentRegistry: componentRegistry, - sourceFile: sourceFile - ), - SubcomponentVisitor( - componentRegistry: componentRegistry, - sourceFile: sourceFile - ), - DependencyVisitor( - dependencyRegistry: dependencyRegistry, - sourceFile: sourceFile - ), - ModuleVisitor( - moduleRegistry: moduleRegistry, - sourceFile: sourceFile - ), - ImportVisitor( - importRegistry: importRegistry, - sourceFile: sourceFile - ), - ] - for visitor in visitors { - visitor.walk(sourceFile.tree) + group.addTask { + parse(sourceFile) } } - - for target in targets { - importRegistry.register(target) - } - } - - let componentValidationResult = Logging.recordInterval(name: "makeComponentTree") { - ComponentValidator(componentRegistry: componentRegistry) - .validate() - } - let dependencyValidationResult = Logging.recordInterval(name: "makeDependencies") { - DependencyValidator(dependencyRegistry: dependencyRegistry) - .validate() - } - let moduleValidationResult = Logging.recordInterval(name: "makeModules") { - ModuleValidator(moduleRegistry: moduleRegistry).validate() - } - guard - case .valid((let component, let subcomponentsByParent)) = componentValidationResult, - case .valid(let dependenciesByComponentName) = dependencyValidationResult, - case .valid(let modulesByComponentName) = moduleValidationResult - else { - let reports = [ - componentValidationResult.reports, - dependencyValidationResult.reports, - moduleValidationResult.reports, - ].flatMap { $0 } - for report in reports { - reporter.send(report) - } - exit(1) - } - - let bindingGraph = Logging.recordInterval(name: "makeBindingGraph") { - let bindingGraphFactory = BindingGraphFactory( - subcomponentsByParent: subcomponentsByParent, - dependenciesByComponentName: dependenciesByComponentName, - modulesByComponentName: modulesByComponentName - ) - let bindingGraph = bindingGraphFactory.makeBindingGraph(rootComponent: component) - let bindingGraphValidationResult = BindingGraphValidator(bindingGraph: bindingGraph) - .validate() - guard case .valid = bindingGraphValidationResult else { - for report in bindingGraphValidationResult.reports { - reporter.send(report) - } - exit(1) + var parserResult = Result() + for await result in group { + parserResult.merge(result) } - - return bindingGraph + return parserResult } + } - return ( - bindingGraph, - Array(importRegistry.imports) + private func parse(_ sourceFile: SourceFile) -> Result { + Result( + rootComponentDescriptors: ComponentVisitor(sourceFile: sourceFile).walk(), + subcomponentDescriptors: SubcomponentVisitor(sourceFile: sourceFile).walk(), + dependencyDescriptors: DependencyVisitor(sourceFile: sourceFile).walk(), + moduleDescriptors: ModuleVisitor(sourceFile: sourceFile).walk(), + imports: ImportVisitor(sourceFile: sourceFile).walk() ) } } diff --git a/Sources/SwordGenerator/Parser/Validator/BindingGraphValidator.swift b/Sources/SwordGenerator/Parser/Validator/BindingGraphValidator.swift deleted file mode 100644 index 9e35886..0000000 --- a/Sources/SwordGenerator/Parser/Validator/BindingGraphValidator.swift +++ /dev/null @@ -1,155 +0,0 @@ -import SwiftGraph -import SwiftSyntax -import SwordFoundation - -struct BindingGraphValidator { - private let bindingGraph: BindingGraph - - init(bindingGraph: BindingGraph) { - self.bindingGraph = bindingGraph - } - - func validate() -> ValidationResult { - var bindingsByKey = [Key: [Binding]]() - var missingDependencyRequests = [DependencyRequest]() - for node in bindingGraph.nodes { - switch node { - case .component: - break - case .binding(let binding): - bindingsByKey[binding.key, default: []].append(binding) - case .missingBinding(let dependencyRequest): - missingDependencyRequests.append(dependencyRequest) - } - } - - var reports = [Report]() - for bindings in bindingsByKey.values { - if bindings.count > 1 { - for binding in bindings { - reports.append( - Report( - message: "\(binding.type.value) is duplicate", - severity: .error, - location: binding.location - ) - ) - } - } - } - for binding in bindingsByKey.values.flatMap({ $0 }) { - let requiredBindings = bindingGraph.requiredBindings(for: binding) - for requiredBinding in requiredBindings { - switch requiredBinding.kind { - case .registration(let parameters, _, _, _): - if parameters.contains(where: \.isAssisted) { - reports.append( - Report( - message: - "Sword does not support the injection of dependencies with @Assisted parameters, \(requiredBinding.type.value)", - severity: .error, - location: binding.location - ) - ) - } - case .componentArgument: - break - } - } - } - for missingDependencyRequest in missingDependencyRequests { - reports.append( - Report( - message: "\(missingDependencyRequest.type.value) is missing", - severity: .error, - location: missingDependencyRequest.location - ) - ) - } - if bindingGraph.isDAG { - for binding in bindingsByKey.values.flatMap({ $0 }) { - switch binding.kind { - case .registration(_, _, _, let scope): - guard let scope else { break } - - switch scope { - case .single: - detectCaptiveDependency( - binding: binding, - originalBinding: binding, - reports: &reports - ) - case .weakReference: - break - } - case .componentArgument: - break - } - } - } else { - for cycle in bindingGraph.cycles { - for node in cycle { - switch node { - case .component(let component): - reports.append( - Report( - message: "A component cycle is found", - severity: .error, - location: component.location - ) - ) - case .binding(let binding): - reports.append( - Report( - message: "A dependency cycle is found", - severity: .error, - location: binding.location - ) - ) - case .missingBinding: - break - } - } - } - } - - if reports.isEmpty { - return .valid(()) - } else { - return .invalid(reports) - } - } - - private func detectCaptiveDependency( - binding: Binding, - originalBinding: Binding, - reports: inout [Report] - ) { - let requiredBindings = bindingGraph.requiredBindings(for: binding) - for requiredBinding in requiredBindings { - switch requiredBinding.kind { - case .registration(_, _, _, let scope): - if let scope { - switch scope { - case .single: break - case .weakReference: - reports.append( - Report( - message: "A captive dependency found, \(requiredBinding.type.value)", - severity: .error, - location: originalBinding.location - ) - ) - } - } - case .componentArgument: break - } - - detectCaptiveDependency( - binding: requiredBinding, - originalBinding: originalBinding, - reports: &reports - ) - } - } -} diff --git a/Sources/SwordGenerator/Parser/Validator/ComponentValidator.swift b/Sources/SwordGenerator/Parser/Validator/ComponentValidator.swift deleted file mode 100644 index a12edd1..0000000 --- a/Sources/SwordGenerator/Parser/Validator/ComponentValidator.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Foundation - -struct ComponentValidator { - private let componentRegistry: ComponentRegistry - - init(componentRegistry: ComponentRegistry) { - self.componentRegistry = componentRegistry - } - - func validate() -> ValidationResult<(Component, [ComponentName: [Component]])> { - guard let component = componentRegistry.components.first else { - return .invalid([ - Report( - message: "'@Component' must be declared", - severity: .error - ) - ]) - } - - if componentRegistry.components.count > 1 { - return .invalid([ - Report( - message: "'@Component' must be just one", - severity: .error - ) - ]) - } - - var subcomponentsByParent = [ComponentName: [Component]]() - - for (parentName, subcomponents) in componentRegistry.subcomponentsByParent { - subcomponentsByParent[parentName, default: []].append( - contentsOf: subcomponents.map { subcomponent in - Component( - name: subcomponent.name, - arguments: subcomponent.arguments, - parentComponentName: parentName, - location: subcomponent.location - ) - } - ) - } - - return .valid( - ( - Component( - name: component.name, - arguments: component.arguments, - parentComponentName: nil, - location: component.location - ), - subcomponentsByParent - ) - ) - } -} diff --git a/Sources/SwordGenerator/Parser/Validator/DependencyValidator.swift b/Sources/SwordGenerator/Parser/Validator/DependencyValidator.swift deleted file mode 100644 index 0af0ad5..0000000 --- a/Sources/SwordGenerator/Parser/Validator/DependencyValidator.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation - -struct DependencyValidator { - let dependencyRegistry: DependencyRegistry - - func validate() -> ValidationResult<[ComponentName: [Dependency]]> { - var reports = [Report]() - var dependenciesByComponentName = [ComponentName: [Dependency]]() - for (componentName, descriptors) in dependencyRegistry.descriptorsByComponentName { - for descriptor in descriptors { - if !descriptor.isReferenceType, descriptor.scope != nil { - reports.append( - Report( - message: "Scoped '@Dependency' must be reference type", - severity: .error, - location: descriptor.location - ) - ) - continue - } - - guard let injectedInitializer = descriptor.injectedInitializers.first else { - reports.append( - Report( - message: "'@Dependency' requires an '@Injected' initializer", - severity: .error, - location: descriptor.location - ) - ) - continue - } - - if descriptor.injectedInitializers.count > 1 { - reports.append( - Report( - message: "'@Dependency' must have just one '@Injected' initializer", - severity: .error, - location: descriptor.location - ) - ) - continue - } - - if injectedInitializer.parameters.contains(where: { $0.isAssisted }), descriptor.scope != nil { - reports.append( - Report( - message: "'@Dependency' must not set scope when having '@Assisted' parameter", - severity: .error, - location: descriptor.location - ) - ) - continue - } - - dependenciesByComponentName[componentName, default: []].append( - Dependency( - type: descriptor.type, - interface: descriptor.interface, - initializer: injectedInitializer, - hasMainActor: descriptor.hasMainActor, - scope: descriptor.scope, - location: descriptor.location - ) - ) - } - } - if reports.isEmpty { - return .valid(dependenciesByComponentName) - } else { - return .invalid(reports) - } - } -} diff --git a/Sources/SwordGenerator/Parser/Validator/ModuleValidator.swift b/Sources/SwordGenerator/Parser/Validator/ModuleValidator.swift deleted file mode 100644 index 36d2002..0000000 --- a/Sources/SwordGenerator/Parser/Validator/ModuleValidator.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation - -struct ModuleValidator { - private let moduleRegistry: ModuleRegistry - - init(moduleRegistry: ModuleRegistry) { - self.moduleRegistry = moduleRegistry - } - - func validate() -> ValidationResult<[ComponentName: [Module]]> { - var reports = [Report]() - var modulesByComponentName = [ComponentName: [Module]]() - - for (componentName, descriptors) in moduleRegistry.descriptorsByComponentName { - for descriptor in descriptors { - var providers = [Provider]() - - for provider in descriptor.providers { - if !provider.isStaticFunction { - reports.append( - Report( - message: "'@Provider' must be static function", - severity: .error, - location: provider.location - ) - ) - } - - if provider.parameters.contains(where: { $0.isAssisted }), provider.scope != nil { - reports.append( - Report( - message: "'@Provider' must not set scope when having '@Assisted' parameter", - severity: .error, - location: provider.location - ) - ) - } - - guard let returnType = provider.returnType else { - reports.append( - Report( - message: "'@Provider' must have return type", - severity: .error, - location: provider.location - ) - ) - continue - } - - providers.append( - Provider( - moduleName: descriptor.name, - name: provider.name, - type: returnType, - parameters: provider.parameters, - hasMainActor: provider.hasMainActor, - scope: provider.scope, - location: provider.location - ) - ) - } - - modulesByComponentName[componentName, default: []].append( - Module( - name: descriptor.name, - providers: providers - ) - ) - } - } - - if reports.isEmpty { - return .valid(modulesByComponentName) - } else { - return .invalid(reports) - } - } -} diff --git a/Sources/SwordGenerator/Parser/Validator/ValidationResult.swift b/Sources/SwordGenerator/Parser/Validator/ValidationResult.swift deleted file mode 100644 index 8d127f6..0000000 --- a/Sources/SwordGenerator/Parser/Validator/ValidationResult.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -enum ValidationResult { - case valid(T) - case invalid([Report]) - - var reports: [Report] { - switch self { - case .valid: - [] - case .invalid(let reports): - reports - } - } -} diff --git a/Sources/SwordGenerator/Parser/Visitor/ComponentVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/ComponentVisitor.swift index 50ab050..b60cd06 100644 --- a/Sources/SwordGenerator/Parser/Visitor/ComponentVisitor.swift +++ b/Sources/SwordGenerator/Parser/Visitor/ComponentVisitor.swift @@ -2,30 +2,15 @@ import SwiftSyntax import SwiftSyntaxSupport import SwordComponentArgument -final class ComponentVisitor: SyntaxVisitor { +final class ComponentVisitor: SourceFileVisitor { private struct ComponentAttribute { let arguments: [ComponentArgument] } - private let componentRegistry: ComponentRegistry - private let locationConverter: SourceLocationConverter - - init( - componentRegistry: ComponentRegistry, - sourceFile: SourceFile - ) { - self.componentRegistry = componentRegistry - self.locationConverter = SourceLocationConverter( - fileName: sourceFile.path, - tree: sourceFile.tree - ) - super.init(viewMode: .sourceAccurate) - } - override func visitPost(_ node: ClassDeclSyntax) { guard let componentAttribute = extractComponentAttribute(from: node.attributes) else { return } - componentRegistry.register( + results.append( RootComponentDescriptor( name: ComponentName(value: node.name.text), arguments: componentAttribute.arguments, diff --git a/Sources/SwordGenerator/Parser/Visitor/DependencyVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/DependencyVisitor.swift index b4de6b8..776494a 100644 --- a/Sources/SwordGenerator/Parser/Visitor/DependencyVisitor.swift +++ b/Sources/SwordGenerator/Parser/Visitor/DependencyVisitor.swift @@ -1,28 +1,13 @@ import SwiftSyntax import SwordFoundation -final class DependencyVisitor: SyntaxVisitor { +final class DependencyVisitor: SourceFileVisitor { private struct DependencyAttribute { let component: String let interface: String? let scope: Scope? } - private let dependencyRegistry: DependencyRegistry - private let locationConverter: SourceLocationConverter - - init( - dependencyRegistry: DependencyRegistry, - sourceFile: SourceFile - ) { - self.dependencyRegistry = dependencyRegistry - self.locationConverter = SourceLocationConverter( - fileName: sourceFile.path, - tree: sourceFile.tree - ) - super.init(viewMode: .sourceAccurate) - } - override func visitPost(_ node: StructDeclSyntax) { registerDependencyIfNeeded( name: node.name, @@ -83,6 +68,7 @@ final class DependencyVisitor: SyntaxVisitor { } let hasMainActor = attributes.first(named: "MainActor") != nil let dependencyDescriptor = DependencyDescriptor( + componentName: ComponentName(value: dependencyAttribute.component), type: Type(value: name.text), interface: dependencyAttribute.interface.map(Interface.init), injectedInitializers: injectedInitializers, @@ -91,10 +77,7 @@ final class DependencyVisitor: SyntaxVisitor { isReferenceType: isReferenceType, location: location ) - dependencyRegistry.register( - dependencyDescriptor, - by: ComponentName(value: dependencyAttribute.component) - ) + results.append(dependencyDescriptor) } private func extractDependencyAttribute(from attributes: AttributeListSyntax) diff --git a/Sources/SwordGenerator/Parser/Visitor/ImportVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/ImportVisitor.swift index 2336ea9..bcf14b9 100644 --- a/Sources/SwordGenerator/Parser/Visitor/ImportVisitor.swift +++ b/Sources/SwordGenerator/Parser/Visitor/ImportVisitor.swift @@ -1,22 +1,12 @@ import SwiftSyntax -final class ImportVisitor: SyntaxVisitor { - private let importRegistry: ImportRegistry - private let locationConverter: SourceLocationConverter - - init( - importRegistry: ImportRegistry, - sourceFile: SourceFile - ) { - self.importRegistry = importRegistry - self.locationConverter = SourceLocationConverter( - fileName: sourceFile.path, - tree: sourceFile.tree - ) - super.init(viewMode: .sourceAccurate) - } - +final class ImportVisitor: SourceFileVisitor { override func visitPost(_ node: ImportDeclSyntax) { - importRegistry.register(node.trimmed) + results.append( + Import( + path: "\(node.trimmed.path)", + kind: node.trimmed.importKindSpecifier?.text + ) + ) } } diff --git a/Sources/SwordGenerator/Parser/Visitor/ModuleVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/ModuleVisitor.swift index 2bd00fa..ef9fe12 100644 --- a/Sources/SwordGenerator/Parser/Visitor/ModuleVisitor.swift +++ b/Sources/SwordGenerator/Parser/Visitor/ModuleVisitor.swift @@ -1,7 +1,7 @@ import SwiftSyntax import SwordFoundation -final class ModuleVisitor: SyntaxVisitor { +final class ModuleVisitor: SourceFileVisitor { private struct ModuleAttribute { let component: String } @@ -10,21 +10,6 @@ final class ModuleVisitor: SyntaxVisitor { let scope: Scope? } - private let moduleRegistry: ModuleRegistry - private let locationConverter: SourceLocationConverter - - init( - moduleRegistry: ModuleRegistry, - sourceFile: SourceFile - ) { - self.moduleRegistry = moduleRegistry - self.locationConverter = SourceLocationConverter( - fileName: sourceFile.path, - tree: sourceFile.tree - ) - super.init(viewMode: .sourceAccurate) - } - override func visitPost(_ node: StructDeclSyntax) { guard let moduleAttribute = extractModuleAttribute(from: node.attributes) else { return } @@ -59,12 +44,12 @@ final class ModuleVisitor: SyntaxVisitor { ) } - moduleRegistry.register( + results.append( ModuleDescriptor( name: node.name.text, + componentName: ComponentName(value: moduleAttribute.component), providers: providers - ), - by: ComponentName(value: moduleAttribute.component) + ) ) } diff --git a/Sources/SwordGenerator/Parser/Visitor/SourceFileVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/SourceFileVisitor.swift new file mode 100644 index 0000000..761444c --- /dev/null +++ b/Sources/SwordGenerator/Parser/Visitor/SourceFileVisitor.swift @@ -0,0 +1,22 @@ +import SwiftSyntax + +class SourceFileVisitor: SyntaxVisitor { + let locationConverter: SourceLocationConverter + var results = [T]() + + private let sourceFile: SourceFile + + init(sourceFile: SourceFile) { + self.sourceFile = sourceFile + self.locationConverter = SourceLocationConverter( + fileName: sourceFile.path, + tree: sourceFile.tree + ) + super.init(viewMode: .sourceAccurate) + } + + func walk() -> [T] { + walk(sourceFile.tree) + return results + } +} diff --git a/Sources/SwordGenerator/Parser/Visitor/SubcomponentVisitor.swift b/Sources/SwordGenerator/Parser/Visitor/SubcomponentVisitor.swift index 99fa28e..0c7b8f3 100644 --- a/Sources/SwordGenerator/Parser/Visitor/SubcomponentVisitor.swift +++ b/Sources/SwordGenerator/Parser/Visitor/SubcomponentVisitor.swift @@ -2,37 +2,21 @@ import SwiftSyntax import SwiftSyntaxSupport import SwordComponentArgument -final class SubcomponentVisitor: SyntaxVisitor { +final class SubcomponentVisitor: SourceFileVisitor { private struct SubcomponentAttribute { let parent: String let arguments: [ComponentArgument] } - private let componentRegistry: ComponentRegistry - private let locationConverter: SourceLocationConverter - - init( - componentRegistry: ComponentRegistry, - sourceFile: SourceFile - ) { - self.componentRegistry = componentRegistry - self.locationConverter = SourceLocationConverter( - fileName: sourceFile.path, - tree: sourceFile.tree - ) - super.init(viewMode: .sourceAccurate) - } - override func visitPost(_ node: ClassDeclSyntax) { if let subcomponentAttribute = extractSubcomponentAttribute(from: node.attributes) { - componentRegistry.register( + results.append( SubcomponentDescriptor( name: ComponentName(value: node.name.text), arguments: subcomponentAttribute.arguments, parentName: subcomponentAttribute.parent, location: node.startLocation(converter: locationConverter) - ), - by: ComponentName(value: subcomponentAttribute.parent) + ) ) } } diff --git a/Sources/SwordGenerator/SwordGenerator.swift b/Sources/SwordGenerator/SwordGenerator.swift index 9b010be..a7dbcb7 100644 --- a/Sources/SwordGenerator/SwordGenerator.swift +++ b/Sources/SwordGenerator/SwordGenerator.swift @@ -5,10 +5,16 @@ import SwiftSyntax package struct SwordGenerator { private let parser: SwordParser + private let reporter: SwordReporter private let exporter: SwordExporter - package init(parser: SwordParser, exporter: SwordExporter) { + package init( + parser: SwordParser, + reporter: SwordReporter, + exporter: SwordExporter + ) { self.parser = parser + self.reporter = reporter self.exporter = exporter } @@ -16,12 +22,60 @@ package struct SwordGenerator { sourceFiles: [SourceFile], targets: [String], output: String - ) throws { - let (bindingGraph, imports) = try parser.parse(sourceFiles: sourceFiles, targets: targets) + ) async throws { + let parserResult = await Logging.recordInterval(name: "parseSourceFiles") { + await parser.parse(sourceFiles) + } + + async let componentTreeFactoryResult = Logging.recordInterval(name: "makeComponentTree") { + await ComponentTreeFactory( + rootComponentDescriptors: parserResult.rootComponentDescriptors, + subcomponentDescriptors: parserResult.subcomponentDescriptors + ).make() + } + async let dependenciesFactoryResult = Logging.recordInterval(name: "makeDependencies") { + await DependenciesFactory(dependencyDescriptors: parserResult.dependencyDescriptors).make() + } + async let moduleFactoryResult = Logging.recordInterval(name: "makeModules") { + await ModulesFactory(moduleDescriptors: parserResult.moduleDescriptors).make() + } + guard + case .success(let componentTree) = await componentTreeFactoryResult, + case .success(let dependencies) = await dependenciesFactoryResult, + case .success(let modules) = await moduleFactoryResult + else { + let reports = + await componentTreeFactoryResult.reports + dependenciesFactoryResult.reports + moduleFactoryResult.reports + for report in reports { + reporter.send(report) + } + exit(1) + } + + let bindingGraphFactoryResult = await Logging.recordInterval(name: "makeBindingGraph") { + await BindingGraphFactory( + componentTree: componentTree, + dependencies: dependencies, + modules: modules + ).make() + } + guard case .success(let bindingGraph) = bindingGraphFactoryResult else { + for report in bindingGraphFactoryResult.reports { + reporter.send(report) + } + exit(1) + } + + var imports = Set() + let targetImports = targets.map { Import(path: $0) } + for `import` in (parserResult.imports + targetImports) { + imports.insert(`import`) + } + try Logging.recordInterval(name: "exportBindingGraph") { try exporter.export( bindingGraph: bindingGraph, - imports: imports, + imports: Array(imports), outputPath: URL(filePath: output) ) }