diff --git a/Package.swift b/Package.swift index b8da017f322..8ad34c3beb9 100644 --- a/Package.swift +++ b/Package.swift @@ -379,7 +379,7 @@ let package = Package( .testTarget( name: "SwiftRefactorTest", - dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"] + dependencies: ["_SwiftSyntaxTestSupport", "SwiftIDEUtils", "SwiftRefactor"] ), // MARK: - Deprecated targets diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index 1864c71f2ab..d7d05c0ade0 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -21,6 +21,26 @@ add_swift_syntax_library(SwiftRefactor RefactoringProvider.swift RemoveSeparatorsFromIntegerLiteral.swift SyntaxUtils.swift + + PackageManifest/AbsolutePath.swift + PackageManifest/AddPackageDependency.swift + PackageManifest/AddPackageTarget.swift + PackageManifest/AddPluginUsage.swift + PackageManifest/AddProduct.swift + PackageManifest/AddTargetDependency.swift + PackageManifest/ManifestEditError.swift + PackageManifest/ManifestEditRefactoringProvider.swift + PackageManifest/ManifestSyntaxRepresentable.swift + PackageManifest/PackageDependency.swift + PackageManifest/PackageEdit.swift + PackageManifest/PackageIdentity.swift + PackageManifest/ProductDescription.swift + PackageManifest/RelativePath.swift + PackageManifest/SemanticVersion.swift + PackageManifest/SourceControlURL.swift + PackageManifest/StringUtils.swift + PackageManifest/SyntaxEditUtils.swift + PackageManifest/Target.swift ) target_link_swift_syntax_libraries(SwiftRefactor PUBLIC diff --git a/Sources/SwiftRefactor/PackageManifest/AbsolutePath.swift b/Sources/SwiftRefactor/PackageManifest/AbsolutePath.swift new file mode 100644 index 00000000000..d6ed26d345e --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AbsolutePath.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Syntactic wrapper type that describes an absolute path for refactoring +/// purposes but does not interpret its contents. +public struct AbsolutePath: CustomStringConvertible, Equatable, Hashable, Sendable { + public private(set) var description: String + + public init(_ description: String) { + self.description = description + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift new file mode 100644 index 00000000000..80db72a0e3d --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +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. + /// + /// TODO: Could we generate this from the the PackageDescription module, so + /// we don't have keep it up-to-date manually? + private static let argumentLabelsAfterDependencies: Set = [ + "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 + } + + let newPackageCall = try addPackageDependencyLocal( + dependency, + to: packageCall + ) + + return PackageEdit( + manifestEdits: [ + .replace(packageCall, with: newPackageCall.description) + ] + ) + } + + /// 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", + trailingLabels: Self.argumentLabelsAfterDependencies, + newElement: dependency.asSyntax() + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift new file mode 100644 index 00000000000..8ca778f7205 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift @@ -0,0 +1,418 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public struct AddPackageTarget: ManifestEditRefactoringProvider { + public struct Context { + public let target: Target + public let configuration: Configuration + + public init(target: Target, configuration: Configuration = .init()) { + self.target = target + self.configuration = configuration + } + } + + /// The set of argument labels that can occur after the "targets" + /// argument in the Package initializers. + /// + /// TODO: Could we generate this from the the PackageDescription module, so + /// we don't have keep it up-to-date manually? + private static let argumentLabelsAfterTargets: Set = [ + "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 = .xctest + } + + /// Additional configuration information to guide the package editing + /// process. + public struct Configuration { + /// The test harness to use. + public var testHarness: TestHarness + + public let swiftSyntaxVersion: SemanticVersion + public let swiftTestingVersion: SemanticVersion + + public init( + testHarness: TestHarness = .default, + swiftSyntaxVersion: SemanticVersion = SemanticVersion("600.0.0-latest"), + swiftTestingVersion: SemanticVersion = SemanticVersion("0.8.0") + ) { + self.testHarness = testHarness + self.swiftSyntaxVersion = swiftSyntaxVersion + self.swiftTestingVersion = swiftTestingVersion + } + } + + /// 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 { + let configuration = context.configuration + 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", + trailingLabels: 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 = RelativePath(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, + configuration: configuration, + 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( + version: configuration.swiftSyntaxVersion + ), + 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.. ExpressionMacro + /// @attached(member) macro --> MemberMacro + } + """ + + case .test: + switch configuration.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: RelativePath, + target: Target, + to auxiliaryFiles: inout AuxiliaryFiles + ) { + auxiliaryFiles.addSourceFile( + path: outerPath.appending( + components: [target.name, "ProvidedMacros.swift"] + ), + sourceCode: """ + import SwiftCompilerPlugin + + @main + struct \(raw: target.sanitizedName)Macros: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + \(raw: target.sanitizedName).self, + ] + } + """ + ) + } +} + +fileprivate extension Target.Dependency { + /// Retrieve the name of the dependency + var name: String { + switch self { + case .target(name: let name), + .byName(name: let name), + .product(name: let name, package: _): + return name + } + } +} + +/// The array of auxiliary files that can be added by a package editing +/// operation. +fileprivate typealias AuxiliaryFiles = [(RelativePath, SourceFileSyntax)] + +fileprivate extension AuxiliaryFiles { + /// Add a source file to the list of auxiliary files. + mutating func addSourceFile( + path: RelativePath, + sourceCode: SourceFileSyntax + ) { + self.append((path, sourceCode)) + } +} + +/// The set of dependencies we need to introduce to a newly-created macro +/// target. +fileprivate let macroTargetDependencies: [Target.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: SourceControlURL { + .init("https://github.com/swiftlang/swift-syntax.git") + } + + /// Package dependency on the swift-syntax package. + static func swiftSyntax( + version: SemanticVersion + ) -> PackageDependency { + return .sourceControl( + .init( + identity: PackageIdentity("swift-syntax"), + location: .remote(swiftSyntaxURL), + requirement: .rangeFrom(version) + ) + ) + } +} + +fileprivate extension Target { + var sanitizedName: String { + name + .mangledToC99ExtendedIdentifier() + .localizedFirstWordCapitalized() + } +} + +fileprivate extension String { + func localizedFirstWordCapitalized() -> String { prefix(1).uppercased() + dropFirst() } +} + +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 + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift new file mode 100644 index 00000000000..8a93dd2adf3 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public struct AddPluginUsage: ManifestEditRefactoringProvider { + public struct Context { + public let targetName: String + public let pluginUsage: Target.PluginUsage + + public init(targetName: String, pluginUsage: Target.PluginUsage) { + self.targetName = targetName + self.pluginUsage = pluginUsage + } + } + + /// The set of argument labels that can occur after the "plugins" + /// argument in the Target initializers. (There aren't any right now) + /// + /// TODO: Could we generate this from the the PackageDescription module, so + /// we don't have keep it up-to-date manually? + private static let argumentLabelsAfterPluginUsages: Set = [] + + /// 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", + trailingLabels: Self.argumentLabelsAfterPluginUsages, + newElement: pluginUsage.asSyntax() + ) + + return PackageEdit( + manifestEdits: [ + .replace(targetCall, with: newTargetCall.description) + ] + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddProduct.swift b/Sources/SwiftRefactor/PackageManifest/AddProduct.swift new file mode 100644 index 00000000000..ad862d004a5 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddProduct.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +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. + /// + /// TODO: Could we generate this from the the PackageDescription module, so + /// we don't have keep it up-to-date manually? + private static let argumentLabelsAfterProducts: Set = [ + "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", + trailingLabels: argumentLabelsAfterProducts, + newElement: product.asSyntax() + ) + + return PackageEdit( + manifestEdits: [ + .replace(packageCall, with: newPackageCall.description) + ] + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift new file mode 100644 index 00000000000..ab822131bc8 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public struct AddTargetDependency: ManifestEditRefactoringProvider { + public struct Context { + /// The dependency to add. + public var dependency: Target.Dependency + + /// The name of the target to which the dependency will be added. + public var targetName: String + + public init(dependency: Target.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. + /// + /// TODO: Could we generate this from the the PackageDescription module, so + /// we don't have keep it up-to-date manually? + private static let argumentLabelsAfterDependencies: Set = [ + "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: Target.Dependency, + to targetCall: FunctionCallExprSyntax + ) throws -> FunctionCallExprSyntax { + try targetCall.appendingToArrayArgument( + label: "dependencies", + trailingLabels: Self.argumentLabelsAfterDependencies, + newElement: dependency.asSyntax() + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift b/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift new file mode 100644 index 00000000000..17ad41df4ae --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public enum ManifestEditError: Error { + case cannotFindPackage + case cannotFindTargets + case cannotFindTarget(targetName: String) + case cannotFindArrayLiteralArgument(argumentName: String, node: Syntax) +} + +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, node: _): + return "unable to find array literal for '\(name)' argument" + } + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift b/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift new file mode 100644 index 00000000000..6e47412ffd3 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +public protocol ManifestEditRefactoringProvider: EditRefactoringProvider +where Self.Input == SourceFileSyntax { + + static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEdit +} + +extension EditRefactoringProvider where Self: ManifestEditRefactoringProvider { + public static func textRefactor(syntax: Input, in context: Context) -> [SourceEdit] { + return (try? manifestRefactor(syntax: syntax, in: context).manifestEdits) ?? [] + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/ManifestSyntaxRepresentable.swift b/Sources/SwiftRefactor/PackageManifest/ManifestSyntaxRepresentable.swift new file mode 100644 index 00000000000..e9899333fdb --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ManifestSyntaxRepresentable.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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)" } +} diff --git a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift new file mode 100644 index 00000000000..eb2a62f1944 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public enum PackageDependency: Sendable { + case fileSystem(FileSystem) + case sourceControl(SourceControl) + case registry(Registry) + + public struct FileSystem: Sendable { + public let identity: PackageIdentity + public let nameForTargetDependencyResolutionOnly: String? + public let path: AbsolutePath + } + + public struct SourceControl: Sendable { + public let identity: PackageIdentity + public let location: Location + public let requirement: Requirement + + public init(identity: PackageIdentity, location: Location, requirement: Requirement) { + self.identity = identity + self.location = location + self.requirement = requirement + } + + public enum Requirement: Sendable { + case exact(SemanticVersion) + case rangeFrom(SemanticVersion) + case range(lowerBound: SemanticVersion, upperBound: SemanticVersion) + case revision(String) + case branch(String) + } + + public enum Location: Sendable { + case local(AbsolutePath) + case remote(SourceControlURL) + } + } + + public struct Registry: Sendable { + public let identity: PackageIdentity + public let requirement: Requirement + + /// The dependency requirement. + public enum Requirement: Sendable { + case exact(SemanticVersion) + case rangeFrom(SemanticVersion) + case range(lowerBound: SemanticVersion, upperBound: SemanticVersion) + } + } +} + +extension PackageDependency: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + 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 { + // TODO: Not handling identity, nameForTargetDependencyResolutionOnly, + // or productFilter yet. + switch location { + case .local: + fatalError() + case .remote(let url): + return ".package(url: \(literal: url.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: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" 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 + ) + } + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/PackageEdit.swift b/Sources/SwiftRefactor/PackageManifest/PackageEdit.swift new file mode 100644 index 00000000000..875a8fc66aa --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/PackageEdit.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public struct PackageEdit { + /// Edits to perform to the package manifest. + public var manifestEdits: [SourceEdit] = [] + + /// Auxiliary files to write. + public var auxiliaryFiles: [(RelativePath, SourceFileSyntax)] = [] +} diff --git a/Sources/SwiftRefactor/PackageManifest/PackageIdentity.swift b/Sources/SwiftRefactor/PackageManifest/PackageIdentity.swift new file mode 100644 index 00000000000..d51561b33f9 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/PackageIdentity.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Describes a package identity for refactoring purposes. This is a syntactic +/// subset of the full package manifest's notion of package identity. +public struct PackageIdentity: CustomStringConvertible, Equatable, Hashable, Sendable { + public private(set) var description: String + + public init(_ description: String) { + self.description = description + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift new file mode 100644 index 00000000000..640075f6473 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +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))" + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/RelativePath.swift b/Sources/SwiftRefactor/PackageManifest/RelativePath.swift new file mode 100644 index 00000000000..1eb2a46e4e3 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/RelativePath.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Syntactic wrapper type that describes a relative path for refactoring +/// purposes but does not interpret its contents. +public struct RelativePath: CustomStringConvertible, Equatable, Hashable, Sendable { + public private(set) var description: String + + public init(_ description: String) { + self.description = description + } + + #if os(Windows) + static let pathSeparator: Character = "\\" + #else + static let pathSeparator: Character = "/" + #endif + + public func appending(_ component: String) -> Self { + Self(description + String(RelativePath.pathSeparator) + component) + } + + public func appending(components: [String]) -> Self { + appending(components.joined(separator: String(RelativePath.pathSeparator))) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/SemanticVersion.swift b/Sources/SwiftRefactor/PackageManifest/SemanticVersion.swift new file mode 100644 index 00000000000..bcd819207c4 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/SemanticVersion.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Syntactic wrapper type that describes a semantic version for refactoring +/// purposes but does not interpret its contents. +public struct SemanticVersion: CustomStringConvertible, Equatable, Hashable, Sendable { + public private(set) var description: String + + public init(_ description: String) { + self.description = description + } +} + +extension SemanticVersion: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + "\(literal: description)" + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/SourceControlURL.swift b/Sources/SwiftRefactor/PackageManifest/SourceControlURL.swift new file mode 100644 index 00000000000..70b31ae9109 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/SourceControlURL.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Syntactic wrapper type that describes a source control URL for refactoring +/// purposes but does not interpret its contents. +public struct SourceControlURL: CustomStringConvertible, Equatable, Hashable, Sendable { + public private(set) var description: String + + public init(_ description: String) { + self.description = description + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/StringUtils.swift b/Sources/SwiftRefactor/PackageManifest/StringUtils.swift new file mode 100644 index 00000000000..ff412739ca8 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/StringUtils.swift @@ -0,0 +1,223 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +// +//===----------------------------------------------------------------------===// + +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 + 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 mangledUnichars.reduce("") { $0 + String($1) } + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift new file mode 100644 index 00000000000..a5fcc20eb9d --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift @@ -0,0 +1,552 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftBasicFormat +import SwiftParser +import SwiftSyntax + +/// Default indent when we have to introduce indentation but have no context +/// to get it right. +let defaultIndent = TriviaPiece.spaces(4) + +extension Trivia { + /// Determine whether this trivia has newlines or not. + var hasNewlines: Bool { + contains(where: \.isNewline) + } + + /// Produce trivia from the last newline to the end, dropping anything + /// prior to that. + func onlyLastLine() -> Trivia { + guard let lastNewline = pieces.lastIndex(where: { $0.isNewline }) else { + return self + } + + return Trivia(pieces: pieces[lastNewline...]) + } +} + +/// Syntax walker to find the first occurrence of a given node kind that +/// matches a specific predicate. +private class FirstNodeFinder: SyntaxAnyVisitor { + var predicate: (Node) -> Bool + var found: Node? = nil + + init(predicate: @escaping (Node) -> Bool) { + self.predicate = predicate + super.init(viewMode: .sourceAccurate) + } + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if found != nil { + return .skipChildren + } + + if let matchedNode = node.as(Node.self), predicate(matchedNode) { + found = matchedNode + return .skipChildren + } + + return .visitChildren + } +} + +extension SyntaxProtocol { + /// Find the first node of the Self type that matches the given predicate. + static func findFirst( + in node: some SyntaxProtocol, + matching predicate: (Self) -> Bool + ) -> Self? { + withoutActuallyEscaping(predicate) { escapingPredicate in + let visitor = FirstNodeFinder(predicate: escapingPredicate) + visitor.walk(node) + return visitor.found + } + } +} + +extension FunctionCallExprSyntax { + /// Check whether this call expression has a callee that is a reference + /// to a declaration with the given name. + func hasCallee(named name: String) -> Bool { + guard let calleeDeclRef = calledExpression.as(DeclReferenceExprSyntax.self) else { + return false + } + + return calleeDeclRef.baseName.text == name + } + + /// Find a call argument based on its label. + func findArgument(labeled label: String) -> LabeledExprSyntax? { + arguments.first { $0.label?.text == label } + } + + /// Find a call argument index based on its label. + func findArgumentIndex(labeled label: String) -> LabeledExprListSyntax.Index? { + arguments.firstIndex { $0.label?.text == label } + } +} + +extension LabeledExprListSyntax { + /// Find the index at which the one would insert a new argument given + /// the set of argument labels that could come after the argument we + /// want to insert. + func findArgumentInsertionPosition( + labelsAfter: Set + ) -> SyntaxChildrenIndex { + firstIndex { + guard let label = $0.label else { + return false + } + + return labelsAfter.contains(label.text) + } ?? endIndex + } + + /// Form a new argument list that inserts a new argument at the specified + /// position in this argument list. + /// + /// This operation will attempt to introduce trivia to match the + /// surrounding context where possible. The actual argument will be + /// created by the `generator` function, which is provided with leading + /// trivia and trailing comma it should use to match the surrounding + /// context. + func insertingArgument( + at position: SyntaxChildrenIndex, + generator: (Trivia, TokenSyntax?) -> LabeledExprSyntax + ) -> LabeledExprListSyntax { + // Turn the arguments into an array so we can manipulate them. + var arguments = Array(self) + + let positionIdx = distance(from: startIndex, to: position) + + let commaToken = TokenSyntax.commaToken() + + // Figure out leading trivia and adjust the prior argument (if there is + // one) by adding a comma, if necessary. + let leadingTrivia: Trivia + if position > startIndex { + let priorArgument = arguments[positionIdx - 1] + + // Our leading trivia will be based on the prior argument's leading + // trivia. + leadingTrivia = priorArgument.leadingTrivia + + // If the prior argument is missing a trailing comma, add one. + if priorArgument.trailingComma == nil { + arguments[positionIdx - 1].trailingComma = commaToken + } + } else if positionIdx + 1 < count { + leadingTrivia = arguments[positionIdx + 1].leadingTrivia + } else { + leadingTrivia = Trivia() + } + + // Determine whether we need a trailing comma on this argument. + let trailingComma: TokenSyntax? + if position < endIndex { + trailingComma = commaToken + } else { + trailingComma = nil + } + + // Create the argument and insert it into the argument list. + let argument = generator(leadingTrivia, trailingComma) + arguments.insert(argument, at: positionIdx) + + return LabeledExprListSyntax(arguments) + } +} + +extension SyntaxProtocol { + /// Look for a call expression to a callee with the given name. + func findCall(calleeName: String) -> FunctionCallExprSyntax? { + return FunctionCallExprSyntax.findFirst(in: self) { call in + return call.hasCallee(named: calleeName) + } + } +} + +extension FunctionCallExprSyntax { + /// Find the call that forms a target with the given name in this + /// package manifest. + func findManifestTargetCall(targetName: String) throws -> FunctionCallExprSyntax { + // Dig out the array of targets. + guard let targetsArgument = findArgument(labeled: "targets"), + let targetArray = targetsArgument.expression.findArrayArgument() + else { + throw ManifestEditError.cannotFindTargets + } + + // Look for a call whose name is a string literal matching the + // requested target name. + func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool { + guard let nameArgument = call.findArgument(labeled: "name") else { + return false + } + + guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self), + let literalValue = stringLiteral.representedLiteralValue + else { + return false + } + + return literalValue == targetName + } + + guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else { + throw ManifestEditError.cannotFindTarget(targetName: targetName) + } + + return targetCall + } +} + +extension ArrayExprSyntax { + /// Produce a new array literal expression that appends the given + /// element, while trying to maintain similar indentation. + func appending( + element: ExprSyntax, + outerLeadingTrivia: Trivia + ) -> ArrayExprSyntax { + var elements = self.elements + + let commaToken = TokenSyntax.commaToken() + + // If there are already elements, tack it on. + let leadingTrivia: Trivia + let trailingTrivia: Trivia + let leftSquareTrailingTrivia: Trivia + if let last = elements.last { + // The leading trivia of the new element should match that of the + // last element. + leadingTrivia = last.leadingTrivia.onlyLastLine() + + // Add a trailing comma to the last element if it isn't already + // there. + if last.trailingComma == nil { + var newElements = Array(elements) + newElements[newElements.count - 1].trailingComma = commaToken + newElements[newElements.count - 1].expression.trailingTrivia = + Trivia() + newElements[newElements.count - 1].trailingTrivia = last.trailingTrivia + elements = ArrayElementListSyntax(newElements) + } + + trailingTrivia = Trivia() + leftSquareTrailingTrivia = leftSquare.trailingTrivia + } else { + leadingTrivia = outerLeadingTrivia.appending(defaultIndent) + trailingTrivia = outerLeadingTrivia + if leftSquare.trailingTrivia.hasNewlines { + leftSquareTrailingTrivia = leftSquare.trailingTrivia + } else { + leftSquareTrailingTrivia = Trivia() + } + } + + elements.append( + ArrayElementSyntax( + expression: element.with(\.leadingTrivia, leadingTrivia), + trailingComma: commaToken.with(\.trailingTrivia, trailingTrivia) + ) + ) + + let newLeftSquare = leftSquare.with( + \.trailingTrivia, + leftSquareTrailingTrivia + ) + + return with(\.elements, elements).with(\.leftSquare, newLeftSquare) + } +} + +extension ExprSyntax { + /// Find an array argument either at the top level or within a sequence + /// expression. + func findArrayArgument() -> ArrayExprSyntax? { + if let arrayExpr = self.as(ArrayExprSyntax.self) { + return arrayExpr + } + + if let sequenceExpr = self.as(SequenceExprSyntax.self) { + return sequenceExpr.elements.lazy.compactMap { + $0.findArrayArgument() + }.first + } + + return nil + } +} + +// MARK: Utilities to oeprate on arrays of array literal elements. +extension Array { + /// Append a new argument expression. + mutating func append(expression: ExprSyntax) { + // Add a comma on the prior expression, if there is one. + let leadingTrivia: Trivia? + if count > 0 { + self[count - 1].trailingComma = TokenSyntax.commaToken() + leadingTrivia = .newline + + // Adjust the first element to start with a newline + if count == 1 { + self[0].leadingTrivia = .newline + } + } else { + leadingTrivia = nil + } + + append( + ArrayElementSyntax( + leadingTrivia: leadingTrivia, + expression: expression + ) + ) + } +} + +// MARK: Utilities to operate on arrays of call arguments. + +extension Array { + /// Append a potentially labeled argument with the argument expression. + mutating func append(label: String?, expression: ExprSyntax) { + // Add a comma on the prior expression, if there is one. + let leadingTrivia: Trivia + if count > 0 { + self[count - 1].trailingComma = TokenSyntax.commaToken() + leadingTrivia = .newline + + // Adjust the first element to start with a newline + if count == 1 { + self[0].leadingTrivia = .newline + } + } else { + leadingTrivia = Trivia() + } + + // Add the new expression. + append( + LabeledExprSyntax( + label: label, + expression: expression + ).with(\.leadingTrivia, leadingTrivia) + ) + } + + /// Append a potentially labeled argument with a string literal. + mutating func append(label: String?, stringLiteral: String) { + append(label: label, expression: "\(literal: stringLiteral)") + } + + /// Append a potentially labeled argument with a string literal, but only + /// when the string literal is not nil. + mutating func appendIf(label: String?, stringLiteral: String?) { + if let stringLiteral { + append(label: label, stringLiteral: stringLiteral) + } + } + + /// Append an array literal containing elements that can be rendered + /// into expression syntax nodes. + mutating func append( + label: String?, + arrayLiteral: [T] + ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { + var elements: [ArrayElementSyntax] = [] + for element in arrayLiteral { + elements.append(expression: element.asSyntax()) + } + + // Figure out the trivia for the left and right square + let leftSquareTrailingTrivia: Trivia + let rightSquareLeadingTrivia: Trivia + switch elements.count { + case 0: + // Put a single space between the square brackets. + leftSquareTrailingTrivia = Trivia() + rightSquareLeadingTrivia = .space + + case 1: + // Put spaces around the single element + leftSquareTrailingTrivia = .space + rightSquareLeadingTrivia = .space + + default: + // Each of the elements will have a leading newline. Add a leading + // newline before the close bracket. + leftSquareTrailingTrivia = Trivia() + rightSquareLeadingTrivia = .newline + } + + let array = ArrayExprSyntax( + leftSquare: .leftSquareToken( + trailingTrivia: leftSquareTrailingTrivia + ), + elements: ArrayElementListSyntax(elements), + rightSquare: .rightSquareToken( + leadingTrivia: rightSquareLeadingTrivia + ) + ) + append(label: label, expression: ExprSyntax(array)) + } + + /// Append an array literal containing elements that can be rendered + /// into expression syntax nodes. + mutating func appendIf( + label: String?, + arrayLiteral: [T]? + ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { + guard let arrayLiteral else { return } + append(label: label, arrayLiteral: arrayLiteral) + } + + /// Append an array literal containing elements that can be rendered + /// into expression syntax nodes, but only if it's not empty. + mutating func appendIfNonEmpty( + label: String?, + arrayLiteral: [T] + ) where T: ManifestSyntaxRepresentable, T.PreferredSyntax == ExprSyntax { + if arrayLiteral.isEmpty { return } + + append(label: label, arrayLiteral: arrayLiteral) + } +} + +// MARK: Utilities for adding arguments into calls. +fileprivate class ReplacingRewriter: SyntaxRewriter { + let childNode: Syntax + let newChildNode: Syntax + + init(childNode: Syntax, newChildNode: Syntax) { + self.childNode = childNode + self.newChildNode = newChildNode + super.init() + } + + override func visitAny(_ node: Syntax) -> Syntax? { + if node == childNode { + return newChildNode + } + + return nil + } +} + +fileprivate extension SyntaxProtocol { + /// Replace the given child with a new child node. + func replacingChild(_ childNode: Syntax, with newChildNode: Syntax) -> Self { + return ReplacingRewriter( + childNode: childNode, + newChildNode: newChildNode + ).rewrite(self).cast(Self.self) + } +} + +extension FunctionCallExprSyntax { + /// Produce source edits that will add the given new element to the + /// array for an argument with the given label (if there is one), or + /// introduce a new argument with an array literal containing only the + /// new element. + /// + /// - Parameters: + /// - label: The argument label for the argument whose array will be + /// added or modified. + /// - trailingLabels: The argument labels that could follow the label, + /// which helps determine where the argument should be inserted if + /// it doesn't exist yet. + /// - newElement: The new element. + /// - Returns: the function call after making this change. + func appendingToArrayArgument( + label: String, + trailingLabels: Set, + newElement: ExprSyntax + ) throws -> FunctionCallExprSyntax { + // If there is already an argument with this name, append to the array + // literal in there. + if let arg = findArgument(labeled: label) { + guard let argArray = arg.expression.findArrayArgument() else { + throw ManifestEditError.cannotFindArrayLiteralArgument( + argumentName: label, + node: Syntax(arg.expression) + ) + } + + // Format the element appropriately for the context. + let indentation = Trivia( + pieces: arg.leadingTrivia.filter { $0.isSpaceOrTab } + ) + let format = BasicFormat( + indentationWidth: [defaultIndent], + initialIndentation: indentation.appending(defaultIndent) + ) + let formattedElement = newElement.formatted(using: format) + .cast(ExprSyntax.self) + + let updatedArgArray = argArray.appending( + element: formattedElement, + outerLeadingTrivia: arg.leadingTrivia + ) + + return replacingChild(Syntax(argArray), with: Syntax(updatedArgArray)) + } + + // There was no argument, so we need to create one. + + // Insert the new argument at the appropriate place in the call. + let insertionPos = arguments.findArgumentInsertionPosition( + labelsAfter: trailingLabels + ) + let newArguments = arguments.insertingArgument( + at: insertionPos + ) { (leadingTrivia, trailingComma) in + // Format the element appropriately for the context. + let indentation = Trivia(pieces: leadingTrivia.filter { $0.isSpaceOrTab }) + let format = BasicFormat( + indentationWidth: [defaultIndent], + initialIndentation: indentation.appending(defaultIndent) + ) + let formattedElement = newElement.formatted(using: format) + .cast(ExprSyntax.self) + + // Form the array. + let newArgument = ArrayExprSyntax( + leadingTrivia: .space, + leftSquare: .leftSquareToken( + trailingTrivia: .newline + ), + elements: ArrayElementListSyntax( + [ + ArrayElementSyntax( + expression: formattedElement, + trailingComma: .commaToken() + ) + ] + ), + rightSquare: .rightSquareToken( + leadingTrivia: leadingTrivia + ) + ) + + // Create the labeled argument for the array. + return LabeledExprSyntax( + leadingTrivia: leadingTrivia, + label: "\(raw: label)", + colon: .colonToken(), + expression: ExprSyntax(newArgument), + trailingComma: trailingComma + ) + } + + return with(\.arguments, newArguments) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/Target.swift b/Sources/SwiftRefactor/PackageManifest/Target.swift new file mode 100644 index 00000000000..2da65957b09 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/Target.swift @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://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. +public struct Target { + 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 Target: 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" : "" + let argumentsSyntax = LabeledExprListSyntax(arguments) + return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" + } +} + +extension Target.Dependency: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + switch self { + case .byName(name: let name): + return "\(literal: name)" + + case .target(name: let name): + return ".target(name: \(literal: name))" + + case .product(name: let name, package: nil): + return ".product(name: \(literal: name))" + + case .product(name: let name, package: let package): + return ".product(name: \(literal: name), package: \(literal: package))" + } + } +} + +extension Target.PluginUsage: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + switch self { + case .plugin(name: let name, package: nil): + return ".plugin(name: \(literal: name))" + + case .plugin(name: let name, package: let package): + return ".plugin(name: \(literal: name), package: \(literal: package))" + } + } +} diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift new file mode 100644 index 00000000000..234ce2aade5 --- /dev/null +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -0,0 +1,728 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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(FixItApplier) import SwiftIDEUtils +import SwiftParser +import SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest +import _SwiftSyntaxTestSupport + +final class ManifestEditTests: XCTestCase { + static let swiftSystemURL: SourceControlURL = SourceControlURL( + "https://github.com/apple/swift-system.git" + ) + + static let swiftSystemPackageDependency: PackageDependency = .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(swiftSystemURL), + requirement: .branch("main") + ) + ) + + func testAddPackageDependencyExistingComma() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), + .package(url: "https://github.com/apple/swift-system.git", branch: "main"), + ] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(Self.swiftSystemURL), + requirement: .branch("main") + ) + ) + ) + ) + } + + func testAddPackageDependencyExistingNoComma() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), + .package(url: "https://github.com/apple/swift-system.git", exact: "510.0.0"), + ] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(Self.swiftSystemURL), + requirement: .exact(SemanticVersion("510.0.0")) + ) + ) + ) + ) + } + + func testAddPackageDependencyExistingAppended() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") + ] + [] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), + .package(url: "https://github.com/apple/swift-system.git", from: "510.0.0"), + ] + [] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(Self.swiftSystemURL), + requirement: .rangeFrom(SemanticVersion("510.0.0")) + ) + ) + ) + ) + } + + func testAddPackageDependencyExistingOneLine() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1") ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "510.0.1"), .package(url: "https://github.com/apple/swift-system.git", from: "510.0.0"),] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(Self.swiftSystemURL), + requirement: .rangeFrom(SemanticVersion("510.0.0")) + ) + ) + ) + ) + } + + func testAddPackageDependencyExistingEmpty() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-system.git", "508.0.0" ..< "510.0.0"), + ] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: .sourceControl( + .init( + identity: PackageIdentity("swift-system"), + location: .remote(Self.swiftSystemURL), + requirement: .range(lowerBound: SemanticVersion("508.0.0"), upperBound: SemanticVersion("510.0.0")) + ) + ) + ) + ) + } + + func testAddPackageDependencyNoExistingAtEnd() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-system.git", branch: "main"), + ] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: Self.swiftSystemPackageDependency + ) + ) + } + + func testAddPackageDependencyNoExistingMiddle() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/apple/swift-system.git", branch: "main"), + ], + targets: [] + ) + """, + provider: AddPackageDependency.self, + context: .init( + dependency: Self.swiftSystemPackageDependency + ) + ) + } + + func testAddPackageDependencyErrors() { + XCTAssertThrows( + try AddPackageDependency.manifestRefactor( + syntax: """ + // swift-tools-version: 5.5 + let package: Package = .init( + name: "packages" + ) + """, + in: .init(dependency: Self.swiftSystemPackageDependency) + ) + ) { (error: ManifestEditError) in + if case .cannotFindPackage = error { + return true + } else { + return false + } + } + + XCTAssertThrows( + try AddPackageDependency.manifestRefactor( + syntax: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: blah + ) + """, + in: .init(dependency: Self.swiftSystemPackageDependency) + ) + ) { (error: ManifestEditError) in + if case .cannotFindArrayLiteralArgument(argumentName: "dependencies", node: _) = error { + return true + } else { + return false + } + } + } + + func testAddLibraryProduct() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + .target(name: "MyLib"), + ], + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + products: [ + .library( + name: "MyLib", + type: .dynamic, + targets: [ "MyLib" ] + ), + ], + targets: [ + .target(name: "MyLib"), + ], + ) + """, + provider: AddProduct.self, + context: .init( + product: + ProductDescription( + name: "MyLib", + type: .library(.dynamic), + targets: ["MyLib"] + ) + ) + ) + } + + func testAddLibraryTarget() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + .target(name: "MyLib"), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Sources/MyLib/MyLib.swift"): """ + + """ + ], + provider: AddPackageTarget.self, + context: .init( + target: Target(name: "MyLib") + ) + ) + } + + func testAddLibraryTargetWithDependencies() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + .target( + name: "MyLib", + dependencies: [ + "OtherLib", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .target(name: "TargetLib") + ] + ), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Sources/MyLib/MyLib.swift"): """ + import OtherLib + import SwiftSyntax + import TargetLib + + """ + ], + provider: AddPackageTarget.self, + context: .init( + target: Target( + name: "MyLib", + dependencies: [ + .byName(name: "OtherLib"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .target(name: "TargetLib"), + ] + ) + ) + ) + } + + func testAddExecutableTargetWithDependencies() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + // These are the targets + .target(name: "MyLib") + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + // These are the targets + .target(name: "MyLib"), + .executableTarget( + name: "MyProgram target-name", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + .target(name: "TargetLib"), + "MyLib" + ] + ), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Sources/MyProgram target-name/MyProgram target-name.swift"): """ + import MyLib + import SwiftSyntax + import TargetLib + + @main + struct MyProgram_target_nameMain { + static func main() { + print("Hello, world") + } + } + """ + ], + provider: AddPackageTarget.self, + context: .init( + target: Target( + name: "MyProgram target-name", + type: .executable, + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + .target(name: "TargetLib"), + .byName(name: "MyLib"), + ] + ), + ) + ) + } + + func testAddMacroTarget() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + import PackageDescription + + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + import CompilerPluginSupport + import PackageDescription + + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), + ], + targets: [ + .macro( + name: "MyMacro target-name", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax") + ] + ), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Sources/MyMacro target-name/MyMacro target-name.swift"): """ + import SwiftCompilerPlugin + import SwiftSyntaxMacros + + struct MyMacro_target_name: Macro { + /// TODO: Implement one or more of the protocols that inherit + /// from Macro. The appropriate macro protocol is determined + /// by the "macro" declaration that MyMacro_target_name implements. + /// Examples include: + /// @freestanding(expression) macro --> ExpressionMacro + /// @attached(member) macro --> MemberMacro + } + """, + RelativePath("Sources/MyMacro target-name/ProvidedMacros.swift"): """ + import SwiftCompilerPlugin + + @main + struct MyMacro_target_nameMacros: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + MyMacro_target_name.self, + ] + } + """, + ], + provider: AddPackageTarget.self, + context: .init( + target: Target( + name: "MyMacro target-name", + type: .macro + ) + ) + ) + } + + func testAddSwiftTestingTestTarget() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages" + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + targets: [ + .testTarget(name: "MyTest target-name"), + ] + ) + """, + expectedAuxiliarySources: [ + RelativePath("Tests/MyTest target-name/MyTest target-name.swift"): """ + import Testing + + @Suite + struct MyTest_target_nameTests { + @Test("MyTest_target_name tests") + func example() { + #expect(42 == 17 + 25) + } + } + """ + ], + provider: AddPackageTarget.self, + context: .init( + target: Target( + name: "MyTest target-name", + type: .test + ), + configuration: .init( + testHarness: .swiftTesting + ) + ) + ) + } + + func testAddTargetDependency() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"), + ], + targets: [ + .testTarget( + name: "MyTest" + ), + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.5 + let package = Package( + name: "packages", + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-example.git", from: "1.2.3"), + ], + targets: [ + .testTarget( + name: "MyTest", + dependencies: [ + .product(name: "SomethingOrOther", package: "swift-example"), + ] + ), + ] + ) + """, + provider: AddTargetDependency.self, + context: .init( + dependency: .product(name: "SomethingOrOther", package: "swift-example"), + targetName: "MyTest" + ) + ) + } + + func testAddJava2SwiftPlugin() throws { + try assertManifestRefactor( + """ + // swift-tools-version: 5.7 + let package = Package( + name: "packages", + targets: [ + .target( + name: "MyLib" + ) + ] + ) + """, + expectedManifest: """ + // swift-tools-version: 5.7 + let package = Package( + name: "packages", + targets: [ + .target( + name: "MyLib", + plugins: [ + .plugin(name: "Java2SwiftPlugin", package: "swift-java"), + ] + ) + ] + ) + """, + provider: AddPluginUsage.self, + context: .init( + targetName: "MyLib", + pluginUsage: .plugin(name: "Java2SwiftPlugin", package: "swift-java") + ) + ) + } +} + +/// Assert that applying the given edit/refactor operation to the manifest +/// produces the expected manifest source file and the expected auxiliary +/// files. +func assertManifestRefactor( + _ originalManifest: SourceFileSyntax, + expectedManifest: SourceFileSyntax, + expectedAuxiliarySources: [RelativePath: SourceFileSyntax] = [:], + provider: Provider.Type, + context: Provider.Context, + file: StaticString = #filePath, + line: UInt = #line +) throws { + return try assertManifestRefactor( + originalManifest, + expectedManifest: expectedManifest, + expectedAuxiliarySources: expectedAuxiliarySources, + file: file, + line: line + ) { (manifest) in + try provider.manifestRefactor(syntax: manifest, in: context) + } +} + +/// Assert that applying the given edit/refactor operation to the manifest +/// produces the expected manifest source file and the expected auxiliary +/// files. +func assertManifestRefactor( + _ originalManifest: SourceFileSyntax, + expectedManifest: SourceFileSyntax, + expectedAuxiliarySources: [RelativePath: SourceFileSyntax] = [:], + file: StaticString = #filePath, + line: UInt = #line, + operation: (SourceFileSyntax) throws -> PackageEdit +) rethrows { + let edits = try operation(originalManifest) + let editedManifestSource = FixItApplier.apply( + edits: edits.manifestEdits, + to: originalManifest + ) + + let editedManifest = Parser.parse(source: editedManifestSource) + assertStringsEqualWithDiff( + editedManifest.description, + expectedManifest.description, + file: file, + line: line + ) + + // Check all of the auxiliary sources. + for (auxSourcePath, auxSourceSyntax) in edits.auxiliaryFiles { + guard let expectedSyntax = expectedAuxiliarySources[auxSourcePath] else { + XCTFail("unexpected auxiliary source file \(auxSourcePath)") + return + } + + assertStringsEqualWithDiff( + auxSourceSyntax.description, + expectedSyntax.description, + file: file, + line: line + ) + } + + XCTAssertEqual( + edits.auxiliaryFiles.count, + expectedAuxiliarySources.count, + "didn't get all of the auxiliary files we expected" + ) +} + +func XCTAssertThrows( + _ expression: @autoclosure () throws -> Ignore, + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (T) -> Bool +) { + do { + let result = try expression() + XCTFail("body completed successfully: \(result)", file: file, line: line) + } catch let error as T { + XCTAssertTrue(errorHandler(error), "Error handler returned false", file: file, line: line) + } catch { + XCTFail("unexpected error thrown: \(error)", file: file, line: line) + } +}