Skip to content

Commit 4061163

Browse files
committed
Add a package manifest edit refactor to introduce a new plugin usage for a target
This allows one to add a plugin usage to a given target, given the target name and description of how the plugin should be used.
1 parent 30ae9e6 commit 4061163

File tree

6 files changed

+165
-27
lines changed

6 files changed

+165
-27
lines changed

Sources/SwiftRefactor/CMakeLists.txt

+1
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/AddPluginUsage.swift
2728
PackageManifest/AddProduct.swift
2829
PackageManifest/AddTarget.swift
2930
PackageManifest/AddTargetDependency.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 plugin usage to a particular target in the manifest's source
18+
/// code.
19+
public struct AddPluginUsage: ManifestEditRefactoringProvider {
20+
public struct Context {
21+
public let targetName: String
22+
public let pluginUsage: TargetDescription.PluginUsage
23+
24+
public init(targetName: String, pluginUsage: TargetDescription.PluginUsage) {
25+
self.targetName = targetName
26+
self.pluginUsage = pluginUsage
27+
}
28+
}
29+
30+
/// The set of argument labels that can occur after the "plugins"
31+
/// argument in the Target initializers. (There aren't any right now)
32+
///
33+
/// TODO: Could we generate this from the the PackageDescription module, so
34+
/// we don't have keep it up-to-date manually?
35+
private static let argumentLabelsAfterPluginUsages: Set<String> = []
36+
37+
/// Produce the set of source edits needed to add the given package
38+
/// dependency to the given manifest file.
39+
public static func manifestRefactor(
40+
syntax manifest: SourceFileSyntax,
41+
in context: Context
42+
) throws -> PackageEditResult {
43+
let targetName = context.targetName
44+
let pluginUsage = context.pluginUsage
45+
46+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
47+
throw ManifestEditError.cannotFindPackage
48+
}
49+
50+
// Find the target to be modified.
51+
let targetCall = try packageCall.findManifestTargetCall(targetName: targetName)
52+
53+
let newTargetCall = try targetCall.appendingToArrayArgument(
54+
label: "plugins",
55+
trailingLabels: Self.argumentLabelsAfterPluginUsages,
56+
newElement: pluginUsage.asSyntax()
57+
)
58+
59+
return PackageEditResult(
60+
manifestEdits: [
61+
.replace(targetCall, with: newTargetCall.description)
62+
]
63+
)
64+
}
65+
}

Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift

+2-26
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,8 @@ public struct AddTargetDependency: ManifestEditRefactoringProvider {
6161
throw ManifestEditError.cannotFindPackage
6262
}
6363

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-
}
64+
// Find the target to be modified.
65+
let targetCall = try packageCall.findManifestTargetCall(targetName: targetName)
9066

9167
let newTargetCall = try addTargetDependencyLocal(
9268
dependency,

Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift

+35
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,41 @@ extension SyntaxProtocol {
176176
}
177177
}
178178

179+
extension FunctionCallExprSyntax {
180+
/// Find the call that forms a target with the given name in this
181+
/// package manifest.
182+
func findManifestTargetCall(targetName: String) throws -> FunctionCallExprSyntax {
183+
// Dig out the array of targets.
184+
guard let targetsArgument = findArgument(labeled: "targets"),
185+
let targetArray = targetsArgument.expression.findArrayArgument()
186+
else {
187+
throw ManifestEditError.cannotFindTargets
188+
}
189+
190+
// Look for a call whose name is a string literal matching the
191+
// requested target name.
192+
func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool {
193+
guard let nameArgument = call.findArgument(labeled: "name") else {
194+
return false
195+
}
196+
197+
guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
198+
let literalValue = stringLiteral.representedLiteralValue
199+
else {
200+
return false
201+
}
202+
203+
return literalValue == targetName
204+
}
205+
206+
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else {
207+
throw ManifestEditError.cannotFindTarget(targetName: targetName)
208+
}
209+
210+
return targetCall
211+
}
212+
}
213+
179214
extension ArrayExprSyntax {
180215
/// Produce a new array literal expression that appends the given
181216
/// element, while trying to maintain similar indentation.

Sources/SwiftRefactor/PackageManifest/TargetDescription.swift

+28-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ public struct TargetDescription {
2828

2929
public let checksum: String?
3030

31+
/// The usages of package plugins by the target.
32+
public let pluginUsages: [PluginUsage]?
33+
34+
/// Represents a target's usage of a plugin target or product.
35+
public enum PluginUsage {
36+
case plugin(name: String, package: String?)
37+
}
38+
3139
public enum TargetKind: String {
3240
case binary
3341
case executable
@@ -43,20 +51,23 @@ public struct TargetDescription {
4351
case target(name: String)
4452
case product(name: String, package: String?)
4553
}
54+
4655
public init(
4756
name: String,
4857
type: TargetKind = .library,
4958
dependencies: [Dependency] = [],
5059
path: String? = nil,
5160
url: String? = nil,
52-
checksum: String? = nil
61+
checksum: String? = nil,
62+
pluginUsages: [PluginUsage]? = nil
5363
) {
5464
self.name = name
5565
self.type = type
5666
self.dependencies = dependencies
5767
self.path = path
5868
self.url = url
5969
self.checksum = checksum
70+
self.pluginUsages = pluginUsages
6071
}
6172
}
6273

@@ -90,6 +101,10 @@ extension TargetDescription: ManifestSyntaxRepresentable {
90101
// Only for plugins
91102
arguments.appendIf(label: "checksum", stringLiteral: checksum)
92103

104+
if let pluginUsages {
105+
arguments.appendIfNonEmpty(label: "plugins", arrayLiteral: pluginUsages)
106+
}
107+
93108
let separateParen: String = arguments.count > 1 ? "\n" : ""
94109
let argumentsSyntax = LabeledExprListSyntax(arguments)
95110
return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))"
@@ -113,3 +128,15 @@ extension TargetDescription.Dependency: ManifestSyntaxRepresentable {
113128
}
114129
}
115130
}
131+
132+
extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable {
133+
func asSyntax() -> ExprSyntax {
134+
switch self {
135+
case .plugin(name: let name, package: nil):
136+
".plugin(name: \(literal: name))"
137+
138+
case .plugin(name: let name, package: let package):
139+
".plugin(name: \(literal: name), package: \(literal: package))"
140+
}
141+
}
142+
}

Tests/SwiftRefactorTest/ManifestEditTests.swift

+34
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,40 @@ final class ManifestEditTests: XCTestCase {
605605
)
606606
}
607607

608+
func testAddJava2SwiftPlugin() throws {
609+
try assertManifestRefactor(
610+
"""
611+
// swift-tools-version: 5.7
612+
let package = Package(
613+
name: "packages",
614+
targets: [
615+
.target(
616+
name: "MyLib"
617+
)
618+
]
619+
)
620+
""",
621+
expectedManifest: """
622+
// swift-tools-version: 5.7
623+
let package = Package(
624+
name: "packages",
625+
targets: [
626+
.target(
627+
name: "MyLib",
628+
plugins: [
629+
.plugin(name: "Java2SwiftPlugin", package: "swift-java"),
630+
]
631+
)
632+
]
633+
)
634+
""",
635+
provider: AddPluginUsage.self,
636+
context: .init(
637+
targetName: "MyLib",
638+
pluginUsage: .plugin(name: "Java2SwiftPlugin", package: "swift-java")
639+
)
640+
)
641+
}
608642
}
609643

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

0 commit comments

Comments
 (0)