Skip to content

Commit c4002f2

Browse files
authored
Merge pull request #2904 from DougGregor/package-manifest-refactor
Swift package manifest refactoring actions
2 parents 9e40b40 + 2f6937d commit c4002f2

18 files changed

+3335
-1
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ let package = Package(
393393

394394
.testTarget(
395395
name: "SwiftRefactorTest",
396-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"]
396+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftIDEUtils", "SwiftRefactor"]
397397
),
398398

399399
// MARK: - Deprecated targets

Sources/SwiftRefactor/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ add_swift_syntax_library(SwiftRefactor
2222
RefactoringProvider.swift
2323
RemoveSeparatorsFromIntegerLiteral.swift
2424
SyntaxUtils.swift
25+
26+
PackageManifest/AddPackageDependency.swift
27+
PackageManifest/AddPackageTarget.swift
28+
PackageManifest/AddPluginUsage.swift
29+
PackageManifest/AddProduct.swift
30+
PackageManifest/AddSwiftSetting.swift
31+
PackageManifest/AddTargetDependency.swift
32+
PackageManifest/ManifestEditError.swift
33+
PackageManifest/ManifestEditRefactoringProvider.swift
34+
PackageManifest/ManifestSyntaxRepresentable.swift
35+
PackageManifest/PackageDependency.swift
36+
PackageManifest/PackageEdit.swift
37+
PackageManifest/PackageTarget.swift
38+
PackageManifest/ProductDescription.swift
39+
PackageManifest/StringUtils.swift
40+
PackageManifest/SyntaxEditUtils.swift
2541
)
2642

2743
target_link_swift_syntax_libraries(SwiftRefactor PUBLIC
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://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 package dependency to a package manifest's source code.
18+
@_spi(PackageRefactor)
19+
public struct AddPackageDependency: ManifestEditRefactoringProvider {
20+
public struct Context {
21+
public var dependency: PackageDependency
22+
23+
public init(dependency: PackageDependency) {
24+
self.dependency = dependency
25+
}
26+
}
27+
28+
/// The set of argument labels that can occur after the "dependencies"
29+
/// argument in the Package initializers.
30+
private static let argumentLabelsAfterDependencies: Set<String> = [
31+
"targets",
32+
"swiftLanguageVersions",
33+
"cLanguageStandard",
34+
"cxxLanguageStandard",
35+
]
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 -> PackageEdit {
43+
let dependency = context.dependency
44+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
45+
throw ManifestEditError.cannotFindPackage
46+
}
47+
48+
guard
49+
try !dependencyAlreadyAdded(
50+
dependency,
51+
in: packageCall
52+
)
53+
else {
54+
return PackageEdit(manifestEdits: [])
55+
}
56+
57+
let newPackageCall = try addPackageDependencyLocal(
58+
dependency,
59+
to: packageCall
60+
)
61+
62+
return PackageEdit(
63+
manifestEdits: [
64+
.replace(packageCall, with: newPackageCall.description)
65+
]
66+
)
67+
}
68+
69+
/// Return `true` if the dependency already exists in the manifest, otherwise return `false`.
70+
/// Throws an error if a dependency already exists with the same id or url, but different arguments.
71+
private static func dependencyAlreadyAdded(
72+
_ dependency: PackageDependency,
73+
in packageCall: FunctionCallExprSyntax
74+
) throws -> Bool {
75+
let dependencySyntax = dependency.asSyntax()
76+
guard let dependencyFnSyntax = dependencySyntax.as(FunctionCallExprSyntax.self) else {
77+
throw ManifestEditError.cannotFindPackage
78+
}
79+
80+
guard
81+
let id = dependencyFnSyntax.arguments.first(where: {
82+
$0.label?.text == "url" || $0.label?.text == "id" || $0.label?.text == "path"
83+
})
84+
else {
85+
throw ManifestEditError.malformedManifest(error: "missing id or url argument in dependency syntax")
86+
}
87+
88+
if let existingDependencies = packageCall.findArgument(labeled: "dependencies") {
89+
// If we have an existing dependencies array, we need to check if
90+
// it's already added.
91+
if let expr = existingDependencies.expression.as(ArrayExprSyntax.self) {
92+
// Iterate through existing dependencies and look for an argument that matches
93+
// either the `id` or `url` argument of the new dependency.
94+
let existingArgument = expr.elements.first { elem in
95+
if let funcExpr = elem.expression.as(FunctionCallExprSyntax.self) {
96+
return funcExpr.arguments.contains {
97+
$0.trimmedDescription == id.trimmedDescription
98+
}
99+
}
100+
return true
101+
}
102+
103+
if let existingArgument {
104+
let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil)
105+
// This exact dependency already exists, return false to indicate we should do nothing.
106+
if normalizedExistingArgument.trimmedDescription == dependencySyntax.trimmedDescription {
107+
return true
108+
}
109+
throw ManifestEditError.existingDependency(dependencyName: dependency.identifier)
110+
}
111+
}
112+
}
113+
return false
114+
}
115+
116+
/// Implementation of adding a package dependency to an existing call.
117+
static func addPackageDependencyLocal(
118+
_ dependency: PackageDependency,
119+
to packageCall: FunctionCallExprSyntax
120+
) throws -> FunctionCallExprSyntax {
121+
try packageCall.appendingToArrayArgument(
122+
label: "dependencies",
123+
labelsAfter: Self.argumentLabelsAfterDependencies,
124+
newElement: dependency.asSyntax()
125+
)
126+
}
127+
}
128+
129+
fileprivate extension PackageDependency {
130+
var identifier: String {
131+
switch self {
132+
case .sourceControl(let info):
133+
return info.location
134+
case .fileSystem(let info):
135+
return info.path
136+
case .registry(let info):
137+
return info.identity
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)