Skip to content

Commit fad7fda

Browse files
committed
Add idempotentancy to AddTargetPlugin
1 parent 0c74056 commit fad7fda

File tree

5 files changed

+99
-9
lines changed

5 files changed

+99
-9
lines changed

Sources/Commands/PackageCommands/AddTargetPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import SwiftSyntax
2020
import TSCBasic
2121
import TSCUtility
2222
import Workspace
23+
import PackageGraph
24+
import Foundation
2325

2426
extension SwiftPackageCommand {
2527
struct AddTargetPlugin: SwiftCommand {

Sources/PackageModelSyntax/AddPackageDependency.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,4 @@ fileprivate extension MappablePackageDependency.Kind {
130130
return id
131131
}
132132
}
133-
}
133+
}

Sources/PackageModelSyntax/AddTargetPlugin.swift

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import SwiftSyntax
1818
import SwiftSyntaxBuilder
1919

2020
/// Add a target plugin to a manifest's source code.
21-
public struct AddTargetPlugin {
21+
public enum AddTargetPlugin {
2222
/// The set of argument labels that can occur after the "plugins"
2323
/// argument in the various target initializers.
2424
///
@@ -41,8 +41,10 @@ public struct AddTargetPlugin {
4141
}
4242

4343
// Dig out the array of targets.
44-
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
45-
let targetArray = targetsArgument.expression.findArrayArgument() else {
44+
guard
45+
let targetsArgument = packageCall.findArgument(labeled: "targets"),
46+
let targetArray = targetsArgument.expression.findArrayArgument()
47+
else {
4648
throw ManifestEditError.cannotFindTargets
4749
}
4850

@@ -53,20 +55,40 @@ public struct AddTargetPlugin {
5355
return false
5456
}
5557

56-
guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
57-
let literalValue = stringLiteral.representedLiteralValue else {
58+
guard
59+
let stringLiteral = nameArgument.expression.as(
60+
StringLiteralExprSyntax.self
61+
),
62+
let literalValue = stringLiteral.representedLiteralValue
63+
else {
5864
return false
5965
}
6066

6167
return literalValue == targetName
6268
}
6369

64-
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else {
70+
guard
71+
let targetCall = FunctionCallExprSyntax.findFirst(
72+
in: targetArray,
73+
matching: matchesTargetCall
74+
)
75+
else {
6576
throw ManifestEditError.cannotFindTarget(targetName: targetName)
6677
}
6778

79+
guard
80+
try !self.pluginAlreadyAdded(
81+
plugin,
82+
to: targetName,
83+
in: targetCall
84+
)
85+
else {
86+
return PackageEditResult(manifestEdits: [])
87+
}
88+
6889
let newTargetCall = try addTargetPluginLocal(
69-
plugin, to: targetCall
90+
plugin,
91+
to: targetCall
7092
)
7193

7294
return PackageEditResult(
@@ -76,16 +98,78 @@ public struct AddTargetPlugin {
7698
)
7799
}
78100

101+
private static func pluginAlreadyAdded(
102+
_ plugin: TargetDescription.PluginUsage,
103+
to targetName: String,
104+
in packageCall: FunctionCallExprSyntax
105+
) throws -> Bool {
106+
let pluginSyntax = plugin.asSyntax()
107+
guard let pluginFnSyntax = pluginSyntax.as(FunctionCallExprSyntax.self)
108+
else {
109+
throw ManifestEditError.cannotFindPackage
110+
}
111+
112+
guard
113+
let id = pluginFnSyntax.arguments.first(where: {
114+
$0.label?.text == "name"
115+
})
116+
else {
117+
throw InternalError("Missing 'name' argument in plugin syntax")
118+
}
119+
120+
if let existingPlugins = packageCall.findArgument(labeled: "plugins") {
121+
// If we have an existing plugins array, we need to check if
122+
if let expr = existingPlugins.expression.as(ArrayExprSyntax.self) {
123+
// Iterate through existing dependencies and look for an argument that matches
124+
// either the `id` or `url` argument of the new dependency.
125+
let existingArgument = expr.elements.first { elem in
126+
if let funcExpr = elem.expression.as(
127+
FunctionCallExprSyntax.self
128+
) {
129+
return funcExpr.arguments.contains {
130+
$0.with(\.trailingComma, nil).trimmedDescription
131+
== id.with(\.trailingComma, nil)
132+
.trimmedDescription
133+
}
134+
}
135+
return true
136+
}
137+
138+
if let existingArgument {
139+
let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil)
140+
// This exact dependency already exists, return false to indicate we should do nothing.
141+
if normalizedExistingArgument.trimmedDescription == pluginSyntax.trimmedDescription {
142+
return true
143+
}
144+
throw ManifestEditError.existingPlugin(
145+
pluginName: plugin.identifier,
146+
taget: targetName
147+
)
148+
}
149+
}
150+
}
151+
152+
return false
153+
}
154+
79155
/// Implementation of adding a target dependency to an existing call.
80156
static func addTargetPluginLocal(
81157
_ plugin: TargetDescription.PluginUsage,
82158
to targetCall: FunctionCallExprSyntax
83159
) throws -> FunctionCallExprSyntax {
84160
try targetCall.appendingToArrayArgument(
85161
label: "plugins",
86-
trailingLabels: Self.argumentLabelsAfterDependencies,
162+
trailingLabels: self.argumentLabelsAfterDependencies,
87163
newElement: plugin.asSyntax()
88164
)
89165
}
90166
}
91167

168+
extension TargetDescription.PluginUsage {
169+
fileprivate var identifier: String {
170+
switch self {
171+
case .plugin(let name, _):
172+
name
173+
}
174+
}
175+
}

Sources/PackageModelSyntax/ManifestEditError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package enum ManifestEditError: Error {
2424
case oldManifest(ToolsVersion, expected: ToolsVersion)
2525
case cannotAddSettingsToPluginTarget
2626
case existingDependency(dependencyName: String)
27+
case existingPlugin(pluginName: String, taget: String)
2728
}
2829

2930
extension ToolsVersion {
@@ -49,6 +50,8 @@ extension ManifestEditError: CustomStringConvertible {
4950
"plugin targets do not support settings"
5051
case .existingDependency(let name):
5152
"unable to add dependency '\(name)' because it already exists in the list of dependencies"
53+
case .existingPlugin(let name, let taget):
54+
"unable to add plugin '\(name)' to taget '\(taget)' because it already exists in the list of plugins"
5255
}
5356
}
5457
}

Sources/PackageModelSyntax/PluginUsage+Syntax.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import PackageModel
1414
import SwiftSyntax
15+
import SwiftSyntaxBuilder
1516

1617
extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable {
1718
func asSyntax() -> ExprSyntax {

0 commit comments

Comments
 (0)