-
Notifications
You must be signed in to change notification settings - Fork 447
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
Changes from all commits
8e71dfd
4a6e7bd
86581a0
1b624fc
d308aca
11649fd
1aa14e6
89c2854
ba0ac8a
18c9f0e
4b2073f
7293a79
e6bd577
a1ac7ca
9370e43
902b53e
95190dc
de9afa3
cd7758e
b44a72f
58a01d3
621ae13
fb6939c
68432ea
b896109
966e0d4
db2b78f
6a98663
9e08670
2f6937d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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. | ||
@_spi(PackageRefactor) | ||
public struct AddPackageDependency: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
public var dependency: PackageDependency | ||
|
||
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 | ||
xedin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// it's already added. | ||
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.location | ||
case .fileSystem(let info): | ||
return info.path | ||
case .registry(let info): | ||
return info.identity | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,389 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 target to a manifest's source code. | ||
@_spi(PackageRefactor) | ||
public struct AddPackageTarget: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
public let target: PackageTarget | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as adding a package dependency, I could see it being useful to add a placeholder target. Although maybe I'm wrong and you could just use completion for that. Thoughts? |
||
public var testHarness: TestHarness | ||
|
||
public init( | ||
target: PackageTarget, | ||
testHarness: TestHarness = .default | ||
) { | ||
self.target = target | ||
self.testHarness = testHarness | ||
} | ||
} | ||
|
||
/// The set of argument labels that can occur after the "targets" | ||
/// argument in the Package initializers. | ||
private static let argumentLabelsAfterTargets: Set<String> = [ | ||
"swiftLanguageVersions", | ||
"cLanguageStandard", | ||
"cxxLanguageStandard", | ||
] | ||
|
||
/// The kind of test harness to use. This isn't part of the manifest | ||
/// itself, but is used to guide the generation process. | ||
public enum TestHarness: String, Codable, Sendable { | ||
/// Don't use any library | ||
case none | ||
|
||
/// Create a test using the XCTest library. | ||
case xctest | ||
|
||
/// Create a test using the swift-testing package. | ||
case swiftTesting = "swift-testing" | ||
|
||
/// The default testing library to use. | ||
public static let `default`: TestHarness = .swiftTesting | ||
} | ||
|
||
/// Add the given target to the manifest, producing a set of edit results | ||
/// that updates the manifest and adds some source files to stub out the | ||
/// new target. | ||
public static func manifestRefactor( | ||
syntax manifest: SourceFileSyntax, | ||
in context: Context | ||
) throws -> PackageEdit { | ||
guard let packageCall = manifest.findCall(calleeName: "Package") else { | ||
throw ManifestEditError.cannotFindPackage | ||
} | ||
|
||
// Create a mutable version of target to which we can add more | ||
// content when needed. | ||
var target = context.target | ||
|
||
// Add dependencies needed for various targets. | ||
switch target.type { | ||
case .macro: | ||
// Macro targets need to depend on a couple of libraries from | ||
// SwiftSyntax. | ||
target.dependencies.append(contentsOf: macroTargetDependencies) | ||
|
||
default: | ||
break | ||
} | ||
|
||
var newPackageCall = try packageCall.appendingToArrayArgument( | ||
label: "targets", | ||
labelsAfter: Self.argumentLabelsAfterTargets, | ||
newElement: target.asSyntax() | ||
) | ||
|
||
let outerDirectory: String? | ||
switch target.type { | ||
case .binary, .plugin, .system: outerDirectory = nil | ||
case .executable, .library, .macro: outerDirectory = "Sources" | ||
case .test: outerDirectory = "Tests" | ||
} | ||
|
||
guard let outerDirectory else { | ||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(packageCall, with: newPackageCall.description) | ||
] | ||
) | ||
} | ||
|
||
let outerPath = outerDirectory | ||
|
||
/// The set of auxiliary files this refactoring will create. | ||
var auxiliaryFiles: AuxiliaryFiles = [] | ||
|
||
// Add the primary source file. Every target type has this. | ||
addPrimarySourceFile( | ||
outerPath: outerPath, | ||
target: target, | ||
in: context, | ||
to: &auxiliaryFiles | ||
) | ||
|
||
// Perform any other actions that are needed for this target type. | ||
var extraManifestEdits: [SourceEdit] = [] | ||
switch target.type { | ||
case .macro: | ||
addProvidedMacrosSourceFile( | ||
outerPath: outerPath, | ||
target: target, | ||
to: &auxiliaryFiles | ||
) | ||
|
||
if !manifest.containsStringLiteral("swift-syntax") { | ||
newPackageCall = | ||
try AddPackageDependency | ||
.addPackageDependencyLocal( | ||
.swiftSyntax(from: "<#version#>"), | ||
to: newPackageCall | ||
) | ||
|
||
// Look for the first import declaration and insert an | ||
// import of `CompilerPluginSupport` there. | ||
let newImport = "import CompilerPluginSupport\n" | ||
for node in manifest.statements { | ||
if let importDecl = node.item.as(ImportDeclSyntax.self) { | ||
let insertPos = importDecl | ||
.positionAfterSkippingLeadingTrivia | ||
extraManifestEdits.append( | ||
SourceEdit( | ||
range: insertPos..<insertPos, | ||
replacement: newImport | ||
) | ||
) | ||
break | ||
} | ||
} | ||
} | ||
|
||
default: break | ||
} | ||
|
||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(packageCall, with: newPackageCall.description) | ||
] + extraManifestEdits, | ||
auxiliaryFiles: auxiliaryFiles | ||
) | ||
} | ||
|
||
/// Add the primary source file for a target to the list of auxiliary | ||
/// source files. | ||
fileprivate static func addPrimarySourceFile( | ||
outerPath: String, | ||
target: PackageTarget, | ||
in context: Context, | ||
to auxiliaryFiles: inout AuxiliaryFiles | ||
) { | ||
let sourceFilePath = "\(outerPath)/\(target.name)/\(target.name).swift" | ||
|
||
// Introduce imports for each of the dependencies that were specified. | ||
var importModuleNames = target.dependencies.map { | ||
$0.name | ||
} | ||
|
||
// Add appropriate test module dependencies. | ||
if target.type == .test { | ||
switch context.testHarness { | ||
case .none: | ||
break | ||
|
||
case .xctest: | ||
importModuleNames.append("XCTest") | ||
|
||
case .swiftTesting: | ||
importModuleNames.append("Testing") | ||
} | ||
} | ||
|
||
let importDecls = importModuleNames.lazy.sorted().map { name in | ||
DeclSyntax("import \(raw: name)\n") | ||
} | ||
|
||
let imports = CodeBlockItemListSyntax { | ||
for importDecl in importDecls { | ||
importDecl | ||
} | ||
} | ||
|
||
let sourceFileText: SourceFileSyntax | ||
switch target.type { | ||
case .binary, .plugin, .system: | ||
fatalError("should have exited above") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is |
||
|
||
case .macro: | ||
sourceFileText = """ | ||
\(imports) | ||
struct \(raw: target.sanitizedName): Macro { | ||
/// TODO: Implement one or more of the protocols that inherit | ||
/// from Macro. The appropriate macro protocol is determined | ||
/// by the "macro" declaration that \(raw: target.sanitizedName) implements. | ||
/// Examples include: | ||
/// @freestanding(expression) macro --> ExpressionMacro | ||
/// @attached(member) macro --> MemberMacro | ||
} | ||
""" | ||
|
||
case .test: | ||
switch context.testHarness { | ||
case .none: | ||
sourceFileText = """ | ||
\(imports) | ||
// Test code here | ||
""" | ||
|
||
case .xctest: | ||
sourceFileText = """ | ||
\(imports) | ||
class \(raw: target.sanitizedName)Tests: XCTestCase { | ||
func test\(raw: target.sanitizedName)() { | ||
XCTAssertEqual(42, 17 + 25) | ||
} | ||
} | ||
""" | ||
|
||
case .swiftTesting: | ||
sourceFileText = """ | ||
\(imports) | ||
@Suite | ||
struct \(raw: target.sanitizedName)Tests { | ||
@Test("\(raw: target.sanitizedName) tests") | ||
func example() { | ||
#expect(42 == 17 + 25) | ||
} | ||
} | ||
""" | ||
} | ||
|
||
case .library: | ||
sourceFileText = """ | ||
\(imports) | ||
""" | ||
|
||
case .executable: | ||
sourceFileText = """ | ||
\(imports) | ||
@main | ||
struct \(raw: target.sanitizedName)Main { | ||
static func main() { | ||
print("Hello, world") | ||
} | ||
} | ||
""" | ||
} | ||
|
||
auxiliaryFiles.addSourceFile( | ||
path: sourceFilePath, | ||
sourceCode: sourceFileText | ||
) | ||
} | ||
|
||
/// Add a file that introduces the main entrypoint and provided macros | ||
/// for a macro target. | ||
fileprivate static func addProvidedMacrosSourceFile( | ||
outerPath: String, | ||
target: PackageTarget, | ||
to auxiliaryFiles: inout AuxiliaryFiles | ||
) { | ||
auxiliaryFiles.addSourceFile( | ||
path: "\(outerPath)/\(target.name)/ProvidedMacros.swift", | ||
sourceCode: """ | ||
import SwiftCompilerPlugin | ||
@main | ||
struct \(raw: target.sanitizedName)Macros: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
\(raw: target.sanitizedName).self, | ||
] | ||
} | ||
""" | ||
) | ||
} | ||
} | ||
|
||
fileprivate extension PackageTarget.Dependency { | ||
/// Retrieve the name of the dependency | ||
var name: String { | ||
switch self { | ||
case .target(let name), | ||
.byName(let name), | ||
.product(let name, package: _): | ||
return name | ||
} | ||
} | ||
} | ||
|
||
/// The array of auxiliary files that can be added by a package editing | ||
/// operation. | ||
private typealias AuxiliaryFiles = [(String, SourceFileSyntax)] | ||
|
||
fileprivate extension AuxiliaryFiles { | ||
/// Add a source file to the list of auxiliary files. | ||
mutating func addSourceFile( | ||
path: String, | ||
sourceCode: SourceFileSyntax | ||
) { | ||
self.append((path, sourceCode)) | ||
} | ||
} | ||
|
||
/// The set of dependencies we need to introduce to a newly-created macro | ||
/// target. | ||
private let macroTargetDependencies: [PackageTarget.Dependency] = [ | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"), | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
] | ||
|
||
/// The package dependency for swift-syntax, for use in macros. | ||
fileprivate extension PackageDependency { | ||
/// Source control URL for the swift-syntax package. | ||
static var swiftSyntaxURL: String { | ||
"https://github.com/swiftlang/swift-syntax.git" | ||
} | ||
|
||
/// Package dependency on the swift-syntax package starting from a partial version. | ||
static func swiftSyntax(from version: String) -> PackageDependency { | ||
return .sourceControl( | ||
.init( | ||
location: swiftSyntaxURL, | ||
requirement: .rangeFrom(version) | ||
) | ||
) | ||
} | ||
} | ||
|
||
fileprivate extension PackageTarget { | ||
var sanitizedName: String { | ||
name | ||
.mangledToC99ExtendedIdentifier() | ||
.localizedFirstWordCapitalized() | ||
} | ||
} | ||
|
||
fileprivate extension String { | ||
func localizedFirstWordCapitalized() -> String { prefix(1).uppercased() + dropFirst() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What’s the |
||
} | ||
|
||
extension SourceFileSyntax { | ||
private class ContainsLiteralVisitor: SyntaxVisitor { | ||
let string: String | ||
var found: Bool = false | ||
|
||
init(string: String) { | ||
self.string = string | ||
super.init(viewMode: .sourceAccurate) | ||
} | ||
|
||
override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind { | ||
if let representedLiteralValue = node.representedLiteralValue, | ||
representedLiteralValue == string | ||
{ | ||
found = true | ||
} | ||
|
||
return .skipChildren | ||
} | ||
} | ||
|
||
/// Determine whether this source file contains a string literal | ||
/// matching the given contents. | ||
fileprivate func containsStringLiteral(_ contents: String) -> Bool { | ||
let visitor = ContainsLiteralVisitor(string: contents) | ||
visitor.walk(self) | ||
return visitor.found | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 plugin usage to a particular target in the manifest's source | ||
/// code. | ||
@_spi(PackageRefactor) | ||
public struct AddPluginUsage: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
public let targetName: String | ||
public let pluginUsage: PackageTarget.PluginUsage | ||
|
||
public init(targetName: String, pluginUsage: PackageTarget.PluginUsage) { | ||
self.targetName = targetName | ||
self.pluginUsage = pluginUsage | ||
} | ||
} | ||
|
||
/// 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 targetName = context.targetName | ||
let pluginUsage = context.pluginUsage | ||
|
||
guard let packageCall = manifest.findCall(calleeName: "Package") else { | ||
throw ManifestEditError.cannotFindPackage | ||
} | ||
|
||
// Find the target to be modified. | ||
let targetCall = try packageCall.findManifestTargetCall(targetName: targetName) | ||
|
||
let newTargetCall = try targetCall.appendingToArrayArgument( | ||
label: "plugins", | ||
labelsAfter: [], | ||
newElement: pluginUsage.asSyntax() | ||
) | ||
|
||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(targetCall, with: newTargetCall.description) | ||
] | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 product to the manifest's source code. | ||
@_spi(PackageRefactor) | ||
public struct AddProduct: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
public let product: ProductDescription | ||
|
||
public init(product: ProductDescription) { | ||
self.product = product | ||
} | ||
} | ||
/// The set of argument labels that can occur after the "products" | ||
/// argument in the Package initializers. | ||
private static let argumentLabelsAfterProducts: Set<String> = [ | ||
"dependencies", | ||
"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 product = context.product | ||
|
||
guard let packageCall = manifest.findCall(calleeName: "Package") else { | ||
throw ManifestEditError.cannotFindPackage | ||
} | ||
|
||
let newPackageCall = try packageCall.appendingToArrayArgument( | ||
label: "products", | ||
labelsAfter: argumentLabelsAfterProducts, | ||
newElement: product.asSyntax() | ||
) | ||
|
||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(packageCall, with: newPackageCall.description) | ||
] | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 swift setting to a manifest's source code. | ||
@_spi(PackageRefactor) | ||
public struct AddSwiftSetting: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
let target: String | ||
let setting: String | ||
let value: ExprSyntax? | ||
} | ||
|
||
/// The set of argument labels that can occur after the "targets" | ||
/// argument in the Package initializers. | ||
private static let argumentLabelsAfterSwiftSettings: Set<String> = [ | ||
"linkerSettings", | ||
"plugins", | ||
] | ||
|
||
public static func upcomingFeature( | ||
to target: String, | ||
name: String, | ||
manifest: SourceFileSyntax | ||
) throws -> PackageEdit { | ||
try manifestRefactor( | ||
syntax: manifest, | ||
in: .init( | ||
target: target, | ||
setting: "enableUpcomingFeature", | ||
value: name.asSyntax() | ||
) | ||
) | ||
} | ||
|
||
public static func experimentalFeature( | ||
to target: String, | ||
name: String, | ||
manifest: SourceFileSyntax | ||
) throws -> PackageEdit { | ||
try manifestRefactor( | ||
syntax: manifest, | ||
in: .init( | ||
target: target, | ||
setting: "enableExperimentalFeature", | ||
value: name.asSyntax() | ||
) | ||
) | ||
} | ||
|
||
public static func languageMode( | ||
to target: String, | ||
mode rawMode: String, | ||
manifest: SourceFileSyntax | ||
) throws -> PackageEdit { | ||
let mode: String | ||
switch rawMode { | ||
case "3", "4", "5", "6": | ||
mode = ".v\(rawMode)" | ||
case "4.2": | ||
mode = ".v4_2" | ||
default: | ||
mode = ".version(\"\(rawMode)\")" | ||
} | ||
|
||
return try manifestRefactor( | ||
syntax: manifest, | ||
in: .init( | ||
target: target, | ||
setting: "swiftLanguageMode", | ||
value: "\(raw: mode)" | ||
) | ||
) | ||
} | ||
|
||
public static func strictMemorySafety( | ||
to target: String, | ||
manifest: SourceFileSyntax | ||
) throws -> PackageEdit { | ||
try manifestRefactor( | ||
syntax: manifest, | ||
in: .init( | ||
target: target, | ||
setting: "strictMemorySafety()", | ||
value: .none | ||
) | ||
) | ||
} | ||
|
||
public static func manifestRefactor( | ||
syntax manifest: SourceFileSyntax, | ||
in context: Context | ||
) throws -> PackageEdit { | ||
guard let packageCall = manifest.findCall(calleeName: "Package") else { | ||
throw ManifestEditError.cannotFindPackage | ||
} | ||
|
||
guard let targetsArgument = packageCall.findArgument(labeled: "targets"), | ||
let targetArray = targetsArgument.expression.findArrayArgument() | ||
else { | ||
throw ManifestEditError.cannotFindTargets | ||
} | ||
|
||
let targetCall = targetArray | ||
.elements | ||
.lazy | ||
.compactMap { | ||
$0.expression.as(FunctionCallExprSyntax.self) | ||
}.first { targetCall in | ||
if let nameArgument = targetCall.findArgument(labeled: "name"), | ||
let nameLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self), | ||
nameLiteral.representedLiteralValue == context.target | ||
{ | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
guard let targetCall else { | ||
throw ManifestEditError.cannotFindTarget(targetName: context.target) | ||
} | ||
|
||
if let memberRef = targetCall.calledExpression.as(MemberAccessExprSyntax.self), | ||
memberRef.declName.baseName.text == "plugin" | ||
{ | ||
throw ManifestEditError.cannotAddSettingsToPluginTarget | ||
} | ||
|
||
let newTargetCall = | ||
if let value = context.value { | ||
try targetCall.appendingToArrayArgument( | ||
label: "swiftSettings", | ||
labelsAfter: self.argumentLabelsAfterSwiftSettings, | ||
newElement: ".\(raw: context.setting)(\(value))" | ||
) | ||
} else { | ||
try targetCall.appendingToArrayArgument( | ||
label: "swiftSettings", | ||
labelsAfter: self.argumentLabelsAfterSwiftSettings, | ||
newElement: ".\(raw: context.setting)" | ||
) | ||
} | ||
|
||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(targetCall, with: newTargetCall.description) | ||
] | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 target dependency to a manifest's source code. | ||
@_spi(PackageRefactor) | ||
public struct AddTargetDependency: ManifestEditRefactoringProvider { | ||
public struct Context { | ||
/// The dependency to add. | ||
public var dependency: PackageTarget.Dependency | ||
|
||
/// The name of the target to which the dependency will be added. | ||
public var targetName: String | ||
|
||
public init(dependency: PackageTarget.Dependency, targetName: String) { | ||
self.dependency = dependency | ||
self.targetName = targetName | ||
} | ||
} | ||
|
||
/// The set of argument labels that can occur after the "dependencies" | ||
/// argument in the various target initializers. | ||
private static let argumentLabelsAfterDependencies: Set<String> = [ | ||
"path", | ||
"exclude", | ||
"sources", | ||
"resources", | ||
"publicHeadersPath", | ||
"packageAccess", | ||
"cSettings", | ||
"cxxSettings", | ||
"swiftSettings", | ||
"linkerSettings", | ||
"plugins", | ||
] | ||
|
||
/// Produce the set of source edits needed to add the given target | ||
/// dependency to the given manifest file. | ||
public static func manifestRefactor( | ||
syntax manifest: SourceFileSyntax, | ||
in context: Context | ||
) throws -> PackageEdit { | ||
let dependency = context.dependency | ||
let targetName = context.targetName | ||
|
||
guard let packageCall = manifest.findCall(calleeName: "Package") else { | ||
throw ManifestEditError.cannotFindPackage | ||
} | ||
|
||
// Find the target to be modified. | ||
let targetCall = try packageCall.findManifestTargetCall(targetName: targetName) | ||
|
||
let newTargetCall = try addTargetDependencyLocal( | ||
dependency, | ||
to: targetCall | ||
) | ||
|
||
return PackageEdit( | ||
manifestEdits: [ | ||
.replace(targetCall, with: newTargetCall.description) | ||
] | ||
) | ||
} | ||
|
||
/// Implementation of adding a target dependency to an existing call. | ||
static func addTargetDependencyLocal( | ||
_ dependency: PackageTarget.Dependency, | ||
to targetCall: FunctionCallExprSyntax | ||
) throws -> FunctionCallExprSyntax { | ||
try targetCall.appendingToArrayArgument( | ||
label: "dependencies", | ||
labelsAfter: Self.argumentLabelsAfterDependencies, | ||
newElement: dependency.asSyntax() | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
/// An error describing problems that can occur when attempting to edit a | ||
/// package manifest programattically. | ||
@_spi(PackageRefactor) | ||
public enum ManifestEditError: Error, Equatable { | ||
case cannotFindPackage | ||
case cannotFindTargets | ||
case cannotFindTarget(targetName: String) | ||
case cannotFindArrayLiteralArgument(argumentName: String) | ||
case cannotAddSettingsToPluginTarget | ||
case existingDependency(dependencyName: String) | ||
case malformedManifest(error: String) | ||
} | ||
|
||
extension ManifestEditError: CustomStringConvertible { | ||
public var description: String { | ||
switch self { | ||
case .cannotFindPackage: | ||
return "invalid manifest: unable to find 'Package' declaration" | ||
case .cannotFindTargets: | ||
return "unable to find package targets in manifest" | ||
case .cannotFindTarget(targetName: let name): | ||
return "unable to find target named '\(name)' in package" | ||
case .cannotFindArrayLiteralArgument(argumentName: let name): | ||
return "unable to find array literal for '\(name)' argument" | ||
case .cannotAddSettingsToPluginTarget: | ||
return "plugin targets do not support settings" | ||
case .existingDependency(let name): | ||
return "unable to add dependency '\(name)' because it already exists in the list of dependencies" | ||
case .malformedManifest(let error): | ||
return "invalid manifest: \(error)" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
@_spi(PackageRefactor) | ||
public protocol ManifestEditRefactoringProvider: EditRefactoringProvider | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be a public protocol? I think it would be better of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my comment about this - #2904 (comment) |
||
where Self.Input == SourceFileSyntax { | ||
|
||
static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEdit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: I'm planning to follow-up to this PR and remove this, I think all we need to do is change result type of |
||
} | ||
|
||
extension ManifestEditRefactoringProvider { | ||
public static func textRefactor(syntax: Input, in context: Context) -> [SourceEdit] { | ||
return (try? manifestRefactor(syntax: syntax, in: context).manifestEdits) ?? [] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
/// Describes an entity in the package model that can be represented as | ||
/// a syntax node. | ||
protocol ManifestSyntaxRepresentable { | ||
/// The most specific kind of syntax node that best describes this entity | ||
/// in the manifest. | ||
/// | ||
/// There might be other kinds of syntax nodes that can also represent | ||
/// the syntax, but this is the one that a canonical manifest will use. | ||
/// As an example, a package dependency is usually expressed as, e.g., | ||
/// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") | ||
/// | ||
/// However, there could be other forms, e.g., this is also valid: | ||
/// Package.Dependency.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") | ||
associatedtype PreferredSyntax: SyntaxProtocol | ||
|
||
/// Provides a suitable syntax node to describe this entity in the package | ||
/// model. | ||
/// | ||
/// The resulting syntax is a fragment that describes just this entity, | ||
/// and it's enclosing entity will need to understand how to fit it in. | ||
/// For example, a `PackageDependency` entity would map to syntax for | ||
/// something like | ||
/// .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") | ||
func asSyntax() -> PreferredSyntax | ||
} | ||
|
||
extension String: ManifestSyntaxRepresentable { | ||
typealias PreferredSyntax = ExprSyntax | ||
|
||
func asSyntax() -> ExprSyntax { "\(literal: self)" } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
|
||
/// Describes a package dependency for refactoring purposes. This is a syntactic | ||
/// subset of the full package manifest's description of a package dependency. | ||
@_spi(PackageRefactor) | ||
public enum PackageDependency: Sendable { | ||
case fileSystem(FileSystem) | ||
case sourceControl(SourceControl) | ||
case registry(Registry) | ||
|
||
public struct FileSystem: Sendable { | ||
public let path: String | ||
} | ||
|
||
public struct SourceControl: Sendable { | ||
public let location: String | ||
public let requirement: Requirement | ||
|
||
public init(location: String, requirement: Requirement) { | ||
self.location = location | ||
self.requirement = requirement | ||
} | ||
|
||
public enum Requirement: Sendable { | ||
case exact(String) | ||
case rangeFrom(String) | ||
case range(lowerBound: String, upperBound: String) | ||
case revision(String) | ||
case branch(String) | ||
} | ||
} | ||
|
||
public struct Registry: Sendable { | ||
public let identity: String | ||
public let requirement: Requirement | ||
|
||
/// The dependency requirement. | ||
public enum Requirement: Sendable { | ||
case exact(String) | ||
case rangeFrom(String) | ||
case range(lowerBound: String, upperBound: String) | ||
} | ||
} | ||
} | ||
|
||
extension PackageDependency: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this make more sense as a computed property? |
||
switch self { | ||
case .fileSystem(let filesystem): return filesystem.asSyntax() | ||
case .sourceControl(let sourceControl): return sourceControl.asSyntax() | ||
case .registry(let registry): return registry.asSyntax() | ||
} | ||
} | ||
} | ||
|
||
extension PackageDependency.FileSystem: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
".package(path: \(literal: path.description))" | ||
} | ||
} | ||
|
||
extension PackageDependency.SourceControl: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
".package(url: \(literal: location.description), \(requirement.asSyntax()))" | ||
} | ||
} | ||
|
||
extension PackageDependency.Registry: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
".package(id: \(literal: identity.description), \(requirement.asSyntax()))" | ||
} | ||
} | ||
|
||
extension PackageDependency.SourceControl.Requirement: ManifestSyntaxRepresentable { | ||
func asSyntax() -> LabeledExprSyntax { | ||
switch self { | ||
case .exact(let version): | ||
return LabeledExprSyntax( | ||
label: "exact", | ||
expression: version.asSyntax() | ||
) | ||
|
||
case .rangeFrom(let range): | ||
return LabeledExprSyntax( | ||
label: "from", | ||
expression: range.asSyntax() | ||
) | ||
|
||
case .range(let lowerBound, let upperBound): | ||
return LabeledExprSyntax( | ||
expression: "\(literal: lowerBound)..<\(literal: upperBound)" as ExprSyntax | ||
) | ||
|
||
case .revision(let revision): | ||
return LabeledExprSyntax( | ||
label: "revision", | ||
expression: "\(literal: revision)" as ExprSyntax | ||
) | ||
|
||
case .branch(let branch): | ||
return LabeledExprSyntax( | ||
label: "branch", | ||
expression: "\(literal: branch)" as ExprSyntax | ||
) | ||
} | ||
} | ||
} | ||
|
||
extension PackageDependency.Registry.Requirement: ManifestSyntaxRepresentable { | ||
func asSyntax() -> LabeledExprSyntax { | ||
switch self { | ||
case .exact(let version): | ||
return LabeledExprSyntax( | ||
label: "exact", | ||
expression: version.asSyntax() | ||
) | ||
|
||
case .rangeFrom(let range): | ||
return LabeledExprSyntax( | ||
label: "from", | ||
expression: range.asSyntax() | ||
) | ||
|
||
case .range(let lowerBound, let upperBound): | ||
return LabeledExprSyntax( | ||
expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
/// The result of editing a package, including any edits to the package | ||
/// manifest and any new files that are introduced. | ||
@_spi(PackageRefactor) | ||
public struct PackageEdit { | ||
/// Edits to perform to the package manifest. | ||
public var manifestEdits: [SourceEdit] = [] | ||
|
||
/// Auxiliary files to write. | ||
public var auxiliaryFiles: [(relativePath: String, contents: SourceFileSyntax)] = [] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
/// Syntactic wrapper type that describes a target for refactoring | ||
/// purposes but does not interpret its contents. | ||
@_spi(PackageRefactor) | ||
public struct PackageTarget { | ||
public let name: String | ||
|
||
/// The type of target. | ||
public let type: TargetKind | ||
|
||
public internal(set) var dependencies: [Dependency] | ||
|
||
public let path: String? | ||
|
||
public let url: String? | ||
|
||
public let checksum: String? | ||
|
||
/// The usages of package plugins by the target. | ||
public let pluginUsages: [PluginUsage]? | ||
|
||
/// Represents a target's usage of a plugin target or product. | ||
public enum PluginUsage { | ||
case plugin(name: String, package: String?) | ||
} | ||
|
||
public enum TargetKind: String { | ||
case binary | ||
case executable | ||
case library | ||
case macro | ||
case plugin | ||
case system | ||
case test | ||
} | ||
|
||
public enum Dependency: Sendable { | ||
case byName(name: String) | ||
case target(name: String) | ||
case product(name: String, package: String?) | ||
} | ||
|
||
public init( | ||
name: String, | ||
type: TargetKind = .library, | ||
dependencies: [Dependency] = [], | ||
path: String? = nil, | ||
url: String? = nil, | ||
checksum: String? = nil, | ||
pluginUsages: [PluginUsage]? = nil | ||
) { | ||
self.name = name | ||
self.type = type | ||
self.dependencies = dependencies | ||
self.path = path | ||
self.url = url | ||
self.checksum = checksum | ||
self.pluginUsages = pluginUsages | ||
} | ||
} | ||
|
||
extension PackageTarget: ManifestSyntaxRepresentable { | ||
/// The function name in the package manifest. | ||
private var functionName: String { | ||
switch type { | ||
case .binary: return "binaryTarget" | ||
case .executable: return "executableTarget" | ||
case .library: return "target" | ||
case .macro: return "macro" | ||
case .plugin: return "plugin" | ||
case .system: return "systemLibrary" | ||
case .test: return "testTarget" | ||
} | ||
} | ||
|
||
func asSyntax() -> ExprSyntax { | ||
var arguments: [LabeledExprSyntax] = [] | ||
arguments.append(label: "name", stringLiteral: name) | ||
// FIXME: pluginCapability | ||
|
||
arguments.appendIfNonEmpty( | ||
label: "dependencies", | ||
arrayLiteral: dependencies | ||
) | ||
|
||
arguments.appendIf(label: "path", stringLiteral: path) | ||
arguments.appendIf(label: "url", stringLiteral: url) | ||
|
||
// Only for plugins | ||
arguments.appendIf(label: "checksum", stringLiteral: checksum) | ||
|
||
if let pluginUsages { | ||
arguments.appendIfNonEmpty(label: "plugins", arrayLiteral: pluginUsages) | ||
} | ||
|
||
let separateParen: String = arguments.count > 1 ? "\n" : "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of doing some formatting here, should we run |
||
let argumentsSyntax = LabeledExprListSyntax(arguments) | ||
return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" | ||
} | ||
} | ||
|
||
extension PackageTarget.Dependency: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
switch self { | ||
case .byName(let name): | ||
return "\(literal: name)" | ||
|
||
case .target(let name): | ||
return ".target(name: \(literal: name))" | ||
|
||
case .product(let name, package: nil): | ||
return ".product(name: \(literal: name))" | ||
|
||
case .product(let name, let package): | ||
return ".product(name: \(literal: name), package: \(literal: package))" | ||
} | ||
} | ||
} | ||
|
||
extension PackageTarget.PluginUsage: ManifestSyntaxRepresentable { | ||
func asSyntax() -> ExprSyntax { | ||
switch self { | ||
case .plugin(let name, package: nil): | ||
return ".plugin(name: \(literal: name))" | ||
|
||
case .plugin(let name, let package): | ||
return ".plugin(name: \(literal: name), package: \(literal: package))" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 SwiftSyntax | ||
|
||
/// Syntactic wrapper type that describes a product for refactoring | ||
/// purposes but does not interpret its contents. | ||
@_spi(PackageRefactor) | ||
public struct ProductDescription { | ||
/// The name of the product. | ||
public let name: String | ||
|
||
/// The targets in the product. | ||
public let targets: [String] | ||
|
||
/// The type of product. | ||
public let type: ProductType | ||
|
||
public enum ProductType { | ||
/// The type of library. | ||
public enum LibraryType: String, Codable, Sendable { | ||
|
||
/// Static library. | ||
case `static` | ||
|
||
/// Dynamic library. | ||
case `dynamic` | ||
|
||
/// The type of library is unspecified and should be decided by package manager. | ||
case automatic | ||
} | ||
|
||
/// A library product. | ||
case library(LibraryType) | ||
|
||
/// An executable product. | ||
case executable | ||
|
||
/// An executable code snippet. | ||
case snippet | ||
|
||
/// An plugin product. | ||
case plugin | ||
|
||
/// A test product. | ||
case test | ||
|
||
/// A macro product. | ||
case `macro` | ||
} | ||
|
||
public init( | ||
name: String, | ||
type: ProductType, | ||
targets: [String] | ||
) { | ||
self.name = name | ||
self.type = type | ||
self.targets = targets | ||
} | ||
} | ||
|
||
extension ProductDescription: ManifestSyntaxRepresentable { | ||
/// The function name in the package manifest. | ||
/// | ||
/// Some of these are actually invalid, but it's up to the caller | ||
/// to check the precondition. | ||
private var functionName: String { | ||
switch type { | ||
case .executable: return "executable" | ||
case .library: return "library" | ||
case .macro: return "macro" | ||
case .plugin: return "plugin" | ||
case .snippet: return "snippet" | ||
case .test: return "test" | ||
} | ||
} | ||
|
||
func asSyntax() -> ExprSyntax { | ||
var arguments: [LabeledExprSyntax] = [] | ||
arguments.append(label: "name", stringLiteral: name) | ||
|
||
// Libraries have a type. | ||
if case .library(let libraryType) = type { | ||
switch libraryType { | ||
case .automatic: | ||
break | ||
|
||
case .dynamic, .static: | ||
arguments.append( | ||
label: "type", | ||
expression: ".\(raw: libraryType.rawValue)" | ||
) | ||
} | ||
} | ||
|
||
arguments.appendIfNonEmpty( | ||
label: "targets", | ||
arrayLiteral: targets | ||
) | ||
|
||
let separateParen: String = arguments.count > 1 ? "\n" : "" | ||
let argumentsSyntax = LabeledExprListSyntax(arguments) | ||
return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" | ||
ahoppen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
@_spi(PackageRefactor) | ||
extension String { | ||
/// Returns a form of the string that is valid C99 Extended Identifier (by | ||
/// replacing any invalid characters in an unspecified but consistent way). | ||
/// The output string is guaranteed to be non-empty as long as the input | ||
/// string is non-empty. | ||
func mangledToC99ExtendedIdentifier() -> String { | ||
// Map invalid C99-invalid Unicode scalars to a replacement character. | ||
let replacementUnichar: UnicodeScalar = "_" | ||
var mangledUnichars: [UnicodeScalar] = self.unicodeScalars.map { | ||
switch $0.value { | ||
case // A-Z | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a definition of this list in some spec that we could link to? It feels pretty arbitrary as a standalone switch. |
||
0x0041...0x005A, | ||
// a-z | ||
0x0061...0x007A, | ||
// 0-9 | ||
0x0030...0x0039, | ||
// _ | ||
0x005F, | ||
// Latin (1) | ||
0x00AA...0x00AA, | ||
// Special characters (1) | ||
0x00B5...0x00B5, 0x00B7...0x00B7, | ||
// Latin (2) | ||
0x00BA...0x00BA, 0x00C0...0x00D6, 0x00D8...0x00F6, | ||
0x00F8...0x01F5, 0x01FA...0x0217, 0x0250...0x02A8, | ||
// Special characters (2) | ||
0x02B0...0x02B8, 0x02BB...0x02BB, 0x02BD...0x02C1, | ||
0x02D0...0x02D1, 0x02E0...0x02E4, 0x037A...0x037A, | ||
// Greek (1) | ||
0x0386...0x0386, 0x0388...0x038A, 0x038C...0x038C, | ||
0x038E...0x03A1, 0x03A3...0x03CE, 0x03D0...0x03D6, | ||
0x03DA...0x03DA, 0x03DC...0x03DC, 0x03DE...0x03DE, | ||
0x03E0...0x03E0, 0x03E2...0x03F3, | ||
// Cyrillic | ||
0x0401...0x040C, 0x040E...0x044F, 0x0451...0x045C, | ||
0x045E...0x0481, 0x0490...0x04C4, 0x04C7...0x04C8, | ||
0x04CB...0x04CC, 0x04D0...0x04EB, 0x04EE...0x04F5, | ||
0x04F8...0x04F9, | ||
// Armenian (1) | ||
0x0531...0x0556, | ||
// Special characters (3) | ||
0x0559...0x0559, | ||
// Armenian (2) | ||
0x0561...0x0587, | ||
// Hebrew | ||
0x05B0...0x05B9, 0x05BB...0x05BD, 0x05BF...0x05BF, | ||
0x05C1...0x05C2, 0x05D0...0x05EA, 0x05F0...0x05F2, | ||
// Arabic (1) | ||
0x0621...0x063A, 0x0640...0x0652, | ||
// Digits (1) | ||
0x0660...0x0669, | ||
// Arabic (2) | ||
0x0670...0x06B7, 0x06BA...0x06BE, 0x06C0...0x06CE, | ||
0x06D0...0x06DC, 0x06E5...0x06E8, 0x06EA...0x06ED, | ||
// Digits (2) | ||
0x06F0...0x06F9, | ||
// Devanagari and Special character 0x093D. | ||
0x0901...0x0903, 0x0905...0x0939, 0x093D...0x094D, | ||
0x0950...0x0952, 0x0958...0x0963, | ||
// Digits (3) | ||
0x0966...0x096F, | ||
// Bengali (1) | ||
0x0981...0x0983, 0x0985...0x098C, 0x098F...0x0990, | ||
0x0993...0x09A8, 0x09AA...0x09B0, 0x09B2...0x09B2, | ||
0x09B6...0x09B9, 0x09BE...0x09C4, 0x09C7...0x09C8, | ||
0x09CB...0x09CD, 0x09DC...0x09DD, 0x09DF...0x09E3, | ||
// Digits (4) | ||
0x09E6...0x09EF, | ||
// Bengali (2) | ||
0x09F0...0x09F1, | ||
// Gurmukhi (1) | ||
0x0A02...0x0A02, 0x0A05...0x0A0A, 0x0A0F...0x0A10, | ||
0x0A13...0x0A28, 0x0A2A...0x0A30, 0x0A32...0x0A33, | ||
0x0A35...0x0A36, 0x0A38...0x0A39, 0x0A3E...0x0A42, | ||
0x0A47...0x0A48, 0x0A4B...0x0A4D, 0x0A59...0x0A5C, | ||
0x0A5E...0x0A5E, | ||
// Digits (5) | ||
0x0A66...0x0A6F, | ||
// Gurmukhi (2) | ||
0x0A74...0x0A74, | ||
// Gujarti | ||
0x0A81...0x0A83, 0x0A85...0x0A8B, 0x0A8D...0x0A8D, | ||
0x0A8F...0x0A91, 0x0A93...0x0AA8, 0x0AAA...0x0AB0, | ||
0x0AB2...0x0AB3, 0x0AB5...0x0AB9, 0x0ABD...0x0AC5, | ||
0x0AC7...0x0AC9, 0x0ACB...0x0ACD, 0x0AD0...0x0AD0, | ||
0x0AE0...0x0AE0, | ||
// Digits (6) | ||
0x0AE6...0x0AEF, | ||
// Oriya and Special character 0x0B3D | ||
0x0B01...0x0B03, 0x0B05...0x0B0C, 0x0B0F...0x0B10, | ||
0x0B13...0x0B28, 0x0B2A...0x0B30, 0x0B32...0x0B33, | ||
0x0B36...0x0B39, 0x0B3D...0x0B43, 0x0B47...0x0B48, | ||
0x0B4B...0x0B4D, 0x0B5C...0x0B5D, 0x0B5F...0x0B61, | ||
// Digits (7) | ||
0x0B66...0x0B6F, | ||
// Tamil | ||
0x0B82...0x0B83, 0x0B85...0x0B8A, 0x0B8E...0x0B90, | ||
0x0B92...0x0B95, 0x0B99...0x0B9A, 0x0B9C...0x0B9C, | ||
0x0B9E...0x0B9F, 0x0BA3...0x0BA4, 0x0BA8...0x0BAA, | ||
0x0BAE...0x0BB5, 0x0BB7...0x0BB9, 0x0BBE...0x0BC2, | ||
0x0BC6...0x0BC8, 0x0BCA...0x0BCD, | ||
// Digits (8) | ||
0x0BE7...0x0BEF, | ||
// Telugu | ||
0x0C01...0x0C03, 0x0C05...0x0C0C, 0x0C0E...0x0C10, | ||
0x0C12...0x0C28, 0x0C2A...0x0C33, 0x0C35...0x0C39, | ||
0x0C3E...0x0C44, 0x0C46...0x0C48, 0x0C4A...0x0C4D, | ||
0x0C60...0x0C61, | ||
// Digits (9) | ||
0x0C66...0x0C6F, | ||
// Kannada | ||
0x0C82...0x0C83, 0x0C85...0x0C8C, 0x0C8E...0x0C90, | ||
0x0C92...0x0CA8, 0x0CAA...0x0CB3, 0x0CB5...0x0CB9, | ||
0x0CBE...0x0CC4, 0x0CC6...0x0CC8, 0x0CCA...0x0CCD, | ||
0x0CDE...0x0CDE, 0x0CE0...0x0CE1, | ||
// Digits (10) | ||
0x0CE6...0x0CEF, | ||
// Malayam | ||
0x0D02...0x0D03, 0x0D05...0x0D0C, 0x0D0E...0x0D10, | ||
0x0D12...0x0D28, 0x0D2A...0x0D39, 0x0D3E...0x0D43, | ||
0x0D46...0x0D48, 0x0D4A...0x0D4D, 0x0D60...0x0D61, | ||
// Digits (11) | ||
0x0D66...0x0D6F, | ||
// Thai...including Digits 0x0E50...0x0E59 } | ||
0x0E01...0x0E3A, 0x0E40...0x0E5B, | ||
// Lao (1) | ||
0x0E81...0x0E82, 0x0E84...0x0E84, 0x0E87...0x0E88, | ||
0x0E8A...0x0E8A, 0x0E8D...0x0E8D, 0x0E94...0x0E97, | ||
0x0E99...0x0E9F, 0x0EA1...0x0EA3, 0x0EA5...0x0EA5, | ||
0x0EA7...0x0EA7, 0x0EAA...0x0EAB, 0x0EAD...0x0EAE, | ||
0x0EB0...0x0EB9, 0x0EBB...0x0EBD, 0x0EC0...0x0EC4, | ||
0x0EC6...0x0EC6, 0x0EC8...0x0ECD, | ||
// Digits (12) | ||
0x0ED0...0x0ED9, | ||
// Lao (2) | ||
0x0EDC...0x0EDD, | ||
// Tibetan (1) | ||
0x0F00...0x0F00, 0x0F18...0x0F19, | ||
// Digits (13) | ||
0x0F20...0x0F33, | ||
// Tibetan (2) | ||
0x0F35...0x0F35, 0x0F37...0x0F37, 0x0F39...0x0F39, | ||
0x0F3E...0x0F47, 0x0F49...0x0F69, 0x0F71...0x0F84, | ||
0x0F86...0x0F8B, 0x0F90...0x0F95, 0x0F97...0x0F97, | ||
0x0F99...0x0FAD, 0x0FB1...0x0FB7, 0x0FB9...0x0FB9, | ||
// Georgian | ||
0x10A0...0x10C5, 0x10D0...0x10F6, | ||
// Latin (3) | ||
0x1E00...0x1E9B, 0x1EA0...0x1EF9, | ||
// Greek (2) | ||
0x1F00...0x1F15, 0x1F18...0x1F1D, 0x1F20...0x1F45, | ||
0x1F48...0x1F4D, 0x1F50...0x1F57, 0x1F59...0x1F59, | ||
0x1F5B...0x1F5B, 0x1F5D...0x1F5D, 0x1F5F...0x1F7D, | ||
0x1F80...0x1FB4, 0x1FB6...0x1FBC, | ||
// Special characters (4) | ||
0x1FBE...0x1FBE, | ||
// Greek (3) | ||
0x1FC2...0x1FC4, 0x1FC6...0x1FCC, 0x1FD0...0x1FD3, | ||
0x1FD6...0x1FDB, 0x1FE0...0x1FEC, 0x1FF2...0x1FF4, | ||
0x1FF6...0x1FFC, | ||
// Special characters (5) | ||
0x203F...0x2040, | ||
// Latin (4) | ||
0x207F...0x207F, | ||
// Special characters (6) | ||
0x2102...0x2102, 0x2107...0x2107, 0x210A...0x2113, | ||
0x2115...0x2115, 0x2118...0x211D, 0x2124...0x2124, | ||
0x2126...0x2126, 0x2128...0x2128, 0x212A...0x2131, | ||
0x2133...0x2138, 0x2160...0x2182, 0x3005...0x3007, | ||
0x3021...0x3029, | ||
// Hiragana | ||
0x3041...0x3093, 0x309B...0x309C, | ||
// Katakana | ||
0x30A1...0x30F6, 0x30FB...0x30FC, | ||
// Bopmofo [sic] | ||
0x3105...0x312C, | ||
// CJK Unified Ideographs | ||
0x4E00...0x9FA5, | ||
// Hangul, | ||
0xAC00...0xD7A3: | ||
return $0 | ||
default: | ||
return replacementUnichar | ||
} | ||
} | ||
|
||
// Apply further restrictions to the prefix. | ||
LOOP: for (idx, c) in mangledUnichars.enumerated() { | ||
switch c.value { | ||
case // 0-9 | ||
0x0030...0x0039, | ||
// Annex D. | ||
0x0660...0x0669, 0x06F0...0x06F9, 0x0966...0x096F, | ||
0x09E6...0x09EF, 0x0A66...0x0A6F, 0x0AE6...0x0AEF, | ||
0x0B66...0x0B6F, 0x0BE7...0x0BEF, 0x0C66...0x0C6F, | ||
0x0CE6...0x0CEF, 0x0D66...0x0D6F, 0x0E50...0x0E59, | ||
0x0ED0...0x0ED9, 0x0F20...0x0F33: | ||
mangledUnichars[idx] = replacementUnichar | ||
break LOOP | ||
default: | ||
break LOOP | ||
} | ||
} | ||
|
||
// Combine the characters as a string again and return it. | ||
// FIXME: We should only construct a new string if anything changed. | ||
// FIXME: There doesn't seem to be a way to create a string from an | ||
// array of Unicode scalars; but there must be a better way. | ||
return String(decoding: mangledUnichars.flatMap { $0.utf8 }, as: UTF8.self) | ||
} | ||
} |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd just pick a reasonable default, ie.
SourceControl
andfrom
:(though those placeholders could also be typed)
There was a problem hiding this comment.
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.