diff --git a/Package.swift b/Package.swift index 622ec204e7d..5c7ca0fae32 100644 --- a/Package.swift +++ b/Package.swift @@ -62,7 +62,6 @@ let swiftPMDataModelProduct = ( "PackageLoading", "PackageMetadata", "PackageModel", - "PackageModelSyntax", "SourceControl", "Workspace", ] @@ -368,20 +367,6 @@ let package = Package( ] ), - .target( - /** Primary Package model objects relationship to SwiftSyntax */ - name: "PackageModelSyntax", - dependencies: [ - "Basics", - "PackageLoading", - "PackageModel", - ] + swiftSyntaxDependencies(["SwiftBasicFormat", "SwiftDiagnostics", "SwiftIDEUtils", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]), - exclude: ["CMakeLists.txt"], - swiftSettings: commonExperimentalFeatures + [ - .unsafeFlags(["-static"]), - ] - ), - .target( /** Package model conventions and loading support */ name: "PackageLoading", @@ -623,13 +608,12 @@ let package = Package( "Build", "CoreCommands", "PackageGraph", - "PackageModelSyntax", "SourceControl", "Workspace", "XCBuildSupport", "SwiftBuildSupport", "SwiftFixIt", - ] + swiftSyntaxDependencies(["SwiftIDEUtils"]), + ] + swiftSyntaxDependencies(["SwiftIDEUtils", "SwiftRefactor"]), exclude: ["CMakeLists.txt", "README.md"], swiftSettings: swift6CompatibleExperimentalFeatures + [ .unsafeFlags(["-static"]), @@ -928,13 +912,6 @@ let package = Package( name: "PackageModelTests", dependencies: ["PackageModel", "_InternalTestSupport"] ), - .testTarget( - name: "PackageModelSyntaxTests", - dependencies: [ - "PackageModelSyntax", - "_InternalTestSupport", - ] + swiftSyntaxDependencies(["SwiftIDEUtils"]) - ), .testTarget( name: "PackageGraphTests", dependencies: ["PackageGraph", "_InternalTestSupport"], @@ -1064,7 +1041,6 @@ if ProcessInfo.processInfo.environment["SWIFTCI_DISABLE_SDK_DEPENDENT_TESTS"] == "Build", "Commands", "PackageModel", - "PackageModelSyntax", "PackageRegistryCommand", "SourceControl", "_InternalTestSupport", diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 0698b97e71e..0cfc57b2e4a 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -25,7 +25,6 @@ add_subdirectory(PackageFingerprint) add_subdirectory(PackageGraph) add_subdirectory(PackageLoading) add_subdirectory(PackageModel) -add_subdirectory(PackageModelSyntax) add_subdirectory(PackagePlugin) add_subdirectory(PackageRegistry) add_subdirectory(PackageRegistryCommand) diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index c62c7424ed2..e29fdd0e091 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -53,11 +53,13 @@ add_library(Commands Utilities/MultiRootSupport.swift Utilities/PlainTextEncoder.swift Utilities/PluginDelegate.swift + Utilities/RefactoringSupport.swift Utilities/SymbolGraphExtract.swift Utilities/TestingSupport.swift Utilities/XCTEvents.swift) target_link_libraries(Commands PUBLIC SwiftCollections::OrderedCollections + SwiftSyntax::SwiftRefactor ArgumentParser Basics BinarySymbols @@ -65,7 +67,6 @@ target_link_libraries(Commands PUBLIC CoreCommands LLBuildManifest PackageGraph - PackageModelSyntax SourceControl SwiftFixIt TSCBasic diff --git a/Sources/Commands/PackageCommands/AddDependency.swift b/Sources/Commands/PackageCommands/AddDependency.swift index 7f901bd47cf..e6086e11781 100644 --- a/Sources/Commands/PackageCommands/AddDependency.swift +++ b/Sources/Commands/PackageCommands/AddDependency.swift @@ -15,14 +15,15 @@ import Basics import CoreCommands import Foundation import PackageGraph -import PackageModel -import PackageModelSyntax import SwiftParser +@_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax import TSCBasic import TSCUtility import Workspace +import class PackageModel.Manifest + extension SwiftPackageCommand { struct AddDependency: SwiftCommand { package static let configuration = CommandConfiguration( @@ -98,7 +99,7 @@ extension SwiftPackageCommand { // Collect all of the possible version requirements. var requirements: [PackageDependency.SourceControl.Requirement] = [] if let exact { - requirements.append(.exact(exact)) + requirements.append(.exact(exact.description)) } if let branch { @@ -110,11 +111,17 @@ extension SwiftPackageCommand { } if let from { - requirements.append(.range(.upToNextMajor(from: from))) + requirements.append(.rangeFrom(from.description)) } if let upToNextMinorFrom { - requirements.append(.range(.upToNextMinor(from: upToNextMinorFrom))) + let range: Range = .upToNextMinor(from: upToNextMinorFrom) + requirements.append( + .range( + lowerBound: range.lowerBound.description, + upperBound: range.upperBound.description + ) + ) } if requirements.count > 1 { @@ -130,13 +137,14 @@ extension SwiftPackageCommand { } let requirement: PackageDependency.SourceControl.Requirement - if case .range(let range) = firstRequirement { - if let to { - requirement = .range(range.lowerBound ..< to) + switch firstRequirement { + case .range(let lowerBound, _), .rangeFrom(let lowerBound): + requirement = if let to { + .range(lowerBound: lowerBound, upperBound: to.description) } else { - requirement = .range(range) + firstRequirement } - } else { + default: requirement = firstRequirement if self.to != nil { @@ -147,7 +155,7 @@ extension SwiftPackageCommand { try self.applyEdits( packagePath: packagePath, workspace: workspace, - packageDependency: .sourceControl(name: nil, location: url, requirement: requirement) + packageDependency: .sourceControl(.init(location: url, requirement: requirement)) ) } @@ -159,15 +167,21 @@ extension SwiftPackageCommand { // Collect all of the possible version requirements. var requirements: [PackageDependency.Registry.Requirement] = [] if let exact { - requirements.append(.exact(exact)) + requirements.append(.exact(exact.description)) } if let from { - requirements.append(.range(.upToNextMajor(from: from))) + requirements.append(.rangeFrom(from.description)) } if let upToNextMinorFrom { - requirements.append(.range(.upToNextMinor(from: upToNextMinorFrom))) + let range: Range = .upToNextMinor(from: upToNextMinorFrom) + requirements.append( + .range( + lowerBound: range.lowerBound.description, + upperBound: range.upperBound.description + ) + ) } if requirements.count > 1 { @@ -183,13 +197,14 @@ extension SwiftPackageCommand { } let requirement: PackageDependency.Registry.Requirement - if case .range(let range) = firstRequirement { - if let to { - requirement = .range(range.lowerBound ..< to) + switch firstRequirement { + case .range(let lowerBound, _), .rangeFrom(let lowerBound): + requirement = if let to { + .range(lowerBound: lowerBound, upperBound: to.description) } else { - requirement = .range(range) + firstRequirement } - } else { + default: requirement = firstRequirement if self.to != nil { @@ -200,7 +215,7 @@ extension SwiftPackageCommand { try self.applyEdits( packagePath: packagePath, workspace: workspace, - packageDependency: .registry(id: id, requirement: requirement) + packageDependency: .registry(.init(identity: id, requirement: requirement)) ) } @@ -212,14 +227,14 @@ extension SwiftPackageCommand { try self.applyEdits( packagePath: packagePath, workspace: workspace, - packageDependency: .fileSystem(name: nil, path: directory) + packageDependency: .fileSystem(.init(path: directory)) ) } private func applyEdits( packagePath: Basics.AbsolutePath, workspace: Workspace, - packageDependency: MappablePackageDependency.Kind + packageDependency: PackageDependency ) throws { // Load the manifest file let fileSystem = workspace.fileSystem @@ -240,9 +255,9 @@ extension SwiftPackageCommand { } } - let editResult = try AddPackageDependency.addPackageDependency( - packageDependency, - to: manifestSyntax + let editResult = try AddPackageDependency.manifestRefactor( + syntax: manifestSyntax, + in: .init(dependency: packageDependency) ) try editResult.applyEdits( diff --git a/Sources/Commands/PackageCommands/AddProduct.swift b/Sources/Commands/PackageCommands/AddProduct.swift index 51f8d664de3..9410b035b76 100644 --- a/Sources/Commands/PackageCommands/AddProduct.swift +++ b/Sources/Commands/PackageCommands/AddProduct.swift @@ -15,9 +15,8 @@ import Basics import CoreCommands import Foundation import PackageGraph -import PackageModel -import PackageModelSyntax import SwiftParser +@_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax import TSCBasic import TSCUtility @@ -80,7 +79,7 @@ extension SwiftPackageCommand { } // Map the product type. - let type: ProductType = switch self.type { + let type: ProductDescription.ProductType = switch self.type { case .executable: .executable case .library: .library(.automatic) case .dynamicLibrary: .library(.dynamic) @@ -88,15 +87,15 @@ extension SwiftPackageCommand { case .plugin: .plugin } - let product = try ProductDescription( + let product = ProductDescription( name: name, type: type, targets: targets ) - let editResult = try PackageModelSyntax.AddProduct.addProduct( - product, - to: manifestSyntax + let editResult = try SwiftRefactor.AddProduct.manifestRefactor( + syntax: manifestSyntax, + in: .init(product: product) ) try editResult.applyEdits( diff --git a/Sources/Commands/PackageCommands/AddSetting.swift b/Sources/Commands/PackageCommands/AddSetting.swift index e62388ff4bf..9a1e3988895 100644 --- a/Sources/Commands/PackageCommands/AddSetting.swift +++ b/Sources/Commands/PackageCommands/AddSetting.swift @@ -15,9 +15,11 @@ import Basics import CoreCommands import Foundation import PackageGraph +import PackageLoading import PackageModel -import PackageModelSyntax import SwiftParser +import SwiftSyntax +@_spi(PackageRefactor) import SwiftRefactor import TSCBasic import TSCUtility import Workspace @@ -124,32 +126,40 @@ extension SwiftPackageCommand { } } - let editResult: PackageEditResult + let editResult: PackageEdit switch setting { case .experimentalFeature: + try manifestSyntax.checkManifestAtLeast(.v5_8) + editResult = try AddSwiftSetting.experimentalFeature( to: target, name: value, manifest: manifestSyntax ) case .upcomingFeature: + try manifestSyntax.checkManifestAtLeast(.v5_8) + editResult = try AddSwiftSetting.upcomingFeature( to: target, name: value, manifest: manifestSyntax ) case .languageMode: + try manifestSyntax.checkManifestAtLeast(.v6_0) + guard let mode = SwiftLanguageVersion(string: value) else { throw ValidationError("Unknown Swift language mode: \(value)") } editResult = try AddSwiftSetting.languageMode( to: target, - mode: mode, + mode: mode.rawValue, manifest: manifestSyntax ) case .strictMemorySafety: + try manifestSyntax.checkManifestAtLeast(.v6_2) + guard value.isEmpty || value == SwiftSetting.strictMemorySafety.rawValue else { throw ValidationError("'strictMemorySafety' does not support argument '\(value)'") } @@ -170,3 +180,12 @@ extension SwiftPackageCommand { } } } + +fileprivate extension SourceFileSyntax { + func checkManifestAtLeast(_ version: ToolsVersion) throws { + let toolsVersion = try ToolsVersionParser.parse(utf8String: description) + if toolsVersion < version { + throw StringError("package manifest version \(toolsVersion) is too old: please update to manifest version \(version) or newer") + } + } +} diff --git a/Sources/Commands/PackageCommands/AddTarget.swift b/Sources/Commands/PackageCommands/AddTarget.swift index 413f19363fc..3912eee87d8 100644 --- a/Sources/Commands/PackageCommands/AddTarget.swift +++ b/Sources/Commands/PackageCommands/AddTarget.swift @@ -16,17 +16,17 @@ import CoreCommands import Foundation import PackageGraph import PackageModel -import PackageModelSyntax import SwiftParser +@_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax import TSCBasic import TSCUtility import Workspace -extension AddTarget.TestHarness: ExpressibleByArgument { } +extension AddPackageTarget.TestHarness: @retroactive ExpressibleByArgument { } extension SwiftPackageCommand { - struct AddTarget: SwiftCommand { + struct AddTarget: AsyncSwiftCommand { /// The type of target that can be specified on the command line. enum TargetType: String, Codable, ExpressibleByArgument, CaseIterable { case library @@ -63,9 +63,9 @@ extension SwiftPackageCommand { var checksum: String? @Option(help: "The testing library to use when generating test targets, which can be one of 'xctest', 'swift-testing', or 'none'.") - var testingLibrary: PackageModelSyntax.AddTarget.TestHarness = .default + var testingLibrary: AddPackageTarget.TestHarness = .default - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { let workspace = try swiftCommandState.getActiveWorkspace() guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else { @@ -92,43 +92,39 @@ extension SwiftPackageCommand { } // Move sources into their own folder if they're directly in `./Sources`. - try PackageModelSyntax.AddTarget.moveSingleTargetSources( + try await moveSingleTargetSources( + workspace: workspace, packagePath: packagePath, - manifest: manifestSyntax, - fileSystem: fileSystem, - verbose: !globalOptions.logging.quiet + verbose: !globalOptions.logging.quiet, + observabilityScope: swiftCommandState.observabilityScope ) // Map the target type. - let type: TargetDescription.TargetKind = switch self.type { - case .library: .regular + let type: PackageTarget.TargetKind = switch self.type { + case .library: .library case .executable: .executable case .test: .test case .macro: .macro } // Map dependencies - let dependencies: [TargetDescription.Dependency] = - self.dependencies.map { - .byName(name: $0, condition: nil) - } - - let target = try TargetDescription( - name: name, - dependencies: dependencies, - path: path, - url: url, - type: type, - checksum: checksum - ) + let dependencies: [PackageTarget.Dependency] = self.dependencies.map { + .byName(name: $0) + } - let editResult = try PackageModelSyntax.AddTarget.addTarget( - target, - to: manifestSyntax, - configuration: .init(testHarness: testingLibrary), - installedSwiftPMConfiguration: swiftCommandState - .getHostToolchain() - .installedSwiftPMConfiguration + let editResult = try AddPackageTarget.manifestRefactor( + syntax: manifestSyntax, + in: .init( + target: .init( + name: name, + type: type, + dependencies: dependencies, + path: path, + url: url, + checksum: checksum + ), + testHarness: testingLibrary + ) ) try editResult.applyEdits( @@ -138,6 +134,57 @@ extension SwiftPackageCommand { verbose: !globalOptions.logging.quiet ) } + + // Check if the package has a single target with that target's sources located + // directly in `./Sources`. If so, move the sources into a folder named after + // the target before adding a new target. + private func moveSingleTargetSources( + workspace: Workspace, + packagePath: Basics.AbsolutePath, + verbose: Bool = false, + observabilityScope: ObservabilityScope + ) async throws { + let manifest = try await workspace.loadRootManifest( + at: packagePath, + observabilityScope: observabilityScope + ) + + guard let target = manifest.targets.first, manifest.targets.count == 1 else { + return + } + + let sourcesFolder = packagePath.appending("Sources") + let expectedTargetFolder = sourcesFolder.appending(target.name) + + let fileSystem = workspace.fileSystem + // If there is one target then pull its name out and use that to look for a folder in `Sources/TargetName`. + // If the folder doesn't exist then we know we have a single target package and we need to migrate files + // into this folder before we can add a new target. + if !fileSystem.isDirectory(expectedTargetFolder) { + if verbose { + print( + """ + Moving existing files from \( + sourcesFolder.relative(to: packagePath) + ) to \( + expectedTargetFolder.relative(to: packagePath) + )... + """, + terminator: "" + ) + } + let contentsToMove = try fileSystem.getDirectoryContents(sourcesFolder) + try fileSystem.createDirectory(expectedTargetFolder) + for file in contentsToMove { + let source = sourcesFolder.appending(file) + let destination = expectedTargetFolder.appending(file) + try fileSystem.move(from: source, to: destination) + } + if verbose { + print(" done.") + } + } + } } } diff --git a/Sources/Commands/PackageCommands/AddTargetDependency.swift b/Sources/Commands/PackageCommands/AddTargetDependency.swift index e99cd1a9e57..ad8cb7829fc 100644 --- a/Sources/Commands/PackageCommands/AddTargetDependency.swift +++ b/Sources/Commands/PackageCommands/AddTargetDependency.swift @@ -16,8 +16,8 @@ import CoreCommands import Foundation import PackageGraph import PackageModel -import PackageModelSyntax import SwiftParser +@_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax import TSCBasic import TSCUtility @@ -66,17 +66,19 @@ extension SwiftPackageCommand { } } - let dependency: TargetDescription.Dependency + let dependency: PackageTarget.Dependency if let package { dependency = .product(name: dependencyName, package: package) } else { - dependency = .target(name: dependencyName, condition: nil) + dependency = .target(name: dependencyName) } - let editResult = try PackageModelSyntax.AddTargetDependency.addTargetDependency( - dependency, - targetName: targetName, - to: manifestSyntax + let editResult = try SwiftRefactor.AddTargetDependency.manifestRefactor( + syntax: manifestSyntax, + in: .init( + dependency: dependency, + targetName: targetName + ) ) try editResult.applyEdits( diff --git a/Sources/Commands/PackageCommands/Migrate.swift b/Sources/Commands/PackageCommands/Migrate.swift index 9afe959d307..fca4dd4b3fe 100644 --- a/Sources/Commands/PackageCommands/Migrate.swift +++ b/Sources/Commands/PackageCommands/Migrate.swift @@ -23,7 +23,9 @@ import OrderedCollections import PackageGraph import PackageModel -import enum PackageModelSyntax.ManifestEditError + +@_spi(PackageRefactor) +import enum SwiftRefactor.ManifestEditError import SPMBuildCore import SwiftFixIt @@ -283,16 +285,14 @@ extension SwiftPackageCommand { switch error { case .cannotFindPackage, .cannotAddSettingsToPluginTarget, - .existingDependency: + .existingDependency, + .malformedManifest: break case .cannotFindArrayLiteralArgument, // This means the target could not be found // syntactically, not that it does not exist. .cannotFindTargets, - .cannotFindTarget, - // This means the swift-tools-version is lower than - // the version where one of the setting was introduced. - .oldManifest: + .cannotFindTarget: let settings = try features.map { try $0.swiftSettingDescription }.joined(separator: ", ") diff --git a/Sources/PackageModelSyntax/PackageEditResult.swift b/Sources/Commands/Utilities/RefactoringSupport.swift similarity index 82% rename from Sources/PackageModelSyntax/PackageEditResult.swift rename to Sources/Commands/Utilities/RefactoringSupport.swift index 6de70765eeb..7ab88b3ad51 100644 --- a/Sources/PackageModelSyntax/PackageEditResult.swift +++ b/Sources/Commands/Utilities/RefactoringSupport.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift open source project // -// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See http://swift.org/LICENSE.txt for license information @@ -12,22 +12,13 @@ import Basics @_spi(FixItApplier) import SwiftIDEUtils +@_spi(PackageRefactor) import SwiftRefactor import SwiftSyntax -/// The result of editing a package, including any edits to the package -/// manifest and any new files that are introduced. -public struct PackageEditResult { - /// Edits to perform to the package manifest. - public var manifestEdits: [SourceEdit] = [] - - /// Auxiliary files to write. - public var auxiliaryFiles: [(RelativePath, SourceFileSyntax)] = [] -} - -extension PackageEditResult { +package extension PackageEdit { /// Apply the edits for the given manifest to the specified file system, /// updating the manifest to the given manifest - public func applyEdits( + func applyEdits( to filesystem: any FileSystem, manifest: SourceFileSyntax, manifestPath: AbsolutePath, @@ -55,7 +46,7 @@ extension PackageEditResult { // Write all of the auxiliary files. for (auxiliaryFileRelPath, auxiliaryFileSyntax) in auxiliaryFiles { // If the file already exists, skip it. - let filePath = rootPath.appending(auxiliaryFileRelPath) + let filePath = try rootPath.appending(RelativePath(validating: auxiliaryFileRelPath)) if filesystem.exists(filePath) { if verbose { print("Skipping \(filePath.relative(to: rootPath)) because it already exists.") @@ -93,5 +84,4 @@ extension PackageEditResult { } } } - } diff --git a/Sources/PackageModelSyntax/AddPackageDependency.swift b/Sources/PackageModelSyntax/AddPackageDependency.swift deleted file mode 100644 index af017889d33..00000000000 --- a/Sources/PackageModelSyntax/AddPackageDependency.swift +++ /dev/null @@ -1,133 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageLoading -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder - -/// Add a package dependency to a manifest's source code. -public enum AddPackageDependency { - /// The set of argument labels that can occur after the "dependencies" - /// argument in the Package initializers. - /// - /// TODO: Could we generate this from the the PackageDescription module, so - /// we don't have keep it up-to-date manually? - private static let argumentLabelsAfterDependencies: Set = [ - "targets", - "swiftLanguageVersions", - "cLanguageStandard", - "cxxLanguageStandard", - ] - - /// Produce the set of source edits needed to add the given package - /// dependency to the given manifest file. - public static func addPackageDependency( - _ dependency: MappablePackageDependency.Kind, - to manifest: SourceFileSyntax - ) throws -> PackageEditResult { - // Make sure we have a suitable tools version in the manifest. - try manifest.checkEditManifestToolsVersion() - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - guard try !dependencyAlreadyAdded( - dependency, - in: packageCall - ) else { - return PackageEditResult(manifestEdits: []) - } - - let newPackageCall = try addPackageDependencyLocal( - dependency, to: packageCall - ) - - return PackageEditResult( - manifestEdits: [ - .replace(packageCall, with: newPackageCall.description), - ] - ) - } - - /// Return `true` if the dependency already exists in the manifest, otherwise return `false`. - /// Throws an error if a dependency already exists with the same id or url, but different arguments. - private static func dependencyAlreadyAdded( - _ dependency: MappablePackageDependency.Kind, - in packageCall: FunctionCallExprSyntax - ) throws -> Bool { - let dependencySyntax = dependency.asSyntax() - guard let dependenctFnSyntax = dependencySyntax.as(FunctionCallExprSyntax.self) else { - throw ManifestEditError.cannotFindPackage - } - - guard let id = dependenctFnSyntax.arguments.first(where: { - $0.label?.text == "url" || $0.label?.text == "id" || $0.label?.text == "path" - }) else { - throw InternalError("Missing id or url argument in dependency syntax") - } - - if let existingDependencies = packageCall.findArgument(labeled: "dependencies") { - // If we have an existing dependencies array, we need to check if - if let expr = existingDependencies.expression.as(ArrayExprSyntax.self) { - // Iterate through existing dependencies and look for an argument that matches - // either the `id` or `url` argument of the new dependency. - let existingArgument = expr.elements.first { elem in - if let funcExpr = elem.expression.as(FunctionCallExprSyntax.self) { - return funcExpr.arguments.contains { - $0.trimmedDescription == id.trimmedDescription - } - } - return true - } - - if let existingArgument { - let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil) - // This exact dependency already exists, return false to indicate we should do nothing. - if normalizedExistingArgument.trimmedDescription == dependencySyntax.trimmedDescription { - return true - } - throw ManifestEditError.existingDependency(dependencyName: dependency.identifier) - } - } - } - return false - } - - /// Implementation of adding a package dependency to an existing call. - static func addPackageDependencyLocal( - _ dependency: MappablePackageDependency.Kind, - to packageCall: FunctionCallExprSyntax - ) throws -> FunctionCallExprSyntax { - try packageCall.appendingToArrayArgument( - label: "dependencies", - trailingLabels: self.argumentLabelsAfterDependencies, - newElement: dependency.asSyntax() - ) - } -} - -fileprivate extension MappablePackageDependency.Kind { - var identifier: String { - switch self { - case .sourceControl(let name, let path, _): - return name ?? path - case .fileSystem(let name, let location): - return name ?? location - case .registry(let id, _): - return id - } - } -} \ No newline at end of file diff --git a/Sources/PackageModelSyntax/AddProduct.swift b/Sources/PackageModelSyntax/AddProduct.swift deleted file mode 100644 index 3e058232f3a..00000000000 --- a/Sources/PackageModelSyntax/AddProduct.swift +++ /dev/null @@ -1,58 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder - -/// Add a product to the manifest's source code. -public struct AddProduct { - /// The set of argument labels that can occur after the "products" - /// argument in the Package initializers. - /// - /// TODO: Could we generate this from the the PackageDescription module, so - /// we don't have keep it up-to-date manually? - private static let argumentLabelsAfterProducts: Set = [ - "dependencies", - "targets", - "swiftLanguageVersions", - "cLanguageStandard", - "cxxLanguageStandard" - ] - - /// Produce the set of source edits needed to add the given package - /// dependency to the given manifest file. - public static func addProduct( - _ product: ProductDescription, - to manifest: SourceFileSyntax - ) throws -> PackageEditResult { - // Make sure we have a suitable tools version in the manifest. - try manifest.checkEditManifestToolsVersion() - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - let newPackageCall = try packageCall.appendingToArrayArgument( - label: "products", - trailingLabels: argumentLabelsAfterProducts, - newElement: product.asSyntax() - ) - - return PackageEditResult( - manifestEdits: [ - .replace(packageCall, with: newPackageCall.description) - ] - ) - } -} diff --git a/Sources/PackageModelSyntax/AddSwiftSetting.swift b/Sources/PackageModelSyntax/AddSwiftSetting.swift deleted file mode 100644 index ed39451bdfc..00000000000 --- a/Sources/PackageModelSyntax/AddSwiftSetting.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder -import struct TSCUtility.Version - -/// Add a swift setting to a manifest's source code. -public enum AddSwiftSetting { - /// The set of argument labels that can occur after the "targets" - /// argument in the Package initializers. - private static let argumentLabelsAfterSwiftSettings: Set = [ - "linkerSettings", - "plugins", - ] - - public static func upcomingFeature( - to target: String, - name: String, - manifest: SourceFileSyntax - ) throws -> PackageEditResult { - try self.addToTarget( - target, - name: "enableUpcomingFeature", - value: name, - firstIntroduced: .v5_8, - manifest: manifest - ) - } - - public static func experimentalFeature( - to target: String, - name: String, - manifest: SourceFileSyntax - ) throws -> PackageEditResult { - try self.addToTarget( - target, - name: "enableExperimentalFeature", - value: name, - firstIntroduced: .v5_8, - manifest: manifest - ) - } - - public static func languageMode( - to target: String, - mode: SwiftLanguageVersion, - manifest: SourceFileSyntax - ) throws -> PackageEditResult { - try self.addToTarget( - target, - name: "swiftLanguageMode", - value: mode, - firstIntroduced: .v6_0, - manifest: manifest - ) - } - - public static func strictMemorySafety( - to target: String, - manifest: SourceFileSyntax - ) throws -> PackageEditResult { - try self.addToTarget( - target, name: "strictMemorySafety()", - value: String?.none, - firstIntroduced: .v6_2, - manifest: manifest - ) - } - - private static func addToTarget( - _ target: String, - name: String, - value: (some ManifestSyntaxRepresentable)?, - firstIntroduced: ToolsVersion, - manifest: SourceFileSyntax - ) throws -> PackageEditResult { - try manifest.checkManifestAtLeast(firstIntroduced) - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - guard let targetsArgument = packageCall.findArgument(labeled: "targets"), - let targetArray = targetsArgument.expression.findArrayArgument() - else { - throw ManifestEditError.cannotFindTargets - } - - let targetCall = targetArray - .elements - .lazy - .compactMap { - $0.expression.as(FunctionCallExprSyntax.self) - }.first { targetCall in - if let nameArgument = targetCall.findArgument(labeled: "name"), - let nameLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self), - nameLiteral.representedLiteralValue == target - { - return true - } - - return false - } - - guard let targetCall else { - throw ManifestEditError.cannotFindTarget(targetName: target) - } - - if let memberRef = targetCall.calledExpression.as(MemberAccessExprSyntax.self), - memberRef.declName.baseName.text == "plugin" - { - throw ManifestEditError.cannotAddSettingsToPluginTarget - } - - let newTargetCall = if let value { - try targetCall.appendingToArrayArgument( - label: "swiftSettings", - trailingLabels: self.argumentLabelsAfterSwiftSettings, - newElement: ".\(raw: name)(\(value.asSyntax()))" - ) - } else { - try targetCall.appendingToArrayArgument( - label: "swiftSettings", - trailingLabels: self.argumentLabelsAfterSwiftSettings, - newElement: ".\(raw: name)" - ) - } - - return PackageEditResult( - manifestEdits: [ - .replace(targetCall, with: newTargetCall.description), - ] - ) - } -} - -extension SwiftLanguageVersion: ManifestSyntaxRepresentable { - func asSyntax() -> ExprSyntax { - if !Self.supportedSwiftLanguageVersions.contains(self) { - return ".version(\"\(raw: rawValue)\")" - } - - if minor == 0 { - return ".v\(raw: major)" - } - - return ".v\(raw: major)_\(raw: minor)" - } -} diff --git a/Sources/PackageModelSyntax/AddTarget.swift b/Sources/PackageModelSyntax/AddTarget.swift deleted file mode 100644 index b6a081a7d67..00000000000 --- a/Sources/PackageModelSyntax/AddTarget.swift +++ /dev/null @@ -1,439 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import Foundation -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder -import struct TSCUtility.Version - -/// Add a target to a manifest's source code. -public enum AddTarget { - /// The set of argument labels that can occur after the "targets" - /// argument in the Package initializers. - /// - /// TODO: Could we generate this from the the PackageDescription module, so - /// we don't have keep it up-to-date manually? - private static let argumentLabelsAfterTargets: Set = [ - "swiftLanguageVersions", - "cLanguageStandard", - "cxxLanguageStandard", - ] - - /// The kind of test harness to use. This isn't part of the manifest - /// itself, but is used to guide the generation process. - public enum TestHarness: String, Codable { - /// Don't use any library - case none - - /// Create a test using the XCTest library. - case xctest - - /// Create a test using the swift-testing package. - case swiftTesting = "swift-testing" - - /// The default testing library to use. - public static var `default`: TestHarness = .xctest - } - - /// Additional configuration information to guide the package editing - /// process. - public struct Configuration { - /// The test harness to use. - public var testHarness: TestHarness - - public init(testHarness: TestHarness = .default) { - self.testHarness = testHarness - } - } - - // Check if the package has a single target with that target's sources located - // directly in `./Sources`. If so, move the sources into a folder named after - // the target before adding a new target. - package static func moveSingleTargetSources( - packagePath: AbsolutePath, - manifest: SourceFileSyntax, - fileSystem: any FileSystem, - verbose: Bool = false - ) throws { - // Make sure we have a suitable tools version in the manifest. - try manifest.checkEditManifestToolsVersion() - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - if let arg = packageCall.findArgument(labeled: "targets") { - guard let argArray = arg.expression.findArrayArgument() else { - throw ManifestEditError.cannotFindArrayLiteralArgument( - argumentName: "targets", - node: Syntax(arg.expression) - ) - } - - // Check the contents of the `targets` array to see if there is only one target defined. - guard argArray.elements.count == 1, - let firstTarget = argArray.elements.first?.expression.as(FunctionCallExprSyntax.self), - let targetStringLiteral = firstTarget.arguments.first?.expression.as(StringLiteralExprSyntax.self) else { - return - } - - let targetName = targetStringLiteral.segments.description - let sourcesFolder = packagePath.appending("Sources") - let expectedTargetFolder = sourcesFolder.appending(targetName) - - // If there is one target then pull its name out and use that to look for a folder in `Sources/TargetName`. - // If the folder doesn't exist then we know we have a single target package and we need to migrate files - // into this folder before we can add a new target. - if !fileSystem.isDirectory(expectedTargetFolder) { - if verbose { - print( - """ - Moving existing files from \( - sourcesFolder.relative(to: packagePath) - ) to \( - expectedTargetFolder.relative(to: packagePath) - )... - """, - terminator: "" - ) - } - let contentsToMove = try fileSystem.getDirectoryContents(sourcesFolder) - try fileSystem.createDirectory(expectedTargetFolder) - for file in contentsToMove { - let source = sourcesFolder.appending(file) - let destination = expectedTargetFolder.appending(file) - try fileSystem.move(from: source, to: destination) - } - if verbose { - print(" done.") - } - } - } - } - - /// Add the given target to the manifest, producing a set of edit results - /// that updates the manifest and adds some source files to stub out the - /// new target. - public static func addTarget( - _ target: TargetDescription, - to manifest: SourceFileSyntax, - configuration: Configuration = .init(), - installedSwiftPMConfiguration: InstalledSwiftPMConfiguration = .default - ) throws -> PackageEditResult { - // Make sure we have a suitable tools version in the manifest. - try manifest.checkEditManifestToolsVersion() - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - // Create a mutable version of target to which we can add more - // content when needed. - var target = target - - // Add dependencies needed for various targets. - switch target.type { - case .macro: - // Macro targets need to depend on a couple of libraries from - // SwiftSyntax. - target.dependencies.append(contentsOf: macroTargetDependencies) - - default: - break - } - - var newPackageCall = try packageCall.appendingToArrayArgument( - label: "targets", - trailingLabels: Self.argumentLabelsAfterTargets, - newElement: target.asSyntax() - ) - - let outerDirectory: String? = switch target.type { - case .binary, .plugin, .system: nil - case .executable, .regular, .macro: "Sources" - case .test: "Tests" - } - - guard let outerDirectory else { - return PackageEditResult( - manifestEdits: [ - .replace(packageCall, with: newPackageCall.description), - ] - ) - } - - let outerPath = try RelativePath(validating: outerDirectory) - - /// The set of auxiliary files this refactoring will create. - var auxiliaryFiles: AuxiliaryFiles = [] - - // Add the primary source file. Every target type has this. - self.addPrimarySourceFile( - outerPath: outerPath, - target: target, - configuration: configuration, - to: &auxiliaryFiles - ) - - // Perform any other actions that are needed for this target type. - var extraManifestEdits: [SourceEdit] = [] - switch target.type { - case .macro: - self.addProvidedMacrosSourceFile( - outerPath: outerPath, - target: target, - to: &auxiliaryFiles - ) - - if !manifest.description.contains("swift-syntax") { - newPackageCall = try AddPackageDependency - .addPackageDependencyLocal( - .swiftSyntax( - configuration: installedSwiftPMConfiguration - ), - to: newPackageCall - ) - - // Look for the first import declaration and insert an - // import of `CompilerPluginSupport` there. - let newImport = "import CompilerPluginSupport\n" - for node in manifest.statements { - if let importDecl = node.item.as(ImportDeclSyntax.self) { - let insertPos = importDecl - .positionAfterSkippingLeadingTrivia - extraManifestEdits.append( - SourceEdit( - range: insertPos ..< insertPos, - replacement: newImport - ) - ) - break - } - } - } - - default: break - } - - return PackageEditResult( - manifestEdits: [ - .replace(packageCall, with: newPackageCall.description), - ] + extraManifestEdits, - auxiliaryFiles: auxiliaryFiles - ) - } - - /// Add the primary source file for a target to the list of auxiliary - /// source files. - fileprivate static func addPrimarySourceFile( - outerPath: RelativePath, - target: TargetDescription, - configuration: Configuration, - to auxiliaryFiles: inout AuxiliaryFiles - ) { - let sourceFilePath = outerPath.appending( - components: [target.name, "\(target.name).swift"] - ) - - // Introduce imports for each of the dependencies that were specified. - var importModuleNames = target.dependencies.map(\.name) - - // Add appropriate test module dependencies. - if target.type == .test { - switch configuration.testHarness { - case .none: - break - - case .xctest: - importModuleNames.append("XCTest") - - case .swiftTesting: - importModuleNames.append("Testing") - } - } - - let importDecls = importModuleNames.lazy.sorted().map { name in - DeclSyntax("import \(raw: name)").with(\.trailingTrivia, .newline) - } - - let imports = CodeBlockItemListSyntax { - for importDecl in importDecls { - importDecl - } - } - - let sourceFileText: SourceFileSyntax = switch target.type { - case .binary, .plugin, .system: - fatalError("should have exited above") - - case .macro: - """ - \(imports) - struct \(raw: target.sanitizedName): Macro { - /// TODO: Implement one or more of the protocols that inherit - /// from Macro. The appropriate macro protocol is determined - /// by the "macro" declaration that \(raw: target.sanitizedName) implements. - /// Examples include: - /// @freestanding(expression) macro --> ExpressionMacro - /// @attached(member) macro --> MemberMacro - } - """ - - case .test: - switch configuration.testHarness { - case .none: - """ - \(imports) - // Test code here - """ - - case .xctest: - """ - \(imports) - class \(raw: target.sanitizedName)Tests: XCTestCase { - func test\(raw: target.sanitizedName)() { - XCTAssertEqual(42, 17 + 25) - } - } - """ - - case .swiftTesting: - """ - \(imports) - @Suite - struct \(raw: target.sanitizedName)Tests { - @Test("\(raw: target.sanitizedName) tests") - func example() { - #expect(42 == 17 + 25) - } - } - """ - } - - case .regular: - """ - \(imports) - """ - - case .executable: - """ - \(imports) - @main - struct \(raw: target.sanitizedName)Main { - static func main() { - print("Hello, world") - } - } - """ - } - - auxiliaryFiles.addSourceFile( - path: sourceFilePath, - sourceCode: sourceFileText - ) - } - - /// Add a file that introduces the main entrypoint and provided macros - /// for a macro target. - fileprivate static func addProvidedMacrosSourceFile( - outerPath: RelativePath, - target: TargetDescription, - to auxiliaryFiles: inout AuxiliaryFiles - ) { - auxiliaryFiles.addSourceFile( - path: outerPath.appending( - components: [target.name, "ProvidedMacros.swift"] - ), - sourceCode: """ - import SwiftCompilerPlugin - - @main - struct \(raw: target.sanitizedName)Macros: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - \(raw: target.sanitizedName).self, - ] - } - """ - ) - } -} - -extension TargetDescription.Dependency { - /// Retrieve the name of the dependency - fileprivate var name: String { - switch self { - case .target(name: let name, condition: _), - .byName(name: let name, condition: _), - .product(name: let name, package: _, moduleAliases: _, condition: _): - name - } - } -} - -/// The array of auxiliary files that can be added by a package editing -/// operation. -private typealias AuxiliaryFiles = [(RelativePath, SourceFileSyntax)] - -extension AuxiliaryFiles { - /// Add a source file to the list of auxiliary files. - fileprivate mutating func addSourceFile( - path: RelativePath, - sourceCode: SourceFileSyntax - ) { - self.append((path, sourceCode)) - } -} - -/// The set of dependencies we need to introduce to a newly-created macro -/// target. -private let macroTargetDependencies: [TargetDescription.Dependency] = [ - .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), -] - -/// The package dependency for swift-syntax, for use in macros. -extension MappablePackageDependency.Kind { - /// Source control URL for the swift-syntax package. - fileprivate static var swiftSyntaxURL: SourceControlURL { - "https://github.com/swiftlang/swift-syntax.git" - } - - /// Package dependency on the swift-syntax package. - fileprivate static func swiftSyntax( - configuration: InstalledSwiftPMConfiguration - ) -> MappablePackageDependency.Kind { - let swiftSyntaxVersionDefault = configuration - .swiftSyntaxVersionForMacroTemplate - let swiftSyntaxVersion = Version(swiftSyntaxVersionDefault.description)! - - return .sourceControl( - name: nil, - location: self.swiftSyntaxURL.absoluteString, - requirement: .range(.upToNextMajor(from: swiftSyntaxVersion)) - ) - } -} - -extension TargetDescription { - fileprivate var sanitizedName: String { - self.name - .spm_mangledToC99ExtendedIdentifier() - .localizedFirstWordCapitalized() - } -} - -extension String { - fileprivate func localizedFirstWordCapitalized() -> String { prefix(1).localizedCapitalized + dropFirst() } -} diff --git a/Sources/PackageModelSyntax/AddTargetDependency.swift b/Sources/PackageModelSyntax/AddTargetDependency.swift deleted file mode 100644 index fde0a5e69e6..00000000000 --- a/Sources/PackageModelSyntax/AddTargetDependency.swift +++ /dev/null @@ -1,103 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageLoading -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder - -/// Add a target dependency to a manifest's source code. -public struct AddTargetDependency { - /// The set of argument labels that can occur after the "dependencies" - /// argument in the various target initializers. - /// - /// TODO: Could we generate this from the the PackageDescription module, so - /// we don't have keep it up-to-date manually? - private static let argumentLabelsAfterDependencies: Set = [ - "path", - "exclude", - "sources", - "resources", - "publicHeadersPath", - "packageAccess", - "cSettings", - "cxxSettings", - "swiftSettings", - "linkerSettings", - "plugins", - ] - - /// Produce the set of source edits needed to add the given target - /// dependency to the given manifest file. - public static func addTargetDependency( - _ dependency: TargetDescription.Dependency, - targetName: String, - to manifest: SourceFileSyntax - ) throws -> PackageEditResult { - // Make sure we have a suitable tools version in the manifest. - try manifest.checkEditManifestToolsVersion() - - guard let packageCall = manifest.findCall(calleeName: "Package") else { - throw ManifestEditError.cannotFindPackage - } - - // Dig out the array of targets. - guard let targetsArgument = packageCall.findArgument(labeled: "targets"), - let targetArray = targetsArgument.expression.findArrayArgument() else { - throw ManifestEditError.cannotFindTargets - } - - // Look for a call whose name is a string literal matching the - // requested target name. - func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool { - guard let nameArgument = call.findArgument(labeled: "name") else { - return false - } - - guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self), - let literalValue = stringLiteral.representedLiteralValue else { - return false - } - - return literalValue == targetName - } - - guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else { - throw ManifestEditError.cannotFindTarget(targetName: targetName) - } - - let newTargetCall = try addTargetDependencyLocal( - dependency, to: targetCall - ) - - return PackageEditResult( - manifestEdits: [ - .replace(targetCall, with: newTargetCall.description) - ] - ) - } - - /// Implementation of adding a target dependency to an existing call. - static func addTargetDependencyLocal( - _ dependency: TargetDescription.Dependency, - to targetCall: FunctionCallExprSyntax - ) throws -> FunctionCallExprSyntax { - try targetCall.appendingToArrayArgument( - label: "dependencies", - trailingLabels: Self.argumentLabelsAfterDependencies, - newElement: dependency.asSyntax() - ) - } -} - diff --git a/Sources/PackageModelSyntax/CMakeLists.txt b/Sources/PackageModelSyntax/CMakeLists.txt deleted file mode 100644 index 02142c690da..00000000000 --- a/Sources/PackageModelSyntax/CMakeLists.txt +++ /dev/null @@ -1,45 +0,0 @@ -# This source file is part of the Swift open source project -# -# Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors - -add_library(PackageModelSyntax - AddPackageDependency.swift - AddProduct.swift - AddSwiftSetting.swift - AddTarget.swift - AddTargetDependency.swift - ManifestEditError.swift - ManifestSyntaxRepresentable.swift - PackageDependency+Syntax.swift - PackageEditResult.swift - ProductDescription+Syntax.swift - SyntaxEditUtils.swift - TargetDescription+Syntax.swift -) - -target_link_libraries(PackageModelSyntax PUBLIC - Basics - PackageLoading - PackageModel - - SwiftSyntax::SwiftBasicFormat - SwiftSyntax::SwiftDiagnostics - SwiftSyntax::SwiftIDEUtils - SwiftSyntax::SwiftParser - SwiftSyntax::SwiftSyntax - SwiftSyntax::SwiftSyntaxBuilder -) - -# NOTE(compnerd) workaround for CMake not setting up include flags yet -set_target_properties(PackageModelSyntax PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) - -install(TARGETS PackageModelSyntax - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin) -set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS PackageModelSyntax) diff --git a/Sources/PackageModelSyntax/ManifestEditError.swift b/Sources/PackageModelSyntax/ManifestEditError.swift deleted file mode 100644 index 6c4cdde0806..00000000000 --- a/Sources/PackageModelSyntax/ManifestEditError.swift +++ /dev/null @@ -1,72 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import PackageLoading -import PackageModel -import SwiftSyntax - -/// An error describing problems that can occur when attempting to edit a -/// package manifest programattically. -package enum ManifestEditError: Error { - case cannotFindPackage - case cannotFindTargets - case cannotFindTarget(targetName: String) - case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax) - case oldManifest(ToolsVersion, expected: ToolsVersion) - case cannotAddSettingsToPluginTarget - case existingDependency(dependencyName: String) -} - -extension ToolsVersion { - /// The minimum tools version of the manifest file that we support edit - /// operations on. - static let minimumManifestEditVersion = v5_5 -} - -extension ManifestEditError: CustomStringConvertible { - package var description: String { - switch self { - case .cannotFindPackage: - "invalid manifest: unable to find 'Package' declaration" - case .cannotFindTargets: - "unable to find package targets in manifest" - case .cannotFindTarget(targetName: let name): - "unable to find target named '\(name)' in package" - case .cannotFindArrayLiteralArgument(argumentName: let name, node: _): - "unable to find array literal for '\(name)' argument" - case .oldManifest(let version, let expectedVersion): - "package manifest version \(version) is too old: please update to manifest version \(expectedVersion) or newer" - case .cannotAddSettingsToPluginTarget: - "plugin targets do not support settings" - case .existingDependency(let name): - "unable to add dependency '\(name)' because it already exists in the list of dependencies" - } - } -} - -extension SourceFileSyntax { - /// Check that the manifest described by this source file meets the minimum - /// tools version requirements for editing the manifest. - func checkEditManifestToolsVersion() throws { - let toolsVersion = try ToolsVersionParser.parse(utf8String: description) - if toolsVersion < ToolsVersion.minimumManifestEditVersion { - throw ManifestEditError.oldManifest(toolsVersion, expected: ToolsVersion.minimumManifestEditVersion) - } - } - - func checkManifestAtLeast(_ version: ToolsVersion) throws { - let toolsVersion = try ToolsVersionParser.parse(utf8String: description) - if toolsVersion < version { - throw ManifestEditError.oldManifest(toolsVersion, expected: version) - } - } -} diff --git a/Sources/PackageModelSyntax/ManifestSyntaxRepresentable.swift b/Sources/PackageModelSyntax/ManifestSyntaxRepresentable.swift deleted file mode 100644 index 146bc6e3b74..00000000000 --- a/Sources/PackageModelSyntax/ManifestSyntaxRepresentable.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import SwiftSyntax -import SwiftSyntaxBuilder - -/// Describes an entity in the package model that can be represented as -/// a syntax node. -protocol ManifestSyntaxRepresentable { - /// The most specific kind of syntax node that best describes this entity - /// in the manifest. - /// - /// There might be other kinds of syntax nodes that can also represent - /// the syntax, but this is the one that a canonical manifest will use. - /// As an example, a package dependency is usually expressed as, e.g., - /// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") - /// - /// However, there could be other forms, e.g., this is also valid: - /// Package.Dependency.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") - associatedtype PreferredSyntax: SyntaxProtocol - - /// Provides a suitable syntax node to describe this entity in the package - /// model. - /// - /// The resulting syntax is a fragment that describes just this entity, - /// and it's enclosing entity will need to understand how to fit it in. - /// For example, a `PackageDependency` entity would map to syntax for - /// something like - /// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") - func asSyntax() -> PreferredSyntax -} - -extension String: ManifestSyntaxRepresentable { - typealias PreferredSyntax = ExprSyntax - - func asSyntax() -> ExprSyntax { "\(literal: self)" } -} diff --git a/Sources/PackageModelSyntax/PackageDependency+Syntax.swift b/Sources/PackageModelSyntax/PackageDependency+Syntax.swift deleted file mode 100644 index 6133b9d7344..00000000000 --- a/Sources/PackageModelSyntax/PackageDependency+Syntax.swift +++ /dev/null @@ -1,95 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageModel -import SwiftParser -import SwiftSyntax -import SwiftSyntaxBuilder -import struct TSCUtility.Version - -extension MappablePackageDependency.Kind: ManifestSyntaxRepresentable { - func asSyntax() -> ExprSyntax { - switch self { - case .fileSystem(name: _, path: let path): - ".package(path: \(literal: path.description))" - case .sourceControl(name: _, location: let location, requirement: let requirement): - ".package(url: \(literal: location.description), \(requirement.asSyntax()))" - case .registry(id: let id, requirement: let requirement): - ".package(id: \(literal: id.description), \(requirement.asSyntax()))" - } - } -} - -extension PackageDependency.SourceControl.Requirement: ManifestSyntaxRepresentable { - func asSyntax() -> LabeledExprSyntax { - switch self { - case .exact(let version): - LabeledExprSyntax( - label: "exact", - expression: version.asSyntax() - ) - - case .range(let range) where range == .upToNextMajor(from: range.lowerBound): - LabeledExprSyntax( - label: "from", - expression: range.lowerBound.asSyntax() - ) - - case .range(let range): - LabeledExprSyntax( - expression: "\(range.lowerBound.asSyntax())..<\(range.upperBound.asSyntax())" as ExprSyntax - ) - - case .revision(let revision): - LabeledExprSyntax( - label: "revision", - expression: "\(literal: revision)" as ExprSyntax - ) - - case .branch(let branch): - LabeledExprSyntax( - label: "branch", - expression: "\(literal: branch)" as ExprSyntax - ) - } - } -} - -extension PackageDependency.Registry.Requirement: ManifestSyntaxRepresentable { - func asSyntax() -> LabeledExprSyntax { - switch self { - case .exact(let version): - LabeledExprSyntax( - label: "exact", - expression: version.asSyntax() - ) - - case .range(let range) where range == .upToNextMajor(from: range.lowerBound): - LabeledExprSyntax( - label: "from", - expression: range.lowerBound.asSyntax() - ) - - case .range(let range): - LabeledExprSyntax( - expression: "\(range.lowerBound.asSyntax())..<\(range.upperBound.asSyntax())" as ExprSyntax - ) - } - } -} - -extension Version: ManifestSyntaxRepresentable { - func asSyntax() -> ExprSyntax { - "\(literal: description)" - } -} diff --git a/Sources/PackageModelSyntax/ProductDescription+Syntax.swift b/Sources/PackageModelSyntax/ProductDescription+Syntax.swift deleted file mode 100644 index 614d5db8bf4..00000000000 --- a/Sources/PackageModelSyntax/ProductDescription+Syntax.swift +++ /dev/null @@ -1,62 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageModel -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftParser - -extension ProductDescription: ManifestSyntaxRepresentable { - /// The function name in the package manifest. - /// - /// Some of these are actually invalid, but it's up to the caller - /// to check the precondition. - private var functionName: String { - switch type { - case .executable: "executable" - case .library(_): "library" - case .macro: "macro" - case .plugin: "plugin" - case .snippet: "snippet" - case .test: "test" - } - } - - func asSyntax() -> ExprSyntax { - var arguments: [LabeledExprSyntax] = [] - arguments.append(label: "name", stringLiteral: name) - - // Libraries have a type. - if case .library(let libraryType) = type { - switch libraryType { - case .automatic: - break - - case .dynamic, .static: - arguments.append( - label: "type", - expression: ".\(raw: libraryType.rawValue)" - ) - } - } - - arguments.appendIfNonEmpty( - label: "targets", - arrayLiteral: targets - ) - - let separateParen: String = arguments.count > 1 ? "\n" : "" - let argumentsSyntax = LabeledExprListSyntax(arguments) - return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" - } -} diff --git a/Sources/PackageModelSyntax/SyntaxEditUtils.swift b/Sources/PackageModelSyntax/SyntaxEditUtils.swift deleted file mode 100644 index 6e3c2a2a518..00000000000 --- a/Sources/PackageModelSyntax/SyntaxEditUtils.swift +++ /dev/null @@ -1,520 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageModel -import SwiftBasicFormat -import SwiftSyntax -import SwiftParser -import SwiftSyntaxBuilder - -/// Default indent when we have to introduce indentation but have no context -/// to get it right. -let defaultIndent = TriviaPiece.spaces(4) - -extension Trivia { - /// Determine whether this trivia has newlines or not. - var hasNewlines: Bool { - contains(where: \.isNewline) - } - - /// Produce trivia from the last newline to the end, dropping anything - /// prior to that. - func onlyLastLine() -> Trivia { - guard let lastNewline = pieces.lastIndex(where: { $0.isNewline }) else { - return self - } - - return Trivia(pieces: pieces[lastNewline...]) - } -} - -/// Syntax walker to find the first occurrence of a given node kind that -/// matches a specific predicate. -private class FirstNodeFinder: SyntaxAnyVisitor { - var predicate: (Node) -> Bool - var found: Node? = nil - - init(predicate: @escaping (Node) -> Bool) { - self.predicate = predicate - super.init(viewMode: .sourceAccurate) - } - - override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { - if found != nil { - return .skipChildren - } - - if let matchedNode = node.as(Node.self), predicate(matchedNode) { - found = matchedNode - return .skipChildren - } - - return .visitChildren - } -} - -extension SyntaxProtocol { - /// Find the first node of the Self type that matches the given predicate. - static func findFirst( - in node: some SyntaxProtocol, - matching predicate: (Self) -> Bool - ) -> Self? { - withoutActuallyEscaping(predicate) { escapingPredicate in - let visitor = FirstNodeFinder(predicate: escapingPredicate) - visitor.walk(node) - return visitor.found - } - } -} - -extension FunctionCallExprSyntax { - /// Check whether this call expression has a callee that is a reference - /// to a declaration with the given name. - func hasCallee(named name: String) -> Bool { - guard let calleeDeclRef = calledExpression.as(DeclReferenceExprSyntax.self) else { - return false - } - - return calleeDeclRef.baseName.text == name - } - - /// Find a call argument based on its label. - func findArgument(labeled label: String) -> LabeledExprSyntax? { - arguments.first { $0.label?.text == label } - } - - /// Find a call argument index based on its label. - func findArgumentIndex(labeled label: String) -> LabeledExprListSyntax.Index? { - arguments.firstIndex { $0.label?.text == label } - } -} - -extension LabeledExprListSyntax { - /// Find the index at which the one would insert a new argument given - /// the set of argument labels that could come after the argument we - /// want to insert. - func findArgumentInsertionPosition( - labelsAfter: Set - ) -> SyntaxChildrenIndex { - firstIndex { - guard let label = $0.label else { - return false - } - - return labelsAfter.contains(label.text) - } ?? endIndex - } - - /// Form a new argument list that inserts a new argument at the specified - /// position in this argument list. - /// - /// This operation will attempt to introduce trivia to match the - /// surrounding context where possible. The actual argument will be - /// created by the `generator` function, which is provided with leading - /// trivia and trailing comma it should use to match the surrounding - /// context. - func insertingArgument( - at position: SyntaxChildrenIndex, - generator: (Trivia, TokenSyntax?) -> LabeledExprSyntax - ) -> LabeledExprListSyntax { - // Turn the arguments into an array so we can manipulate them. - var arguments = Array(self) - - let positionIdx = distance(from: startIndex, to: position) - - let commaToken = TokenSyntax.commaToken() - - // Figure out leading trivia and adjust the prior argument (if there is - // one) by adding a comma, if necessary. - let leadingTrivia: Trivia - if position > startIndex { - let priorArgument = arguments[positionIdx - 1] - - // Our leading trivia will be based on the prior argument's leading - // trivia. - leadingTrivia = priorArgument.leadingTrivia - - // If the prior argument is missing a trailing comma, add one. - if priorArgument.trailingComma == nil { - arguments[positionIdx - 1].trailingComma = commaToken - } - } else if positionIdx + 1 < count { - leadingTrivia = arguments[positionIdx + 1].leadingTrivia - } else { - leadingTrivia = Trivia() - } - - // Determine whether we need a trailing comma on this argument. - let trailingComma: TokenSyntax? - if position < endIndex { - trailingComma = commaToken - } else { - trailingComma = nil - } - - // Create the argument and insert it into the argument list. - let argument = generator(leadingTrivia, trailingComma) - arguments.insert(argument, at: positionIdx) - - return LabeledExprListSyntax(arguments) - } -} - -extension SyntaxProtocol { - /// Look for a call expression to a callee with the given name. - func findCall(calleeName: String) -> FunctionCallExprSyntax? { - return FunctionCallExprSyntax.findFirst(in: self) { call in - return call.hasCallee(named: calleeName) - } - } -} - -extension ArrayExprSyntax { - /// Produce a new array literal expression that appends the given - /// element, while trying to maintain similar indentation. - func appending( - element: ExprSyntax, - outerLeadingTrivia: Trivia - ) -> ArrayExprSyntax { - var elements = self.elements - - let commaToken = TokenSyntax.commaToken() - - // If there are already elements, tack it on. - let leadingTrivia: Trivia - let trailingTrivia: Trivia - let leftSquareTrailingTrivia: Trivia - if let last = elements.last { - // The leading trivia of the new element should match that of the - // last element. - leadingTrivia = last.leadingTrivia.onlyLastLine() - - // Add a trailing comma to the last element if it isn't already - // there. - if last.trailingComma == nil { - var newElements = Array(elements) - newElements[newElements.count - 1].trailingComma = commaToken - newElements[newElements.count - 1].expression.trailingTrivia = - Trivia() - newElements[newElements.count - 1].trailingTrivia = last.trailingTrivia - elements = ArrayElementListSyntax(newElements) - } - - trailingTrivia = Trivia() - leftSquareTrailingTrivia = leftSquare.trailingTrivia - } else { - leadingTrivia = outerLeadingTrivia.appending(defaultIndent) - trailingTrivia = outerLeadingTrivia - if leftSquare.trailingTrivia.hasNewlines { - leftSquareTrailingTrivia = leftSquare.trailingTrivia - } else { - leftSquareTrailingTrivia = Trivia() - } - } - - elements.append( - ArrayElementSyntax( - expression: element.with(\.leadingTrivia, leadingTrivia), - trailingComma: commaToken.with(\.trailingTrivia, trailingTrivia) - ) - ) - - let newLeftSquare = leftSquare.with( - \.trailingTrivia, - leftSquareTrailingTrivia - ) - - return with(\.elements, elements).with(\.leftSquare, newLeftSquare) - } -} - -extension ExprSyntax { - /// Find an array argument either at the top level or within a sequence - /// expression. - func findArrayArgument() -> ArrayExprSyntax? { - if let arrayExpr = self.as(ArrayExprSyntax.self) { - return arrayExpr - } - - if let sequenceExpr = self.as(SequenceExprSyntax.self) { - return sequenceExpr.elements.lazy.compactMap { - $0.findArrayArgument() - }.first - } - - return nil - } -} - -// MARK: Utilities to oeprate on arrays of array literal elements. -extension Array { - /// Append a new argument expression. - mutating func append(expression: ExprSyntax) { - // Add a comma on the prior expression, if there is one. - let leadingTrivia: Trivia? - if count > 0 { - self[count - 1].trailingComma = TokenSyntax.commaToken() - leadingTrivia = .newline - - // Adjust the first element to start with a newline - if count == 1 { - self[0].leadingTrivia = .newline - } - } else { - leadingTrivia = nil - } - - append( - ArrayElementSyntax( - leadingTrivia: leadingTrivia, - expression: expression - ) - ) - } -} - -// MARK: Utilities to operate on arrays of call arguments. - -extension Array { - /// Append a potentially labeled argument with the argument expression. - mutating func append(label: String?, expression: ExprSyntax) { - // Add a comma on the prior expression, if there is one. - let leadingTrivia: Trivia - if count > 0 { - self[count - 1].trailingComma = TokenSyntax.commaToken() - leadingTrivia = .newline - - // Adjust the first element to start with a newline - if count == 1 { - self[0].leadingTrivia = .newline - } - } else { - leadingTrivia = Trivia() - } - - // Add the new expression. - append( - LabeledExprSyntax( - label: label, - expression: expression - ).with(\.leadingTrivia, leadingTrivia) - ) - } - - /// Append a potentially labeled argument with a string literal. - mutating func append(label: String?, stringLiteral: String) { - append(label: label, expression: "\(literal: stringLiteral)") - } - - /// Append a potentially labeled argument with a string literal, but only - /// when the string literal is not nil. - mutating func appendIf(label: String?, stringLiteral: String?) { - if let stringLiteral { - append(label: label, stringLiteral: stringLiteral) - } - } - - /// Append an array literal containing elements that can be rendered - /// into expression syntax nodes. - mutating func append( - label: String?, - arrayLiteral: [T] - ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { - var elements: [ArrayElementSyntax] = [] - for element in arrayLiteral { - elements.append(expression: element.asSyntax()) - } - - // Figure out the trivia for the left and right square - let leftSquareTrailingTrivia: Trivia - let rightSquareLeadingTrivia: Trivia - switch elements.count { - case 0: - // Put a single space between the square brackets. - leftSquareTrailingTrivia = Trivia() - rightSquareLeadingTrivia = .space - - case 1: - // Put spaces around the single element - leftSquareTrailingTrivia = .space - rightSquareLeadingTrivia = .space - - default: - // Each of the elements will have a leading newline. Add a leading - // newline before the close bracket. - leftSquareTrailingTrivia = Trivia() - rightSquareLeadingTrivia = .newline - } - - let array = ArrayExprSyntax( - leftSquare: .leftSquareToken( - trailingTrivia: leftSquareTrailingTrivia - ), - elements: ArrayElementListSyntax(elements), - rightSquare: .rightSquareToken( - leadingTrivia: rightSquareLeadingTrivia - ) - ) - append(label: label, expression: ExprSyntax(array)) - } - - /// Append an array literal containing elements that can be rendered - /// into expression syntax nodes. - mutating func appendIf( - label: String?, - arrayLiteral: [T]? - ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { - guard let arrayLiteral else { return } - append(label: label, arrayLiteral: arrayLiteral) - } - - /// Append an array literal containing elements that can be rendered - /// into expression syntax nodes, but only if it's not empty. - mutating func appendIfNonEmpty( - label: String?, - arrayLiteral: [T] - ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { - if arrayLiteral.isEmpty { return } - - append(label: label, arrayLiteral: arrayLiteral) - } -} - -// MARK: Utilities for adding arguments into calls. -fileprivate class ReplacingRewriter: SyntaxRewriter { - let childNode: Syntax - let newChildNode: Syntax - - init(childNode: Syntax, newChildNode: Syntax) { - self.childNode = childNode - self.newChildNode = newChildNode - super.init() - } - - override func visitAny(_ node: Syntax) -> Syntax? { - if node == childNode { - return newChildNode - } - - return nil - } -} - -fileprivate extension SyntaxProtocol { - /// Replace the given child with a new child node. - func replacingChild(_ childNode: Syntax, with newChildNode: Syntax) -> Self { - return ReplacingRewriter( - childNode: childNode, - newChildNode: newChildNode - ).rewrite(self).cast(Self.self) - } -} - -extension FunctionCallExprSyntax { - /// Produce source edits that will add the given new element to the - /// array for an argument with the given label (if there is one), or - /// introduce a new argument with an array literal containing only the - /// new element. - /// - /// - Parameters: - /// - label: The argument label for the argument whose array will be - /// added or modified. - /// - trailingLabels: The argument labels that could follow the label, - /// which helps determine where the argument should be inserted if - /// it doesn't exist yet. - /// - newElement: The new element. - /// - Returns: the function call after making this change. - func appendingToArrayArgument( - label: String, - trailingLabels: Set, - newElement: ExprSyntax - ) throws -> FunctionCallExprSyntax { - // If there is already an argument with this name, append to the array - // literal in there. - if let arg = findArgument(labeled: label) { - guard let argArray = arg.expression.findArrayArgument() else { - throw ManifestEditError.cannotFindArrayLiteralArgument( - argumentName: label, - node: Syntax(arg.expression) - ) - } - - // Format the element appropriately for the context. - let indentation = Trivia( - pieces: arg.leadingTrivia.filter { $0.isSpaceOrTab } - ) - let format = BasicFormat( - indentationWidth: [ defaultIndent ], - initialIndentation: indentation.appending(defaultIndent) - ) - let formattedElement = newElement.formatted(using: format) - .cast(ExprSyntax.self) - - let updatedArgArray = argArray.appending( - element: formattedElement, - outerLeadingTrivia: arg.leadingTrivia - ) - - return replacingChild(Syntax(argArray), with: Syntax(updatedArgArray)) - } - - // There was no argument, so we need to create one. - - // Insert the new argument at the appropriate place in the call. - let insertionPos = arguments.findArgumentInsertionPosition( - labelsAfter: trailingLabels - ) - let newArguments = arguments.insertingArgument( - at: insertionPos - ) { (leadingTrivia, trailingComma) in - // Format the element appropriately for the context. - let indentation = Trivia(pieces: leadingTrivia.filter { $0.isSpaceOrTab }) - let format = BasicFormat( - indentationWidth: [ defaultIndent ], - initialIndentation: indentation.appending(defaultIndent) - ) - let formattedElement = newElement.formatted(using: format) - .cast(ExprSyntax.self) - - // Form the array. - let newArgument = ArrayExprSyntax( - leadingTrivia: .space, - leftSquare: .leftSquareToken( - trailingTrivia: .newline - ), - elements: ArrayElementListSyntax( - [ - ArrayElementSyntax( - expression: formattedElement, - trailingComma: .commaToken() - ) - ] - ), - rightSquare: .rightSquareToken( - leadingTrivia: leadingTrivia - ) - ) - - // Create the labeled argument for the array. - return LabeledExprSyntax( - leadingTrivia: leadingTrivia, - label: "\(raw: label)", - colon: .colonToken(), - expression: ExprSyntax(newArgument), - trailingComma: trailingComma - ) - } - - return with(\.arguments, newArguments) - } -} diff --git a/Sources/PackageModelSyntax/TargetDescription+Syntax.swift b/Sources/PackageModelSyntax/TargetDescription+Syntax.swift deleted file mode 100644 index eed59e5fcae..00000000000 --- a/Sources/PackageModelSyntax/TargetDescription+Syntax.swift +++ /dev/null @@ -1,99 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Basics -import PackageModel -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftParser - -extension TargetDescription: ManifestSyntaxRepresentable { - /// The function name in the package manifest. - private var functionName: String { - switch type { - case .binary: "binaryTarget" - case .executable: "executableTarget" - case .macro: "macro" - case .plugin: "plugin" - case .regular: "target" - case .system: "systemLibrary" - case .test: "testTarget" - } - } - - func asSyntax() -> ExprSyntax { - var arguments: [LabeledExprSyntax] = [] - arguments.append(label: "name", stringLiteral: name) - // FIXME: pluginCapability - - arguments.appendIfNonEmpty( - label: "dependencies", - arrayLiteral: dependencies - ) - - arguments.appendIf(label: "path", stringLiteral: path) - arguments.appendIf(label: "url", stringLiteral: url) - arguments.appendIfNonEmpty(label: "exclude", arrayLiteral: exclude) - arguments.appendIf(label: "sources", arrayLiteral: sources) - - // FIXME: resources - - arguments.appendIf( - label: "publicHeadersPath", - stringLiteral: publicHeadersPath - ) - - if !packageAccess { - arguments.append( - label: "packageAccess", - expression: "false" - ) - } - - // FIXME: cSettings - // FIXME: cxxSettings - // FIXME: swiftSettings - // FIXME: linkerSettings - // FIXME: plugins - - arguments.appendIf(label: "pkgConfig", stringLiteral: pkgConfig) - // FIXME: providers - - // Only for plugins - arguments.appendIf(label: "checksum", stringLiteral: checksum) - - let separateParen: String = arguments.count > 1 ? "\n" : "" - let argumentsSyntax = LabeledExprListSyntax(arguments) - return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" - } -} - -extension TargetDescription.Dependency: ManifestSyntaxRepresentable { - func asSyntax() -> ExprSyntax { - switch self { - case .byName(name: let name, condition: nil): - "\(literal: name)" - - case .target(name: let name, condition: nil): - ".target(name: \(literal: name))" - - case .product(name: let name, package: nil, moduleAliases: nil, condition: nil): - ".product(name: \(literal: name))" - - case .product(name: let name, package: let package, moduleAliases: nil, condition: nil): - ".product(name: \(literal: name), package: \(literal: package))" - - default: - fatalError() - } - } -} diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 8469e246e44..34fe38063a2 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -2333,8 +2333,8 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { error.stderr, .contains( """ - error: Could not update manifest to enable requested features for target 'A' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' - error: Could not update manifest to enable requested features for target 'B' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' + error: Could not update manifest to enable requested features for target 'A' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer) + error: Could not update manifest to enable requested features for target 'B' (package manifest version 5.8.0 is too old: please update to manifest version 6.2.0 or newer) error: Could not update manifest to enable requested features for target 'CannotFindSettings' (unable to find array literal for 'swiftSettings' argument). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' error: Could not update manifest to enable requested features for target 'CannotFindTarget' (unable to find target named 'CannotFindTarget' in package). Please enable them manually by adding the following Swift settings to the target: '.enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InferIsolatedConformances"), .strictMemorySafety()' """ diff --git a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift b/Tests/PackageModelSyntaxTests/ManifestEditTests.swift deleted file mode 100644 index c0f8d7d5e69..00000000000 --- a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift +++ /dev/null @@ -1,1207 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See http://swift.org/LICENSE.txt for license information -// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -import Basics -import PackageModel -import PackageModelSyntax -import _InternalTestSupport -@_spi(FixItApplier) import SwiftIDEUtils -import SwiftParser -import SwiftSyntax -import struct TSCUtility.Version -import XCTest - -/// Assert that applying the given edit/refactor operation to the manifest -/// produces the expected manifest source file and the expected auxiliary -/// files. -func assertManifestRefactor( - _ originalManifest: SourceFileSyntax, - expectedManifest: SourceFileSyntax, - expectedAuxiliarySources: [RelativePath: SourceFileSyntax] = [:], - file: StaticString = #filePath, - line: UInt = #line, - operation: (SourceFileSyntax) throws -> PackageEditResult -) rethrows { - let edits = try operation(originalManifest) - let editedManifestSource = FixItApplier.apply( - edits: edits.manifestEdits, - to: originalManifest - ) - - let editedManifest = Parser.parse(source: editedManifestSource) - assertStringsEqualWithDiff( - editedManifest.description, - expectedManifest.description, - file: file, - line: line - ) - - // Check all of the auxiliary sources. - for (auxSourcePath, auxSourceSyntax) in edits.auxiliaryFiles { - guard let expectedSyntax = expectedAuxiliarySources[auxSourcePath] else { - XCTFail("unexpected auxiliary source file \(auxSourcePath)") - return - } - - assertStringsEqualWithDiff( - auxSourceSyntax.description, - expectedSyntax.description, - file: file, - line: line - ) - } - - XCTAssertEqual( - edits.auxiliaryFiles.count, - expectedAuxiliarySources.count, - "didn't get all of the auxiliary files we expected" - ) -} - -class ManifestEditTests: XCTestCase { - static let swiftSystemURL: String = "https://github.com/apple/swift-system.git" - static let swiftSystemPackageDependency: MappablePackageDependency.Kind = .sourceControl( - name: nil, - location: swiftSystemURL, - requirement: .branch("main") - ) - func testAddPackageDependencyExistingComma() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), - ] - ) - """, expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), - .package(url: "https://github.com/apple/swift-system.git", branch: "main"), - ] - ) - """) { manifest in - try AddPackageDependency.addPackageDependency( - .sourceControl(name: nil, location: Self.swiftSystemURL, requirement: .branch("main")), - to: manifest - ) - } - } - - func testAddPackageDependencyExistingNoComma() throws { - try XCTSkipOnWindows( - because: "Test appears to hang", - skipPlatformCi: true, - ) - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") - ] - ) - """, expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), - .package(url: "https://github.com/apple/swift-system.git", exact: "510.0.0"), - ] - ) - """) { manifest in - try AddPackageDependency.addPackageDependency( - .sourceControl(name: nil, location: Self.swiftSystemURL, requirement: .exact("510.0.0")), - to: manifest - ) - } - } - - func testAddPackageDependencyExistingAppended() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") - ] + [] - ) - """, expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), - .package(url: "https://github.com/apple/swift-system.git", from: "510.0.0"), - ] + [] - ) - """) { manifest in - let versionRange = Range.upToNextMajor(from: Version(510, 0, 0)) - - return try AddPackageDependency.addPackageDependency( - .sourceControl(name: nil, location: Self.swiftSystemURL, requirement: .range(versionRange)), - to: manifest - ) - } - } - - func testAddPackageDependencyExistingOneLine() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") ] - ) - """, expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), .package(url: "https://github.com/apple/swift-system.git", from: "510.0.0"),] - ) - """) { manifest in - let versionRange = Range.upToNextMajor(from: Version(510, 0, 0)) - - return try AddPackageDependency.addPackageDependency( - .sourceControl(name: nil, location: Self.swiftSystemURL, requirement: .range(versionRange)), - to: manifest - ) - } - } - func testAddPackageDependencyExistingEmpty() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ ] - ) - """, - expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/apple/swift-system.git", "508.0.0" ..< "510.0.0"), - ] - ) - """) { manifest in - try AddPackageDependency.addPackageDependency( - .sourceControl(name: nil, location: Self.swiftSystemURL, requirement: .range(Version(508,0,0).. ExpressionMacro - /// @attached(member) macro --> MemberMacro - } - """, - RelativePath("Sources/MyMacro target-name/ProvidedMacros.swift") : """ - import SwiftCompilerPlugin - - @main - struct MyMacro_target_nameMacros: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - MyMacro_target_name.self, - ] - } - """ - ] - ) { manifest in - try AddTarget.addTarget( - TargetDescription(name: "MyMacro target-name", type: .macro), - to: manifest - ) - } - } - - func testAddSwiftTestingTestTarget() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages" - ) - """, - expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - targets: [ - .testTarget(name: "MyTest target-name"), - ] - ) - """, - expectedAuxiliarySources: [ - RelativePath("Tests/MyTest target-name/MyTest target-name.swift") : """ - import Testing - - @Suite - struct MyTest_target_nameTests { - @Test("MyTest_target_name tests") - func example() { - #expect(42 == 17 + 25) - } - } - """ - ]) { manifest in - try AddTarget.addTarget( - TargetDescription( - name: "MyTest target-name", - type: .test - ), - to: manifest, - configuration: .init( - testHarness: .swiftTesting - ) - ) - } - } - - func testAddTargetDependency() throws { - try assertManifestRefactor(""" - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"), - ], - targets: [ - .testTarget( - name: "MyTest" - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - dependencies: [ - .package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"), - ], - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - .product(name: "SomethingOrOther", package: "swift-example"), - ] - ), - ] - ) - """) { manifest in - try AddTargetDependency.addTargetDependency( - .product(name: "SomethingOrOther", package: "swift-example"), - targetName: "MyTest", - to: manifest - ) - } - } - - func testAddSwiftSettings() throws { - XCTAssertThrows( - try AddSwiftSetting.upcomingFeature( - to: "MyTest", - name: "ExistentialAny", - manifest: """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - targets: [ - .executableTarget( - name: "MyTest" - ) - ] - ) - """ - ) - ) { (error: ManifestEditError) in - if case .oldManifest(.v5_5, .v5_8) = error { - return true - } else { - return false - } - } - - XCTAssertThrows( - try AddSwiftSetting.upcomingFeature( - to: "OtherTest", - name: "ExistentialAny", - manifest: """ - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .executableTarget( - name: "MyTest" - ) - ] - ) - """ - ) - ) { (error: ManifestEditError) in - if case .cannotFindTarget("OtherTest") = error { - return true - } else { - return false - } - } - - XCTAssertThrows( - try AddSwiftSetting.upcomingFeature( - to: "MyPlugin", - name: "ExistentialAny", - manifest: """ - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .plugin( - name: "MyPlugin", - capability: .buildTool - ) - ] - ) - """ - ) - ) { (error: ManifestEditError) in - if case .cannotAddSettingsToPluginTarget = error { - return true - } else { - return false - } - } - - - try assertManifestRefactor(""" - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ] - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .enableUpcomingFeature("ExistentialAny:migratable"), - ] - ), - ] - ) - """) { manifest in - try AddSwiftSetting.upcomingFeature( - to: "MyTest", - name: "ExistentialAny:migratable", - manifest: manifest - ) - } - - try assertManifestRefactor(""" - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .enableExperimentalFeature("Extern") - ] - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .enableExperimentalFeature("Extern"), - .enableExperimentalFeature("TrailingComma"), - ] - ), - ] - ) - """) { manifest in - try AddSwiftSetting.experimentalFeature( - to: "MyTest", - name: "TrailingComma", - manifest: manifest - ) - } - - try assertManifestRefactor(""" - // swift-tools-version: 6.2 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ] - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 6.2 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .strictMemorySafety(), - ] - ), - ] - ) - """) { manifest in - try AddSwiftSetting.strictMemorySafety( - to: "MyTest", - manifest: manifest - ) - } - - try assertManifestRefactor(""" - // swift-tools-version: 6.0 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ] - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 6.0 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .swiftLanguageMode(.v5), - ] - ), - ] - ) - """) { manifest in - try AddSwiftSetting.languageMode( - to: "MyTest", - mode: .init(string: "5")!, - manifest: manifest - ) - } - - // Custom language mode - try assertManifestRefactor(""" - // swift-tools-version: 6.0 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ] - ), - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 6.0 - let package = Package( - name: "packages", - targets: [ - .testTarget( - name: "MyTest", - dependencies: [ - ], - swiftSettings: [ - .swiftLanguageMode(.version("6.2")), - ] - ), - ] - ) - """) { manifest in - try AddSwiftSetting.languageMode( - to: "MyTest", - mode: .init(string: "6.2")!, - manifest: manifest - ) - } - - try assertManifestRefactor(""" - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .target( - name: "MyTest", - dependencies: [ - .byName(name: "Dependency") - ] - ), - .target( - name: "Dependency" - ) - ] - ) - """, - expectedManifest: """ - // swift-tools-version: 5.8 - let package = Package( - name: "packages", - targets: [ - .target( - name: "MyTest", - dependencies: [ - .byName(name: "Dependency") - ] - ), - .target( - name: "Dependency", - swiftSettings: [ - .enableUpcomingFeature("ExistentialAny"), - ] - ) - ] - ) - """) { manifest in - try AddSwiftSetting.upcomingFeature( - to: "Dependency", - name: "ExistentialAny", - manifest: manifest - ) - } - } -} -/// Assert that applying the moveSingleTargetSources operation to the manifest -/// produces the expected file system. -func assertFileSystemRefactor( - _ manifest: String, - inputFileLayout: [AbsolutePath] = [], - expectedFileLayout: [AbsolutePath] = [], - file: StaticString = #filePath, - line: UInt = #line -) throws { - let mockFileSystem = InMemoryFileSystem(); - for path in inputFileLayout { - try mockFileSystem.writeFileContents(path, string: "print(\"Hello, world!\")") - } - - try AddTarget.moveSingleTargetSources( - packagePath: AbsolutePath("/"), - manifest: Parser.parse(source: manifest), - fileSystem: mockFileSystem - ) - - for path in expectedFileLayout { - XCTAssertTrue(mockFileSystem.exists(path)) - } - - let unexpectedFiles = inputFileLayout.filter { !expectedFileLayout.contains($0) } - for path in unexpectedFiles { - XCTAssertFalse(mockFileSystem.exists(path)) - } -} - -class SingleTargetSourceTests: XCTestCase { - func testMoveSingleTargetSources() throws { - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - targets: [ - .executableTarget(name: "Foo"), - ] - ) - """, - inputFileLayout: [AbsolutePath("/Sources/Foo.swift")], - expectedFileLayout: [AbsolutePath("/Sources/Foo/Foo.swift")] - ) - } - - func testMoveSingleTargetSourcesNoTargets() throws { - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages" - ) - """, - inputFileLayout: [AbsolutePath("/Sources/Foo.swift")], - expectedFileLayout: [AbsolutePath("/Sources/Foo.swift")] - ) - } - - func testMoveSingleTargetSourcesAlreadyOrganized() throws { - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - targets: [ - .executableTarget(name: "Foo"), - ] - ) - """, - inputFileLayout: [AbsolutePath("/Sources/Foo/Foo.swift")], - expectedFileLayout: [AbsolutePath("/Sources/Foo/Foo.swift")] - ) - } - - func testMoveSingleTargetSourcesInvalidManifestTargets() throws { - XCTAssertThrowsError( - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.5 - let package = Package( - name: "packages", - targets: "invalid" - ) - """ - ) - ) { error in - XCTAssertTrue(error is ManifestEditError) - } - } - - func testMoveSingleTargetSourcesInvalidManifestToolsVersion() throws { - XCTAssertThrowsError( - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.4 - let package = Package( - name: "packages", - targets: [] - ) - """ - ) - ) { error in - XCTAssertTrue(error is ManifestEditError) - } - } - - func testMoveSingleTargetSourcesInvalidManifestTarget() throws { - XCTAssertThrowsError( - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.4 - let package = Package( - name: "packages", - targets: [.executableTarget(123)] - ) - """ - ) - ) { error in - XCTAssertTrue(error is ManifestEditError) - } - } - - func testMoveSingleTargetSourcesInvalidManifestPackage() throws { - XCTAssertThrowsError( - try assertFileSystemRefactor( - """ - // swift-tools-version: 5.5 - """ - ) - ) { error in - XCTAssertTrue(error is ManifestEditError) - } - } -} - - -// FIXME: Copy-paste from _SwiftSyntaxTestSupport - -/// Asserts that the two strings are equal, providing Unix `diff`-style output if they are not. -/// -/// - Parameters: -/// - actual: The actual string. -/// - expected: The expected string. -/// - message: An optional description of the failure. -/// - additionalInfo: Additional information about the failed test case that will be printed after the diff -/// - file: The file in which failure occurred. Defaults to the file name of the test case in -/// which this function was called. -/// - line: The line number on which failure occurred. Defaults to the line number on which this -/// function was called. -public func assertStringsEqualWithDiff( - _ actual: String, - _ expected: String, - _ message: String = "", - additionalInfo: @autoclosure () -> String? = nil, - file: StaticString = #filePath, - line: UInt = #line -) { - if actual == expected { - return - } - - failStringsEqualWithDiff( - actual, - expected, - message, - additionalInfo: additionalInfo(), - file: file, - line: line - ) -} - -/// Asserts that the two data are equal, providing Unix `diff`-style output if they are not. -/// -/// - Parameters: -/// - actual: The actual string. -/// - expected: The expected string. -/// - message: An optional description of the failure. -/// - additionalInfo: Additional information about the failed test case that will be printed after the diff -/// - file: The file in which failure occurred. Defaults to the file name of the test case in -/// which this function was called. -/// - line: The line number on which failure occurred. Defaults to the line number on which this -/// function was called. -public func assertDataEqualWithDiff( - _ actual: Data, - _ expected: Data, - _ message: String = "", - additionalInfo: @autoclosure () -> String? = nil, - file: StaticString = #filePath, - line: UInt = #line -) { - if actual == expected { - return - } - - // NOTE: Converting to `Stirng` here looses invalid UTF8 sequence difference, - // but at least we can see something is different. - failStringsEqualWithDiff( - String(decoding: actual, as: UTF8.self), - String(decoding: expected, as: UTF8.self), - message, - additionalInfo: additionalInfo(), - file: file, - line: line - ) -} - -/// `XCTFail` with `diff`-style output. -public func failStringsEqualWithDiff( - _ actual: String, - _ expected: String, - _ message: String = "", - additionalInfo: @autoclosure () -> String? = nil, - file: StaticString = #filePath, - line: UInt = #line -) { - let stringComparison: String - - // Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On - // older platforms, fall back to simple string comparison. - if #available(macOS 10.15, *) { - let actualLines = actual.components(separatedBy: .newlines) - let expectedLines = expected.components(separatedBy: .newlines) - - let difference = actualLines.difference(from: expectedLines) - - var result = "" - - var insertions = [Int: String]() - var removals = [Int: String]() - - for change in difference { - switch change { - case .insert(let offset, let element, _): - insertions[offset] = element - case .remove(let offset, let element, _): - removals[offset] = element - } - } - - var expectedLine = 0 - var actualLine = 0 - - while expectedLine < expectedLines.count || actualLine < actualLines.count { - if let removal = removals[expectedLine] { - result += "–\(removal)\n" - expectedLine += 1 - } else if let insertion = insertions[actualLine] { - result += "+\(insertion)\n" - actualLine += 1 - } else { - result += " \(expectedLines[expectedLine])\n" - expectedLine += 1 - actualLine += 1 - } - } - - stringComparison = result - } else { - // Fall back to simple message on platforms that don't support CollectionDifference. - stringComparison = """ - Expected: - \(expected) - - Actual: - \(actual) - """ - } - - var fullMessage = """ - \(message.isEmpty ? "Actual output does not match the expected" : message) - \(stringComparison) - """ - if let additional = additionalInfo() { - fullMessage = """ - \(fullMessage) - \(additional) - """ - } - XCTFail(fullMessage, file: file, line: line) -}