Skip to content

Commit fe50821

Browse files
committed
Port "Add target dependency" package manifest editing action to SwiftRefactor
This manifest editing action introduces a new dependency to an existing target
1 parent 4161443 commit fe50821

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

Sources/SwiftRefactor/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ add_swift_syntax_library(SwiftRefactor
2424

2525
PackageManifest/AbsolutePath.swift
2626
PackageManifest/AddPackageDependency.swift
27+
PackageManifest/AddTargetDependency.swift
2728
PackageManifest/ManifestEditError.swift
2829
PackageManifest/ManifestEditRefactoringProvider.swift
2930
PackageManifest/ManifestSyntaxRepresentable.swift
@@ -34,6 +35,7 @@ add_swift_syntax_library(SwiftRefactor
3435
PackageManifest/SemanticVersion.swift
3536
PackageManifest/SourceControlURL.swift
3637
PackageManifest/SyntaxEditUtils.swift
38+
PackageManifest/TargetDescription.swift
3739
)
3840

3941
target_link_swift_syntax_libraries(SwiftRefactor PUBLIC
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 SwiftParser
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
17+
/// Add a target dependency to a manifest's source code.
18+
public struct AddTargetDependency: ManifestEditRefactoringProvider {
19+
public struct Context {
20+
/// The dependency to add.
21+
public var dependency: TargetDescription.Dependency
22+
23+
/// The name of the target to which the dependency will be added.
24+
public var targetName: String
25+
26+
public init(dependency: TargetDescription.Dependency, targetName: String) {
27+
self.dependency = dependency
28+
self.targetName = targetName
29+
}
30+
}
31+
32+
/// The set of argument labels that can occur after the "dependencies"
33+
/// argument in the various target initializers.
34+
///
35+
/// TODO: Could we generate this from the the PackageDescription module, so
36+
/// we don't have keep it up-to-date manually?
37+
private static let argumentLabelsAfterDependencies: Set<String> = [
38+
"path",
39+
"exclude",
40+
"sources",
41+
"resources",
42+
"publicHeadersPath",
43+
"packageAccess",
44+
"cSettings",
45+
"cxxSettings",
46+
"swiftSettings",
47+
"linkerSettings",
48+
"plugins",
49+
]
50+
51+
/// Produce the set of source edits needed to add the given target
52+
/// dependency to the given manifest file.
53+
public static func manifestRefactor(
54+
syntax manifest: SourceFileSyntax,
55+
in context: Context
56+
) throws -> PackageEditResult {
57+
let dependency = context.dependency
58+
let targetName = context.targetName
59+
60+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
61+
throw ManifestEditError.cannotFindPackage
62+
}
63+
64+
// Dig out the array of targets.
65+
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
66+
let targetArray = targetsArgument.expression.findArrayArgument()
67+
else {
68+
throw ManifestEditError.cannotFindTargets
69+
}
70+
71+
// Look for a call whose name is a string literal matching the
72+
// requested target name.
73+
func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool {
74+
guard let nameArgument = call.findArgument(labeled: "name") else {
75+
return false
76+
}
77+
78+
guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
79+
let literalValue = stringLiteral.representedLiteralValue
80+
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,
93+
to: targetCall
94+
)
95+
96+
return PackageEditResult(
97+
manifestEdits: [
98+
.replace(targetCall, with: newTargetCall.description)
99+
]
100+
)
101+
}
102+
103+
/// Implementation of adding a target dependency to an existing call.
104+
static func addTargetDependencyLocal(
105+
_ dependency: TargetDescription.Dependency,
106+
to targetCall: FunctionCallExprSyntax
107+
) throws -> FunctionCallExprSyntax {
108+
try targetCall.appendingToArrayArgument(
109+
label: "dependencies",
110+
trailingLabels: Self.argumentLabelsAfterDependencies,
111+
newElement: dependency.asSyntax()
112+
)
113+
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 SwiftSyntax
14+
15+
/// Syntactic wrapper type that describes a target for refactoring
16+
/// purposes but does not interpret its contents.
17+
public struct TargetDescription {
18+
public let name: String
19+
20+
public enum Dependency {
21+
case target(name: String)
22+
case product(name: String, package: String?)
23+
}
24+
}
25+
26+
extension TargetDescription.Dependency: ManifestSyntaxRepresentable {
27+
func asSyntax() -> ExprSyntax {
28+
switch self {
29+
case .target(name: let name):
30+
".target(name: \(literal: name))"
31+
32+
case .product(name: let name, package: nil):
33+
".product(name: \(literal: name))"
34+
35+
case .product(name: let name, package: let package):
36+
".product(name: \(literal: name), package: \(literal: package))"
37+
}
38+
}
39+
}

Tests/SwiftRefactorTest/ManifestEditTests.swift

+42
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,48 @@ final class ManifestEditTests: XCTestCase {
281281
}
282282
}
283283
}
284+
285+
func testAddTargetDependency() throws {
286+
try assertManifestRefactor(
287+
"""
288+
// swift-tools-version: 5.5
289+
let package = Package(
290+
name: "packages",
291+
dependencies: [
292+
.package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"),
293+
],
294+
targets: [
295+
.testTarget(
296+
name: "MyTest"
297+
),
298+
]
299+
)
300+
""",
301+
expectedManifest: """
302+
// swift-tools-version: 5.5
303+
let package = Package(
304+
name: "packages",
305+
dependencies: [
306+
.package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"),
307+
],
308+
targets: [
309+
.testTarget(
310+
name: "MyTest",
311+
dependencies: [
312+
.product(name: "SomethingOrOther", package: "swift-example"),
313+
]
314+
),
315+
]
316+
)
317+
""",
318+
provider: AddTargetDependency.self,
319+
context: .init(
320+
dependency: .product(name: "SomethingOrOther", package: "swift-example"),
321+
targetName: "MyTest"
322+
)
323+
)
324+
}
325+
284326
}
285327

286328
/// Assert that applying the given edit/refactor operation to the manifest

0 commit comments

Comments
 (0)