Skip to content

Commit acf9708

Browse files
committed
Introduce package manifest refactoring action "Add Package Dependency"
Start porting the package manifest editing operations from the Swift Package Manager package over here, so that all of the syntactic refactorings are together in one common place. These refactorings are needed by a number of tools, including SwiftPM, SourceKit-LSP, and (soon) the Swift compiler itself, which can all depend on swift-syntax. The implementation here stubs out the various types used to describe package syntax, using simple string-backed types in place of some of the semantic types that are part of SwiftPM itself, such as SemanticVersion or SourceControlURL. I've also introduced the notion of a ManifestEditRefactoringProvider to generalize over all of the package manifest editing operations. This commit ports over the "Add Package Dependency" command and its tests.
1 parent b721fa8 commit acf9708

15 files changed

+1375
-1
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ let package = Package(
379379

380380
.testTarget(
381381
name: "SwiftRefactorTest",
382-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"]
382+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftIDEUtils", "SwiftRefactor"]
383383
),
384384

385385
// MARK: - Deprecated targets

Sources/SwiftRefactor/CMakeLists.txt

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ add_swift_syntax_library(SwiftRefactor
2121
RefactoringProvider.swift
2222
RemoveSeparatorsFromIntegerLiteral.swift
2323
SyntaxUtils.swift
24+
25+
PackageManifest/AbsolutePath.swift
26+
PackageManifest/AddPackageDependency.swift
27+
PackageManifest/ManifestEditError.swift
28+
PackageManifest/ManifestEditRefactoringProvider.swift
29+
PackageManifest/ManifestSyntaxRepresentable.swift
30+
PackageManifest/PackageDependency.swift
31+
PackageManifest/PackageEditResult.swift
32+
PackageManifest/PackageIdentity.swift
33+
PackageManifest/RelativePath.swift
34+
PackageManifest/SemanticVersion.swift
35+
PackageManifest/SourceControlURL.swift
36+
PackageManifest/SyntaxEditUtils.swift
2437
)
2538

2639
target_link_swift_syntax_libraries(SwiftRefactor PUBLIC
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
/// Syntactic wrapper type that describes an absolute path for refactoring
14+
/// purposes but does not interpret its contents.
15+
public struct AbsolutePath: CustomStringConvertible, Equatable, Hashable, Sendable {
16+
public private(set) var description: String
17+
18+
public init(_ description: String) {
19+
self.description = description
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 package dependency to a package manifest's source code.
18+
public struct AddPackageDependency: ManifestEditRefactoringProvider {
19+
public struct Context {
20+
public var dependency: PackageDependency
21+
22+
public init(dependency: PackageDependency) {
23+
self.dependency = dependency
24+
}
25+
}
26+
27+
/// The set of argument labels that can occur after the "dependencies"
28+
/// argument in the Package initializers.
29+
///
30+
/// TODO: Could we generate this from the the PackageDescription module, so
31+
/// we don't have keep it up-to-date manually?
32+
private static let argumentLabelsAfterDependencies: Set<String> = [
33+
"targets",
34+
"swiftLanguageVersions",
35+
"cLanguageStandard",
36+
"cxxLanguageStandard",
37+
]
38+
39+
/// Produce the set of source edits needed to add the given package
40+
/// dependency to the given manifest file.
41+
public static func manifestRefactor(
42+
syntax manifest: SourceFileSyntax,
43+
in context: Context
44+
) throws -> PackageEditResult {
45+
let dependency = context.dependency
46+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
47+
throw ManifestEditError.cannotFindPackage
48+
}
49+
50+
let newPackageCall = try addPackageDependencyLocal(
51+
dependency,
52+
to: packageCall
53+
)
54+
55+
return PackageEditResult(
56+
manifestEdits: [
57+
.replace(packageCall, with: newPackageCall.description)
58+
]
59+
)
60+
}
61+
62+
/// Implementation of adding a package dependency to an existing call.
63+
static func addPackageDependencyLocal(
64+
_ dependency: PackageDependency,
65+
to packageCall: FunctionCallExprSyntax
66+
) throws -> FunctionCallExprSyntax {
67+
try packageCall.appendingToArrayArgument(
68+
label: "dependencies",
69+
trailingLabels: Self.argumentLabelsAfterDependencies,
70+
newElement: dependency.asSyntax()
71+
)
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 SwiftSyntax
14+
15+
/// An error describing problems that can occur when attempting to edit a
16+
/// package manifest programattically.
17+
public enum ManifestEditError: Error {
18+
case cannotFindPackage
19+
case cannotFindTargets
20+
case cannotFindTarget(targetName: String)
21+
case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax)
22+
}
23+
24+
extension ManifestEditError: CustomStringConvertible {
25+
public var description: String {
26+
switch self {
27+
case .cannotFindPackage:
28+
"invalid manifest: unable to find 'Package' declaration"
29+
case .cannotFindTargets:
30+
"unable to find package targets in manifest"
31+
case .cannotFindTarget(targetName: let name):
32+
"unable to find target named '\(name)' in package"
33+
case .cannotFindArrayLiteralArgument(argumentName: let name, node: _):
34+
"unable to find array literal for '\(name)' argument"
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 SwiftSyntax
14+
15+
public protocol ManifestEditRefactoringProvider: EditRefactoringProvider
16+
where Self.Input == SourceFileSyntax {
17+
18+
static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEditResult
19+
}
20+
21+
extension EditRefactoringProvider where Self: ManifestEditRefactoringProvider {
22+
public static func textRefactor(syntax: Input, in context: Context) -> [SourceEdit] {
23+
return (try? manifestRefactor(syntax: syntax, in: context).manifestEdits) ?? []
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-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 SwiftSyntax
14+
15+
/// Describes an entity in the package model that can be represented as
16+
/// a syntax node.
17+
protocol ManifestSyntaxRepresentable {
18+
/// The most specific kind of syntax node that best describes this entity
19+
/// in the manifest.
20+
///
21+
/// There might be other kinds of syntax nodes that can also represent
22+
/// the syntax, but this is the one that a canonical manifest will use.
23+
/// As an example, a package dependency is usually expressed as, e.g.,
24+
/// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1")
25+
///
26+
/// However, there could be other forms, e.g., this is also valid:
27+
/// Package.Dependency.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1")
28+
associatedtype PreferredSyntax: SyntaxProtocol
29+
30+
/// Provides a suitable syntax node to describe this entity in the package
31+
/// model.
32+
///
33+
/// The resulting syntax is a fragment that describes just this entity,
34+
/// and it's enclosing entity will need to understand how to fit it in.
35+
/// For example, a `PackageDependency` entity would map to syntax for
36+
/// something like
37+
/// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1")
38+
func asSyntax() -> PreferredSyntax
39+
}
40+
41+
extension String: ManifestSyntaxRepresentable {
42+
typealias PreferredSyntax = ExprSyntax
43+
44+
func asSyntax() -> ExprSyntax { "\(literal: self)" }
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-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+
16+
/// Describes a package dependency for refactoring purposes. This is a syntactic
17+
/// subset of the full package manifest's description of a package dependency.
18+
public enum PackageDependency: Sendable {
19+
case fileSystem(FileSystem)
20+
case sourceControl(SourceControl)
21+
case registry(Registry)
22+
23+
public struct FileSystem: Sendable {
24+
public let identity: PackageIdentity
25+
public let nameForTargetDependencyResolutionOnly: String?
26+
public let path: AbsolutePath
27+
}
28+
29+
public struct SourceControl: Sendable {
30+
public let identity: PackageIdentity
31+
public let location: Location
32+
public let requirement: Requirement
33+
34+
public init(identity: PackageIdentity, location: Location, requirement: Requirement) {
35+
self.identity = identity
36+
self.location = location
37+
self.requirement = requirement
38+
}
39+
40+
public enum Requirement: Sendable {
41+
case exact(SemanticVersion)
42+
case rangeFrom(SemanticVersion)
43+
case range(lowerBound: SemanticVersion, upperBound: SemanticVersion)
44+
case revision(String)
45+
case branch(String)
46+
}
47+
48+
public enum Location: Sendable {
49+
case local(AbsolutePath)
50+
case remote(SourceControlURL)
51+
}
52+
}
53+
54+
public struct Registry: Sendable {
55+
public let identity: PackageIdentity
56+
public let requirement: Requirement
57+
58+
/// The dependency requirement.
59+
public enum Requirement: Sendable {
60+
case exact(SemanticVersion)
61+
case rangeFrom(SemanticVersion)
62+
case range(lowerBound: SemanticVersion, upperBound: SemanticVersion)
63+
}
64+
}
65+
}
66+
67+
extension PackageDependency: ManifestSyntaxRepresentable {
68+
func asSyntax() -> ExprSyntax {
69+
switch self {
70+
case .fileSystem(let filesystem): filesystem.asSyntax()
71+
case .sourceControl(let sourceControl): sourceControl.asSyntax()
72+
case .registry(let registry): registry.asSyntax()
73+
}
74+
}
75+
}
76+
77+
extension PackageDependency.FileSystem: ManifestSyntaxRepresentable {
78+
func asSyntax() -> ExprSyntax {
79+
".package(path: \(literal: path.description))"
80+
}
81+
}
82+
83+
extension PackageDependency.SourceControl: ManifestSyntaxRepresentable {
84+
func asSyntax() -> ExprSyntax {
85+
// TODO: Not handling identity, nameForTargetDependencyResolutionOnly,
86+
// or productFilter yet.
87+
switch location {
88+
case .local:
89+
fatalError()
90+
case .remote(let url):
91+
".package(url: \(literal: url.description), \(requirement.asSyntax()))"
92+
}
93+
}
94+
}
95+
96+
extension PackageDependency.Registry: ManifestSyntaxRepresentable {
97+
func asSyntax() -> ExprSyntax {
98+
".package(id: \(literal: identity.description), \(requirement.asSyntax()))"
99+
}
100+
}
101+
102+
extension PackageDependency.SourceControl.Requirement: ManifestSyntaxRepresentable {
103+
func asSyntax() -> LabeledExprSyntax {
104+
switch self {
105+
case .exact(let version):
106+
LabeledExprSyntax(
107+
label: "exact",
108+
expression: version.asSyntax()
109+
)
110+
111+
case .rangeFrom(let range):
112+
LabeledExprSyntax(
113+
label: "from",
114+
expression: range.asSyntax()
115+
)
116+
117+
case .range(let lowerBound, let upperBound):
118+
LabeledExprSyntax(
119+
expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax
120+
)
121+
122+
case .revision(let revision):
123+
LabeledExprSyntax(
124+
label: "revision",
125+
expression: "\(literal: revision)" as ExprSyntax
126+
)
127+
128+
case .branch(let branch):
129+
LabeledExprSyntax(
130+
label: "branch",
131+
expression: "\(literal: branch)" as ExprSyntax
132+
)
133+
}
134+
}
135+
}
136+
137+
extension PackageDependency.Registry.Requirement: ManifestSyntaxRepresentable {
138+
func asSyntax() -> LabeledExprSyntax {
139+
switch self {
140+
case .exact(let version):
141+
LabeledExprSyntax(
142+
label: "exact",
143+
expression: version.asSyntax()
144+
)
145+
146+
case .rangeFrom(let range):
147+
LabeledExprSyntax(
148+
label: "from",
149+
expression: range.asSyntax()
150+
)
151+
152+
case .range(let lowerBound, let upperBound):
153+
LabeledExprSyntax(
154+
expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax
155+
)
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)