Skip to content

Make add-dependency idempotent #8534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions Sources/PackageModelSyntax/AddPackageDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public enum AddPackageDependency {
throw ManifestEditError.cannotFindPackage
}

guard try !dependencyAlreadyAdded(
dependency,
in: packageCall
) else {
return PackageEditResult(manifestEdits: [])
}

let newPackageCall = try addPackageDependencyLocal(
dependency, to: packageCall
)
Expand All @@ -55,6 +62,50 @@ public enum AddPackageDependency {
)
}

/// Return `true` if the dependency already exists in the manifest, otherwise return `false`.
/// Throws an error if a dependency already exists with the same id or url, but different arguments.
private static func dependencyAlreadyAdded(
_ dependency: MappablePackageDependency.Kind,
in packageCall: FunctionCallExprSyntax
) throws -> Bool {
let dependencySyntax = dependency.asSyntax()
guard let dependenctFnSyntax = dependencySyntax.as(FunctionCallExprSyntax.self) else {
throw ManifestEditError.cannotFindPackage
}

guard let id = dependenctFnSyntax.arguments.first(where: {
$0.label?.text == "url" || $0.label?.text == "id" || $0.label?.text == "path"
}) else {
throw InternalError("Missing id or url argument in dependency syntax")
}

if let existingDependencies = packageCall.findArgument(labeled: "dependencies") {
// If we have an existing dependencies array, we need to check if
if let expr = existingDependencies.expression.as(ArrayExprSyntax.self) {
// Iterate through existing dependencies and look for an argument that matches
// either the `id` or `url` argument of the new dependency.
let existingArgument = expr.elements.first { elem in
if let funcExpr = elem.expression.as(FunctionCallExprSyntax.self) {
return funcExpr.arguments.contains {
$0.trimmedDescription == id.trimmedDescription
}
}
return true
}

if let existingArgument {
let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil)
// This exact dependency already exists, return false to indicate we should do nothing.
if normalizedExistingArgument.trimmedDescription == dependencySyntax.trimmedDescription {
return true
}
throw ManifestEditError.existingDependency(dependencyName: dependency.identifier)
}
}
}
return false
}

/// Implementation of adding a package dependency to an existing call.
static func addPackageDependencyLocal(
_ dependency: MappablePackageDependency.Kind,
Expand All @@ -67,3 +118,16 @@ public enum AddPackageDependency {
)
}
}

fileprivate extension MappablePackageDependency.Kind {
var identifier: String {
switch self {
case .sourceControl(let name, let path, _):
return name ?? path
case .fileSystem(let name, let location):
return name ?? location
case .registry(let id, _):
return id
}
}
}
3 changes: 3 additions & 0 deletions Sources/PackageModelSyntax/ManifestEditError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package enum ManifestEditError: Error {
case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax)
case oldManifest(ToolsVersion, expected: ToolsVersion)
case cannotAddSettingsToPluginTarget
case existingDependency(dependencyName: String)
}

extension ToolsVersion {
Expand All @@ -46,6 +47,8 @@ extension ManifestEditError: CustomStringConvertible {
"package manifest version \(version) is too old: please update to manifest version \(expectedVersion) or newer"
case .cannotAddSettingsToPluginTarget:
"plugin targets do not support settings"
case .existingDependency(let name):
"unable to add dependency '\(name)' because it already exists in the list of dependencies"
}
}
}
Expand Down
Loading