Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import SwiftSyntax
public enum ManifestEditError: Error, Equatable {
case cannotFindPackage
case cannotFindTargets
case cannotFindDependencies
case cannotFindTarget(targetName: String)
case cannotFindArrayLiteralArgument(argumentName: String)
case cannotAddSettingsToPluginTarget
Expand All @@ -32,6 +33,8 @@ extension ManifestEditError: CustomStringConvertible {
return "invalid manifest: unable to find 'Package' declaration"
case .cannotFindTargets:
return "unable to find package targets in manifest"
case .cannotFindDependencies:
return "unable to find package dependencies in manifest"
case .cannotFindTarget(targetName: let name):
return "unable to find target named '\(name)' in package"
case .cannotFindArrayLiteralArgument(argumentName: let name):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//===----------------------------------------------------------------------===//
//
// 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

/// Upgrade all package dependencies that have a `from` or `exact` version restriction to their latest released version,
/// including new major versions.
///
/// The intention of this refactoring is to update leaf package (like executables) to the latest version of their
/// dependencies, allowing them to also pick up potentially API-breaking new major or prerelease versions that can
/// easily go unnoticed using `swift package update`.
@_spi(PackageRefactor)
public struct UpgradePackageDependencies: EditRefactoringProvider {
public typealias Input = SourceFileSyntax

public struct Context {
let latestVersions: [String: String]

public init(
resolvingLatestVersionIn manifest: SourceFileSyntax,
using resolveLatestPackageVersion: @Sendable @escaping (_ url: String, _ currentVersion: String) async -> String?
) async throws {
let dependencies = try findDependencies(in: manifest)
self.latestVersions = await withTaskGroup(of: (url: String, latestVersion: String)?.self) { taskGroup in
for dependency in dependencies {
taskGroup.addTask {
guard let url = dependency.url.representedLiteralValue,
let currentVersion = dependency.currentVersion.representedLiteralValue
else {
return nil
}
guard let latestVersion = await resolveLatestPackageVersion(url, currentVersion) else {
return nil
}
return (url, latestVersion)
}
}
var latestVersions: [String: String] = [:]
for await case .some(let result) in taskGroup {
latestVersions[result.url] = result.latestVersion
}
return latestVersions
}
}
}

private struct VersionedDependency {
let url: StringLiteralExprSyntax
let currentVersion: StringLiteralExprSyntax
}

private static func findDependencies(in manifest: SourceFileSyntax) throws -> [VersionedDependency] {
guard let packageCall = manifest.findCall(calleeName: "Package") else {
throw ManifestEditError.cannotFindPackage
}
guard let dependencies = packageCall.findArgument(labeled: "dependencies")?.expression.as(ArrayExprSyntax.self)
else {
throw ManifestEditError.cannotFindDependencies
}
return dependencies.elements.compactMap { (dependency) -> VersionedDependency? in
guard let functionCall = dependency.expression.as(FunctionCallExprSyntax.self),
functionCall.calledExpression.as(MemberAccessExprSyntax.self)?.declName.baseName.tokenKind
== .identifier("package"),
let url = functionCall.findArgument(labeled: "url")?.expression.as(StringLiteralExprSyntax.self)
else {
return nil
}

// TODO: Support version initialized using `Version` initializer

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will address this if there is a consensus that we want swiftlang/swift-package-manager#10212.

if let version = functionCall.findArgument(labeled: "from")?.expression.as(StringLiteralExprSyntax.self) {
return VersionedDependency(url: url, currentVersion: version)
} else if let version = functionCall.findArgument(labeled: "exact")?.expression.as(StringLiteralExprSyntax.self) {
return VersionedDependency(url: url, currentVersion: version)
} else {
return nil
}
}
}

public static func textRefactor(syntax manifest: SourceFileSyntax, in context: Context) throws -> [SourceEdit] {
return try findDependencies(in: manifest).compactMap { dependency in
guard let urlValue = dependency.url.representedLiteralValue, let latestVersion = context.latestVersions[urlValue]
else {
return nil
}
guard dependency.currentVersion.representedLiteralValue != latestVersion else {
return nil
}
return SourceEdit(
range: dependency.currentVersion.trimmedRange,
replacement: StringLiteralExprSyntax(content: latestVersion).description
)
}
}
}
72 changes: 72 additions & 0 deletions Tests/SwiftRefactorTest/ManifestEditTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,56 @@ final class ManifestEditTests: XCTestCase {
)
}
}

func testUpgradePackageDependencies() async throws {
try await assertManifestRefactor(
"""
// swift-tools-version: 6.0
let package = Package(
name: "packages",
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax", from: "510.0.0"),
.package(url: "https://github.com/swiftlang/swift-format", exact: "510.0.0"),
.package(url: "https://github.com/unable-to-resolve", exact: "1.0.0"),
.package(path: "my-local-dependency"),
],
targets: []
)
""",
expectedManifest: """
// swift-tools-version: 6.0
let package = Package(
name: "packages",
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax", from: "603.0.1"),
.package(url: "https://github.com/swiftlang/swift-format", exact: "603.0.0"),
.package(url: "https://github.com/unable-to-resolve", exact: "1.0.0"),
.package(path: "my-local-dependency"),
],
targets: []
)
"""
) { manifest in
let context = try await UpgradePackageDependencies.Context(resolvingLatestVersionIn: manifest) {
(url, currentVersion) in
switch url {
case "https://github.com/swiftlang/swift-syntax":
XCTAssertEqual(currentVersion, "510.0.0")
return "603.0.1"
case "https://github.com/swiftlang/swift-format":
XCTAssertEqual(currentVersion, "510.0.0")
return "603.0.0"
case "https://github.com/unable-to-resolve":
XCTAssertEqual(currentVersion, "1.0.0")
return nil
default:
XCTFail("Resolving package version called with an unexpected url: \(url)")
return nil
}
}
return try UpgradePackageDependencies.textRefactor(syntax: manifest, in: context)
}
}
}

/// Assert that applying the given edit/refactor operation to the manifest
Expand Down Expand Up @@ -1010,3 +1060,25 @@ func assertManifestRefactor(
line: line
)
}

func assertManifestRefactor(
_ originalManifest: SourceFileSyntax,
expectedManifest: SourceFileSyntax,
file: StaticString = #filePath,
line: UInt = #line,
operation: (SourceFileSyntax) async throws -> [SourceEdit]
) async rethrows {
let edits = try await operation(originalManifest)
let editedManifestSource = FixItApplier.apply(
edits: edits,
to: originalManifest
)

let editedManifest = Parser.parse(source: editedManifestSource)
assertStringsEqualWithDiff(
editedManifest.description,
expectedManifest.description,
file: file,
line: line
)
}
Loading