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
Changes from all 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
@@ -393,7 +393,7 @@ let package = Package(

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

// MARK: - Deprecated targets
16 changes: 16 additions & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
140 changes: 140 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift
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
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
// 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
}
}
}
389 changes: 389 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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")
Copy link
Member

Choose a reason for hiding this comment

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

Where is above? I don’t see the check where this should have exited.


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() }
Copy link
Member

Choose a reason for hiding this comment

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

What’s the localize part of this function name?

}

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
}
}
59 changes: 59 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift
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)
]
)
}
}
61 changes: 61 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddProduct.swift
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)
]
)
}
}
163 changes: 163 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddSwiftSetting.swift
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)
]
)
}
}
88 changes: 88 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift
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()
)
}
}
47 changes: 47 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift
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
Copy link
Member

Choose a reason for hiding this comment

The 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 EditRefactoringProvider would gain the ability to modify auxiliary files. At that point a package refactoring wouldn’t be anything special, it would just be a normal refactoring.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 textRefactor to not only produce edits but also auxiliary files.

}

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)" }
}
144 changes: 144 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/PackageDependency.swift
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 {
Copy link
Member

Choose a reason for hiding this comment

The 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
)
}
}
}
24 changes: 24 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/PackageEdit.swift
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)] = []
}
143 changes: 143 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/PackageTarget.swift
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" : ""
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing some formatting here, should we run BasicFormat?

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))"
}
}
}
115 changes: 115 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/ProductDescription.swift
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))"
}
}
224 changes: 224 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/StringUtils.swift
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
Copy link
Member

Choose a reason for hiding this comment

The 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)
}
}
547 changes: 547 additions & 0 deletions Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift

Large diffs are not rendered by default.

1,103 changes: 1,103 additions & 0 deletions Tests/SwiftRefactorTest/ManifestEditTests.swift

Large diffs are not rendered by default.