Skip to content

Swift package manifest refactoring actions #2904

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 30 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8e71dfd
Introduce package manifest refactoring action "Add Package Dependency"
DougGregor Nov 30, 2024
4a6e7bd
FIXUP for adding package dependency
DougGregor Dec 1, 2024
86581a0
Port "Add target dependency" package manifest editing action to Swift…
DougGregor Nov 30, 2024
1b624fc
Port "Add target" from the Swift Package Manager code base to SwiftRe…
DougGregor Dec 1, 2024
d308aca
Port "Add Product" manifest edit refactor over from SwiftPM
DougGregor Dec 1, 2024
11649fd
Add a package manifest edit refactor to introduce a new plugin usage …
DougGregor Dec 1, 2024
1aa14e6
Implement a better "contains string literal" check for a source file
DougGregor Dec 1, 2024
89c2854
Remove more uses of switch expressions
DougGregor Dec 1, 2024
ba0ac8a
Remove yet more uses of switch expressions
DougGregor Dec 2, 2024
18c9f0e
Rename PackageEditResult -> PackageEdit
DougGregor Dec 2, 2024
4b2073f
Rename TargetDescription -> Target
DougGregor Dec 2, 2024
7293a79
Sink ProductType into ProductDescription
DougGregor Dec 2, 2024
e6bd577
Rename AddTarget -> AddPackageTarget
DougGregor Dec 2, 2024
a1ac7ca
[PackageManifest] Address NFC review feedback
xedin Aug 5, 2025
9370e43
[SwiftRefactor] Replace custom `XCTAssertThrows` with `XCTAssertThrow…
xedin Aug 5, 2025
902b53e
[SwiftRefactor] Fix handling of leading trivia for new elements/argum…
xedin Aug 6, 2025
95190dc
[Tests] Add more information to unexpected aux files failure to diagn…
xedin Aug 6, 2025
de9afa3
[SwiftRefactor] PackageManifest: Rename `Target` to `PackageTarget`
xedin Aug 6, 2025
cd7758e
[SwiftRefactor] PackageManifest: Replace custom types with a simple `…
xedin Aug 6, 2025
b44a72f
[SwiftRefactor] PackageManifest: Replace `SemanticVersion` with a `St…
xedin Aug 6, 2025
58a01d3
[SwiftRefactor] PackageManifest: Fix formatting for license headers
xedin Aug 6, 2025
621ae13
[SwiftRefactor] PackageManifest: Add duplicate dependency checking to…
xedin Aug 6, 2025
fb6939c
[SwiftRefactor] PackageManifest: Add refactoring to add introduce swi…
xedin Aug 7, 2025
68432ea
[SwiftRefactor] PackageManifest: Remove an outdated TODO that is no l…
xedin Aug 7, 2025
b896109
[SwiftRefactor] PackageManifest: Change source control dependency to …
xedin Aug 7, 2025
966e0d4
[SwiftRefactor] PackageManifest: Remove identity from `FileSystem` an…
xedin Aug 8, 2025
db2b78f
[SwiftRefactor] PackageManifest: Remove unused property from `AddPlug…
xedin Aug 8, 2025
6a98663
[SwiftRefactor] PackageManifest: Inline `Configuration` into `AddPack…
xedin Aug 8, 2025
9e08670
[SwiftRefactor] PackageManifest: Address stylistic review feedback
xedin Aug 8, 2025
2f6937d
[SwiftRefactor] PackageManifest: Mark all of the refactorings and sup…
xedin Aug 8, 2025
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ let package = Package(

.testTarget(
name: "SwiftRefactorTest",
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"]
dependencies: ["_SwiftSyntaxTestSupport", "SwiftIDEUtils", "SwiftRefactor"]
),

// MARK: - Deprecated targets
Expand Down
16 changes: 16 additions & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ add_swift_syntax_library(SwiftRefactor
RefactoringProvider.swift
RemoveSeparatorsFromIntegerLiteral.swift
SyntaxUtils.swift

PackageManifest/AddPackageDependency.swift
PackageManifest/AddPackageTarget.swift
PackageManifest/AddPluginUsage.swift
PackageManifest/AddProduct.swift
PackageManifest/AddSwiftSetting.swift
PackageManifest/AddTargetDependency.swift
PackageManifest/ManifestEditError.swift
PackageManifest/ManifestEditRefactoringProvider.swift
PackageManifest/ManifestSyntaxRepresentable.swift
PackageManifest/PackageDependency.swift
PackageManifest/PackageEdit.swift
PackageManifest/PackageTarget.swift
PackageManifest/ProductDescription.swift
PackageManifest/StringUtils.swift
PackageManifest/SyntaxEditUtils.swift
)

target_link_swift_syntax_libraries(SwiftRefactor PUBLIC
Expand Down
138 changes: 138 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder

/// Add a package dependency to a package manifest's source code.
public struct AddPackageDependency: ManifestEditRefactoringProvider {
public struct Context {
public var dependency: PackageDependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to say this should be optional and we could add placeholder if not given?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think that the whole point of the refactoring is to actually add a dependency that's why it's non-optional at the moment, I'm not sure how we'd placeholder that efficiently because different dependency kinds have different arguments...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how we'd placeholder that efficiently

We'd just pick a reasonable default, ie. SourceControl and from:

.package(url: <#url#>, from: <#version#>)

(though those placeholders could also be typed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe that is the way to go here but instead of having it optional we can make it a defaulted parameter on initializer in both cases.


public init(dependency: PackageDependency) {
self.dependency = dependency
}
}

/// The set of argument labels that can occur after the "dependencies"
/// argument in the Package initializers.
private static let argumentLabelsAfterDependencies: Set<String> = [
"targets",
"swiftLanguageVersions",
"cLanguageStandard",
"cxxLanguageStandard",
]

/// Produce the set of source edits needed to add the given package
/// dependency to the given manifest file.
public static func manifestRefactor(
syntax manifest: SourceFileSyntax,
in context: Context
) throws -> PackageEdit {
let dependency = context.dependency
guard let packageCall = manifest.findCall(calleeName: "Package") else {
throw ManifestEditError.cannotFindPackage
}

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

let newPackageCall = try addPackageDependencyLocal(
dependency,
to: packageCall
)

return PackageEdit(
manifestEdits: [
.replace(packageCall, with: newPackageCall.description)
]
)
}

/// 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: PackageDependency,
in packageCall: FunctionCallExprSyntax
) throws -> Bool {
let dependencySyntax = dependency.asSyntax()
guard let dependencyFnSyntax = dependencySyntax.as(FunctionCallExprSyntax.self) else {
throw ManifestEditError.cannotFindPackage
}

guard
let id = dependencyFnSyntax.arguments.first(where: {
$0.label?.text == "url" || $0.label?.text == "id" || $0.label?.text == "path"
})
else {
throw ManifestEditError.malformedManifest(error: "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: PackageDependency,
to packageCall: FunctionCallExprSyntax
) throws -> FunctionCallExprSyntax {
try packageCall.appendingToArrayArgument(
label: "dependencies",
labelsAfter: Self.argumentLabelsAfterDependencies,
newElement: dependency.asSyntax()
)
}
}

fileprivate extension PackageDependency {
var identifier: String {
switch self {
case .sourceControl(let info):
return info.identity
case .fileSystem(let info):
return info.identity
case .registry(let info):
return info.identity
}
}
}
Loading