diff --git a/Sources/PackageModelSyntax/AddTargetDependency.swift b/Sources/PackageModelSyntax/AddTargetDependency.swift new file mode 100644 index 00000000000..fde0a5e69e6 --- /dev/null +++ b/Sources/PackageModelSyntax/AddTargetDependency.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// 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 index cfab869efc1..c034d8d1705 100644 --- a/Sources/PackageModelSyntax/CMakeLists.txt +++ b/Sources/PackageModelSyntax/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(PackageModelSyntax AddPackageDependency.swift AddProduct.swift AddTarget.swift + AddTargetDependency.swift ManifestEditError.swift ManifestSyntaxRepresentable.swift PackageDependency+Syntax.swift diff --git a/Sources/PackageModelSyntax/ManifestEditError.swift b/Sources/PackageModelSyntax/ManifestEditError.swift index cba8eb520dd..aaaf3351166 100644 --- a/Sources/PackageModelSyntax/ManifestEditError.swift +++ b/Sources/PackageModelSyntax/ManifestEditError.swift @@ -18,6 +18,8 @@ import SwiftSyntax /// package manifest programattically. package enum ManifestEditError: Error { case cannotFindPackage + case cannotFindTargets + case cannotFindTarget(targetName: String) case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax) case oldManifest(ToolsVersion) } @@ -33,6 +35,10 @@ extension ManifestEditError: CustomStringConvertible { 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): diff --git a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift b/Tests/PackageModelSyntaxTests/ManifestEditTests.swift index 812b72bfe84..614ca912d3e 100644 --- a/Tests/PackageModelSyntaxTests/ManifestEditTests.swift +++ b/Tests/PackageModelSyntaxTests/ManifestEditTests.swift @@ -611,6 +611,46 @@ class ManifestEditTests: XCTestCase { ) } } + + func testAddTargetDependency() throws { + try assertManifestRefactor(""" + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-testing.git", from: "0.8.0"), + ], + targets: [ + .testTarget( + name: "MyTest" + ), + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-testing.git", from: "0.8.0"), + ], + targets: [ + .testTarget( + name: "MyTest", + dependencies: [ + .product(name: "Testing", package: "swift-testing"), + ] + ), + ] + ) + """) { manifest in + try AddTargetDependency.addTargetDependency( + .product(name: "Testing", package: "swift-testing"), + targetName: "MyTest", + to: manifest + ) + } + } }