Skip to content

Commit 2a14c67

Browse files
committed
Introduce manifest editing API for "add target dependency"
As a convenience for when we want to be able to add new target dependencies, introduce a manifest editing API that adds a new target dependency to an existing target in the manifest.
1 parent 9183b7c commit 2a14c67

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Basics
14+
import PackageLoading
15+
import PackageModel
16+
import SwiftParser
17+
import SwiftSyntax
18+
import SwiftSyntaxBuilder
19+
20+
/// Add a target dependency to a manifest's source code.
21+
public struct AddTargetDependency {
22+
/// The set of argument labels that can occur after the "targets"
23+
/// argument in the Package initializers.
24+
///
25+
/// TODO: Could we generate this from the the PackageDescription module, so
26+
/// we don't have keep it up-to-date manually?
27+
private static let argumentLabelsAfterTargets: Set<String> = [
28+
"swiftLanguageVersions",
29+
"cLanguageStandard",
30+
"cxxLanguageStandard"
31+
]
32+
33+
/// The set of argument labels that can occur after the "dependencies"
34+
/// argument in the various target initializers.
35+
///
36+
/// TODO: Could we generate this from the the PackageDescription module, so
37+
/// we don't have keep it up-to-date manually?
38+
private static let argumentLabelsAfterDependencies: Set<String> = [
39+
"path",
40+
"exclude",
41+
"sources",
42+
"resources",
43+
"publicHeadersPath",
44+
"packageAccess",
45+
"cSettings",
46+
"cxxSettings",
47+
"swiftSettings",
48+
"linkerSettings",
49+
"plugins",
50+
]
51+
52+
/// Produce the set of source edits needed to add the given target
53+
/// dependency to the given manifest file.
54+
public static func addTargetDependency(
55+
_ dependency: TargetDescription.Dependency,
56+
targetName: String,
57+
to manifest: SourceFileSyntax
58+
) throws -> PackageEditResult {
59+
// Make sure we have a suitable tools version in the manifest.
60+
try manifest.checkEditManifestToolsVersion()
61+
62+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
63+
throw ManifestEditError.cannotFindPackage
64+
}
65+
66+
// Dig out the array of targets.
67+
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
68+
let targetArray = targetsArgument.expression.findArrayArgument() else {
69+
throw ManifestEditError.cannotFindTargets
70+
}
71+
72+
// Look for a call whose name is a string literal matching the
73+
// requested target name.
74+
func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool {
75+
guard let nameArgument = call.findArgument(labeled: "name") else {
76+
return false
77+
}
78+
79+
guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
80+
let literalValue = stringLiteral.representedLiteralValue else {
81+
return false
82+
}
83+
84+
return literalValue == targetName
85+
}
86+
87+
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else {
88+
throw ManifestEditError.cannotFindTarget(targetName: targetName)
89+
}
90+
91+
let newTargetCall = try addTargetDependencyLocal(
92+
dependency, to: targetCall
93+
)
94+
95+
return PackageEditResult(
96+
manifestEdits: [
97+
.replace(targetCall, with: newTargetCall.description)
98+
]
99+
)
100+
}
101+
102+
/// Implementation of adding a target dependency to an existing call.
103+
static func addTargetDependencyLocal(
104+
_ dependency: TargetDescription.Dependency,
105+
to targetCall: FunctionCallExprSyntax
106+
) throws -> FunctionCallExprSyntax {
107+
try targetCall.appendingToArrayArgument(
108+
label: "dependencies",
109+
trailingLabels: Self.argumentLabelsAfterDependencies,
110+
newElement: dependency.asSyntax()
111+
)
112+
}
113+
}
114+

Sources/PackageModelSyntax/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_library(PackageModelSyntax
1010
AddPackageDependency.swift
1111
AddProduct.swift
1212
AddTarget.swift
13+
AddTargetDependency.swift
1314
ManifestEditError.swift
1415
ManifestSyntaxRepresentable.swift
1516
PackageDependency+Syntax.swift

Sources/PackageModelSyntax/ManifestEditError.swift

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import SwiftSyntax
1818
/// package manifest programattically.
1919
package enum ManifestEditError: Error {
2020
case cannotFindPackage
21+
case cannotFindTargets
22+
case cannotFindTarget(targetName: String)
2123
case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax)
2224
case oldManifest(ToolsVersion)
2325
}
@@ -33,6 +35,10 @@ extension ManifestEditError: CustomStringConvertible {
3335
switch self {
3436
case .cannotFindPackage:
3537
"invalid manifest: unable to find 'Package' declaration"
38+
case .cannotFindTargets:
39+
"unable to find package targets in manifest"
40+
case .cannotFindTarget(targetName: let name):
41+
"unable to find target named '\(name)' in package"
3642
case .cannotFindArrayLiteralArgument(argumentName: let name, node: _):
3743
"unable to find array literal for '\(name)' argument"
3844
case .oldManifest(let version):

Tests/PackageModelSyntaxTests/ManifestEditTests.swift

+40
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,46 @@ class ManifestEditTests: XCTestCase {
611611
)
612612
}
613613
}
614+
615+
func testAddTargetDependency() throws {
616+
try assertManifestRefactor("""
617+
// swift-tools-version: 5.5
618+
let package = Package(
619+
name: "packages",
620+
dependencies: [
621+
.package(url: "https://github.com/apple/swift-testing.git", from: "0.8.0"),
622+
],
623+
targets: [
624+
.testTarget(
625+
name: "MyTest"
626+
),
627+
]
628+
)
629+
""",
630+
expectedManifest: """
631+
// swift-tools-version: 5.5
632+
let package = Package(
633+
name: "packages",
634+
dependencies: [
635+
.package(url: "https://github.com/apple/swift-testing.git", from: "0.8.0"),
636+
],
637+
targets: [
638+
.testTarget(
639+
name: "MyTest",
640+
dependencies: [
641+
.product(name: "Testing", package: "swift-testing"),
642+
]
643+
),
644+
]
645+
)
646+
""") { manifest in
647+
try AddTargetDependency.addTargetDependency(
648+
.product(name: "Testing", package: "swift-testing"),
649+
targetName: "MyTest",
650+
to: manifest
651+
)
652+
}
653+
}
614654
}
615655

616656

0 commit comments

Comments
 (0)