diff --git a/Documentation/Manual.md b/Documentation/Manual.md index 6739c279..46c59690 100644 --- a/Documentation/Manual.md +++ b/Documentation/Manual.md @@ -521,6 +521,10 @@ The executable heavily utilizes asynchronous processing to avoid `SafeDITool` be Due to limitations in Apple’s [Swift Package Manager Plugins](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageManagerDocs/Documentation.docc/Plugins.md), the `SafeDIGenerator` plugin parses all of your first-party Swift files in a single pass. Projects that utilize `SafeDITool` directly can process Swift files on a per-module basis to further reduce the build-time bottleneck. +### Custom build system integration + +If you are integrating SafeDI with a build system other than SPM (e.g. Bazel, Buck, or a prebuild script), you can invoke `SafeDITool` directly with a JSON manifest file that describes the desired outputs. The manifest uses the [`SafeDIToolManifest`](../Sources/SafeDICore/Models/SafeDIToolManifest.swift) format, mapping input Swift files containing `@Instantiable(isRoot: true)` to output file paths. Paths are relative to the working directory. See the [example prebuild script](../Examples/PrebuildScript/safeditool.sh) for a working example. + ## Introspecting a SafeDI tree You can create a [GraphViz DOT file](https://graphviz.org/doc/info/lang.html) to introspect a SafeDI dependency tree by running `swift run SafeDITool` and utilizing the `--dot-file-output` parameter. This command will create a `DOT` file that you can pipe into `GraphViz`’s `dot` command to create a pdf. diff --git a/Examples/PrebuildScript/safeditool.sh b/Examples/PrebuildScript/safeditool.sh index 6c2dbdb3..286b465d 100755 --- a/Examples/PrebuildScript/safeditool.sh +++ b/Examples/PrebuildScript/safeditool.sh @@ -28,6 +28,20 @@ fi # Run the tool. SOURCE_DIR="$PROJECT_DIR/ExampleCocoaPodsIntegration" -SAFEDI_OUTPUT="$PROJECT_DIR/SafeDIOutput/SafeDI.swift" -mkdir -p $PROJECT_DIR/SafeDIOutput -$SAFEDI_LOCATION --include "$SOURCE_DIR" --dependency-tree-output "$SAFEDI_OUTPUT" +SAFEDI_OUTPUT_DIR="$PROJECT_DIR/SafeDIOutput" +mkdir -p "$SAFEDI_OUTPUT_DIR" + +# Create the manifest JSON mapping input files to output files. +# See SafeDIToolManifest in SafeDICore for the expected format. +cat > "$SAFEDI_OUTPUT_DIR/SafeDIManifest.json" << MANIFEST +{ + "dependencyTreeGeneration": [ + { + "inputFilePath": "$SOURCE_DIR/Views/ExampleApp.swift", + "outputFilePath": "$SAFEDI_OUTPUT_DIR/ExampleApp+SafeDI.swift" + } + ] +} +MANIFEST + +$SAFEDI_LOCATION --include "$SOURCE_DIR" --swift-manifest "$SAFEDI_OUTPUT_DIR/SafeDIManifest.json" diff --git a/Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift b/Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift index 1ff2d3c3..cb208dab 100644 --- a/Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift +++ b/Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift @@ -31,7 +31,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { return [] } - let outputSwiftFile = context.pluginWorkDirectoryURL.appending(path: "SafeDI.swift") + let outputDirectory = context.pluginWorkDirectoryURL.appending(path: "SafeDIOutput") // Swift Package Plugins did not (as of Swift 5.9) allow for // creating dependencies between plugin output at the time of writing. // Since our current build system did not support depending on the @@ -46,8 +46,21 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { .sourceFiles(withSuffix: ".swift") .map(\.url) } + + let allSwiftFiles = targetSwiftFiles + dependenciesSourceFiles + let rootFiles = findFilesWithRoots(in: allSwiftFiles) + guard !rootFiles.isEmpty else { + return [] + } + + let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in + outputDirectory.appending(path: name) + } + + let packageRoot = context.package.directoryURL let inputSourcesFile = context.pluginWorkDirectoryURL.appending(path: "InputSwiftFiles.csv") - try (targetSwiftFiles.map { $0.path(percentEncoded: false) } + dependenciesSourceFiles.map { $0.path(percentEncoded: false) }) + try allSwiftFiles + .map { relativePath(for: $0, relativeTo: packageRoot) } .joined(separator: ",") .write( to: inputSourcesFile, @@ -55,10 +68,18 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { encoding: .utf8, ) + let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json") + try writeManifest( + dependencyTreeInputFiles: rootFiles, + outputDirectory: outputDirectory, + to: manifestFile, + relativeTo: packageRoot, + ) + let arguments = [ inputSourcesFile.path(percentEncoded: false), - "--dependency-tree-output", - outputSwiftFile.path(percentEncoded: false), + "--swift-manifest", + manifestFile.path(percentEncoded: false), ] let downloadedToolLocation = context.downloadedToolLocation @@ -84,8 +105,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { executable: toolLocation, arguments: arguments, environment: [:], - inputFiles: targetSwiftFiles + dependenciesSourceFiles, - outputFiles: [outputSwiftFile], + inputFiles: allSwiftFiles, + outputFiles: outputFiles, ), ] } @@ -142,10 +163,20 @@ extension Target { return [] } - let outputSwiftFile = context.pluginWorkDirectoryURL.appending(path: "SafeDI.swift") + let rootFiles = findFilesWithRoots(in: inputSwiftFiles) + guard !rootFiles.isEmpty else { + return [] + } + + let outputDirectory = context.pluginWorkDirectoryURL.appending(path: "SafeDIOutput") + let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in + outputDirectory.appending(path: name) + } + + let projectRoot = context.xcodeProject.directoryURL let inputSourcesFile = context.pluginWorkDirectoryURL.appending(path: "InputSwiftFiles.csv") try inputSwiftFiles - .map { $0.path(percentEncoded: false) } + .map { relativePath(for: $0, relativeTo: projectRoot) } .joined(separator: ",") .write( to: inputSourcesFile, @@ -153,10 +184,18 @@ extension Target { encoding: .utf8, ) + let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json") + try writeManifest( + dependencyTreeInputFiles: rootFiles, + outputDirectory: outputDirectory, + to: manifestFile, + relativeTo: projectRoot, + ) + let arguments = [ inputSourcesFile.path(percentEncoded: false), - "--dependency-tree-output", - outputSwiftFile.path(percentEncoded: false), + "--swift-manifest", + manifestFile.path(percentEncoded: false), ] let downloadedToolLocation = context.downloadedToolLocation @@ -173,14 +212,14 @@ extension Target { try context.tool(named: "SafeDITool").url } - return try [ + return [ .buildCommand( displayName: "SafeDIGenerateDependencyTree", executable: toolLocation, arguments: arguments, environment: [:], inputFiles: inputSwiftFiles, - outputFiles: [outputSwiftFile], + outputFiles: outputFiles, ), ] } diff --git a/Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift b/Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift index 4d4ba618..6d91fa8e 100644 --- a/Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift +++ b/Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift @@ -31,7 +31,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { return [] } - let outputSwiftFile = context.pluginWorkDirectoryURL.appending(path: "SafeDI.swift") + let outputDirectory = context.pluginWorkDirectoryURL.appending(path: "SafeDIOutput") // Swift Package Plugins did not (as of Swift 5.9) allow for // creating dependencies between plugin output at the time of writing. // Since our current build system did not support depending on the @@ -46,8 +46,21 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { .sourceFiles(withSuffix: ".swift") .map(\.url) } + + let allSwiftFiles = targetSwiftFiles + dependenciesSourceFiles + let rootFiles = findFilesWithRoots(in: allSwiftFiles) + guard !rootFiles.isEmpty else { + return [] + } + + let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in + outputDirectory.appending(path: name) + } + + let packageRoot = context.package.directoryURL let inputSourcesFile = context.pluginWorkDirectoryURL.appending(path: "InputSwiftFiles.csv") - try (targetSwiftFiles.map { $0.path(percentEncoded: false) } + dependenciesSourceFiles.map { $0.path(percentEncoded: false) }) + try allSwiftFiles + .map { relativePath(for: $0, relativeTo: packageRoot) } .joined(separator: ",") .write( to: inputSourcesFile, @@ -55,10 +68,18 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { encoding: .utf8, ) + let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json") + try writeManifest( + dependencyTreeInputFiles: rootFiles, + outputDirectory: outputDirectory, + to: manifestFile, + relativeTo: packageRoot, + ) + let arguments = [ inputSourcesFile.path(percentEncoded: false), - "--dependency-tree-output", - outputSwiftFile.path(percentEncoded: false), + "--swift-manifest", + manifestFile.path(percentEncoded: false), ] let downloadedToolLocation = context.downloadedToolLocation @@ -94,8 +115,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin { executable: toolLocation, arguments: arguments, environment: [:], - inputFiles: targetSwiftFiles + dependenciesSourceFiles, - outputFiles: [outputSwiftFile], + inputFiles: allSwiftFiles, + outputFiles: outputFiles, ), ] } @@ -152,10 +173,20 @@ extension Target { return [] } - let outputSwiftFile = context.pluginWorkDirectoryURL.appending(path: "SafeDI.swift") + let rootFiles = findFilesWithRoots(in: inputSwiftFiles) + guard !rootFiles.isEmpty else { + return [] + } + + let outputDirectory = context.pluginWorkDirectoryURL.appending(path: "SafeDIOutput") + let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in + outputDirectory.appending(path: name) + } + + let projectRoot = context.xcodeProject.directoryURL let inputSourcesFile = context.pluginWorkDirectoryURL.appending(path: "InputSwiftFiles.csv") try inputSwiftFiles - .map { $0.path(percentEncoded: false) } + .map { relativePath(for: $0, relativeTo: projectRoot) } .joined(separator: ",") .write( to: inputSourcesFile, @@ -163,10 +194,18 @@ extension Target { encoding: .utf8, ) + let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json") + try writeManifest( + dependencyTreeInputFiles: rootFiles, + outputDirectory: outputDirectory, + to: manifestFile, + relativeTo: projectRoot, + ) + let arguments = [ inputSourcesFile.path(percentEncoded: false), - "--dependency-tree-output", - outputSwiftFile.path(percentEncoded: false), + "--swift-manifest", + manifestFile.path(percentEncoded: false), ] let downloadedToolLocation = context.downloadedToolLocation @@ -188,7 +227,7 @@ extension Target { arguments: arguments, environment: [:], inputFiles: inputSwiftFiles, - outputFiles: [outputSwiftFile], + outputFiles: outputFiles, ), ] } diff --git a/Plugins/Shared.swift b/Plugins/Shared.swift index dc3d23b3..89b626e2 100644 --- a/Plugins/Shared.swift +++ b/Plugins/Shared.swift @@ -112,3 +112,85 @@ extension PackagePlugin.PluginContext { return expectedToolLocation } } + +/// Find Swift files that contain `@Instantiable(isRoot: true)` declarations. +func findFilesWithRoots(in swiftFiles: [URL]) -> [URL] { + swiftFiles.filter { fileURL in + guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { return false } + guard content.contains("isRoot") else { return false } + guard let regex = try? Regex(#"@Instantiable\s*\([^)]*isRoot\s*:\s*true[^)]*\)"#) else { return false } + // Check each match is not inside a comment or backtick-quoted code span. + for match in content.matches(of: regex) { + let lineStart = content[content.startIndex.. [String] { + let baseNames = inputURLs.map { $0.deletingPathExtension().lastPathComponent } + + // Count occurrences of each base name. + var nameCounts = [String: Int]() + for name in baseNames { + nameCounts[name, default: 0] += 1 + } + + return zip(inputURLs, baseNames).map { url, baseName in + if nameCounts[baseName, default: 1] > 1 { + // Disambiguate by prepending the parent directory name. + let parent = url.deletingLastPathComponent().lastPathComponent + return "\(parent)_\(baseName)+SafeDI.swift" + } else { + return "\(baseName)+SafeDI.swift" + } + } +} + +/// Compute a path string relative to a base directory, for use in the CSV and manifest. +/// Falls back to the absolute path if the URL is not under the base directory. +func relativePath(for url: URL, relativeTo base: URL) -> String { + let urlPath = url.path(percentEncoded: false) + let basePath = base.path(percentEncoded: false).hasSuffix("/") + ? base.path(percentEncoded: false) + : base.path(percentEncoded: false) + "/" + if urlPath.hasPrefix(basePath) { + return String(urlPath.dropFirst(basePath.count)) + } + return urlPath +} + +/// Write a SafeDIToolManifest JSON file mapping input file paths to output file paths. +/// Input paths are written relative to `relativeTo` for remote cache compatibility. +/// Output paths are absolute since they reference the build system's plugin work directory. +/// +/// Note: The JSON keys here must match the property names in `SafeDIToolManifest` and +/// `SafeDIToolManifest.InputOutputMap` (in SafeDICore). Plugins cannot import SafeDICore, +/// so these are duplicated as string literals. +func writeManifest( + dependencyTreeInputFiles: [URL], + outputDirectory: URL, + to manifestURL: URL, + relativeTo base: URL, +) throws { + let fileNames = outputFileNames(for: dependencyTreeInputFiles) + var entries = [[String: String]]() + for (inputURL, fileName) in zip(dependencyTreeInputFiles, fileNames) { + entries.append([ + "inputFilePath": relativePath(for: inputURL, relativeTo: base), + "outputFilePath": outputDirectory.appending(path: fileName).path(percentEncoded: false), + ]) + } + let manifest = ["dependencyTreeGeneration": entries] + let data = try JSONSerialization.data(withJSONObject: manifest, options: [.sortedKeys]) + try data.write(to: manifestURL) +} diff --git a/README.md b/README.md index 4421cc36..edfb9754 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,31 @@ This plugin will: 3. If you have `.safedi/configuration/include.csv` or `.safedi/configuration/additionalImportedModules.csv`, create a `@SafeDIConfiguration` enum in your root module with the equivalent values and delete the CSV files 4. If you don't have CSV configuration files, create a `@SafeDIConfiguration`-decorated enum in your root module +### Migrating prebuild scripts or custom build system integrations + +If you invoke `SafeDITool` directly (not via the provided SPM plugin), the `--dependency-tree-output` flag has been replaced with `--swift-manifest`. The tool now takes a JSON manifest file that maps input Swift files to output files. See [`SafeDIToolManifest`](Sources/SafeDICore/Models/SafeDIToolManifest.swift) for the expected format. + +Before (1.x): +```bash +safedi-tool input.csv --dependency-tree-output ./generated/SafeDI.swift +``` + +After (2.x): +```bash +# Create a manifest mapping root files to outputs +cat > manifest.json << 'EOF' +{ + "dependencyTreeGeneration": [ + { + "inputFilePath": "Sources/App/Root.swift", + "outputFilePath": "generated/Root+SafeDI.swift" + } + ] +} +EOF +safedi-tool input.csv --swift-manifest manifest.json +``` + ## Contributing I’m glad you’re interested in SafeDI, and I’d love to see where you take it. Please review the [contributing guidelines](Contributing.md) prior to submitting a Pull Request. diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index 43c867cf..3df2bd84 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -33,31 +33,45 @@ public actor DependencyTreeGenerator { // MARK: Public - public func generateCodeTree() async throws -> String { + public struct GeneratedRoot: Sendable { + public let typeDescription: TypeDescription + public let sourceFilePath: String? + /// The generated extension code for this root, without the file header. + public let code: String + } + + /// The file header to prepend to each generated output file. + public var fileHeader: String { + let importsWhitespace = imports.isEmpty ? "" : "\n" + return "// This file was generated by the SafeDIGenerateDependencyTree build tool plugin.\n// Any modifications made to this file will be overwritten on subsequent builds.\n// Please refrain from editing this file directly.\n\(importsWhitespace)\(imports)\(importsWhitespace)\n" + } + + public func generatePerRootCodeTrees() async throws -> [GeneratedRoot] { let rootScopeGenerators = try rootScopeGenerators - let dependencyTree = try await withThrowingTaskGroup( - of: String.self, - returning: String.self, + return try await withThrowingTaskGroup( + of: GeneratedRoot?.self, + returning: [GeneratedRoot].self, ) { taskGroup in - for rootScopeGenerator in rootScopeGenerators { - taskGroup.addTask { try await rootScopeGenerator.generateCode() } + for rootInfo in rootScopeGenerators { + taskGroup.addTask { + let code = try await rootInfo.scopeGenerator.generateCode() + guard !code.isEmpty else { return nil } + return GeneratedRoot( + typeDescription: rootInfo.typeDescription, + sourceFilePath: rootInfo.sourceFilePath, + code: code, + ) + } } - var generatedRoots = [String]() + var generatedRoots = [GeneratedRoot]() for try await generatedRoot in taskGroup { - generatedRoots.append(generatedRoot) + if let generatedRoot { + generatedRoots.append(generatedRoot) + } } - return generatedRoots.removingEmpty().sorted().joined(separator: "\n\n") + return generatedRoots } - - let importsWhitespace = imports.isEmpty ? "" : "\n" - return """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - \(importsWhitespace)\(imports)\(importsWhitespace) - \(dependencyTree.isEmpty ? "// No root @\(InstantiableVisitor.macroName)-decorated types found, or root types already had a `public init()` method." : dependencyTree) - """ } public func generateDOTTree() async throws -> String { @@ -67,8 +81,8 @@ public actor DependencyTreeGenerator { of: String.self, returning: String.self, ) { taskGroup in - for rootScopeGenerator in rootScopeGenerators { - taskGroup.addTask { try await rootScopeGenerator.generateDOT() } + for rootInfo in rootScopeGenerators { + taskGroup.addTask { try await rootInfo.scopeGenerator.generateDOT() } } var generatedRoots = [String]() for try await generatedRoot in taskGroup { @@ -164,22 +178,34 @@ public actor DependencyTreeGenerator { private let importStatements: [ImportStatement] private let typeDescriptionToFulfillingInstantiableMap: [TypeDescription: Instantiable] - private var rootScopeGenerators: [ScopeGenerator] { + private struct RootScopeInfo { + let typeDescription: TypeDescription + let sourceFilePath: String? + let scopeGenerator: ScopeGenerator + } + + private var rootScopeGenerators: [RootScopeInfo] { get throws { - let rootScopeGenerators: [ScopeGenerator] = try { + let rootScopeGenerators: [RootScopeInfo] = try { try validateReachableTypeDescriptions() let typeDescriptionToScopeMap = try createTypeDescriptionToScopeMapping() try validatePropertiesAreFulfillable(typeDescriptionToScopeMap: typeDescriptionToScopeMap) return try rootInstantiables .sorted() - .compactMap { - try typeDescriptionToScopeMap[$0]?.createScopeGenerator( - for: nil, - propertyStack: [], - receivableProperties: [], - erasedToConcreteExistential: false, - ) + .compactMap { typeDescription in + try typeDescriptionToScopeMap[typeDescription].map { scope in + try RootScopeInfo( + typeDescription: typeDescription, + sourceFilePath: typeDescriptionToFulfillingInstantiableMap[typeDescription]?.sourceFilePath, + scopeGenerator: scope.createScopeGenerator( + for: nil, + propertyStack: [], + receivableProperties: [], + erasedToConcreteExistential: false, + ), + ) + } } }() return rootScopeGenerators diff --git a/Sources/SafeDICore/Models/InstantiableStruct.swift b/Sources/SafeDICore/Models/InstantiableStruct.swift index c733e2aa..129cbf73 100644 --- a/Sources/SafeDICore/Models/InstantiableStruct.swift +++ b/Sources/SafeDICore/Models/InstantiableStruct.swift @@ -55,6 +55,9 @@ public struct Instantiable: Codable, Hashable, Sendable { /// The declaration type of the Instantiable’s concrete type. public let declarationType: DeclarationType + /// The path to the source file that declared this Instantiable. + public var sourceFilePath: String? + /// The type of declaration where this Instantiable was defined. public enum DeclarationType: Codable, Hashable, Sendable { case classType diff --git a/Sources/SafeDICore/Models/SafeDIToolManifest.swift b/Sources/SafeDICore/Models/SafeDIToolManifest.swift new file mode 100644 index 00000000..83cf3f52 --- /dev/null +++ b/Sources/SafeDICore/Models/SafeDIToolManifest.swift @@ -0,0 +1,49 @@ +// Distributed under the MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/// A manifest that describes the desired outputs of the SafeDITool. +/// All input file paths are relative to the working directory where the tool is invoked. +/// Output file paths may be absolute or relative to the working directory. +public struct SafeDIToolManifest: Codable, Sendable { + /// A mapping from an input Swift file to an output file. + public struct InputOutputMap: Codable, Sendable { + /// The path to the input Swift file containing one or more root `@Instantiable` declarations. + /// This path is relative to the working directory where the tool is invoked. + public var inputFilePath: String + + /// The path where the generated Swift code should be written. + /// This path may be absolute or relative to the working directory. + public var outputFilePath: String + + public init(inputFilePath: String, outputFilePath: String) { + self.inputFilePath = inputFilePath + self.outputFilePath = outputFilePath + } + } + + /// The list of input-to-output file mappings for dependency tree code generation. + /// Each entry maps a Swift file containing `@Instantiable(isRoot: true)` to the + /// output file where the generated `public init()` extension should be written. + public var dependencyTreeGeneration: [InputOutputMap] + + public init(dependencyTreeGeneration: [InputOutputMap]) { + self.dependencyTreeGeneration = dependencyTreeGeneration + } +} diff --git a/Sources/SafeDITool/SafeDITool.swift b/Sources/SafeDITool/SafeDITool.swift index b6d4cc99..7ba6c2c6 100644 --- a/Sources/SafeDITool/SafeDITool.swift +++ b/Sources/SafeDITool/SafeDITool.swift @@ -39,7 +39,7 @@ struct SafeDITool: AsyncParsableCommand { @Option(help: "A path to a CSV file containing paths of SafeDI representations of other modules to parse.") var dependentModuleInfoFilePath: String? - @Option(help: "The desired output location of the Swift dependency injection tree. Only include this option when running on a project‘s root module.") var dependencyTreeOutput: String? + @Option(help: "A path to a JSON manifest file describing the desired Swift output files. The manifest maps input file paths to output file paths. See SafeDIToolManifest for the expected format.") var swiftManifest: String? @Option(help: "The desired output location of the DOT file expressing the Swift dependency injection tree. Only include this option when running on a project‘s root module.") var dotFileOutput: String? @@ -59,14 +59,6 @@ struct SafeDITool: AsyncParsableCommand { throw ValidationError("Must provide 'swift-sources-file-path' or '--include'.") } - async let existingGeneratedCode: String? = Task.detached { - if let dependencyTreeOutput { - try? String(contentsOf: dependencyTreeOutput.asFileURL, encoding: .utf8) - } else { - nil - } - }.value - let (dependentModuleInfo, initialModule) = try await ( loadSafeDIModuleInfo(), parsedModule(), @@ -142,7 +134,7 @@ struct SafeDITool: AsyncParsableCommand { $0 } } - return Instantiable( + var normalized = Instantiable( instantiableType: unnormalizedInstantiable.concreteInstantiable, isRoot: unnormalizedInstantiable.isRoot, initializer: normalizedInitializer, @@ -150,6 +142,8 @@ struct SafeDITool: AsyncParsableCommand { dependencies: normalizedDependencies, declarationType: unnormalizedInstantiable.declarationType, ) + normalized.sourceFilePath = unnormalizedInstantiable.sourceFilePath + return normalized } let generator = try DependencyTreeGenerator( importStatements: dependentModuleInfo.flatMap(\.imports) + resolvedAdditionalImportedModules.map { ImportStatement(moduleName: $0) } + module.imports, @@ -157,18 +151,20 @@ struct SafeDITool: AsyncParsableCommand { instantiables: normalizedInstantiables, ), ) - async let generatedCode: String? = try dependencyTreeOutput != nil - ? generator.generateCodeTree() - : nil - if let moduleInfoOutput { try JSONEncoder().encode(module).write(toPath: moduleInfoOutput) } - if let dependencyTreeOutput { + if let swiftManifest { + let manifest = try JSONDecoder().decode( + SafeDIToolManifest.self, + from: Data(contentsOf: swiftManifest.asFileURL), + ) + let filesWithUnexpectedNodes = dependentModuleInfo.compactMap(\.filesWithUnexpectedNodes).flatMap(\.self) + (module.filesWithUnexpectedNodes ?? []) if !filesWithUnexpectedNodes.isEmpty { - try """ + // Write error to all manifest output files. + let errorContent = """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -177,12 +173,49 @@ struct SafeDITool: AsyncParsableCommand { Compiler errors prevented the generation of the dependency tree. Files with errors: \(filesWithUnexpectedNodes.joined(separator: "\n\t")) \""") - """.write(toPath: dependencyTreeOutput) - } else if let generatedCode = try await generatedCode, - // Only update the file if the file has changed. - await existingGeneratedCode != generatedCode - { - try generatedCode.write(toPath: dependencyTreeOutput) + """ + for entry in manifest.dependencyTreeGeneration { + try errorContent.write(toPath: entry.outputFilePath) + } + } else { + let generatedRoots = try await generator.generatePerRootCodeTrees() + let fileHeader = await generator.fileHeader + + // Build a map from source file path → extension code(s). + var sourceFileToExtensions = [String: [String]]() + for root in generatedRoots { + if let sourceFilePath = root.sourceFilePath { + sourceFileToExtensions[sourceFilePath, default: []].append(root.code) + } + } + + let emptyRootContent = fileHeader + + // Validate and write output files. + let allRootSourceFiles = Set(normalizedInstantiables.filter(\.isRoot).compactMap(\.sourceFilePath)) + for entry in manifest.dependencyTreeGeneration { + guard allRootSourceFiles.contains(entry.inputFilePath) else { + throw ManifestError.noRootFound(inputPath: entry.inputFilePath) + } + let code: String = if let extensions = sourceFileToExtensions[entry.inputFilePath] { + fileHeader + extensions.sorted().joined(separator: "\n\n") + } else { + emptyRootContent + } + // Only update the file if the content has changed. + let existingContent = try? String(contentsOfFile: entry.outputFilePath, encoding: .utf8) + if existingContent != code { + try code.write(toPath: entry.outputFilePath) + } + } + + // Validate all roots are accounted for in the manifest. + let manifestInputPaths = Set(manifest.dependencyTreeGeneration.map(\.inputFilePath)) + for sourceFile in allRootSourceFiles { + if !manifestInputPaths.contains(sourceFile) { + throw ManifestError.rootNotInManifest(sourceFilePath: sourceFile) + } + } } } @@ -197,6 +230,20 @@ struct SafeDITool: AsyncParsableCommand { } } + private enum ManifestError: Error, CustomStringConvertible { + case noRootFound(inputPath: String) + case rootNotInManifest(sourceFilePath: String) + + var description: String { + switch self { + case let .noRootFound(inputPath): + "Manifest lists '\(inputPath)' as containing a dependency tree root, but no @\(InstantiableVisitor.macroName)(isRoot: true) was found in that file." + case let .rootNotInManifest(sourceFilePath): + "Found @\(InstantiableVisitor.macroName)(isRoot: true) in '\(sourceFilePath)', but this file is not listed in the manifest's dependencyTreeGeneration. Add it to the manifest or remove the isRoot annotation." + } + } + } + struct ModuleInfo: Codable { let imports: [ImportStatement] let instantiables: [Instantiable] @@ -293,9 +340,14 @@ struct SafeDITool: AsyncParsableCommand { || !fileVisitor.configurations.isEmpty || fileVisitor.encounteredUnexpectedNodesSyntax else { return nil } + let instantiables = fileVisitor.instantiables.map { + var instantiable = $0 + instantiable.sourceFilePath = filePath + return instantiable + } return ( imports: fileVisitor.imports, - instantiables: fileVisitor.instantiables, + instantiables: instantiables, configurations: fileVisitor.configurations, encounteredUnexpectedNodeInFile: fileVisitor.encounteredUnexpectedNodesSyntax ? filePath : nil, ) diff --git a/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift b/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift index 6f931ea7..66c671e6 100644 --- a/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift +++ b/Tests/SafeDIToolTests/Helpers/SafeDIToolTestExecution.swift @@ -27,7 +27,7 @@ func executeSafeDIToolTest( swiftFileContent: [String], dependentModuleInfoPaths: [String] = [], additionalImportedModules: [String] = [], - buildDependencyTreeOutput: Bool = false, + buildSwiftOutputDirectory: Bool = false, buildDOTFileOutput: Bool = false, filesToDelete: inout [URL], includeFolders: [String] = [], @@ -50,10 +50,36 @@ func executeSafeDIToolTest( .write(to: dependentModuleInfoFileCSV, atomically: true, encoding: .utf8) let moduleInfoOutput = URL.temporaryFile.appendingPathExtension("safedi") - let dependencyTreeOutput = URL.temporaryFile.appendingPathExtension("swift") + let outputDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let manifestFile = URL.temporaryFile.appendingPathExtension("json") let dotTreeOutput = URL.temporaryFile.appendingPathExtension("dot") return try await SafeDITool.$fileFinder.withValue(StubFileFinder(files: swiftFiles)) { // Successfully execute the file finder code path. + // Build the manifest by scanning for files that contain isRoot: true. + var manifestPath: String? + if buildSwiftOutputDirectory { + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + var entries = [SafeDIToolManifest.InputOutputMap]() + let rootRegex = try Regex(#"@Instantiable\s*\([^)]*isRoot\s*:\s*true[^)]*\)"#) + let typeDeclRegex = try Regex(#"(?:class|struct|actor)\s+(\w+)"#) + for (content, file) in zip(swiftFileContent, swiftFiles) { + if let rootMatch = content.firstMatch(of: rootRegex) { + let afterMacro = content[rootMatch.range.upperBound...] + if let typeMatch = afterMacro.firstMatch(of: typeDeclRegex), + let nameRange = typeMatch.output[1].range + { + let typeName = String(content[nameRange]) + let outputPath = (outputDirectory.relativePath as NSString).appendingPathComponent("\(typeName)+SafeDI.swift") + entries.append(.init(inputFilePath: file.relativePath, outputFilePath: outputPath)) + } + } + } + let manifest = SafeDIToolManifest(dependencyTreeGeneration: entries) + let manifestData = try JSONEncoder().encode(manifest) + try manifestData.write(to: manifestFile) + manifestPath = manifestFile.relativePath + } + var tool = SafeDITool() tool.swiftSourcesFilePath = swiftFileCSV.relativePath tool.showVersion = false @@ -61,24 +87,40 @@ func executeSafeDIToolTest( tool.additionalImportedModules = additionalImportedModules tool.moduleInfoOutput = moduleInfoOutput.relativePath tool.dependentModuleInfoFilePath = dependentModuleInfoPaths.isEmpty ? nil : dependentModuleInfoFileCSV.relativePath - tool.dependencyTreeOutput = buildDependencyTreeOutput ? dependencyTreeOutput.relativePath : nil + tool.swiftManifest = manifestPath tool.dotFileOutput = buildDOTFileOutput ? dotTreeOutput.relativePath : nil try await tool.run() filesToDelete.append(swiftFileCSV) filesToDelete += swiftFiles filesToDelete.append(moduleInfoOutput) - if buildDependencyTreeOutput { - filesToDelete.append(dependencyTreeOutput) + if buildSwiftOutputDirectory { + filesToDelete.append(outputDirectory) + filesToDelete.append(manifestFile) } if buildDOTFileOutput { filesToDelete.append(dotTreeOutput) } + // Read generated files from the output directory. + let generatedFiles: [String: String]? = if buildSwiftOutputDirectory { + { + guard let fileNames = try? FileManager.default.contentsOfDirectory(atPath: outputDirectory.relativePath) else { return [:] } + var result = [String: String]() + for fileName in fileNames where fileName.hasSuffix(".swift") { + let filePath = (outputDirectory.relativePath as NSString).appendingPathComponent(fileName) + result[fileName] = try? String(contentsOfFile: filePath, encoding: .utf8) + } + return result + }() + } else { + nil + } + return try TestOutput( moduleInfo: JSONDecoder().decode(SafeDITool.ModuleInfo.self, from: Data(contentsOf: moduleInfoOutput)), moduleInfoOutputPath: moduleInfoOutput.relativePath, - dependencyTree: buildDependencyTreeOutput ? String(data: Data(contentsOf: dependencyTreeOutput), encoding: .utf8) : nil, + generatedFiles: generatedFiles, dotTree: buildDOTFileOutput ? String(data: Data(contentsOf: dotTreeOutput), encoding: .utf8) : nil, ) } @@ -87,7 +129,7 @@ func executeSafeDIToolTest( struct TestOutput { let moduleInfo: SafeDITool.ModuleInfo let moduleInfoOutputPath: String - let dependencyTree: String? + let generatedFiles: [String: String]? let dotTree: String? } diff --git a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift index 4a8c774c..3a448a8c 100644 --- a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift @@ -57,7 +57,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -103,7 +103,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -149,7 +149,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -184,7 +184,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -219,7 +219,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -253,7 +253,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -288,7 +288,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -333,7 +333,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -378,7 +378,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -421,7 +421,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -464,7 +464,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -506,7 +506,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -549,7 +549,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -595,7 +595,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -643,7 +643,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -677,7 +677,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -724,7 +724,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { public final class Blankie {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -781,7 +781,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { public final class Blankie {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -839,7 +839,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -897,7 +897,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -951,7 +951,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1005,7 +1005,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1063,7 +1063,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1098,7 +1098,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1126,7 +1126,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { public final class RootViewController: UIViewController {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1154,7 +1154,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { public final class RootViewController: UIViewController {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1186,7 +1186,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1218,7 +1218,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1250,7 +1250,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1286,7 +1286,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1314,7 +1314,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { public final class SplashViewController: UIViewController {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1355,7 +1355,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1394,7 +1394,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1435,7 +1435,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1476,7 +1476,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1517,7 +1517,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1558,7 +1558,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1601,7 +1601,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1645,7 +1645,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1688,7 +1688,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1745,7 +1745,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1826,7 +1826,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1909,7 +1909,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) } @@ -1937,7 +1937,7 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { tool.additionalImportedModules = [] tool.moduleInfoOutput = nil tool.dependentModuleInfoFilePath = nil - tool.dependencyTreeOutput = nil + tool.swiftManifest = nil tool.dotFileOutput = nil await assertThrowsError("Could not create file enumerator for directory 'Fake'") { try await tool.run() @@ -1954,13 +1954,89 @@ struct SafeDIToolCodeGenerationErrorTests: ~Copyable { tool.additionalImportedModules = [] tool.moduleInfoOutput = nil tool.dependentModuleInfoFilePath = nil - tool.dependencyTreeOutput = nil + tool.swiftManifest = nil tool.dotFileOutput = nil await assertThrowsError("Must provide 'swift-sources-file-path' or '--include'.") { try await tool.run() } } + // MARK: Manifest Validation Tests + + @Test + mutating func run_throwsError_whenManifestListsFileThatDoesNotContainRoot() async throws { + let swiftFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".swift") + try """ + @Instantiable + public struct NotRoot { + public init() {} + } + """.write(to: swiftFile, atomically: true, encoding: .utf8) + let swiftFileCSV = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + try swiftFile.relativePath.write(to: swiftFileCSV, atomically: true, encoding: .utf8) + let manifestFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".json") + let outputFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".swift") + let manifest = SafeDIToolManifest(dependencyTreeGeneration: [.init(inputFilePath: swiftFile.relativePath, outputFilePath: outputFile.relativePath)]) + try JSONEncoder().encode(manifest).write(to: manifestFile) + let moduleInfoOutput = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".safedi") + + filesToDelete += [swiftFileCSV, swiftFile, manifestFile, moduleInfoOutput] + + await assertThrowsError( + "Manifest lists '\(swiftFile.relativePath)' as containing a dependency tree root, but no @Instantiable(isRoot: true) was found in that file.", + ) { + var tool = SafeDITool() + tool.swiftSourcesFilePath = swiftFileCSV.relativePath + tool.showVersion = false + tool.include = [] + tool.additionalImportedModules = [] + tool.moduleInfoOutput = moduleInfoOutput.relativePath + tool.dependentModuleInfoFilePath = nil + tool.swiftManifest = manifestFile.relativePath + tool.dotFileOutput = nil + try await tool.run() + } + } + + @Test + mutating func run_throwsError_whenRootExistsButNotInManifest() async throws { + let swiftFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".swift") + try """ + @Instantiable(isRoot: true) + public struct Root { + public init(dep: Dep) { self.dep = dep } + @Instantiated let dep: Dep + } + @Instantiable + public struct Dep { + public init() {} + } + """.write(to: swiftFile, atomically: true, encoding: .utf8) + let swiftFileCSV = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + try swiftFile.relativePath.write(to: swiftFileCSV, atomically: true, encoding: .utf8) + let manifestFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".json") + let manifest = SafeDIToolManifest(dependencyTreeGeneration: []) + try JSONEncoder().encode(manifest).write(to: manifestFile) + let moduleInfoOutput = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".safedi") + + filesToDelete += [swiftFileCSV, swiftFile, manifestFile, moduleInfoOutput] + + await assertThrowsError( + "Found @Instantiable(isRoot: true) in '\(swiftFile.relativePath)', but this file is not listed in the manifest's dependencyTreeGeneration. Add it to the manifest or remove the isRoot annotation.", + ) { + var tool = SafeDITool() + tool.swiftSourcesFilePath = swiftFileCSV.relativePath + tool.showVersion = false + tool.include = [] + tool.additionalImportedModules = [] + tool.moduleInfoOutput = moduleInfoOutput.relativePath + tool.dependentModuleInfoFilePath = nil + tool.swiftManifest = manifestFile.relativePath + tool.dotFileOutput = nil + try await tool.run() + } + } + // MARK: Private private var filesToDelete = [URL]() diff --git a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift index fc86afca..9e30b2d4 100644 --- a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift @@ -42,17 +42,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { mutating func run_successfullyGeneratesOutputFileWhenNoCodeInput() async throws { let output = try await executeSafeDIToolTest( swiftFileContent: [], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + #expect(output.generatedFiles == [:]) } @Test @@ -70,21 +64,13 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - #if canImport(Foundation) - import Foundation - #endif - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + let content = try #require(output.generatedFiles?["DefaultNetworkService+SafeDI.swift"]) + #expect(content.contains("// This file was generated by the SafeDIGenerateDependencyTree build tool plugin.")) + #expect(!content.contains("extension")) } @Test @@ -114,11 +100,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(output.generatedFiles?["RootViewController+SafeDI.swift"] == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -163,11 +149,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(output.generatedFiles?["Root+SafeDI.swift"] == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -212,11 +198,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(output.generatedFiles?["Root+SafeDI.swift"] == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -271,11 +257,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(output.generatedFiles?["Root1+SafeDI.swift"] == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -290,6 +276,16 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { self.init(networkService: networkService) } } + """) + + #expect(output.generatedFiles?["Root2+SafeDI.swift"] == """ + // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. + // Any modifications made to this file will be overwritten on subsequent builds. + // Please refrain from editing this file directly. + + #if canImport(Foundation) + import Foundation + #endif extension Root2 { public init() { @@ -329,11 +325,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -382,11 +378,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { public final class OtherType {} """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["SomeInstantiated+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -429,11 +425,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -529,11 +525,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -647,11 +643,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -705,11 +701,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -829,11 +825,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -956,11 +952,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1015,11 +1011,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootView+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1066,11 +1062,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootView+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1183,11 +1179,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1302,11 +1298,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1366,11 +1362,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1422,11 +1418,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1475,11 +1471,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1528,11 +1524,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1636,11 +1632,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1753,11 +1749,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1838,11 +1834,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -1919,11 +1915,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2013,11 +2009,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2122,11 +2118,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2245,11 +2241,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2380,11 +2376,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2473,11 +2469,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2593,11 +2589,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2637,7 +2633,7 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: false, + buildSwiftOutputDirectory: false, filesToDelete: &filesToDelete, ) @@ -2693,7 +2689,7 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], dependentModuleInfoPaths: [greatGrandchildModuleOutput.moduleInfoOutputPath], - buildDependencyTreeOutput: false, + buildSwiftOutputDirectory: false, filesToDelete: &filesToDelete, ) @@ -2732,7 +2728,7 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { greatGrandchildModuleOutput.moduleInfoOutputPath, grandchildModuleOutput.moduleInfoOutputPath, ], - buildDependencyTreeOutput: false, + buildSwiftOutputDirectory: false, filesToDelete: &filesToDelete, ) @@ -2758,11 +2754,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { grandchildModuleOutput.moduleInfoOutputPath, childModuleOutput.moduleInfoOutputPath, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(topLevelModuleOutput.dependencyTree) == """ + #expect(try #require(topLevelModuleOutput.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2850,11 +2846,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -2884,17 +2880,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + #expect(output.generatedFiles == [:]) } @Test @@ -2909,22 +2899,12 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { """, ], additionalImportedModules: ["Test"], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, includeFolders: ["Fake"], ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - #if canImport(Test) - import Test - #endif - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + #expect(output.generatedFiles == [:]) } @Test @@ -2938,17 +2918,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + #expect(output.generatedFiles == [:]) } @Test @@ -3069,11 +3043,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3143,11 +3117,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3292,11 +3266,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3380,11 +3354,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3460,11 +3434,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3606,11 +3580,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3760,11 +3734,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["RootViewController+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -3847,11 +3821,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4211,11 +4185,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4275,11 +4249,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4341,11 +4315,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4416,11 +4390,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4471,11 +4445,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4523,11 +4497,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4587,11 +4561,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4646,11 +4620,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4699,11 +4673,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4781,11 +4755,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4872,11 +4846,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -4964,11 +4938,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5029,11 +5003,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5089,11 +5063,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5140,11 +5114,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["TypeWithDependency+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5191,11 +5165,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["TypeWithDependency+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5243,11 +5217,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5296,11 +5270,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5348,11 +5322,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5410,11 +5384,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5488,11 +5462,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5564,11 +5538,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5623,11 +5597,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5689,11 +5663,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5771,11 +5745,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5832,20 +5806,186 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. + // No roots are declared in this test, so no output files are generated. + // The unexpected nodes are reported via moduleInfo. + #expect(output.generatedFiles == [:]) + #expect(output.moduleInfo.filesWithUnexpectedNodes != nil) + } - #error(\""" - Compiler errors prevented the generation of the dependency tree. Files with errors: - \(try #require(output.moduleInfo.filesWithUnexpectedNodes).joined(separator: "\n\t")) - \""") - """) + @Test + mutating func run_writesErrorToOutputFile_whenUnexpectedSwiftNodesAreEncounteredAndRootExists() async throws { + let output = try await executeSafeDIToolTest( + swiftFileContent: [ + """ + @Instantiable(isRoot: true) + public final class Root: Instantiable { + public init(b: B) { + self.b = b + } + + :::brokenSyntax + + @Instantiated private let b: B + } + + @Instantiable + public final class B: Instantiable { + public init() {} + } + """, + ], + buildSwiftOutputDirectory: true, + filesToDelete: &filesToDelete, + ) + + let rootFile = try #require(output.generatedFiles?["Root+SafeDI.swift"]) + #expect(rootFile.contains("#error")) + #expect(rootFile.contains("Compiler errors prevented the generation of the dependency tree")) + } + + @Test + mutating func run_writesConvenienceExtensionOnRootOfTree_whenMultipleRootsExistInSameFile() async throws { + let output = try await executeSafeDIToolTest( + swiftFileContent: [ + """ + @Instantiable + public struct Dep { + public init() {} + } + """, + """ + @Instantiable(isRoot: true) + public struct Root1 { + public init(dep: Dep) { + self.dep = dep + } + @Instantiated let dep: Dep + } + @Instantiable(isRoot: true) + public struct Root2 { + public init(dep: Dep) { + self.dep = dep + } + @Instantiated let dep: Dep + } + """, + ], + buildSwiftOutputDirectory: true, + filesToDelete: &filesToDelete, + ) + + // Both roots are in the same file, so one output file should contain both extensions. + let files = try #require(output.generatedFiles) + #expect(files.count == 1) + let content = try #require(files.values.first) + #expect(content.contains("extension Root1")) + #expect(content.contains("extension Root2")) + // Verify the file header is not duplicated. + let headerOccurrences = content.components(separatedBy: "// This file was generated by").count - 1 + #expect(headerOccurrences == 1) + } + + @Test + mutating func run_doesNotRewriteOutputFile_whenContentIsUnchanged() async throws { + let swiftFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".swift") + try """ + @Instantiable(isRoot: true) + public struct Root { + public init(dep: Dep) { + self.dep = dep + } + @Instantiated let dep: Dep + } + @Instantiable + public struct Dep { + public init() {} + } + """.write(to: swiftFile, atomically: true, encoding: .utf8) + + let swiftFileCSV = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + try swiftFile.relativePath.write(to: swiftFileCSV, atomically: true, encoding: .utf8) + + let outputDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + let outputFile = outputDirectory.appendingPathComponent("Root+SafeDI.swift") + + let manifestFile = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".json") + let manifest = SafeDIToolManifest(dependencyTreeGeneration: [ + .init(inputFilePath: swiftFile.relativePath, outputFilePath: outputFile.relativePath), + ]) + try JSONEncoder().encode(manifest).write(to: manifestFile) + + let moduleInfoOutput = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString + ".safedi") + + filesToDelete += [swiftFileCSV, swiftFile, manifestFile, moduleInfoOutput, outputDirectory] + + func runTool() async throws { + var tool = SafeDITool() + tool.swiftSourcesFilePath = swiftFileCSV.relativePath + tool.showVersion = false + tool.include = [] + tool.additionalImportedModules = [] + tool.moduleInfoOutput = moduleInfoOutput.relativePath + tool.dependentModuleInfoFilePath = nil + tool.swiftManifest = manifestFile.relativePath + tool.dotFileOutput = nil + try await tool.run() + } + + // First run — generates the file. + try await runTool() + let firstContent = try String(contentsOf: outputFile, encoding: .utf8) + let firstModificationDate = try #require(try FileManager.default.attributesOfItem(atPath: outputFile.relativePath)[.modificationDate] as? Date) + + // Small delay to ensure filesystem timestamp granularity. + try await Task.sleep(for: .seconds(1)) + + // Second run — same inputs, should skip writing. + try await runTool() + let secondContent = try String(contentsOf: outputFile, encoding: .utf8) + let secondModificationDate = try #require(try FileManager.default.attributesOfItem(atPath: outputFile.relativePath)[.modificationDate] as? Date) + + #expect(firstContent == secondContent) + #expect(firstModificationDate == secondModificationDate) + } + + @Test + mutating func run_generatesManifestOutputAndDOTFileSimultaneously() async throws { + let output = try await executeSafeDIToolTest( + swiftFileContent: [ + """ + @Instantiable(fulfillingAdditionalTypes: [NetworkService.self]) + public final class DefaultNetworkService: NetworkService { + public init() {} + } + public protocol NetworkService {} + """, + """ + @Instantiable(isRoot: true) + public struct Root { + public init(networkService: NetworkService) { + self.networkService = networkService + } + @Instantiated let networkService: NetworkService + } + """, + ], + buildSwiftOutputDirectory: true, + buildDOTFileOutput: true, + filesToDelete: &filesToDelete, + ) + + // Verify dependency tree was generated. + let rootFile = try #require(output.generatedFiles?["Root+SafeDI.swift"]) + #expect(rootFile.contains("extension Root")) + + // Verify DOT file was also generated. + let dotTree = try #require(output.dotTree) + #expect(dotTree.contains("Root")) } @Test @@ -5892,11 +6032,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ + #expect(try #require(output.generatedFiles?["Root+SafeDI.swift"]) == """ // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. @@ -5936,21 +6076,11 @@ struct SafeDIToolCodeGenerationTests: ~Copyable { } """, ], - buildDependencyTreeOutput: true, + buildSwiftOutputDirectory: true, filesToDelete: &filesToDelete, ) - #expect(try #require(output.dependencyTree) == """ - // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. - // Any modifications made to this file will be overwritten on subsequent builds. - // Please refrain from editing this file directly. - - #if canImport(Test) - import Test - #endif - - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. - """) + #expect(output.generatedFiles == [:]) } @Test diff --git a/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift b/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift index 071a1d54..936ef1df 100644 --- a/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolVersionTests.swift @@ -40,7 +40,7 @@ struct SafeDIToolVersionTests { tool.additionalImportedModules = [] tool.moduleInfoOutput = nil tool.dependentModuleInfoFilePath = nil - tool.dependencyTreeOutput = nil + tool.swiftManifest = nil tool.dotFileOutput = nil let output = try await captureStandardOutput {