From acf970822679f877bbd9d7c9db717810c068ed8d Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 30 Nov 2024 08:12:57 -0800 Subject: [PATCH 01/13] Introduce package manifest refactoring action "Add Package Dependency" Start porting the package manifest editing operations from the Swift Package Manager package over here, so that all of the syntactic refactorings are together in one common place. These refactorings are needed by a number of tools, including SwiftPM, SourceKit-LSP, and (soon) the Swift compiler itself, which can all depend on swift-syntax. The implementation here stubs out the various types used to describe package syntax, using simple string-backed types in place of some of the semantic types that are part of SwiftPM itself, such as SemanticVersion or SourceControlURL. I've also introduced the notion of a ManifestEditRefactoringProvider to generalize over all of the package manifest editing operations. This commit ports over the "Add Package Dependency" command and its tests. --- Package.swift | 2 +- Sources/SwiftRefactor/CMakeLists.txt | 13 + .../PackageManifest/AbsolutePath.swift | 21 + .../AddPackageDependency.swift | 73 +++ .../PackageManifest/ManifestEditError.swift | 37 ++ .../ManifestEditRefactoringProvider.swift | 25 + .../ManifestSyntaxRepresentable.swift | 45 ++ .../PackageManifest/PackageDependency.swift | 158 ++++++ .../PackageManifest/PackageEditResult.swift | 23 + .../PackageManifest/PackageIdentity.swift | 21 + .../PackageManifest/RelativePath.swift | 21 + .../PackageManifest/SemanticVersion.swift | 29 + .../PackageManifest/SourceControlURL.swift | 21 + .../PackageManifest/SyntaxEditUtils.swift | 517 ++++++++++++++++++ .../SwiftRefactorTest/ManifestEditTests.swift | 370 +++++++++++++ 15 files changed, 1375 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftRefactor/PackageManifest/AbsolutePath.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/ManifestSyntaxRepresentable.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/PackageDependency.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/PackageIdentity.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/RelativePath.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/SemanticVersion.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/SourceControlURL.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift create mode 100644 Tests/SwiftRefactorTest/ManifestEditTests.swift 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..1374b4a4196 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -21,6 +21,19 @@ add_swift_syntax_library(SwiftRefactor RefactoringProvider.swift RemoveSeparatorsFromIntegerLiteral.swift SyntaxUtils.swift + + PackageManifest/AbsolutePath.swift + PackageManifest/AddPackageDependency.swift + PackageManifest/ManifestEditError.swift + PackageManifest/ManifestEditRefactoringProvider.swift + PackageManifest/ManifestSyntaxRepresentable.swift + PackageManifest/PackageDependency.swift + PackageManifest/PackageEditResult.swift + PackageManifest/PackageIdentity.swift + PackageManifest/RelativePath.swift + PackageManifest/SemanticVersion.swift + PackageManifest/SourceControlURL.swift + PackageManifest/SyntaxEditUtils.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..2d5cc6c4bba --- /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 -> PackageEditResult { + let dependency = context.dependency + guard let packageCall = manifest.findCall(calleeName: "Package") else { + throw ManifestEditError.cannotFindPackage + } + + let newPackageCall = try addPackageDependencyLocal( + dependency, + to: packageCall + ) + + return PackageEditResult( + 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/ManifestEditError.swift b/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift new file mode 100644 index 00000000000..3c600ddd9c0 --- /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: + "invalid manifest: unable to find 'Package' declaration" + case .cannotFindTargets: + "unable to find package targets in manifest" + case .cannotFindTarget(targetName: let name): + "unable to find target named '\(name)' in package" + case .cannotFindArrayLiteralArgument(argumentName: let name, node: _): + "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..e6532ec7979 --- /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 -> PackageEditResult +} + +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..6b9c9ac6337 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// 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): filesystem.asSyntax() + case .sourceControl(let sourceControl): sourceControl.asSyntax() + case .registry(let registry): 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): + ".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): + LabeledExprSyntax( + label: "exact", + expression: version.asSyntax() + ) + + case .rangeFrom(let range): + LabeledExprSyntax( + label: "from", + expression: range.asSyntax() + ) + + case .range(let lowerBound, let upperBound): + LabeledExprSyntax( + expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax + ) + + case .revision(let revision): + LabeledExprSyntax( + label: "revision", + expression: "\(literal: revision)" as ExprSyntax + ) + + case .branch(let branch): + LabeledExprSyntax( + label: "branch", + expression: "\(literal: branch)" as ExprSyntax + ) + } + } +} + +extension PackageDependency.Registry.Requirement: ManifestSyntaxRepresentable { + func asSyntax() -> LabeledExprSyntax { + switch self { + case .exact(let version): + LabeledExprSyntax( + label: "exact", + expression: version.asSyntax() + ) + + case .rangeFrom(let range): + LabeledExprSyntax( + label: "from", + expression: range.asSyntax() + ) + + case .range(let lowerBound, let upperBound): + LabeledExprSyntax( + expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax + ) + } + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift b/Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift new file mode 100644 index 00000000000..b3dd59b83da --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/PackageEditResult.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 PackageEditResult { + /// 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/RelativePath.swift b/Sources/SwiftRefactor/PackageManifest/RelativePath.swift new file mode 100644 index 00000000000..c7e07451636 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/RelativePath.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 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 + } +} 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/SyntaxEditUtils.swift b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift new file mode 100644 index 00000000000..2a00e894629 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift @@ -0,0 +1,517 @@ +//===----------------------------------------------------------------------===// +// +// 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 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/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift new file mode 100644 index 00000000000..1375ffbbd3d --- /dev/null +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -0,0 +1,370 @@ +//===----------------------------------------------------------------------===// +// +// 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 + } + } + } +} + +/// 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 -> PackageEditResult +) 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) + } +} From 4161443823829727532f9f346378b6fb65c2b34b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 08:55:11 -0800 Subject: [PATCH 02/13] FIXUP for adding package dependency --- Sources/SwiftRefactor/PackageManifest/PackageDependency.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift index 6b9c9ac6337..83980005201 100644 --- a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift @@ -12,6 +12,7 @@ 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. From fe50821427327e2848d2f6d6694176d702d78f0b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 30 Nov 2024 08:32:30 -0800 Subject: [PATCH 03/13] Port "Add target dependency" package manifest editing action to SwiftRefactor This manifest editing action introduces a new dependency to an existing target --- Sources/SwiftRefactor/CMakeLists.txt | 2 + .../PackageManifest/AddTargetDependency.swift | 114 ++++++++++++++++++ .../PackageManifest/TargetDescription.swift | 39 ++++++ .../SwiftRefactorTest/ManifestEditTests.swift | 42 +++++++ 4 files changed, 197 insertions(+) create mode 100644 Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/TargetDescription.swift diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index 1374b4a4196..ffb6606255f 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -24,6 +24,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/AbsolutePath.swift PackageManifest/AddPackageDependency.swift + PackageManifest/AddTargetDependency.swift PackageManifest/ManifestEditError.swift PackageManifest/ManifestEditRefactoringProvider.swift PackageManifest/ManifestSyntaxRepresentable.swift @@ -34,6 +35,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/SemanticVersion.swift PackageManifest/SourceControlURL.swift PackageManifest/SyntaxEditUtils.swift + PackageManifest/TargetDescription.swift ) target_link_swift_syntax_libraries(SwiftRefactor PUBLIC diff --git a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift new file mode 100644 index 00000000000..ced093c0faf --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// 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: TargetDescription.Dependency + + /// The name of the target to which the dependency will be added. + public var targetName: String + + public init(dependency: TargetDescription.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 -> PackageEditResult { + let dependency = context.dependency + let targetName = context.targetName + + guard let packageCall = manifest.findCall(calleeName: "Package") else { + throw ManifestEditError.cannotFindPackage + } + + // Dig out the array of targets. + guard let targetsArgument = packageCall.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) + } + + let newTargetCall = try addTargetDependencyLocal( + dependency, + to: targetCall + ) + + return PackageEditResult( + manifestEdits: [ + .replace(targetCall, with: newTargetCall.description) + ] + ) + } + + /// Implementation of adding a target dependency to an existing call. + static func addTargetDependencyLocal( + _ dependency: TargetDescription.Dependency, + to targetCall: FunctionCallExprSyntax + ) throws -> FunctionCallExprSyntax { + try targetCall.appendingToArrayArgument( + label: "dependencies", + trailingLabels: Self.argumentLabelsAfterDependencies, + newElement: dependency.asSyntax() + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift new file mode 100644 index 00000000000..23b1f423c1e --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// 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 TargetDescription { + public let name: String + + public enum Dependency { + case target(name: String) + case product(name: String, package: String?) + } +} + +extension TargetDescription.Dependency: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + switch self { + case .target(name: let name): + ".target(name: \(literal: name))" + + case .product(name: let name, package: nil): + ".product(name: \(literal: name))" + + case .product(name: let name, package: let package): + ".product(name: \(literal: name), package: \(literal: package))" + } + } +} diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index 1375ffbbd3d..948a3946f8e 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -281,6 +281,48 @@ final class ManifestEditTests: XCTestCase { } } } + + 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" + ) + ) + } + } /// Assert that applying the given edit/refactor operation to the manifest From 1abc48f7b8263b8f17d84d6b2c8011ef35c65a90 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 30 Nov 2024 17:00:40 -0800 Subject: [PATCH 04/13] Port "Add target" from the Swift Package Manager code base to SwiftRefactor --- Sources/SwiftRefactor/CMakeLists.txt | 2 + .../PackageManifest/AddTarget.swift | 390 ++++++++++++++++++ .../PackageManifest/RelativePath.swift | 14 + .../PackageManifest/StringUtils.swift | 223 ++++++++++ .../PackageManifest/TargetDescription.swift | 78 +++- .../SwiftRefactorTest/ManifestEditTests.swift | 243 +++++++++++ 6 files changed, 949 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftRefactor/PackageManifest/AddTarget.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/StringUtils.swift diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index ffb6606255f..ff3cdad5ce1 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -24,6 +24,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/AbsolutePath.swift PackageManifest/AddPackageDependency.swift + PackageManifest/AddTarget.swift PackageManifest/AddTargetDependency.swift PackageManifest/ManifestEditError.swift PackageManifest/ManifestEditRefactoringProvider.swift @@ -34,6 +35,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/RelativePath.swift PackageManifest/SemanticVersion.swift PackageManifest/SourceControlURL.swift + PackageManifest/StringUtils.swift PackageManifest/SyntaxEditUtils.swift PackageManifest/TargetDescription.swift ) diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift new file mode 100644 index 00000000000..1bf9b125606 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift @@ -0,0 +1,390 @@ +//===----------------------------------------------------------------------===// +// +// 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 AddTarget: ManifestEditRefactoringProvider { + public struct Context { + public let target: TargetDescription + public let configuration: Configuration + + public init(target: TargetDescription, 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 -> PackageEditResult { + 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 PackageEditResult( + 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 #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { + if manifest.description.firstRange(of: "swift-syntax") == nil { + 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: TargetDescription, + 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 TargetDescription.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: _): + 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: [TargetDescription.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 TargetDescription { + var sanitizedName: String { + name + .mangledToC99ExtendedIdentifier() + .localizedFirstWordCapitalized() + } +} + +fileprivate extension String { + func localizedFirstWordCapitalized() -> String { prefix(1).uppercased() + dropFirst() } +} diff --git a/Sources/SwiftRefactor/PackageManifest/RelativePath.swift b/Sources/SwiftRefactor/PackageManifest/RelativePath.swift index c7e07451636..1eb2a46e4e3 100644 --- a/Sources/SwiftRefactor/PackageManifest/RelativePath.swift +++ b/Sources/SwiftRefactor/PackageManifest/RelativePath.swift @@ -18,4 +18,18 @@ public struct RelativePath: CustomStringConvertible, Equatable, Hashable, Sendab 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/StringUtils.swift b/Sources/SwiftRefactor/PackageManifest/StringUtils.swift new file mode 100644 index 00000000000..ebc2878284c --- /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/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift index 23b1f423c1e..d5cb089ed47 100644 --- a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift @@ -17,15 +17,91 @@ import SwiftSyntax public struct TargetDescription { public let name: String - public enum Dependency { + /// 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? + + 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 + ) { + self.name = name + self.type = type + self.dependencies = dependencies + self.path = path + self.url = url + self.checksum = checksum + } +} + +extension TargetDescription: ManifestSyntaxRepresentable { + /// The function name in the package manifest. + private var functionName: String { + switch type { + case .binary: "binaryTarget" + case .executable: "executableTarget" + case .library: "target" + case .macro: "macro" + case .plugin: "plugin" + case .system: "systemLibrary" + case .test: "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) + + let separateParen: String = arguments.count > 1 ? "\n" : "" + let argumentsSyntax = LabeledExprListSyntax(arguments) + return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))" + } } extension TargetDescription.Dependency: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { + case .byName(name: let name): + "\(literal: name)" + case .target(name: let name): ".target(name: \(literal: name))" diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index 948a3946f8e..e60c5617c24 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -282,6 +282,249 @@ final class ManifestEditTests: XCTestCase { } } + 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: AddTarget.self, + context: .init( + target: TargetDescription(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: AddTarget.self, + context: .init( + target: TargetDescription( + 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: AddTarget.self, + context: .init( + target: TargetDescription( + 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: AddTarget.self, + context: .init( + target: TargetDescription( + 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: AddTarget.self, + context: .init( + target: TargetDescription( + name: "MyTest target-name", + type: .test + ), + configuration: .init( + testHarness: .swiftTesting + ) + ) + ) + } + func testAddTargetDependency() throws { try assertManifestRefactor( """ From 30ae9e6fa5b1326c78194a1e96846c9a9fd5c317 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 30 Nov 2024 17:10:38 -0800 Subject: [PATCH 05/13] Port "Add Product" manifest edit refactor over from SwiftPM --- Sources/SwiftRefactor/CMakeLists.txt | 3 + .../PackageManifest/AddProduct.swift | 63 +++++++++++++++ .../PackageManifest/ProductDescription.swift | 81 +++++++++++++++++++ .../PackageManifest/ProductType.swift | 46 +++++++++++ .../PackageManifest/StringUtils.swift | 4 +- .../SwiftRefactorTest/ManifestEditTests.swift | 39 +++++++++ 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 Sources/SwiftRefactor/PackageManifest/AddProduct.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/ProductDescription.swift create mode 100644 Sources/SwiftRefactor/PackageManifest/ProductType.swift diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index ff3cdad5ce1..d511f60b81c 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -24,6 +24,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/AbsolutePath.swift PackageManifest/AddPackageDependency.swift + PackageManifest/AddProduct.swift PackageManifest/AddTarget.swift PackageManifest/AddTargetDependency.swift PackageManifest/ManifestEditError.swift @@ -32,6 +33,8 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/PackageDependency.swift PackageManifest/PackageEditResult.swift PackageManifest/PackageIdentity.swift + PackageManifest/ProductDescription.swift + PackageManifest/ProductType.swift PackageManifest/RelativePath.swift PackageManifest/SemanticVersion.swift PackageManifest/SourceControlURL.swift diff --git a/Sources/SwiftRefactor/PackageManifest/AddProduct.swift b/Sources/SwiftRefactor/PackageManifest/AddProduct.swift new file mode 100644 index 00000000000..7bef553ab34 --- /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 -> PackageEditResult { + 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 PackageEditResult( + manifestEdits: [ + .replace(packageCall, with: newPackageCall.description) + ] + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift new file mode 100644 index 00000000000..d7db1762536 --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// 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 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: "executable" + case .library(_): "library" + case .macro: "macro" + case .plugin: "plugin" + case .snippet: "snippet" + case .test: "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/ProductType.swift b/Sources/SwiftRefactor/PackageManifest/ProductType.swift new file mode 100644 index 00000000000..31e0c3acb7d --- /dev/null +++ b/Sources/SwiftRefactor/PackageManifest/ProductType.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Syntactic wrapper type that describes a product type for refactoring +/// purposes but does not interpret its contents. +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` +} diff --git a/Sources/SwiftRefactor/PackageManifest/StringUtils.swift b/Sources/SwiftRefactor/PackageManifest/StringUtils.swift index ebc2878284c..ff412739ca8 100644 --- a/Sources/SwiftRefactor/PackageManifest/StringUtils.swift +++ b/Sources/SwiftRefactor/PackageManifest/StringUtils.swift @@ -20,7 +20,7 @@ extension String { let replacementUnichar: UnicodeScalar = "_" var mangledUnichars: [UnicodeScalar] = self.unicodeScalars.map({ switch $0.value { - case // A-Z + case // A-Z 0x0041...0x005A, // a-z 0x0061...0x007A, @@ -199,7 +199,7 @@ extension String { // Apply further restrictions to the prefix. loop: for (idx, c) in mangledUnichars.enumerated() { switch c.value { - case // 0-9 + case // 0-9 0x0030...0x0039, // Annex D. 0x0660...0x0669, 0x06F0...0x06F9, 0x0966...0x096F, diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index e60c5617c24..a9788e84a18 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -282,6 +282,45 @@ final class ManifestEditTests: XCTestCase { } } + 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( """ From 406116349420e07bb5091ef9bcbb245262a21a28 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sat, 30 Nov 2024 17:36:40 -0800 Subject: [PATCH 06/13] Add a package manifest edit refactor to introduce a new plugin usage for a target This allows one to add a plugin usage to a given target, given the target name and description of how the plugin should be used. --- Sources/SwiftRefactor/CMakeLists.txt | 1 + .../PackageManifest/AddPluginUsage.swift | 65 +++++++++++++++++++ .../PackageManifest/AddTargetDependency.swift | 28 +------- .../PackageManifest/SyntaxEditUtils.swift | 35 ++++++++++ .../PackageManifest/TargetDescription.swift | 29 ++++++++- .../SwiftRefactorTest/ManifestEditTests.swift | 34 ++++++++++ 6 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index d511f60b81c..cd132fc631b 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -24,6 +24,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/AbsolutePath.swift PackageManifest/AddPackageDependency.swift + PackageManifest/AddPluginUsage.swift PackageManifest/AddProduct.swift PackageManifest/AddTarget.swift PackageManifest/AddTargetDependency.swift diff --git a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift new file mode 100644 index 00000000000..7a6e14f9d04 --- /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: TargetDescription.PluginUsage + + public init(targetName: String, pluginUsage: TargetDescription.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 -> PackageEditResult { + 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 PackageEditResult( + manifestEdits: [ + .replace(targetCall, with: newTargetCall.description) + ] + ) + } +} diff --git a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift index ced093c0faf..6e5d398a52a 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift @@ -61,32 +61,8 @@ public struct AddTargetDependency: ManifestEditRefactoringProvider { throw ManifestEditError.cannotFindPackage } - // Dig out the array of targets. - guard let targetsArgument = packageCall.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) - } + // Find the target to be modified. + let targetCall = try packageCall.findManifestTargetCall(targetName: targetName) let newTargetCall = try addTargetDependencyLocal( dependency, diff --git a/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift index 2a00e894629..a5fcc20eb9d 100644 --- a/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift +++ b/Sources/SwiftRefactor/PackageManifest/SyntaxEditUtils.swift @@ -176,6 +176,41 @@ extension SyntaxProtocol { } } +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. diff --git a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift index d5cb089ed47..d0a6852663c 100644 --- a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift @@ -28,6 +28,14 @@ public struct TargetDescription { 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 @@ -43,13 +51,15 @@ public struct TargetDescription { 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 + checksum: String? = nil, + pluginUsages: [PluginUsage]? = nil ) { self.name = name self.type = type @@ -57,6 +67,7 @@ public struct TargetDescription { self.path = path self.url = url self.checksum = checksum + self.pluginUsages = pluginUsages } } @@ -90,6 +101,10 @@ extension TargetDescription: ManifestSyntaxRepresentable { // 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))" @@ -113,3 +128,15 @@ extension TargetDescription.Dependency: ManifestSyntaxRepresentable { } } } + +extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable { + func asSyntax() -> ExprSyntax { + switch self { + case .plugin(name: let name, package: nil): + ".plugin(name: \(literal: name))" + + case .plugin(name: let name, package: let package): + ".plugin(name: \(literal: name), package: \(literal: package))" + } + } +} diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index a9788e84a18..5df32bc70bf 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -605,6 +605,40 @@ final class ManifestEditTests: XCTestCase { ) } + 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 From 189e29c2560642a48135289786b25444a9f34424 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 11:06:27 -0800 Subject: [PATCH 07/13] Implement a better "contains string literal" check for a source file --- .../PackageManifest/AddTarget.swift | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift index 1bf9b125606..800e47a5214 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift @@ -144,32 +144,30 @@ public struct AddTarget: ManifestEditRefactoringProvider { to: &auxiliaryFiles ) - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - if manifest.description.firstRange(of: "swift-syntax") == nil { - 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.. 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 + } +} From 370eea6eb7a8f67c8607b128a0ecda1e7496efaf Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 11:08:23 -0800 Subject: [PATCH 08/13] Remove more uses of switch expressions --- .../PackageManifest/PackageDependency.swift | 18 +++++++++--------- .../PackageManifest/TargetDescription.swift | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift index 83980005201..e1fd8af46bc 100644 --- a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift @@ -89,7 +89,7 @@ extension PackageDependency.SourceControl: ManifestSyntaxRepresentable { case .local: fatalError() case .remote(let url): - ".package(url: \(literal: url.description), \(requirement.asSyntax()))" + return ".package(url: \(literal: url.description), \(requirement.asSyntax()))" } } } @@ -104,30 +104,30 @@ extension PackageDependency.SourceControl.Requirement: ManifestSyntaxRepresentab func asSyntax() -> LabeledExprSyntax { switch self { case .exact(let version): - LabeledExprSyntax( + return LabeledExprSyntax( label: "exact", expression: version.asSyntax() ) case .rangeFrom(let range): - LabeledExprSyntax( + return LabeledExprSyntax( label: "from", expression: range.asSyntax() ) case .range(let lowerBound, let upperBound): - LabeledExprSyntax( + return LabeledExprSyntax( expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax ) case .revision(let revision): - LabeledExprSyntax( + return LabeledExprSyntax( label: "revision", expression: "\(literal: revision)" as ExprSyntax ) case .branch(let branch): - LabeledExprSyntax( + return LabeledExprSyntax( label: "branch", expression: "\(literal: branch)" as ExprSyntax ) @@ -139,19 +139,19 @@ extension PackageDependency.Registry.Requirement: ManifestSyntaxRepresentable { func asSyntax() -> LabeledExprSyntax { switch self { case .exact(let version): - LabeledExprSyntax( + return LabeledExprSyntax( label: "exact", expression: version.asSyntax() ) case .rangeFrom(let range): - LabeledExprSyntax( + return LabeledExprSyntax( label: "from", expression: range.asSyntax() ) case .range(let lowerBound, let upperBound): - LabeledExprSyntax( + return LabeledExprSyntax( expression: "\(lowerBound.asSyntax())..<\(upperBound.asSyntax())" as ExprSyntax ) } diff --git a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift index d0a6852663c..f949d820725 100644 --- a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift @@ -115,16 +115,16 @@ extension TargetDescription.Dependency: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { case .byName(name: let name): - "\(literal: name)" + return "\(literal: name)" case .target(name: let name): - ".target(name: \(literal: name))" + return ".target(name: \(literal: name))" case .product(name: let name, package: nil): - ".product(name: \(literal: name))" + return ".product(name: \(literal: name))" case .product(name: let name, package: let package): - ".product(name: \(literal: name), package: \(literal: package))" + return ".product(name: \(literal: name), package: \(literal: package))" } } } @@ -133,10 +133,10 @@ extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { case .plugin(name: let name, package: nil): - ".plugin(name: \(literal: name))" + return ".plugin(name: \(literal: name))" case .plugin(name: let name, package: let package): - ".plugin(name: \(literal: name), package: \(literal: package))" + return ".plugin(name: \(literal: name), package: \(literal: package))" } } } From 31291af3ca0b7ac869944bc05825bf23c327177c Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 22:59:26 -0800 Subject: [PATCH 09/13] Remove yet more uses of switch expressions --- .../SwiftRefactor/PackageManifest/AddTarget.swift | 2 +- .../PackageManifest/ManifestEditError.swift | 8 ++++---- .../PackageManifest/PackageDependency.swift | 6 +++--- .../PackageManifest/ProductDescription.swift | 12 ++++++------ .../PackageManifest/TargetDescription.swift | 14 +++++++------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift index 800e47a5214..46522104666 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift @@ -328,7 +328,7 @@ fileprivate extension TargetDescription.Dependency { case .target(name: let name), .byName(name: let name), .product(name: let name, package: _): - name + return name } } } diff --git a/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift b/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift index 3c600ddd9c0..17ad41df4ae 100644 --- a/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift +++ b/Sources/SwiftRefactor/PackageManifest/ManifestEditError.swift @@ -25,13 +25,13 @@ extension ManifestEditError: CustomStringConvertible { public var description: String { switch self { case .cannotFindPackage: - "invalid manifest: unable to find 'Package' declaration" + return "invalid manifest: unable to find 'Package' declaration" case .cannotFindTargets: - "unable to find package targets in manifest" + return "unable to find package targets in manifest" case .cannotFindTarget(targetName: let name): - "unable to find target named '\(name)' in package" + return "unable to find target named '\(name)' in package" case .cannotFindArrayLiteralArgument(argumentName: let name, node: _): - "unable to find array literal for '\(name)' argument" + return "unable to find array literal for '\(name)' argument" } } } diff --git a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift index e1fd8af46bc..eb2a62f1944 100644 --- a/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/PackageDependency.swift @@ -68,9 +68,9 @@ public enum PackageDependency: Sendable { extension PackageDependency: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { - case .fileSystem(let filesystem): filesystem.asSyntax() - case .sourceControl(let sourceControl): sourceControl.asSyntax() - case .registry(let registry): registry.asSyntax() + case .fileSystem(let filesystem): return filesystem.asSyntax() + case .sourceControl(let sourceControl): return sourceControl.asSyntax() + case .registry(let registry): return registry.asSyntax() } } } diff --git a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift index d7db1762536..c000f864fcc 100644 --- a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift @@ -42,12 +42,12 @@ extension ProductDescription: ManifestSyntaxRepresentable { /// to check the precondition. private var functionName: String { switch type { - case .executable: "executable" - case .library(_): "library" - case .macro: "macro" - case .plugin: "plugin" - case .snippet: "snippet" - case .test: "test" + case .executable: return "executable" + case .library(_): return "library" + case .macro: return "macro" + case .plugin: return "plugin" + case .snippet: return "snippet" + case .test: return "test" } } diff --git a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift index f949d820725..848f3c06d93 100644 --- a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift @@ -75,13 +75,13 @@ extension TargetDescription: ManifestSyntaxRepresentable { /// The function name in the package manifest. private var functionName: String { switch type { - case .binary: "binaryTarget" - case .executable: "executableTarget" - case .library: "target" - case .macro: "macro" - case .plugin: "plugin" - case .system: "systemLibrary" - case .test: "testTarget" + 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" } } From ec5afbe8b64617c94fe3fc88d4aec0e8f5ba12e6 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 23:05:13 -0800 Subject: [PATCH 10/13] Rename PackageEditResult -> PackageEdit This removes a conflict with the SwiftPM version. --- Sources/SwiftRefactor/CMakeLists.txt | 2 +- .../PackageManifest/AddPackageDependency.swift | 4 ++-- Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift | 4 ++-- Sources/SwiftRefactor/PackageManifest/AddProduct.swift | 4 ++-- Sources/SwiftRefactor/PackageManifest/AddTarget.swift | 6 +++--- .../SwiftRefactor/PackageManifest/AddTargetDependency.swift | 4 ++-- .../PackageManifest/ManifestEditRefactoringProvider.swift | 2 +- .../{PackageEditResult.swift => PackageEdit.swift} | 2 +- Tests/SwiftRefactorTest/ManifestEditTests.swift | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) rename Sources/SwiftRefactor/PackageManifest/{PackageEditResult.swift => PackageEdit.swift} (96%) diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index cd132fc631b..d81c89e7b77 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -32,7 +32,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/ManifestEditRefactoringProvider.swift PackageManifest/ManifestSyntaxRepresentable.swift PackageManifest/PackageDependency.swift - PackageManifest/PackageEditResult.swift + PackageManifest/PackageEdit.swift PackageManifest/PackageIdentity.swift PackageManifest/ProductDescription.swift PackageManifest/ProductType.swift diff --git a/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift index 2d5cc6c4bba..80db72a0e3d 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddPackageDependency.swift @@ -41,7 +41,7 @@ public struct AddPackageDependency: ManifestEditRefactoringProvider { public static func manifestRefactor( syntax manifest: SourceFileSyntax, in context: Context - ) throws -> PackageEditResult { + ) throws -> PackageEdit { let dependency = context.dependency guard let packageCall = manifest.findCall(calleeName: "Package") else { throw ManifestEditError.cannotFindPackage @@ -52,7 +52,7 @@ public struct AddPackageDependency: ManifestEditRefactoringProvider { to: packageCall ) - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(packageCall, with: newPackageCall.description) ] diff --git a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift index 7a6e14f9d04..6741ba32da5 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift @@ -39,7 +39,7 @@ public struct AddPluginUsage: ManifestEditRefactoringProvider { public static func manifestRefactor( syntax manifest: SourceFileSyntax, in context: Context - ) throws -> PackageEditResult { + ) throws -> PackageEdit { let targetName = context.targetName let pluginUsage = context.pluginUsage @@ -56,7 +56,7 @@ public struct AddPluginUsage: ManifestEditRefactoringProvider { newElement: pluginUsage.asSyntax() ) - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(targetCall, with: newTargetCall.description) ] diff --git a/Sources/SwiftRefactor/PackageManifest/AddProduct.swift b/Sources/SwiftRefactor/PackageManifest/AddProduct.swift index 7bef553ab34..ad862d004a5 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddProduct.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddProduct.swift @@ -41,7 +41,7 @@ public struct AddProduct: ManifestEditRefactoringProvider { public static func manifestRefactor( syntax manifest: SourceFileSyntax, in context: Context - ) throws -> PackageEditResult { + ) throws -> PackageEdit { let product = context.product guard let packageCall = manifest.findCall(calleeName: "Package") else { @@ -54,7 +54,7 @@ public struct AddProduct: ManifestEditRefactoringProvider { newElement: product.asSyntax() ) - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(packageCall, with: newPackageCall.description) ] diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift index 46522104666..145b266e8ce 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift @@ -79,7 +79,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { public static func manifestRefactor( syntax manifest: SourceFileSyntax, in context: Context - ) throws -> PackageEditResult { + ) throws -> PackageEdit { let configuration = context.configuration guard let packageCall = manifest.findCall(calleeName: "Package") else { throw ManifestEditError.cannotFindPackage @@ -114,7 +114,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { } guard let outerDirectory else { - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(packageCall, with: newPackageCall.description) ] @@ -175,7 +175,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { default: break; } - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(packageCall, with: newPackageCall.description) ] + extraManifestEdits, diff --git a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift index 6e5d398a52a..74a560ebeef 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift @@ -53,7 +53,7 @@ public struct AddTargetDependency: ManifestEditRefactoringProvider { public static func manifestRefactor( syntax manifest: SourceFileSyntax, in context: Context - ) throws -> PackageEditResult { + ) throws -> PackageEdit { let dependency = context.dependency let targetName = context.targetName @@ -69,7 +69,7 @@ public struct AddTargetDependency: ManifestEditRefactoringProvider { to: targetCall ) - return PackageEditResult( + return PackageEdit( manifestEdits: [ .replace(targetCall, with: newTargetCall.description) ] diff --git a/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift b/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift index e6532ec7979..6e47412ffd3 100644 --- a/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift +++ b/Sources/SwiftRefactor/PackageManifest/ManifestEditRefactoringProvider.swift @@ -15,7 +15,7 @@ import SwiftSyntax public protocol ManifestEditRefactoringProvider: EditRefactoringProvider where Self.Input == SourceFileSyntax { - static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEditResult + static func manifestRefactor(syntax: SourceFileSyntax, in context: Context) throws -> PackageEdit } extension EditRefactoringProvider where Self: ManifestEditRefactoringProvider { diff --git a/Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift b/Sources/SwiftRefactor/PackageManifest/PackageEdit.swift similarity index 96% rename from Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift rename to Sources/SwiftRefactor/PackageManifest/PackageEdit.swift index b3dd59b83da..875a8fc66aa 100644 --- a/Sources/SwiftRefactor/PackageManifest/PackageEditResult.swift +++ b/Sources/SwiftRefactor/PackageManifest/PackageEdit.swift @@ -14,7 +14,7 @@ import SwiftSyntax /// The result of editing a package, including any edits to the package /// manifest and any new files that are introduced. -public struct PackageEditResult { +public struct PackageEdit { /// Edits to perform to the package manifest. public var manifestEdits: [SourceEdit] = [] diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index 5df32bc70bf..c6196a3b970 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -673,7 +673,7 @@ func assertManifestRefactor( expectedAuxiliarySources: [RelativePath: SourceFileSyntax] = [:], file: StaticString = #filePath, line: UInt = #line, - operation: (SourceFileSyntax) throws -> PackageEditResult + operation: (SourceFileSyntax) throws -> PackageEdit ) rethrows { let edits = try operation(originalManifest) let editedManifestSource = FixItApplier.apply( From 96f01d32ca6fef9d7e1515cd7cb9615e5458daa7 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 23:09:06 -0800 Subject: [PATCH 11/13] Rename TargetDescription -> Target --- Sources/SwiftRefactor/CMakeLists.txt | 2 +- .../PackageManifest/AddPluginUsage.swift | 4 ++-- .../SwiftRefactor/PackageManifest/AddTarget.swift | 14 +++++++------- .../PackageManifest/AddTargetDependency.swift | 6 +++--- .../{TargetDescription.swift => Target.swift} | 8 ++++---- Tests/SwiftRefactorTest/ManifestEditTests.swift | 10 +++++----- 6 files changed, 22 insertions(+), 22 deletions(-) rename Sources/SwiftRefactor/PackageManifest/{TargetDescription.swift => Target.swift} (94%) diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index d81c89e7b77..eec21eeeb46 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -41,7 +41,7 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/SourceControlURL.swift PackageManifest/StringUtils.swift PackageManifest/SyntaxEditUtils.swift - PackageManifest/TargetDescription.swift + PackageManifest/Target.swift ) target_link_swift_syntax_libraries(SwiftRefactor PUBLIC diff --git a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift index 6741ba32da5..8a93dd2adf3 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddPluginUsage.swift @@ -19,9 +19,9 @@ import SwiftSyntaxBuilder public struct AddPluginUsage: ManifestEditRefactoringProvider { public struct Context { public let targetName: String - public let pluginUsage: TargetDescription.PluginUsage + public let pluginUsage: Target.PluginUsage - public init(targetName: String, pluginUsage: TargetDescription.PluginUsage) { + public init(targetName: String, pluginUsage: Target.PluginUsage) { self.targetName = targetName self.pluginUsage = pluginUsage } diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift index 145b266e8ce..667687d8d42 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTarget.swift @@ -17,10 +17,10 @@ import SwiftSyntaxBuilder /// Add a target to a manifest's source code. public struct AddTarget: ManifestEditRefactoringProvider { public struct Context { - public let target: TargetDescription + public let target: Target public let configuration: Configuration - public init(target: TargetDescription, configuration: Configuration = .init()) { + public init(target: Target, configuration: Configuration = .init()) { self.target = target self.configuration = configuration } @@ -187,7 +187,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { /// source files. fileprivate static func addPrimarySourceFile( outerPath: RelativePath, - target: TargetDescription, + target: Target, configuration: Configuration, to auxiliaryFiles: inout AuxiliaryFiles ) { @@ -300,7 +300,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { /// for a macro target. fileprivate static func addProvidedMacrosSourceFile( outerPath: RelativePath, - target: TargetDescription, + target: Target, to auxiliaryFiles: inout AuxiliaryFiles ) { auxiliaryFiles.addSourceFile( @@ -321,7 +321,7 @@ public struct AddTarget: ManifestEditRefactoringProvider { } } -fileprivate extension TargetDescription.Dependency { +fileprivate extension Target.Dependency { /// Retrieve the name of the dependency var name: String { switch self { @@ -349,7 +349,7 @@ fileprivate extension AuxiliaryFiles { /// The set of dependencies we need to introduce to a newly-created macro /// target. -fileprivate let macroTargetDependencies: [TargetDescription.Dependency] = [ +fileprivate let macroTargetDependencies: [Target.Dependency] = [ .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), ] @@ -375,7 +375,7 @@ fileprivate extension PackageDependency { } } -fileprivate extension TargetDescription { +fileprivate extension Target { var sanitizedName: String { name .mangledToC99ExtendedIdentifier() diff --git a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift index 74a560ebeef..ab822131bc8 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddTargetDependency.swift @@ -18,12 +18,12 @@ import SwiftSyntaxBuilder public struct AddTargetDependency: ManifestEditRefactoringProvider { public struct Context { /// The dependency to add. - public var dependency: TargetDescription.Dependency + public var dependency: Target.Dependency /// The name of the target to which the dependency will be added. public var targetName: String - public init(dependency: TargetDescription.Dependency, targetName: String) { + public init(dependency: Target.Dependency, targetName: String) { self.dependency = dependency self.targetName = targetName } @@ -78,7 +78,7 @@ public struct AddTargetDependency: ManifestEditRefactoringProvider { /// Implementation of adding a target dependency to an existing call. static func addTargetDependencyLocal( - _ dependency: TargetDescription.Dependency, + _ dependency: Target.Dependency, to targetCall: FunctionCallExprSyntax ) throws -> FunctionCallExprSyntax { try targetCall.appendingToArrayArgument( diff --git a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift b/Sources/SwiftRefactor/PackageManifest/Target.swift similarity index 94% rename from Sources/SwiftRefactor/PackageManifest/TargetDescription.swift rename to Sources/SwiftRefactor/PackageManifest/Target.swift index 848f3c06d93..2da65957b09 100644 --- a/Sources/SwiftRefactor/PackageManifest/TargetDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/Target.swift @@ -14,7 +14,7 @@ import SwiftSyntax /// Syntactic wrapper type that describes a target for refactoring /// purposes but does not interpret its contents. -public struct TargetDescription { +public struct Target { public let name: String /// The type of target. @@ -71,7 +71,7 @@ public struct TargetDescription { } } -extension TargetDescription: ManifestSyntaxRepresentable { +extension Target: ManifestSyntaxRepresentable { /// The function name in the package manifest. private var functionName: String { switch type { @@ -111,7 +111,7 @@ extension TargetDescription: ManifestSyntaxRepresentable { } } -extension TargetDescription.Dependency: ManifestSyntaxRepresentable { +extension Target.Dependency: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { case .byName(name: let name): @@ -129,7 +129,7 @@ extension TargetDescription.Dependency: ManifestSyntaxRepresentable { } } -extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable { +extension Target.PluginUsage: ManifestSyntaxRepresentable { func asSyntax() -> ExprSyntax { switch self { case .plugin(name: let name, package: nil): diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index c6196a3b970..9982c90f623 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -345,7 +345,7 @@ final class ManifestEditTests: XCTestCase { ], provider: AddTarget.self, context: .init( - target: TargetDescription(name: "MyLib") + target: Target(name: "MyLib") ) ) } @@ -384,7 +384,7 @@ final class ManifestEditTests: XCTestCase { ], provider: AddTarget.self, context: .init( - target: TargetDescription( + target: Target( name: "MyLib", dependencies: [ .byName(name: "OtherLib"), @@ -442,7 +442,7 @@ final class ManifestEditTests: XCTestCase { ], provider: AddTarget.self, context: .init( - target: TargetDescription( + target: Target( name: "MyProgram target-name", type: .executable, dependencies: [ @@ -513,7 +513,7 @@ final class ManifestEditTests: XCTestCase { ], provider: AddTarget.self, context: .init( - target: TargetDescription( + target: Target( name: "MyMacro target-name", type: .macro ) @@ -553,7 +553,7 @@ final class ManifestEditTests: XCTestCase { ], provider: AddTarget.self, context: .init( - target: TargetDescription( + target: Target( name: "MyTest target-name", type: .test ), From a3cc837773a0c5ec0a22481d3b074544d2d7f7dc Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 23:10:47 -0800 Subject: [PATCH 12/13] Sink ProductType into ProductDescription --- Sources/SwiftRefactor/CMakeLists.txt | 1 - .../PackageManifest/ProductDescription.swift | 33 +++++++++++++ .../PackageManifest/ProductType.swift | 46 ------------------- 3 files changed, 33 insertions(+), 47 deletions(-) delete mode 100644 Sources/SwiftRefactor/PackageManifest/ProductType.swift diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index eec21eeeb46..ad3c395152c 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -35,7 +35,6 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/PackageEdit.swift PackageManifest/PackageIdentity.swift PackageManifest/ProductDescription.swift - PackageManifest/ProductType.swift PackageManifest/RelativePath.swift PackageManifest/SemanticVersion.swift PackageManifest/SourceControlURL.swift diff --git a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift index c000f864fcc..640075f6473 100644 --- a/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift +++ b/Sources/SwiftRefactor/PackageManifest/ProductDescription.swift @@ -24,6 +24,39 @@ public struct ProductDescription { /// 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, diff --git a/Sources/SwiftRefactor/PackageManifest/ProductType.swift b/Sources/SwiftRefactor/PackageManifest/ProductType.swift deleted file mode 100644 index 31e0c3acb7d..00000000000 --- a/Sources/SwiftRefactor/PackageManifest/ProductType.swift +++ /dev/null @@ -1,46 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -/// Syntactic wrapper type that describes a product type for refactoring -/// purposes but does not interpret its contents. -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` -} From ede1bea9f27fb8578acb66e175a29b682ea6ed4d Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Sun, 1 Dec 2024 23:11:51 -0800 Subject: [PATCH 13/13] Rename AddTarget -> AddPackageTarget --- Sources/SwiftRefactor/CMakeLists.txt | 2 +- .../{AddTarget.swift => AddPackageTarget.swift} | 2 +- Tests/SwiftRefactorTest/ManifestEditTests.swift | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename Sources/SwiftRefactor/PackageManifest/{AddTarget.swift => AddPackageTarget.swift} (99%) diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index ad3c395152c..d7d05c0ade0 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -24,9 +24,9 @@ add_swift_syntax_library(SwiftRefactor PackageManifest/AbsolutePath.swift PackageManifest/AddPackageDependency.swift + PackageManifest/AddPackageTarget.swift PackageManifest/AddPluginUsage.swift PackageManifest/AddProduct.swift - PackageManifest/AddTarget.swift PackageManifest/AddTargetDependency.swift PackageManifest/ManifestEditError.swift PackageManifest/ManifestEditRefactoringProvider.swift diff --git a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift b/Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift similarity index 99% rename from Sources/SwiftRefactor/PackageManifest/AddTarget.swift rename to Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift index 667687d8d42..8ca778f7205 100644 --- a/Sources/SwiftRefactor/PackageManifest/AddTarget.swift +++ b/Sources/SwiftRefactor/PackageManifest/AddPackageTarget.swift @@ -15,7 +15,7 @@ import SwiftSyntax import SwiftSyntaxBuilder /// Add a target to a manifest's source code. -public struct AddTarget: ManifestEditRefactoringProvider { +public struct AddPackageTarget: ManifestEditRefactoringProvider { public struct Context { public let target: Target public let configuration: Configuration diff --git a/Tests/SwiftRefactorTest/ManifestEditTests.swift b/Tests/SwiftRefactorTest/ManifestEditTests.swift index 9982c90f623..234ce2aade5 100644 --- a/Tests/SwiftRefactorTest/ManifestEditTests.swift +++ b/Tests/SwiftRefactorTest/ManifestEditTests.swift @@ -343,7 +343,7 @@ final class ManifestEditTests: XCTestCase { """ ], - provider: AddTarget.self, + provider: AddPackageTarget.self, context: .init( target: Target(name: "MyLib") ) @@ -382,7 +382,7 @@ final class ManifestEditTests: XCTestCase { """ ], - provider: AddTarget.self, + provider: AddPackageTarget.self, context: .init( target: Target( name: "MyLib", @@ -440,7 +440,7 @@ final class ManifestEditTests: XCTestCase { } """ ], - provider: AddTarget.self, + provider: AddPackageTarget.self, context: .init( target: Target( name: "MyProgram target-name", @@ -511,7 +511,7 @@ final class ManifestEditTests: XCTestCase { } """, ], - provider: AddTarget.self, + provider: AddPackageTarget.self, context: .init( target: Target( name: "MyMacro target-name", @@ -551,7 +551,7 @@ final class ManifestEditTests: XCTestCase { } """ ], - provider: AddTarget.self, + provider: AddPackageTarget.self, context: .init( target: Target( name: "MyTest target-name",