diff --git a/Fixtures/PackageEditor/Empty/LocalBinary.xcframework/contents b/Fixtures/PackageEditor/Empty/LocalBinary.xcframework/contents new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/Fixtures/PackageEditor/Empty/LocalBinary.xcframework/contents @@ -0,0 +1 @@ + diff --git a/Fixtures/PackageEditor/Empty/Package.swift b/Fixtures/PackageEditor/Empty/Package.swift new file mode 100644 index 00000000000..13c25e5d03c --- /dev/null +++ b/Fixtures/PackageEditor/Empty/Package.swift @@ -0,0 +1,6 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "MyPackage" +) \ No newline at end of file diff --git a/Fixtures/PackageEditor/OneProduct/Package.swift b/Fixtures/PackageEditor/OneProduct/Package.swift new file mode 100644 index 00000000000..f1441508203 --- /dev/null +++ b/Fixtures/PackageEditor/OneProduct/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "MyPackage2", + products: [ + .library(name: "Library", targets: ["Library"]) + ], + targets: [ + .target(name: "Library") + ] +) diff --git a/Fixtures/PackageEditor/OneProduct/Sources/Library/Library.swift b/Fixtures/PackageEditor/OneProduct/Sources/Library/Library.swift new file mode 100644 index 00000000000..7fecab12d41 --- /dev/null +++ b/Fixtures/PackageEditor/OneProduct/Sources/Library/Library.swift @@ -0,0 +1 @@ +let x = 42 diff --git a/Package.swift b/Package.swift index 35d719658cc..b81786341ea 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ This allowis some clients (such as IDEs) that use SwiftPM's data model but not i to not have to depend on SwiftDriver, SwiftLLBuild, etc. We should probably have better names here, though that could break some clients. */ -let swiftPMDataModelProduct = ( +var swiftPMDataModelProduct = ( name: "SwiftPMDataModel", targets: [ "SourceControl", @@ -42,6 +42,10 @@ let swiftPMDataModelProduct = ( ] ) +#if compiler(>=5.5) +swiftPMDataModelProduct.targets.append("PackageSyntax") +#endif + /** The `libSwiftPM` set of interfaces to programatically work with Swift packages. `libSwiftPM` includes all of the SwiftPM code except the command line tools, while `libSwiftPMDataModel` includes only the data model. @@ -62,6 +66,14 @@ automatic linking type with `-auto` suffix appended to product's name. */ let autoProducts = [swiftPMProduct, swiftPMDataModelProduct] +var commandsDependencies: [Target.Dependency] = ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections"] +var commandsSwiftSettings: [SwiftSetting]? = nil + +#if compiler(>=5.5) +commandsDependencies.append("PackageSyntax") +commandsSwiftSettings = [.define("BUILD_PACKAGE_SYNTAX")] +#endif + let package = Package( name: "SwiftPM", platforms: [macOSPlatform], @@ -219,7 +231,8 @@ let package = Package( .target( /** High-level commands */ name: "Commands", - dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections"]), + dependencies: commandsDependencies, + swiftSettings: commandsSwiftSettings), .target( /** The main executable provided by SwiftPM */ name: "swift-package", @@ -265,7 +278,8 @@ let package = Package( dependencies: ["Build", "SPMTestSupport"]), .testTarget( name: "CommandsTests", - dependencies: ["swift-build", "swift-package", "swift-test", "swift-run", "Commands", "Workspace", "SPMTestSupport"]), + dependencies: ["swift-build", "swift-package", "swift-test", "swift-run", "Commands", "Workspace", "SPMTestSupport"], + swiftSettings: commandsSwiftSettings), .testTarget( name: "WorkspaceTests", dependencies: ["Workspace", "SPMTestSupport"]), @@ -371,3 +385,23 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(path: "../swift-crypto"), ] } + +#if compiler(>=5.5) +// SwiftSyntax depends on lib_InternalSwiftSyntaxParser from the toolchain, +// which had an ABI break in Swift 5.5. As a result, we shouldn't attempt to +// compile PackageSyntax with an earlier compiler version. Although PackageSyntax +// should compile with any 5.5 compiler, it will only be functional when built +// with a toolchain that has a compatible parser library. +if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [.package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch))] +} else { + package.dependencies += [.package(path: "../swift-syntax")] +} + +package.targets += [ + .target(name: "PackageSyntax", + dependencies: ["Workspace", "PackageModel", "PackageLoading", + "SourceControl", "SwiftSyntax", "SwiftToolsSupport-auto"]), + .testTarget(name: "PackageSyntaxTests", dependencies: ["PackageSyntax", "SPMTestSupport", "SwiftSyntax"]), +] +#endif diff --git a/Sources/Commands/SwiftPackageTool.swift b/Sources/Commands/SwiftPackageTool.swift index 95034772e58..60a96ff6022 100644 --- a/Sources/Commands/SwiftPackageTool.swift +++ b/Sources/Commands/SwiftPackageTool.swift @@ -23,15 +23,15 @@ import XCBuildSupport import Workspace import Foundation +#if BUILD_PACKAGE_SYNTAX +import PackageSyntax +#endif + /// swift-package tool namespace public struct SwiftPackageTool: ParsableCommand { - public static var configuration = CommandConfiguration( - commandName: "package", - _superCommandName: "swift", - abstract: "Perform operations on Swift packages", - discussion: "SEE ALSO: swift build, swift run, swift test", - version: SwiftVersion.currentVersion.completeDisplayString, - subcommands: [ + + private static let subcommands: [ParsableCommand.Type] = { + var subcommands: [ParsableCommand.Type] = [ Clean.self, PurgeCache.self, Reset.self, @@ -39,26 +39,43 @@ public struct SwiftPackageTool: ParsableCommand { Describe.self, Init.self, Format.self, - + APIDiff.self, DumpSymbolGraph.self, DumpPIF.self, DumpPackage.self, - + Edit.self, Unedit.self, - + Config.self, Resolve.self, Fetch.self, - + ShowDependencies.self, ToolsVersionCommand.self, GenerateXcodeProject.self, ComputeChecksum.self, ArchiveSource.self, CompletionTool.self, - ], + ] + #if BUILD_PACKAGE_SYNTAX + subcommands.append(contentsOf: [ + AddDependency.self, + AddTarget.self, + AddProduct.self, + ]) + #endif + return subcommands + }() + + public static var configuration = CommandConfiguration( + commandName: "package", + _superCommandName: "swift", + abstract: "Perform operations on Swift packages", + discussion: "SEE ALSO: swift build, swift run, swift test", + version: SwiftVersion.currentVersion.completeDisplayString, + subcommands: subcommands, helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @OptionGroup() @@ -824,6 +841,262 @@ extension SwiftPackageTool { } } +#if BUILD_PACKAGE_SYNTAX +extension SwiftPackageTool { + struct AddDependency: SwiftCommand { + static let configuration = CommandConfiguration( + abstract: "Add a dependency to the current package.") + + @OptionGroup() + var swiftOptions: SwiftToolOptions + + @Argument(help: "The URL of a remote package, or the path to a local package") + var dependencyURL: String + + @Option(help: "Specifies an exact package version requirement") + var exact: Version? + + @Option(help: "Specifies a package revision requirement") + var revision: String? + + @Option(help: "Specifies a package branch requirement") + var branch: String? + + @Option(help: "Specifies a package version requirement from the specified version up to the next major version") + var from: Version? + + @Option(help: "Specifies a package version requirement from the specified version up to the next minor version") + var upToNextMinorFrom: Version? + + @Option(help: "Specifies the upper bound of a range-based package version requirement") + var to: Version? + + @Option(help: "Specifies the upper bound of a closed range-based package version requirement") + var through: Version? + + func run(_ swiftTool: SwiftTool) throws { + var requirements: [PackageDependencyRequirement] = [] + if let exactVersion = exact { + requirements.append(.exact(exactVersion.description)) + } + if let revision = revision { + requirements.append(.revision(revision)) + } + if let branch = branch { + requirements.append(.branch(branch)) + } + if let version = from { + requirements.append(.upToNextMajor(version.description)) + } + if let version = upToNextMinorFrom { + requirements.append(.upToNextMinor(version.description)) + } + + guard requirements.count <= 1 else { + swiftTool.diagnostics.emit(.error("only one requirement is allowed when specifiying a dependency")) + throw ExitCode.failure + } + + var requirement = requirements.first + + if case .upToNextMajor(let rangeStart) = requirement { + guard to == nil || through == nil else { + swiftTool.diagnostics.emit(.error("'--to' and '--through' may not be used in the same requirement")) + throw ExitCode.failure + } + if let rangeEnd = to { + requirement = .range(rangeStart.description, rangeEnd.description) + } else if let closedRangeEnd = through { + requirement = .closedRange(rangeStart.description, closedRangeEnd.description) + } + } else { + guard to == nil, through == nil else { + swiftTool.diagnostics.emit(.error("'--to' and '--through' may only be used with '--from' to specify a range requirement")) + throw ExitCode.failure + } + } + + let manifestPath = try Manifest.path(atPackagePath: swiftTool.getPackageRoot(), + fileSystem: localFileSystem) + let editor = try PackageEditor(manifestPath: manifestPath, + repositoryManager: swiftTool.getActiveWorkspace().repositoryManager, + toolchain: swiftTool.getToolchain(), + diagnosticsEngine: swiftTool.diagnostics) + do { + try editor.addPackageDependency(url: dependencyURL, requirement: requirement) + } catch Diagnostics.fatalError { + throw ExitCode.failure + } + } + } + + struct AddTarget: SwiftCommand { + static let configuration = CommandConfiguration( + abstract: "Add a target to the current package.") + + @OptionGroup() + var swiftOptions: SwiftToolOptions + + @Argument(help: "The name of the new target") + var name: String + + @Option(help: "The type of the new target (library, executable, test, or binary)") + var type: String = "library" + + @Flag(help: "If present, no corresponding test target will be created for a new library target") + var noTestTarget: Bool = false + + @Option(parsing: .upToNextOption, + help: "A list of target dependency names (targets and/or dependency products)") + var dependencies: [String] = [] + + @Option(help: "The URL for a remote binary target") + var url: String? + + @Option(help: "The checksum for a remote binary target") + var checksum: String? + + @Option(help: "The path for a local binary target") + var path: String? + + func run(_ swiftTool: SwiftTool) throws { + let manifestPath = try Manifest.path(atPackagePath: swiftTool.getPackageRoot(), + fileSystem: localFileSystem) + let editor = try PackageEditor(manifestPath: manifestPath, + repositoryManager: swiftTool.getActiveWorkspace().repositoryManager, + toolchain: swiftTool.getToolchain(), + diagnosticsEngine: swiftTool.diagnostics) + let newTarget: NewTarget + switch type { + case "library": + try verifyNoTargetBinaryOptionsPassed(swiftTool: swiftTool) + newTarget = .library(name: name, + includeTestTarget: !noTestTarget, + dependencyNames: dependencies) + case "executable": + try verifyNoTargetBinaryOptionsPassed(swiftTool: swiftTool) + newTarget = .executable(name: name, + dependencyNames: dependencies) + case "test": + try verifyNoTargetBinaryOptionsPassed(swiftTool: swiftTool) + newTarget = .test(name: name, + dependencyNames: dependencies) + case "binary": + guard dependencies.isEmpty else { + swiftTool.diagnostics.emit(.error("option '--dependencies' is not supported for binary targets")) + throw ExitCode.failure + } + // This check is somewhat forgiving, and does the right thing if + // the user passes a url with --path or a path with --url. + guard let urlOrPath = url ?? path, url == nil || path == nil else { + swiftTool.diagnostics.emit(.error("binary targets must specify either a path or both a URL and a checksum")) + throw ExitCode.failure + } + newTarget = .binary(name: name, + urlOrPath: urlOrPath, + checksum: checksum) + default: + swiftTool.diagnostics.emit(.error("unsupported target type '\(type)'; supported types are library, executable, test, and binary")) + throw ExitCode.failure + } + + do { + try editor.addTarget(newTarget, + productPackageNameMapping: createProductPackageNameMapping(swiftTool: swiftTool)) + } catch Diagnostics.fatalError { + throw ExitCode.failure + } + } + + private func createProductPackageNameMapping(swiftTool: SwiftTool) throws -> [String: String] { + let graph = try swiftTool.loadPackageGraph() + var productPackageNameMapping: [String: String] = [:] + for dependencyPackage in graph.rootPackages.flatMap(\.dependencies) { + for product in dependencyPackage.products { + productPackageNameMapping[product.name] = dependencyPackage.manifestName + } + } + return productPackageNameMapping + } + + private func verifyNoTargetBinaryOptionsPassed(swiftTool: SwiftTool) throws { + guard url == nil else { + swiftTool.diagnostics.emit(.error("option '--url' is only supported for binary targets")) + throw ExitCode.failure + } + guard path == nil else { + swiftTool.diagnostics.emit(.error("option '--path' is only supported for binary targets")) + throw ExitCode.failure + } + guard checksum == nil else { + swiftTool.diagnostics.emit(.error("option '--checksum' is only supported for binary targets")) + throw ExitCode.failure + } + } + } + + struct AddProduct: SwiftCommand { + static let configuration = CommandConfiguration( + abstract: "Add a product to the current package.") + + @OptionGroup() + var swiftOptions: SwiftToolOptions + + @Argument(help: "The name of the new product") + var name: String + + @Option(help: "The type of the new product (library, static-library, dynamic-library, or executable)") + var type: ProductType? + + @Option(parsing: .upToNextOption, + help: "A list of target names to add to the new product") + var targets: [String] + + func run(_ swiftTool: SwiftTool) throws { + let manifestPath = try Manifest.path(atPackagePath: swiftTool.getPackageRoot(), + fileSystem: localFileSystem) + let editor = try PackageEditor(manifestPath: manifestPath, + repositoryManager: swiftTool.getActiveWorkspace().repositoryManager, + toolchain: swiftTool.getToolchain(), + diagnosticsEngine: swiftTool.diagnostics) + do { + try editor.addProduct(name: name, type: type ?? .library(.automatic), targets: targets) + } catch Diagnostics.fatalError { + throw ExitCode.failure + } + } + } + +} + +extension Version: ExpressibleByArgument { + public init?(argument: String) { + self.init(string: argument) + } +} + +extension ProductType: ExpressibleByArgument { + public init?(argument: String) { + switch argument { + case "library": + self = .library(.automatic) + case "static-library": + self = .library(.static) + case "dynamic-library": + self = .library(.dynamic) + case "executable": + self = .executable + default: + return nil + } + } + + public static var defaultCompletionKind: CompletionKind { + .list(["library", "static-library", "dynamic-library", "executable"]) + } +} +#endif + extension SwiftPackageTool { struct Config: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/PackageLoading/ToolsVersionLoader.swift b/Sources/PackageLoading/ToolsVersionLoader.swift index 49fdf816b09..c9bf488788c 100644 --- a/Sources/PackageLoading/ToolsVersionLoader.swift +++ b/Sources/PackageLoading/ToolsVersionLoader.swift @@ -346,7 +346,7 @@ public struct ToolsVersionLoader: ToolsVersionLoaderProtocol { do { manifestContents = try fileSystem.readFileContents(file) } catch { throw Error.inaccessibleManifest(path: file, reason: String(describing: error)) } - + // FIXME: This is doubly inefficient. // `contents`'s value comes from `FileSystem.readFileContents(_)`, which is [inefficient](https://github.com/apple/swift-tools-support-core/blob/8f9838e5d4fefa0e12267a1ff87d67c40c6d4214/Sources/TSCBasic/FileSystem.swift#L167). Calling `ByteString.validDescription` on `contents` is also [inefficient, and possibly incorrect](https://github.com/apple/swift-tools-support-core/blob/8f9838e5d4fefa0e12267a1ff87d67c40c6d4214/Sources/TSCBasic/ByteString.swift#L121). However, this is a one-time thing for each package manifest, and almost necessary in order to work with all Unicode line-terminators. We probably can improve its efficiency and correctness by using `URL` for the file's path, and get is content via `Foundation.String(contentsOf:encoding:)`. Swift System's [`FilePath`](https://github.com/apple/swift-system/blob/8ffa04c0a0592e6f4f9c30926dedd8fa1c5371f9/Sources/System/FilePath.swift) and friends might help as well. // This is source-breaking. @@ -451,7 +451,7 @@ public struct ToolsVersionLoader: ToolsVersionLoaderProtocol { // If you changed the logic in this file, and this fatal error is triggered, then you need to re-check the logic, and make sure all possible error conditions are covered in the Else-block. throw Error.backwardIncompatiblePre5_4(.unidentified, specifiedVersion: version) } - + return version } diff --git a/Sources/PackageSyntax/Formatting.swift b/Sources/PackageSyntax/Formatting.swift new file mode 100644 index 00000000000..b23d0b41261 --- /dev/null +++ b/Sources/PackageSyntax/Formatting.swift @@ -0,0 +1,209 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +import SwiftSyntax + +extension ArrayExprSyntax { + public func withAdditionalElementExpr(_ expr: ExprSyntax) -> ArrayExprSyntax { + if self.elements.count >= 2 { + // If the array expression has >=2 elements, use the trivia between + // the last and second-to-last elements to determine how we insert + // the new one. + let lastElement = self.elements.last! + let secondToLastElement = self.elements[self.elements.index(self.elements.endIndex, offsetBy: -2)] + + let newElements = self.elements + .removingLast() + .appending( + lastElement.withTrailingComma( + SyntaxFactory.makeCommaToken( + trailingTrivia: (lastElement.trailingTrivia ?? []) + + rightSquare.leadingTrivia.droppingPiecesAfterLastComment() + + (secondToLastElement.trailingTrivia ?? []) + ) + ) + ) + .appending( + SyntaxFactory.makeArrayElement( + expression: expr, + trailingComma: SyntaxFactory.makeCommaToken() + ).withLeadingTrivia(lastElement.leadingTrivia?.droppingPiecesUpToAndIncludingLastComment() ?? []) + ) + + return self.withElements(newElements) + .withRightSquare( + self.rightSquare.withLeadingTrivia( + self.rightSquare.leadingTrivia.droppingPiecesUpToAndIncludingLastComment() + ) + ) + } else { + // For empty and single-element array exprs, we determine the indent + // of the line the opening square bracket appears on, and then use + // that to indent the added element and closing brace onto newlines. + let (indentTrivia, unitIndent) = self.leftSquare.determineIndentOfStartingLine() + var newElements: [ArrayElementSyntax] = [] + if !self.elements.isEmpty { + let existingElement = self.elements.first! + newElements.append( + SyntaxFactory.makeArrayElement(expression: existingElement.expression, + trailingComma: SyntaxFactory.makeCommaToken()) + .withLeadingTrivia(indentTrivia + unitIndent) + .withTrailingTrivia((existingElement.trailingTrivia ?? []) + .newlines(1)) + ) + } + + newElements.append( + SyntaxFactory.makeArrayElement(expression: expr, trailingComma: SyntaxFactory.makeCommaToken()) + .withLeadingTrivia(indentTrivia + unitIndent) + ) + + return self.withLeftSquare(self.leftSquare.withTrailingTrivia(.newlines(1))) + .withElements(SyntaxFactory.makeArrayElementList(newElements)) + .withRightSquare(self.rightSquare.withLeadingTrivia(.newlines(1) + indentTrivia)) + } + } +} + +extension ArrayExprSyntax { + func reindentingLastCallExprElement() -> ArrayExprSyntax { + let lastElement = elements.last! + let (indent, unitIndent) = lastElement.determineIndentOfStartingLine() + let formattingVisitor = MultilineArgumentListRewriter(indent: indent, unitIndent: unitIndent) + let formattedLastElement = formattingVisitor.visit(lastElement).as(ArrayElementSyntax.self)! + return self.withElements(elements.replacing(childAt: elements.count - 1, with: formattedLastElement)) + } +} + +fileprivate extension TriviaPiece { + var isComment: Bool { + switch self { + case .spaces, .tabs, .verticalTabs, .formfeeds, .newlines, + .carriageReturns, .carriageReturnLineFeeds, .garbageText: + return false + case .lineComment, .blockComment, .docLineComment, .docBlockComment: + return true + } + } + + var isHorizontalWhitespace: Bool { + switch self { + case .spaces, .tabs: + return true + default: + return false + } + } + + var isSpaces: Bool { + guard case .spaces = self else { return false } + return true + } + + var isTabs: Bool { + guard case .tabs = self else { return false } + return true + } +} + +fileprivate extension Trivia { + func droppingPiecesAfterLastComment() -> Trivia { + Trivia(pieces: .init(self.lazy.reversed().drop(while: { !$0.isComment }).reversed())) + } + + func droppingPiecesUpToAndIncludingLastComment() -> Trivia { + Trivia(pieces: .init(self.lazy.reversed().prefix(while: { !$0.isComment }).reversed())) + } +} + +extension SyntaxProtocol { + func determineIndentOfStartingLine() -> (indent: Trivia, unitIndent: Trivia) { + let sourceLocationConverter = SourceLocationConverter(file: "", tree: self.root.as(SourceFileSyntax.self)!) + let line = startLocation(converter: sourceLocationConverter).line ?? 0 + let visitor = DetermineLineIndentVisitor(lineNumber: line, sourceLocationConverter: sourceLocationConverter) + visitor.walk(self.root) + return (indent: visitor.lineIndent, unitIndent: visitor.lineUnitIndent) + } +} + +public final class DetermineLineIndentVisitor: SyntaxVisitor { + + let lineNumber: Int + let locationConverter: SourceLocationConverter + private var bestMatch: TokenSyntax? + + public var lineIndent: Trivia { + guard let pieces = bestMatch?.leadingTrivia + .lazy + .reversed() + .prefix(while: \.isHorizontalWhitespace) + .reversed() else { return .spaces(4) } + return Trivia(pieces: Array(pieces)) + } + + public var lineUnitIndent: Trivia { + if lineIndent.allSatisfy(\.isSpaces) { + let addedSpaces = lineIndent.reduce(0, { + guard case .spaces(let count) = $1 else { fatalError() } + return $0 + count + }) % 4 == 0 ? 4 : 2 + return .spaces(addedSpaces) + } else if lineIndent.allSatisfy(\.isTabs) { + return .tabs(1) + } else { + // If we can't determine the indent, default to 4 spaces. + return .spaces(4) + } + } + + public init(lineNumber: Int, sourceLocationConverter: SourceLocationConverter) { + self.lineNumber = lineNumber + self.locationConverter = sourceLocationConverter + } + + public override func visit(_ tokenSyntax: TokenSyntax) -> SyntaxVisitorContinueKind { + let range = tokenSyntax.sourceRange(converter: locationConverter, + afterLeadingTrivia: false, + afterTrailingTrivia: true) + guard let startLine = range.start.line, + let endLine = range.end.line, + let startColumn = range.start.column, + let endColumn = range.end.column else { + return .skipChildren + } + + if (startLine, startColumn) <= (lineNumber, 1), + (lineNumber, 1) <= (endLine, endColumn) { + bestMatch = tokenSyntax + return .visitChildren + } else { + return .skipChildren + } + } +} + +/// Moves each argument to a function call expression onto a new line and indents them appropriately. +final class MultilineArgumentListRewriter: SyntaxRewriter { + let indent: Trivia + let unitIndent: Trivia + + init(indent: Trivia, unitIndent: Trivia) { + self.indent = indent + self.unitIndent = unitIndent + } + + override func visit(_ token: TokenSyntax) -> Syntax { + guard token.tokenKind == .rightParen else { return Syntax(token) } + return Syntax(token.withLeadingTrivia(.newlines(1) + indent)) + } + + override func visit(_ node: TupleExprElementSyntax) -> Syntax { + return Syntax(node.withLeadingTrivia(.newlines(1) + indent + unitIndent)) + } +} diff --git a/Sources/PackageSyntax/ManifestRewriter.swift b/Sources/PackageSyntax/ManifestRewriter.swift new file mode 100644 index 00000000000..46e35fec982 --- /dev/null +++ b/Sources/PackageSyntax/ManifestRewriter.swift @@ -0,0 +1,802 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +import SwiftSyntax +import TSCBasic +import TSCUtility +import PackageModel + +/// A package manifest rewriter. +/// +/// This class provides functionality for rewriting the +/// Swift package manifest using the SwiftSyntax library. +/// +/// Similar to SwiftSyntax, this class only deals with the +/// syntax and has no functionality for semantics of the manifest. +public final class ManifestRewriter { + + /// The contents of the original manifest. + public let originalManifest: String + + /// The contents of the edited manifest. + public var editedManifest: String { + return editedSource.description + } + + /// The edited manifest syntax. + private var editedSource: SourceFileSyntax + + /// Engine used to report manifest rewrite failures. + private let diagnosticsEngine: DiagnosticsEngine + + /// Create a new manfiest editor with the given contents. + public init(_ manifest: String, diagnosticsEngine: DiagnosticsEngine) throws { + self.originalManifest = manifest + self.diagnosticsEngine = diagnosticsEngine + self.editedSource = try SyntaxParser.parse(source: manifest) + } + + /// Add a package dependency. + public func addPackageDependency( + name: String?, + url: String, + requirement: PackageDependencyRequirement, + branchAndRevisionConvenienceMethodsSupported: Bool + ) throws { + let initFnExpr = try findPackageInit() + + // Find dependencies section in the argument list of Package(...). + let packageDependenciesFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies") + packageDependenciesFinder.walk(initFnExpr.argumentList) + + let packageDependencies: ArrayExprSyntax + switch packageDependenciesFinder.result { + case .found(let existingPackageDependencies): + packageDependencies = existingPackageDependencies + case .missing: + // We didn't find a dependencies section, so insert one. + let argListWithDependencies = EmptyArrayArgumentWriter(argumentLabel: "dependencies", + followingArgumentLabels: + "targets", + "swiftLanguageVersions", + "cLanguageStandard", + "cxxLanguageStandard") + .visit(initFnExpr.argumentList) + + // Find the inserted section. + let packageDependenciesFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies") + packageDependenciesFinder.walk(argListWithDependencies) + guard case .found(let newPackageDependencies) = packageDependenciesFinder.result else { + fatalError("Could not find just inserted dependencies array") + } + packageDependencies = newPackageDependencies + case .incompatibleExpr: + diagnosticsEngine.emit(.incompatibleArgument(name: "targets")) + throw Diagnostics.fatalError + } + + // Add the the package dependency entry. + let newManifest = PackageDependencyWriter( + name: name, + url: url, + requirement: requirement, + branchAndRevisionConvenienceMethodsSupported: branchAndRevisionConvenienceMethodsSupported + ).visit(packageDependencies).root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + /// Add a target dependency. + public func addByNameTargetDependency( + target: String, + dependency: String + ) throws { + let targetDependencies = try findTargetDependenciesArrayExpr(target: target) + + // Add the target dependency entry. + let newManifest = targetDependencies.withAdditionalElementExpr(ExprSyntax( + SyntaxFactory.makeStringLiteralExpr(dependency) + )).root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + public func addProductTargetDependency( + target: String, + product: String, + package: String + ) throws { + let targetDependencies = try findTargetDependenciesArrayExpr(target: target) + + let dotProductExpr = SyntaxFactory.makeMemberAccessExpr(base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier("product"), + declNameArguments: nil) + + let argumentList = SyntaxFactory.makeTupleExprElementList([ + SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier("name"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(product)), + trailingComma: SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))), + SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier("package"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(package)), + trailingComma: nil) + ]) + + let callExpr = SyntaxFactory.makeFunctionCallExpr(calledExpression: ExprSyntax(dotProductExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: argumentList, + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil) + + // Add the target dependency entry. + let newManifest = targetDependencies.withAdditionalElementExpr(ExprSyntax(callExpr)).root + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + private func findTargetDependenciesArrayExpr(target: String) throws -> ArrayExprSyntax { + let initFnExpr = try findPackageInit() + + // Find the `targets: []` array. + let targetsArrayFinder = ArrayExprArgumentFinder(expectedLabel: "targets") + targetsArrayFinder.walk(initFnExpr.argumentList) + guard case .found(let targetsArrayExpr) = targetsArrayFinder.result else { + diagnosticsEngine.emit(.missingPackageInitArgument(name: "targets")) + throw Diagnostics.fatalError + } + + // Find the target node. + let targetFinder = NamedEntityArgumentListFinder(name: target) + targetFinder.walk(targetsArrayExpr) + guard let targetNode = targetFinder.foundEntity else { + diagnosticsEngine.emit(.missingTarget(name: target)) + throw Diagnostics.fatalError + } + + let targetDependencyFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies") + targetDependencyFinder.walk(targetNode) + + guard case .found(let targetDependencies) = targetDependencyFinder.result else { + diagnosticsEngine.emit(.missingArgument(name: "dependencies", parent: "target '\(target)'")) + throw Diagnostics.fatalError + } + return targetDependencies + } + + /// Add a new target. + public func addTarget( + targetName: String, + factoryMethodName: String + ) throws { + let initFnExpr = try findPackageInit() + let targetsNode = try findOrCreateTargetsList(in: initFnExpr) + + let dotTargetExpr = SyntaxFactory.makeMemberAccessExpr( + base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier(factoryMethodName), + declNameArguments: nil + ) + + let nameArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("name"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(targetName)), + trailingComma: SyntaxFactory.makeCommaToken() + ) + + let emptyArray = SyntaxFactory.makeArrayExpr(leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), elements: SyntaxFactory.makeBlankArrayElementList(), rightSquare: SyntaxFactory.makeRightSquareBracketToken()) + let depenenciesArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("dependencies"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(emptyArray), + trailingComma: nil + ) + + let expr = SyntaxFactory.makeFunctionCallExpr( + calledExpression: ExprSyntax(dotTargetExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: SyntaxFactory.makeTupleExprElementList([ + nameArg, depenenciesArg, + ]), + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil + ) + + let newManifest = targetsNode + .withAdditionalElementExpr(ExprSyntax(expr)) + .reindentingLastCallExprElement() + .root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + public func addBinaryTarget(targetName: String, + urlOrPath: String, + checksum: String?) throws { + let initFnExpr = try findPackageInit() + let targetsNode = try findOrCreateTargetsList(in: initFnExpr) + + let dotTargetExpr = SyntaxFactory.makeMemberAccessExpr( + base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier("binaryTarget"), + declNameArguments: nil + ) + + var args: [TupleExprElementSyntax] = [] + + let nameArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("name"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(targetName)), + trailingComma: SyntaxFactory.makeCommaToken() + ) + args.append(nameArg) + + if TSCUtility.URL.scheme(urlOrPath) == nil { + guard checksum == nil else { + diagnosticsEngine.emit(.unexpectedChecksumForBinaryTarget(path: urlOrPath)) + throw Diagnostics.fatalError + } + + let pathArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("path"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(urlOrPath)), + trailingComma: nil + ) + args.append(pathArg) + } else { + guard let checksum = checksum else { + diagnosticsEngine.emit(.missingChecksumForBinaryTarget(url: urlOrPath)) + throw Diagnostics.fatalError + } + + let urlArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("url"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(urlOrPath)), + trailingComma: SyntaxFactory.makeCommaToken() + ) + args.append(urlArg) + + let checksumArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("checksum"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(checksum)), + trailingComma: nil + ) + args.append(checksumArg) + } + + let expr = SyntaxFactory.makeFunctionCallExpr( + calledExpression: ExprSyntax(dotTargetExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: SyntaxFactory.makeTupleExprElementList(args), + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil + ) + + let newManifest = targetsNode + .withAdditionalElementExpr(ExprSyntax(expr)) + .reindentingLastCallExprElement() + .root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + // Add a new product. + public func addProduct(name: String, type: ProductType) throws { + let initFnExpr = try findPackageInit() + + let productsFinder = ArrayExprArgumentFinder(expectedLabel: "products") + productsFinder.walk(initFnExpr.argumentList) + let productsNode: ArrayExprSyntax + + switch productsFinder.result { + case .found(let existingProducts): + productsNode = existingProducts + case .missing: + // We didn't find a products section, so insert one. + let argListWithProducts = EmptyArrayArgumentWriter(argumentLabel: "products", + followingArgumentLabels: + "dependencies", + "targets", + "swiftLanguageVersions", + "cLanguageStandard", + "cxxLanguageStandard") + .visit(initFnExpr.argumentList) + + // Find the inserted section. + let productsFinder = ArrayExprArgumentFinder(expectedLabel: "products") + productsFinder.walk(argListWithProducts) + guard case .found(let newProducts) = productsFinder.result else { + fatalError("Could not find just inserted products array") + } + productsNode = newProducts + case .incompatibleExpr: + diagnosticsEngine.emit(.incompatibleArgument(name: "products")) + throw Diagnostics.fatalError + } + + let newManifest = NewProductWriter( + name: name, type: type + ).visit(productsNode).root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + // Add a target to a product. + public func addProductTarget(product: String, target: String) throws { + let initFnExpr = try findPackageInit() + + // Find the `products: []` array. + let productsArrayFinder = ArrayExprArgumentFinder(expectedLabel: "products") + productsArrayFinder.walk(initFnExpr.argumentList) + guard case .found(let productsArrayExpr) = productsArrayFinder.result else { + diagnosticsEngine.emit(.missingPackageInitArgument(name: "products")) + throw Diagnostics.fatalError + } + + // Find the product node. + let productFinder = NamedEntityArgumentListFinder(name: product) + productFinder.walk(productsArrayExpr) + guard let productNode = productFinder.foundEntity else { + diagnosticsEngine.emit(.missingProduct(name: product)) + throw Diagnostics.fatalError + } + + let productTargetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets") + productTargetsFinder.walk(productNode) + + guard case .found(let productTargets) = productTargetsFinder.result else { + diagnosticsEngine.emit(.missingArgument(name: "targets", parent: "product '\(product)'")) + throw Diagnostics.fatalError + } + + let newManifest = productTargets.withAdditionalElementExpr(ExprSyntax( + SyntaxFactory.makeStringLiteralExpr(target) + )).root + + self.editedSource = newManifest.as(SourceFileSyntax.self)! + } + + private func findOrCreateTargetsList(in packageInitExpr: FunctionCallExprSyntax) throws -> ArrayExprSyntax { + let targetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets") + targetsFinder.walk(packageInitExpr.argumentList) + + let targetsNode: ArrayExprSyntax + switch targetsFinder.result { + case .found(let existingTargets): + targetsNode = existingTargets + case .missing: + // We didn't find a targets section, so insert one. + let argListWithTargets = EmptyArrayArgumentWriter(argumentLabel: "targets", + followingArgumentLabels: + "swiftLanguageVersions", + "cLanguageStandard", + "cxxLanguageStandard") + .visit(packageInitExpr.argumentList) + + // Find the inserted section. + let targetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets") + targetsFinder.walk(argListWithTargets) + guard case .found(let newTargets) = targetsFinder.result else { + fatalError("Could not find just-inserted targets array") + } + targetsNode = newTargets + case .incompatibleExpr: + diagnosticsEngine.emit(.incompatibleArgument(name: "targets")) + throw Diagnostics.fatalError + } + + return targetsNode + } + + private func findPackageInit() throws -> FunctionCallExprSyntax { + // Find Package initializer. + let packageFinder = PackageInitFinder() + packageFinder.walk(editedSource) + switch packageFinder.result { + case .found(let initFnExpr): + return initFnExpr + case .foundMultiple: + diagnosticsEngine.emit(.multiplePackageInits) + throw Diagnostics.fatalError + case .missing: + diagnosticsEngine.emit(.missingPackageInit) + throw Diagnostics.fatalError + } + } +} + +// MARK: - Syntax Visitors + +/// Package init finder. +final class PackageInitFinder: SyntaxVisitor { + + enum Result { + case found(FunctionCallExprSyntax) + case foundMultiple + case missing + } + + /// Reference to the function call of the package initializer. + private(set) var result: Result = .missing + + override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { + if let fnCall = FunctionCallExprSyntax(Syntax(node.value)), + let identifier = fnCall.calledExpression.firstToken, + identifier.text == "Package" { + if case .missing = result { + result = .found(fnCall) + } else { + result = .foundMultiple + } + } + return .skipChildren + } +} + +/// Finder for an array expression used as or as part of a labeled argument. +final class ArrayExprArgumentFinder: SyntaxVisitor { + + enum Result { + case found(ArrayExprSyntax) + case missing + case incompatibleExpr + } + + private(set) var result: Result + private let expectedLabel: String + + init(expectedLabel: String) { + self.expectedLabel = expectedLabel + self.result = .missing + super.init() + } + + override func visit(_ node: TupleExprElementSyntax) -> SyntaxVisitorContinueKind { + guard node.label?.text == expectedLabel else { + return .skipChildren + } + + // We have custom code like foo + bar + [] (hopefully there is an array expr here). + if let seq = node.expression.as(SequenceExprSyntax.self), + let arrayExpr = seq.elements.first(where: { $0.is(ArrayExprSyntax.self) })?.as(ArrayExprSyntax.self) { + result = .found(arrayExpr) + } else if let arrayExpr = node.expression.as(ArrayExprSyntax.self) { + result = .found(arrayExpr) + } else { + result = .incompatibleExpr + } + + return .skipChildren + } +} + +/// Given an Array expression of call expressions, find the argument list of the call +/// expression with the specified `name` argument. +final class NamedEntityArgumentListFinder: SyntaxVisitor { + + let entityToFind: String + private(set) var foundEntity: TupleExprElementListSyntax? + + init(name: String) { + self.entityToFind = name + } + + override func visit(_ node: TupleExprElementSyntax) -> SyntaxVisitorContinueKind { + guard case .identifier(let label)? = node.label?.tokenKind else { + return .skipChildren + } + guard label == "name", let targetNameExpr = node.expression.as(StringLiteralExprSyntax.self), + targetNameExpr.segments.count == 1, let segment = targetNameExpr.segments.first?.as(StringSegmentSyntax.self) else { + return .skipChildren + } + + guard case .stringSegment(let name) = segment.content.tokenKind else { + return .skipChildren + } + + if name == self.entityToFind { + self.foundEntity = node.parent?.as(TupleExprElementListSyntax.self) + return .skipChildren + } + + return .skipChildren + } +} + +// MARK: - Syntax Rewriters + +/// Writer for an empty array argument. +final class EmptyArrayArgumentWriter: SyntaxRewriter { + let argumentLabel: String + let followingArgumentLabels: Set + + init(argumentLabel: String, followingArgumentLabels: String...) { + self.argumentLabel = argumentLabel + self.followingArgumentLabels = .init(followingArgumentLabels) + } + + override func visit(_ node: TupleExprElementListSyntax) -> Syntax { + let leadingTrivia = node.firstToken?.leadingTrivia ?? .zero + + let existingLabels = node.map(\.label?.text) + let insertionIndex = existingLabels.firstIndex { + followingArgumentLabels.contains($0 ?? "") + } ?? existingLabels.endIndex + + let dependenciesArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier(argumentLabel, leadingTrivia: leadingTrivia), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeArrayExpr( + leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), + elements: SyntaxFactory.makeBlankArrayElementList(), + rightSquare: SyntaxFactory.makeRightSquareBracketToken())), + trailingComma: insertionIndex != existingLabels.endIndex ? SyntaxFactory.makeCommaToken() : nil + ) + + var newNode = node + if let lastArgument = newNode.last, + insertionIndex == existingLabels.endIndex { + // If the new argument is being added at the end of the list, the argument before it needs a comma. + newNode = newNode.replacing(childAt: newNode.count-1, + with: lastArgument.withTrailingComma(SyntaxFactory.makeCommaToken())) + } + + return Syntax(newNode.inserting(dependenciesArg, at: insertionIndex)) + } +} + +/// Package dependency writer. +final class PackageDependencyWriter: SyntaxRewriter { + + /// The dependency name to write. + let name: String? + + /// The dependency url to write. + let url: String + + /// The dependency requirement. + let requirement: PackageDependencyRequirement + + /// Whether convenience methods for branch and revision dependencies are supported. + let branchAndRevisionConvenienceMethodsSupported: Bool + + init(name: String?, + url: String, + requirement: PackageDependencyRequirement, + branchAndRevisionConvenienceMethodsSupported: Bool) { + self.name = name + self.url = url + self.requirement = requirement + self.branchAndRevisionConvenienceMethodsSupported = branchAndRevisionConvenienceMethodsSupported + } + + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + + let dotPackageExpr = SyntaxFactory.makeMemberAccessExpr( + base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier("package"), + declNameArguments: nil + ) + + var args: [TupleExprElementSyntax] = [] + + if let name = self.name { + let nameArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("name"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(name)), + trailingComma: SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1)) + ) + args.append(nameArg) + } + + let locationArgLabel = requirement == .localPackage ? "path" : "url" + let locationArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier(locationArgLabel), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(self.url)), + trailingComma: requirement == .localPackage ? nil : SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1)) + ) + args.append(locationArg) + + let addUnlabeledImplicitMemberCallWithStringArg = { (baseName: String, argumentLabel: String?, argumentString: String) in + let memberExpr = SyntaxFactory.makeMemberAccessExpr(base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier(baseName), + declNameArguments: nil) + let argList = SyntaxFactory.makeTupleExprElementList([ + SyntaxFactory.makeTupleExprElement(label: argumentLabel.map { SyntaxFactory.makeIdentifier($0) }, + colon: argumentLabel.map { _ in SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)) }, + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(argumentString)), + trailingComma: nil) + ]) + let exactExpr = SyntaxFactory.makeFunctionCallExpr(calledExpression: ExprSyntax(memberExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: argList, + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil) + let exactArg = SyntaxFactory.makeTupleExprElement(label: nil, + colon: nil, + expression: ExprSyntax(exactExpr), + trailingComma: nil) + args.append(exactArg) + } + + let addUnlabeledRangeArg = { (start: String, end: String, rangeOperator: String) in + let rangeExpr = SyntaxFactory.makeSequenceExpr(elements: SyntaxFactory.makeExprList([ + ExprSyntax(SyntaxFactory.makeStringLiteralExpr(start)), + ExprSyntax(SyntaxFactory.makeBinaryOperatorExpr( + operatorToken: SyntaxFactory.makeUnspacedBinaryOperator(rangeOperator)) + ), + ExprSyntax(SyntaxFactory.makeStringLiteralExpr(end)) + ])) + let arg = SyntaxFactory.makeTupleExprElement(label: nil, + colon: nil, + expression: ExprSyntax(rangeExpr), + trailingComma: nil) + args.append(arg) + } + + let addLabeledStringArg = { (label: String, literalString: String) in + let arg = SyntaxFactory.makeTupleExprElement(label: SyntaxFactory.makeIdentifier(label), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(literalString)), + trailingComma: nil) + args.append(arg) + } + + switch requirement { + case .exact(let version): + addUnlabeledImplicitMemberCallWithStringArg("exact", nil, version) + case .revision(let revision): + if branchAndRevisionConvenienceMethodsSupported { + addLabeledStringArg("revision", revision) + } else { + addUnlabeledImplicitMemberCallWithStringArg("revision", nil, revision) + } + case .branch(let branch): + if branchAndRevisionConvenienceMethodsSupported { + addLabeledStringArg("branch", branch) + } else { + addUnlabeledImplicitMemberCallWithStringArg("branch", nil, branch) + } + case .upToNextMajor(let version): + addUnlabeledImplicitMemberCallWithStringArg("upToNextMajor", "from", version) + case .upToNextMinor(let version): + addUnlabeledImplicitMemberCallWithStringArg("upToNextMinor", "from", version) + case .range(let start, let end): + addUnlabeledRangeArg(start, end, "..<") + case .closedRange(let start, let end): + addUnlabeledRangeArg(start, end, "...") + case .localPackage: + break + } + + let expr = SyntaxFactory.makeFunctionCallExpr( + calledExpression: ExprSyntax(dotPackageExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: SyntaxFactory.makeTupleExprElementList(args), + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil + ) + + return ExprSyntax(node.withAdditionalElementExpr(ExprSyntax(expr))) + } +} + +/// Writer for inserting a new product in a products array. +final class NewProductWriter: SyntaxRewriter { + + let name: String + let type: ProductType + + init(name: String, type: ProductType) { + self.name = name + self.type = type + } + + override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { + let dotExpr = SyntaxFactory.makeMemberAccessExpr( + base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier(type == .executable ? "executable" : "library"), + declNameArguments: nil + ) + + var args: [TupleExprElementSyntax] = [] + + let nameArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("name"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(name)), + trailingComma: SyntaxFactory.makeCommaToken() + ) + args.append(nameArg) + + if case .library(let kind) = type, kind != .automatic { + let typeExpr = SyntaxFactory.makeMemberAccessExpr(base: nil, + dot: SyntaxFactory.makePeriodToken(), + name: SyntaxFactory.makeIdentifier(kind == .dynamic ? "dynamic" : "static"), + declNameArguments: nil) + let typeArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("type"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(typeExpr), + trailingComma: SyntaxFactory.makeCommaToken() + ) + args.append(typeArg) + } + + let emptyArray = SyntaxFactory.makeArrayExpr(leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), + elements: SyntaxFactory.makeBlankArrayElementList(), + rightSquare: SyntaxFactory.makeRightSquareBracketToken()) + let targetsArg = SyntaxFactory.makeTupleExprElement( + label: SyntaxFactory.makeIdentifier("targets"), + colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), + expression: ExprSyntax(emptyArray), + trailingComma: nil + ) + args.append(targetsArg) + + let expr = SyntaxFactory.makeFunctionCallExpr( + calledExpression: ExprSyntax(dotExpr), + leftParen: SyntaxFactory.makeLeftParenToken(), + argumentList: SyntaxFactory.makeTupleExprElementList(args), + rightParen: SyntaxFactory.makeRightParenToken(), + trailingClosure: nil, + additionalTrailingClosures: nil + ) + + return ExprSyntax(node + .withAdditionalElementExpr(ExprSyntax(expr)) + .reindentingLastCallExprElement()) + } +} + +private extension TSCBasic.Diagnostic.Message { + static var missingPackageInit: Self = + .error("couldn't find Package initializer") + static var multiplePackageInits: Self = + .error("found multiple Package initializers") + static func missingPackageInitArgument(name: String) -> Self { + .error("couldn't find '\(name)' argument in Package initializer") + } + static func missingArgument(name: String, parent: String) -> Self { + .error("couldn't find '\(name)' argument of \(parent)") + } + static func incompatibleArgument(name: String) -> Self { + .error("'\(name)' argument is not an array literal or concatenation of array literals") + } + static func missingProduct(name: String) -> Self { + .error("couldn't find product '\(name)'") + } + static func missingTarget(name: String) -> Self { + .error("couldn't find target '\(name)'") + } + static func unexpectedChecksumForBinaryTarget(path: String) -> Self { + .error("'\(path)' is a local path, but a checksum was specified for the binary target") + } + static func missingChecksumForBinaryTarget(url: String) -> Self { + .error("'\(url)' is a remote URL, but no checksum was specified for the binary target") + } +} diff --git a/Sources/PackageSyntax/PackageEditor.swift b/Sources/PackageSyntax/PackageEditor.swift new file mode 100644 index 00000000000..44554b8a654 --- /dev/null +++ b/Sources/PackageSyntax/PackageEditor.swift @@ -0,0 +1,449 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +import TSCUtility +import TSCBasic +import SourceControl +import PackageLoading +import PackageModel +import Workspace +import Foundation + +/// An editor for Swift packages. +/// +/// This class provides high-level functionality for performing +/// editing operations a package. +public final class PackageEditor { + + /// Reference to the package editor context. + let context: PackageEditorContext + + /// Create a package editor instance. + public convenience init(manifestPath: AbsolutePath, + repositoryManager: RepositoryManager, + toolchain: UserToolchain, + diagnosticsEngine: DiagnosticsEngine) throws { + self.init(context: try PackageEditorContext(manifestPath: manifestPath, + repositoryManager: repositoryManager, + toolchain: toolchain, + diagnosticsEngine: diagnosticsEngine)) + } + + /// Create a package editor instance. + public init(context: PackageEditorContext) { + self.context = context + } + + /// The file system to perform disk operations on. + var fs: FileSystem { + return context.fs + } + + /// Add a package dependency. + public func addPackageDependency(url: String, requirement: PackageDependencyRequirement?) throws { + var requirement = requirement + let manifestPath = context.manifestPath + // Validate that the package doesn't already contain this dependency. + let loadedManifest = try context.loadManifest(at: context.manifestPath.parentDirectory) + + try diagnoseUnsupportedToolsVersions(manifest: loadedManifest) + + let containsDependency = loadedManifest.dependencies.contains { + return PackageIdentity(url: url) == $0.identity + } + guard !containsDependency else { + context.diagnosticsEngine.emit(.packageDependencyAlreadyExists(url: url, + packageName: loadedManifest.name)) + throw Diagnostics.fatalError + } + + // If the input URL is a path, force the requirement to be a local package. + if TSCUtility.URL.scheme(url) == nil { + guard requirement == nil || requirement == .localPackage else { + context.diagnosticsEngine.emit(.nonLocalRequirementSpecifiedForLocalPath(path: url)) + throw Diagnostics.fatalError + } + requirement = .localPackage + } + + // Load the dependency manifest depending on the inputs. + let dependencyManifest: Manifest + if requirement == .localPackage { + let path = AbsolutePath(url, relativeTo: fs.currentWorkingDirectory!) + dependencyManifest = try context.loadManifest(at: path) + requirement = .localPackage + } else { + // Otherwise, first lookup the dependency. + let spec = RepositorySpecifier(url: url) + let handle = try tsc_await{ + context.repositoryManager.lookup(repository: spec, + on: .global(qos: .userInitiated), + completion: $0) + } + let repo = try handle.open() + + // Compute the requirement. + if let inputRequirement = requirement { + requirement = inputRequirement + } else { + // Use the latest version or the main/master branch. + let versions = try repo.getTags().compactMap{ Version(string: $0) } + let latestVersion = versions.filter({ $0.prereleaseIdentifiers.isEmpty }).max() ?? versions.max() + let mainExists = (try? repo.resolveRevision(identifier: "main")) != nil + requirement = latestVersion.map{ PackageDependencyRequirement.upToNextMajor($0.description) } ?? + (mainExists ? PackageDependencyRequirement.branch("main") : PackageDependencyRequirement.branch("master")) + } + + // Load the manifest. + let revision = try repo.resolveRevision(identifier: requirement!.ref!) + let repoFS = try repo.openFileView(revision: revision) + dependencyManifest = try context.loadManifest(at: .root, fs: repoFS) + } + + // Add the package dependency. + let manifestContents = try fs.readFileContents(manifestPath).cString + let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine) + + // Only tools-version 5.2, 5.3, & 5.4 should specify a package name. + // At this point, we've already diagnosed tools-versions less than 5.2 as unsupported + if loadedManifest.toolsVersion < .v5_5 { + try editor.addPackageDependency(name: dependencyManifest.name, + url: url, + requirement: requirement!, + branchAndRevisionConvenienceMethodsSupported: false) + } else { + try editor.addPackageDependency(name: nil, + url: url, + requirement: requirement!, + branchAndRevisionConvenienceMethodsSupported: true) + } + + try context.verifyEditedManifest(contents: editor.editedManifest) + try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest)) + } + + /// Add a new target. + public func addTarget(_ newTarget: NewTarget, productPackageNameMapping: [String: String]) throws { + let manifestPath = context.manifestPath + + // Validate that the package doesn't already contain a target with the same name. + let loadedManifest = try context.loadManifest(at: manifestPath.parentDirectory) + + try diagnoseUnsupportedToolsVersions(manifest: loadedManifest) + + if loadedManifest.targets.contains(where: { $0.name == newTarget.name }) { + context.diagnosticsEngine.emit(.targetAlreadyExists(name: newTarget.name, + packageName: loadedManifest.name)) + throw Diagnostics.fatalError + } + + let manifestContents = try fs.readFileContents(manifestPath).cString + let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine) + + switch newTarget { + case .library(name: let name, includeTestTarget: _, dependencyNames: let dependencyNames), + .executable(name: let name, dependencyNames: let dependencyNames), + .test(name: let name, dependencyNames: let dependencyNames): + try editor.addTarget(targetName: newTarget.name, + factoryMethodName: newTarget.factoryMethodName(for: loadedManifest.toolsVersion)) + + for dependency in dependencyNames { + if loadedManifest.targets.map(\.name).contains(dependency) { + try editor.addByNameTargetDependency(target: name, dependency: dependency) + } else if let productPackage = productPackageNameMapping[dependency] { + if productPackage == dependency { + try editor.addByNameTargetDependency(target: name, dependency: dependency) + } else { + try editor.addProductTargetDependency(target: name, product: dependency, package: productPackage) + } + } else { + context.diagnosticsEngine.emit(.missingProductOrTarget(name: dependency)) + throw Diagnostics.fatalError + } + } + case .binary(name: let name, urlOrPath: let urlOrPath, checksum: let checksum): + guard loadedManifest.toolsVersion >= .v5_3 else { + context.diagnosticsEngine.emit(.unsupportedToolsVersionForBinaryTargets) + throw Diagnostics.fatalError + } + try editor.addBinaryTarget(targetName: name, urlOrPath: urlOrPath, checksum: checksum) + } + + try context.verifyEditedManifest(contents: editor.editedManifest) + try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest)) + + // Write template files. + try writeTemplateFilesForTarget(newTarget) + + if case .library(name: let name, includeTestTarget: true, dependencyNames: _) = newTarget { + try self.addTarget(.test(name: "\(name)Tests", dependencyNames: [name]), + productPackageNameMapping: productPackageNameMapping) + } + } + + private func diagnoseUnsupportedToolsVersions(manifest: Manifest) throws { + guard manifest.toolsVersion >= .v5_2 else { + context.diagnosticsEngine.emit(.unsupportedToolsVersionForEditing) + throw Diagnostics.fatalError + } + } + + private func writeTemplateFilesForTarget(_ newTarget: NewTarget) throws { + switch newTarget { + case .library: + let targetPath = context.manifestPath.parentDirectory.appending(components: "Sources", newTarget.name) + if !localFileSystem.exists(targetPath) { + let file = targetPath.appending(component: "\(newTarget.name).swift") + try fs.createDirectory(targetPath, recursive: true) + try fs.writeFileContents(file, bytes: "") + } + case .executable: + let targetPath = context.manifestPath.parentDirectory.appending(components: "Sources", newTarget.name) + if !localFileSystem.exists(targetPath) { + let file = targetPath.appending(component: "main.swift") + try fs.createDirectory(targetPath, recursive: true) + try fs.writeFileContents(file, bytes: "") + } + case .test: + let testTargetPath = context.manifestPath.parentDirectory.appending(components: "Tests", newTarget.name) + if !fs.exists(testTargetPath) { + let file = testTargetPath.appending(components: newTarget.name + ".swift") + try fs.createDirectory(testTargetPath, recursive: true) + try fs.writeFileContents(file) { + $0 <<< """ + import XCTest + @testable import <#Module#> + + final class <#TestCase#>: XCTestCase { + func testExample() { + + } + } + """ + } + } + case .binary: + break + } + } + + public func addProduct(name: String, type: ProductType, targets: [String]) throws { + let manifestPath = context.manifestPath + + // Validate that the package doesn't already contain a product with the same name. + let loadedManifest = try context.loadManifest(at: manifestPath.parentDirectory) + + try diagnoseUnsupportedToolsVersions(manifest: loadedManifest) + + guard !loadedManifest.products.contains(where: { $0.name == name }) else { + context.diagnosticsEngine.emit(.productAlreadyExists(name: name, + packageName: loadedManifest.name)) + throw Diagnostics.fatalError + } + + let manifestContents = try fs.readFileContents(manifestPath).cString + let editor = try ManifestRewriter(manifestContents, diagnosticsEngine: context.diagnosticsEngine) + try editor.addProduct(name: name, type: type) + + + for target in targets { + guard loadedManifest.targets.map(\.name).contains(target) else { + context.diagnosticsEngine.emit(.noTarget(name: target, packageName: loadedManifest.name)) + throw Diagnostics.fatalError + } + try editor.addProductTarget(product: name, target: target) + } + + try context.verifyEditedManifest(contents: editor.editedManifest) + try fs.writeFileContents(manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest)) + } +} + +extension Array where Element == TargetDescription.Dependency { + func containsDependency(_ other: String) -> Bool { + return self.contains { + switch $0 { + case .target(name: let name, condition: _), + .product(name: let name, package: _, condition: _), + .byName(name: let name, condition: _): + return name == other + } + } + } +} + +/// The types of target. +public enum NewTarget { + case library(name: String, includeTestTarget: Bool, dependencyNames: [String]) + case executable(name: String, dependencyNames: [String]) + case test(name: String, dependencyNames: [String]) + case binary(name: String, urlOrPath: String, checksum: String?) + + /// The name of the factory method for a target type. + func factoryMethodName(for toolsVersion: ToolsVersion) -> String { + switch self { + case .executable: + if toolsVersion >= .v5_4 { + return "executableTarget" + } else { + return "target" + } + case .library: return "target" + case .test: return "testTarget" + case .binary: return "binaryTarget" + } + } + + /// The name of the new target. + var name: String { + switch self { + case .library(name: let name, includeTestTarget: _, dependencyNames: _), + .executable(name: let name, dependencyNames: _), + .test(name: let name, dependencyNames: _), + .binary(name: let name, urlOrPath: _, checksum: _): + return name + } + } +} + +public enum PackageDependencyRequirement: Equatable { + case exact(String) + case revision(String) + case branch(String) + case upToNextMajor(String) + case upToNextMinor(String) + case range(String, String) + case closedRange(String, String) + case localPackage + + var ref: String? { + switch self { + case .exact(let ref): return ref + case .revision(let ref): return ref + case .branch(let ref): return ref + case .upToNextMajor(let ref): return ref + case .upToNextMinor(let ref): return ref + case .range(let start, _): return start + case .closedRange(let start, _): return start + case .localPackage: return nil + } + } +} + +extension ProductType { + var isLibrary: Bool { + switch self { + case .library: + return true + case .executable, .test, .plugin: + return false + } + } +} + +/// The global context for package editor. +public final class PackageEditorContext { + /// Path to the package manifest. + let manifestPath: AbsolutePath + + /// The manifest loader. + let manifestLoader: ManifestLoaderProtocol + + /// The repository manager. + let repositoryManager: RepositoryManager + + /// The file system in use. + let fs: FileSystem + + /// The diagnostics engine used to report errors. + let diagnosticsEngine: DiagnosticsEngine + + public init(manifestPath: AbsolutePath, + repositoryManager: RepositoryManager, + toolchain: UserToolchain, + diagnosticsEngine: DiagnosticsEngine, + fs: FileSystem = localFileSystem) throws { + self.manifestPath = manifestPath + self.repositoryManager = repositoryManager + self.diagnosticsEngine = diagnosticsEngine + self.fs = fs + + self.manifestLoader = ManifestLoader(manifestResources: toolchain.manifestResources) + } + + func verifyEditedManifest(contents: String) throws { + do { + try withTemporaryDirectory { + let path = $0 + try localFileSystem.writeFileContents(path.appending(component: "Package.swift"), + bytes: ByteString(encodingAsUTF8: contents)) + _ = try loadManifest(at: path, fs: localFileSystem) + } + } catch { + diagnosticsEngine.emit(.failedToLoadEditedManifest(error: error)) + throw Diagnostics.fatalError + } + } + + /// Load the manifest at the given path. + func loadManifest( + at path: AbsolutePath, + fs: FileSystem? = nil + ) throws -> Manifest { + let fs = fs ?? self.fs + + let toolsVersion = try ToolsVersionLoader().load( + at: path, fileSystem: fs) + return try tsc_await { + manifestLoader.load( + at: path, + packageIdentity: .plain(""), + packageKind: .local, + packageLocation: path.pathString, + version: nil, + revision: nil, + toolsVersion: toolsVersion, + identityResolver: DefaultIdentityResolver(), + fileSystem: fs, + diagnostics: .init(), + on: .global(), + completion: $0 + ) + } + } +} + +private extension Diagnostic.Message { + static func failedToLoadEditedManifest(error: Error) -> Diagnostic.Message { + .error("discarding changes because the edited manifest could not be loaded: \(error)") + } + static var unsupportedToolsVersionForEditing: Diagnostic.Message = + .error("command line editing of manifests is only supported for packages with a swift-tools-version of 5.2 and later") + static var unsupportedToolsVersionForBinaryTargets: Diagnostic.Message = + .error("binary targets are only supported in packages with a swift-tools-version of 5.3 and later") + static func productAlreadyExists(name: String, packageName: String) -> Diagnostic.Message { + .error("a product named '\(name)' already exists in '\(packageName)'") + } + static func packageDependencyAlreadyExists(url: String, packageName: String) -> Diagnostic.Message { + .error("'\(packageName)' already has a dependency on '\(url)'") + } + static func noTarget(name: String, packageName: String) -> Diagnostic.Message { + .error("no target named '\(name)' in '\(packageName)'") + } + static func targetAlreadyExists(name: String, packageName: String) -> Diagnostic.Message { + .error("a target named '\(name)' already exists in '\(packageName)'") + } + static func nonLocalRequirementSpecifiedForLocalPath(path: String) -> Diagnostic.Message { + .error("'\(path)' is a local package, but a non-local requirement was specified") + } + static func missingProductOrTarget(name: String) -> Diagnostic.Message { + .error("could not find a product or target named '\(name)'") + } +} diff --git a/Sources/SPMPackageEditor/ManifestRewriter.swift b/Sources/SPMPackageEditor/ManifestRewriter.swift deleted file mode 100644 index 97c7bbefcf4..00000000000 --- a/Sources/SPMPackageEditor/ManifestRewriter.swift +++ /dev/null @@ -1,438 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import SwiftSyntax - -/// A package manifest rewriter. -/// -/// This class provides functionality for rewriting the -/// Swift package manifest using the SwiftSyntax library. -/// -/// Similar to SwiftSyntax, this class only deals with the -/// syntax and has no functionality for semantics of the manifest. -public final class ManifestRewriter { - - enum Error: Swift.Error { - case error(String) - } - - /// The contents of the original manifest. - public let originalManifest: String - - /// The contents of the edited manifest. - public var editedManifest: String { - return editedSource.description - } - - /// The edited manifest syntax. - private var editedSource: Syntax - - /// Create a new manfiest editor with the given contents. - public init(_ manifest: String) throws { - self.originalManifest = manifest - self.editedSource = try SyntaxParser.parse(source: manifest) - } - - /// Add a package dependency. - public func addPackageDependency( - url: String, - requirement: PackageDependencyRequirement - ) throws { - // Find Package initializer. - let packageFinder = PackageInitFinder() - editedSource.walk(packageFinder) - - guard let initFnExpr = packageFinder.packageInit else { - throw Error.error("Couldn't find Package initializer") - } - - // Find dependencies section in the argument list of Package(...). - let packageDependenciesFinder = DependenciesArrayFinder() - initFnExpr.argumentList.walk(packageDependenciesFinder) - - let packageDependencies: ArrayExprSyntax - if let existingPackageDependencies = packageDependenciesFinder.dependenciesArrayExpr { - packageDependencies = existingPackageDependencies - } else { - // We didn't find a dependencies section so insert one. - let argListWithDependencies = DependenciesArrayWriter().visit(initFnExpr.argumentList) - - // Find the inserted section. - let packageDependenciesFinder = DependenciesArrayFinder() - argListWithDependencies.walk(packageDependenciesFinder) - packageDependencies = packageDependenciesFinder.dependenciesArrayExpr! - } - - // Add the the package dependency entry. - let newManifest = PackageDependencyWriter( - url: url, - requirement: requirement - ).visit(packageDependencies).root - - self.editedSource = newManifest - } - - /// Add a target dependency. - public func addTargetDependency( - target: String, - dependency: String - ) throws { - // Find Package initializer. - let packageFinder = PackageInitFinder() - editedSource.walk(packageFinder) - - guard let initFnExpr = packageFinder.packageInit else { - throw Error.error("Couldn't find Package initializer") - } - - // Find the `targets: []` array. - let targetsArrayFinder = TargetsArrayFinder() - initFnExpr.argumentList.walk(targetsArrayFinder) - guard let targetsArrayExpr = targetsArrayFinder.targets else { - throw Error.error("Couldn't find targets label") - } - - // Find the target node. - let targetFinder = TargetFinder(name: target) - targetsArrayExpr.walk(targetFinder) - guard let targetNode = targetFinder.foundTarget else { - throw Error.error("Couldn't find target \(target)") - } - - let targetDependencyFinder = DependenciesArrayFinder() - targetNode.walk(targetDependencyFinder) - - guard let targetDependencies = targetDependencyFinder.dependenciesArrayExpr else { - throw Error.error("Couldn't find dependencies section") - } - - // Add the target dependency entry. - let newManifest = TargetDependencyWriter( - dependencyName: dependency - ).visit(targetDependencies).root - - self.editedSource = newManifest - } - - /// Add a new target. - public func addTarget( - targetName: String, - type: TargetType = .regular - ) throws { - // Find Package initializer. - let packageFinder = PackageInitFinder() - editedSource.walk(packageFinder) - - guard let initFnExpr = packageFinder.packageInit else { - throw Error.error("Couldn't find Package initializer") - } - - let targetsFinder = TargetsArrayFinder() - initFnExpr.argumentList.walk(targetsFinder) - - guard let targetsNode = targetsFinder.targets else { - throw Error.error("Couldn't find targets section") - } - - let newManifest = NewTargetWriter( - name: targetName, targetType: type - ).visit(targetsNode).root - - self.editedSource = newManifest - } -} - -// MARK: - Syntax Visitors - -/// Package init finder. -final class PackageInitFinder: SyntaxVisitor { - - /// Reference to the function call of the package initializer. - private(set) var packageInit: FunctionCallExprSyntax? - - override func shouldVisit(_ kind: SyntaxKind) -> Bool { - return kind == .initializerClause - } - - override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { - if let fnCall = node.value as? FunctionCallExprSyntax, - let identifier = fnCall.calledExpression.firstToken, - identifier.text == "Package" { - assert(packageInit == nil, "Found two package initializers") - packageInit = fnCall - } - return .skipChildren - } -} - -/// Finder for "dependencies" array syntax. -final class DependenciesArrayFinder: SyntaxVisitor { - - private(set) var dependenciesArrayExpr: ArrayExprSyntax? - - override func visit(_ node: FunctionCallArgumentSyntax) -> SyntaxVisitorContinueKind { - guard node.label?.text == "dependencies" else { - return .skipChildren - } - - // We have custom code like foo + bar + [] (hopefully there is an array expr here). - if let seq = node.expression as? SequenceExprSyntax { - dependenciesArrayExpr = seq.elements.first(where: { $0 is ArrayExprSyntax }) as? ArrayExprSyntax - } else if let arrayExpr = node.expression as? ArrayExprSyntax { - dependenciesArrayExpr = arrayExpr - } - - // FIXME: If we find a dependencies section but not an array expr, then we should - // not try to insert one later. i.e. return error if depsArray is nil. - - return .skipChildren - } -} - -/// Finder for targets array expression. -final class TargetsArrayFinder: SyntaxVisitor { - - /// The found targets array expr. - private(set) var targets: ArrayExprSyntax? - - override func visit(_ node: FunctionCallArgumentSyntax) -> SyntaxVisitorContinueKind { - if node.label?.text == "targets", - let expr = node.expression as? ArrayExprSyntax { - assert(targets == nil, "Found two targets labels") - targets = expr - } - return .skipChildren - } -} - -/// Finds a given target in a list of targets. -final class TargetFinder: SyntaxVisitor { - - let targetToFind: String - private(set) var foundTarget: FunctionCallArgumentListSyntax? - - init(name: String) { - self.targetToFind = name - } - - override func visit(_ node: FunctionCallArgumentSyntax) -> SyntaxVisitorContinueKind { - guard case .identifier(let label)? = node.label?.tokenKind else { - return .skipChildren - } - guard label == "name", let targetNameExpr = node.expression as? StringLiteralExprSyntax else { - return .skipChildren - } - guard case .stringLiteral(let targetName) = targetNameExpr.stringLiteral.tokenKind else { - return .skipChildren - } - - if targetName == "\"" + self.targetToFind + "\"" { - self.foundTarget = node.parent as? FunctionCallArgumentListSyntax - return .skipChildren - } - - return .skipChildren - } -} - -// MARK: - Syntax Rewriters - -/// Writer for "dependencies" array syntax. -final class DependenciesArrayWriter: SyntaxRewriter { - - override func visit(_ node: FunctionCallArgumentListSyntax) -> Syntax { - let leadingTrivia = node.firstToken?.leadingTrivia ?? .zero - - let dependenciesArg = SyntaxFactory.makeFunctionCallArgument( - label: SyntaxFactory.makeIdentifier("dependencies", leadingTrivia: leadingTrivia), - colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), - expression: SyntaxFactory.makeArrayExpr( - leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), - elements: SyntaxFactory.makeBlankArrayElementList(), - rightSquare: SyntaxFactory.makeRightSquareBracketToken()), - trailingComma: SyntaxFactory.makeCommaToken() - ) - - // FIXME: This is not correct, we need to find the - // proper position for inserting `dependencies: []`. - return node.inserting(dependenciesArg, at: 1) - } -} - -/// Writer for inserting a trailing comma in an array expr. -final class ArrayTrailingCommaWriter: SyntaxRewriter { - let lastElement: ArrayElementSyntax - - init(lastElement: ArrayElementSyntax) { - self.lastElement = lastElement - } - - override func visit(_ node: ArrayElementSyntax) -> Syntax { - guard lastElement == node else { - return node - } - return node.withTrailingComma(SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))) - } -} - -/// Package dependency writer. -final class PackageDependencyWriter: SyntaxRewriter { - - /// The dependency url to write. - let url: String - - /// The dependency requirement. - let requirement: PackageDependencyRequirement - - init(url: String, requirement: PackageDependencyRequirement) { - self.url = url - self.requirement = requirement - } - - override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { - // FIXME: We should get the trivia from the closing brace. - let leadingTrivia: Trivia = [.newlines(1), .spaces(8)] - - let dotPackageExpr = SyntaxFactory.makeMemberAccessExpr( - base: nil, - dot: SyntaxFactory.makePeriodToken(leadingTrivia: leadingTrivia), - name: SyntaxFactory.makeIdentifier("package"), - declNameArguments: nil - ) - - var args: [FunctionCallArgumentSyntax] = [] - - let firstArgLabel = requirement == .localPackage ? "path" : "url" - let url = SyntaxFactory.makeFunctionCallArgument( - label: SyntaxFactory.makeIdentifier(firstArgLabel), - colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), - expression: SyntaxFactory.makeStringLiteralExpr(self.url), - trailingComma: requirement == .localPackage ? nil : SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1)) - ) - args.append(url) - - // FIXME: Handle other types of requirements. - if requirement != .localPackage { - let secondArg = SyntaxFactory.makeFunctionCallArgument( - label: SyntaxFactory.makeIdentifier("from"), - colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), - expression: SyntaxFactory.makeStringLiteralExpr(requirement.ref!), - trailingComma: nil - ) - args.append(secondArg) - } - - let expr = SyntaxFactory.makeFunctionCallExpr( - calledExpression: dotPackageExpr, - leftParen: SyntaxFactory.makeLeftParenToken(), - argumentList: SyntaxFactory.makeFunctionCallArgumentList(args), - rightParen: SyntaxFactory.makeRightParenToken(), - trailingClosure: nil - ) - - let newDependencyElement = SyntaxFactory.makeArrayElement( - expression: expr, - trailingComma: SyntaxFactory.makeCommaToken() - ) - - let rightBrace = SyntaxFactory.makeRightSquareBracketToken( - leadingTrivia: [.newlines(1), .spaces(4)]) - - return node.addArrayElement(newDependencyElement) - .withRightSquare(rightBrace) - } -} - -/// Writer for inserting a target dependency. -final class TargetDependencyWriter: SyntaxRewriter { - - /// The name of the dependency to write. - let dependencyName: String - - init(dependencyName name: String) { - self.dependencyName = name - } - - override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { - var node = node - - // Insert trailing comma, if needed. - if node.elements.count > 0 { - let lastElement = node.elements.map{$0}.last! - let trailingTriviaWriter = ArrayTrailingCommaWriter(lastElement: lastElement) - let newElements = trailingTriviaWriter.visit(node.elements) - node = node.withElements((newElements as! ArrayElementListSyntax)) - } - - let newDependencyElement = SyntaxFactory.makeArrayElement( - expression: SyntaxFactory.makeStringLiteralExpr(self.dependencyName), - trailingComma: nil - ) - - return node.addArrayElement(newDependencyElement) - } -} - -/// Writer for inserting a new target in a targets array. -final class NewTargetWriter: SyntaxRewriter { - - let name: String - let targetType: TargetType - - init(name: String, targetType: TargetType) { - self.name = name - self.targetType = targetType - } - - override func visit(_ node: ArrayExprSyntax) -> ExprSyntax { - - let leadingTrivia: Trivia = [.newlines(1), .spaces(8)] - let leadingTriviaArgs: Trivia = leadingTrivia.appending(.spaces(4)) - - let dotPackageExpr = SyntaxFactory.makeMemberAccessExpr( - base: nil, - dot: SyntaxFactory.makePeriodToken(leadingTrivia: leadingTrivia), - name: SyntaxFactory.makeIdentifier(targetType.factoryMethodName), - declNameArguments: nil - ) - - let nameArg = SyntaxFactory.makeFunctionCallArgument( - label: SyntaxFactory.makeIdentifier("name", leadingTrivia: leadingTriviaArgs), - colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), - expression: SyntaxFactory.makeStringLiteralExpr(self.name), - trailingComma: SyntaxFactory.makeCommaToken() - ) - - let emptyArray = SyntaxFactory.makeArrayExpr(leftSquare: SyntaxFactory.makeLeftSquareBracketToken(), elements: SyntaxFactory.makeBlankArrayElementList(), rightSquare: SyntaxFactory.makeRightSquareBracketToken()) - let depenenciesArg = SyntaxFactory.makeFunctionCallArgument( - label: SyntaxFactory.makeIdentifier("dependencies", leadingTrivia: leadingTriviaArgs), - colon: SyntaxFactory.makeColonToken(trailingTrivia: .spaces(1)), - expression: emptyArray, - trailingComma: nil - ) - - let expr = SyntaxFactory.makeFunctionCallExpr( - calledExpression: dotPackageExpr, - leftParen: SyntaxFactory.makeLeftParenToken(), - argumentList: SyntaxFactory.makeFunctionCallArgumentList([ - nameArg, depenenciesArg, - ]), - rightParen: SyntaxFactory.makeRightParenToken(), - trailingClosure: nil - ) - - let newDependencyElement = SyntaxFactory.makeArrayElement( - expression: expr, - trailingComma: SyntaxFactory.makeCommaToken() - ) - - return node.addArrayElement(newDependencyElement) - } -} diff --git a/Sources/SPMPackageEditor/PackageEditor.swift b/Sources/SPMPackageEditor/PackageEditor.swift deleted file mode 100644 index 3e99b6b2362..00000000000 --- a/Sources/SPMPackageEditor/PackageEditor.swift +++ /dev/null @@ -1,298 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import TSCUtility -import TSCBasic -import SourceControl -import PackageLoading -import PackageModel -import Workspace -import Foundation - -/// An editor for Swift packages. -/// -/// This class provides high-level functionality for performing -/// editing operations a package. -public final class PackageEditor { - - /// Reference to the package editor context. - let context: PackageEditorContext - - /// Create a package editor instance. - public convenience init(buildDir: AbsolutePath) throws { - self.init(context: try PackageEditorContext(buildDir: buildDir)) - } - - /// Create a package editor instance. - public init(context: PackageEditorContext) { - self.context = context - } - - /// The file system to perform disk operations on. - var fs: FileSystem { - return context.fs - } - - /// Add a package dependency. - public func addPackageDependency(options: Options.AddPackageDependency) throws { - var options = options - - // Validate that the package doesn't already contain this dependency. - // FIXME: We need to handle version-specific manifests. - let loadedManifest = try context.loadManifest(at: options.manifestPath.parentDirectory) - let containsDependency = loadedManifest.dependencies.contains { - return PackageIdentity(options.url) == PackageIdentity($0.url) - } - guard !containsDependency else { - throw StringError("Already has dependency \(options.url)") - } - - // If the input URL is a path, force the requirement to be a local package. - if TSCUtility.URL.scheme(options.url) == nil { - assert(options.requirement == nil || options.requirement == .localPackage) - options.requirement = .localPackage - } - - // Load the dependency manifest depending on the inputs. - let dependencyManifest: Manifest - let requirement: PackageDependencyRequirement - if options.requirement == .localPackage { - // For local packages, load the manifest and get the first library product name. - let path = AbsolutePath(options.url, relativeTo: fs.currentWorkingDirectory!) - dependencyManifest = try context.loadManifest(at: path) - requirement = .localPackage - } else { - // Otherwise, first lookup the dependency. - let spec = RepositorySpecifier(url: options.url) - let handle = try temp_await{ context.repositoryManager.lookup(repository: spec, completion: $0) } - let repo = try handle.open() - - // Compute the requirement. - if let inputRequirement = options.requirement { - requirement = inputRequirement - } else { - // Use the latest version or the master branch. - let versions = repo.tags.compactMap{ Version(string: $0) } - let latestVersion = versions.filter({ $0.prereleaseIdentifiers.isEmpty }).max() ?? versions.max() - requirement = latestVersion.map{ PackageDependencyRequirement.upToNextMajor($0.description) } ?? PackageDependencyRequirement.branch("master") - } - - // Load the manifest. - let revision = try repo.resolveRevision(identifier: requirement.ref!) - let repoFS = try repo.openFileView(revision: revision) - dependencyManifest = try context.loadManifest(at: .root, fs: repoFS) - } - - // Add the package dependency. - let manifestContents = try fs.readFileContents(options.manifestPath).cString - let editor = try ManifestRewriter(manifestContents) - try editor.addPackageDependency(url: options.url, requirement: requirement) - - // Add the product in the first regular target, if possible. - let productName = dependencyManifest.products.filter{ $0.type.isLibrary }.map{ $0.name }.first - let destTarget = loadedManifest.targets.filter{ $0.type == .regular }.first - if let product = productName, - let destTarget = destTarget, - !destTarget.dependencies.containsDependency(product) { - try editor.addTargetDependency(target: destTarget.name, dependency: product) - } - - // FIXME: We should verify our edits by loading the edited manifest before writing it to disk. - try fs.writeFileContents(options.manifestPath, bytes: ByteString(encodingAsUTF8: editor.editedManifest)) - } - - /// Add a new target. - public func addTarget(options: Options.AddTarget) throws { - let manifest = options.manifestPath - let targetName = options.targetName - let testTargetName = targetName + "Tests" - - // Validate that the package doesn't already contain this dependency. - // FIXME: We need to handle version-specific manifests. - let loadedManifest = try context.loadManifest(at: options.manifestPath.parentDirectory) - if loadedManifest.targets.contains(where: { $0.name == targetName }) { - throw StringError("Already has a target named \(targetName)") - } - - let manifestContents = try fs.readFileContents(options.manifestPath).cString - let editor = try ManifestRewriter(manifestContents) - try editor.addTarget(targetName: targetName) - try editor.addTarget(targetName: testTargetName, type: .test) - try editor.addTargetDependency(target: testTargetName, dependency: targetName) - - // FIXME: We should verify our edits by loading the edited manifest before writing it to disk. - try fs.writeFileContents(manifest, bytes: ByteString(encodingAsUTF8: editor.editedManifest)) - - // Write template files. - let targetPath = manifest.parentDirectory.appending(components: "Sources", targetName) - if !localFileSystem.exists(targetPath) { - let file = targetPath.appending(components: targetName + ".swift") - try fs.createDirectory(targetPath) - try fs.writeFileContents(file, bytes: "") - } - - let testTargetPath = manifest.parentDirectory.appending(components: "Tests", testTargetName) - if !fs.exists(testTargetPath) { - let file = testTargetPath.appending(components: testTargetName + ".swift") - try fs.createDirectory(testTargetPath) - try fs.writeFileContents(file) { - $0 <<< """ - import XCTest - @testable import \(targetName) - - final class \(testTargetName): XCTestCase { - func testExample() { - } - } - """ - } - } - } -} - -extension Array where Element == TargetDescription.Dependency { - func containsDependency(_ other: String) -> Bool { - return self.contains { - switch $0 { - case .target(let name), .product(let name, _), .byName(let name): - return name == other - } - } - } -} - -/// The types of target. -public enum TargetType { - case regular - case test - - /// The name of the factory method for a target type. - var factoryMethodName: String { - switch self { - case .regular: return "target" - case .test: return "testTarget" - } - } -} - -public enum PackageDependencyRequirement: Equatable { - case exact(String) - case revision(String) - case branch(String) - case upToNextMajor(String) - case upToNextMinor(String) - case localPackage - - var ref: String? { - switch self { - case .exact(let ref): return ref - case .revision(let ref): return ref - case .branch(let ref): return ref - case .upToNextMajor(let ref): return ref - case .upToNextMinor(let ref): return ref - case .localPackage: return nil - } - } -} - -public enum Options { - public struct AddPackageDependency { - public var manifestPath: AbsolutePath - public var url: String - public var requirement: PackageDependencyRequirement? - - public init( - manifestPath: AbsolutePath, - url: String, - requirement: PackageDependencyRequirement? = nil - ) { - self.manifestPath = manifestPath - self.url = url - self.requirement = requirement - } - } - - public struct AddTarget { - public var manifestPath: AbsolutePath - public var targetName: String - public var targetType: TargetType - - public init( - manifestPath: AbsolutePath, - targetName: String, - targetType: TargetType = .regular - ) { - self.manifestPath = manifestPath - self.targetName = targetName - self.targetType = targetType - } - } -} - -extension ProductType { - var isLibrary: Bool { - switch self { - case .library: - return true - case .executable, .test: - return false - } - } -} - -/// The global context for package editor. -public final class PackageEditorContext { - - /// Path to the build directory of the package. - let buildDir: AbsolutePath - - /// The manifest loader. - let manifestLoader: ManifestLoaderProtocol - - /// The repository manager. - let repositoryManager: RepositoryManager - - /// The file system in use. - let fs: FileSystem - - public init(buildDir: AbsolutePath, fs: FileSystem = localFileSystem) throws { - self.buildDir = buildDir - self.fs = fs - - // Create toolchain. - let hostToolchain = try UserToolchain(destination: .hostDestination()) - self.manifestLoader = ManifestLoader(manifestResources: hostToolchain.manifestResources) - - let repositoriesPath = buildDir.appending(component: "repositories") - self.repositoryManager = RepositoryManager( - path: repositoriesPath, - provider: GitRepositoryProvider() - ) - } - - /// Load the manifest at the given path. - func loadManifest( - at path: AbsolutePath, - fs: FileSystem? = nil - ) throws -> Manifest { - let fs = fs ?? self.fs - - let toolsVersion = try ToolsVersionLoader().load( - at: path, fileSystem: fs) - - return try manifestLoader.load( - package: path, - baseURL: path.description, - version: nil, - toolsVersion: toolsVersion, - fileSystem: fs - ) - } -} diff --git a/Sources/swiftpm-manifest-tool/main.swift b/Sources/swiftpm-manifest-tool/main.swift deleted file mode 100644 index 7f84d1ed9c3..00000000000 --- a/Sources/swiftpm-manifest-tool/main.swift +++ /dev/null @@ -1,136 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import Basics -import Foundation -import TSCBasic -import TSCUtility -import func POSIX.exit - -import SPMPackageEditor -import class PackageModel.Manifest - -enum ToolError: Error { - case error(String) - case noManifest -} - -enum Mode: String { - case addPackageDependency = "add-package" - case addTarget = "add-target" - case help -} - -struct Options { - struct AddPackageDependency { - let url: String - } - struct AddTarget { - let name: String - } - var dataPath: AbsolutePath = AbsolutePath("/tmp") - var addPackageDependency: AddPackageDependency? - var addTarget: AddTarget? - var mode = Mode.help -} - -/// Finds the Package.swift manifest file in the current working directory. -func findPackageManifest() -> AbsolutePath? { - guard let cwd = localFileSystem.currentWorkingDirectory else { - return nil - } - - let manifestPath = cwd.appending(component: Manifest.filename) - return localFileSystem.isFile(manifestPath) ? manifestPath : nil -} - -final class PackageIndex { - - struct Entry: Codable { - let name: String - let url: String - } - - // Name -> URL - private(set) var index: [String: String] - - init() throws { - index = [:] - - let indexFile = localFileSystem.homeDirectory.appending(components: ".swiftpm", "package-mapping.json") - guard localFileSystem.isFile(indexFile) else { - return - } - - let bytes = try localFileSystem.readFileContents(indexFile).contents - let entries = try JSONDecoder.makeWithDefaults().decode(Array.self, from: Data(bytes: bytes, count: bytes.count)) - - index = Dictionary(uniqueKeysWithValues: entries.map{($0.name, $0.url)}) - } -} - -do { - let binder = ArgumentBinder() - - let parser = ArgumentParser( - usage: "subcommand", - overview: "Tool for editing the Package.swift manifest file") - - // Add package dependency. - let packageDependencyParser = parser.add(subparser: Mode.addPackageDependency.rawValue, overview: "Add a new package dependency") - binder.bind( - positional: packageDependencyParser.add(positional: "package-url", kind: String.self, usage: "Dependency URL"), - to: { $0.addPackageDependency = Options.AddPackageDependency(url: $1) }) - - // Add Target. - let addTargetParser = parser.add(subparser: Mode.addTarget.rawValue, overview: "Add a new target") - binder.bind( - positional: addTargetParser.add(positional: "target-name", kind: String.self, usage: "Target name"), - to: { $0.addTarget = Options.AddTarget(name: $1) }) - - // Bind the mode. - binder.bind( - parser: parser, - to: { $0.mode = Mode(rawValue: $1)! }) - - // Parse the options. - var options = Options() - let result = try parser.parse(Array(CommandLine.arguments.dropFirst())) - try binder.fill(parseResult: result, into: &options) - - // Find the Package.swift file in cwd. - guard let manifest = findPackageManifest() else { - throw ToolError.noManifest - } - options.dataPath = (localFileSystem.currentWorkingDirectory ?? AbsolutePath("/tmp")).appending(component: ".build") - - switch options.mode { - case .addPackageDependency: - var url = options.addPackageDependency!.url - url = try PackageIndex().index[url] ?? url - - let editor = try PackageEditor(buildDir: options.dataPath) - try editor.addPackageDependency(options: .init(manifestPath: manifest, url: url, requirement: nil)) - - case .addTarget: - let targetOptions = options.addTarget! - - let editor = try PackageEditor(buildDir: options.dataPath) - try editor.addTarget(options: .init(manifestPath: manifest, targetName: targetOptions.name, targetType: .regular)) - - case .help: - parser.printUsage(on: stdoutStream) - } - -} catch { - stderrStream <<< String(describing: error) <<< "\n" - stderrStream.flush() - POSIX.exit(1) -} diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 09d3e677657..6decf93fa6c 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -899,6 +899,17 @@ final class PackageToolTests: XCTestCase { } } + fileprivate func check(stderr: String, _ block: () throws -> ()) { + do { + try block() + XCTFail() + } catch SwiftPMProductError.executionFailure(_, _, let stderrOutput) { + XCTAssertEqual(stderrOutput, stderr) + } catch { + XCTFail("unexpected error: \(error)") + } + } + func testMirrorConfig() throws { try testWithTemporaryDirectory { prefix in let fs = localFileSystem @@ -931,17 +942,6 @@ final class PackageToolTests: XCTestCase { (stdout, _) = try execute(["config", "get-mirror", "--original-url", "git@github.com:apple/swift-package-manager.git"], packagePath: packageRoot) XCTAssertEqual(stdout.spm_chomp(), "git@mygithub.com:foo/swift-package-manager.git") - func check(stderr: String, _ block: () throws -> ()) { - do { - try block() - XCTFail() - } catch SwiftPMProductError.executionFailure(_, _, let stderrOutput) { - XCTAssertEqual(stderrOutput, stderr) - } catch { - XCTFail("unexpected error: \(error)") - } - } - check(stderr: "not found\n") { try execute(["config", "get-mirror", "--original-url", "foo"], packagePath: packageRoot) } @@ -1063,4 +1063,205 @@ final class PackageToolTests: XCTestCase { } } } + + #if BUILD_PACKAGE_SYNTAX + func testAddDependency() throws { + fixture(name: "PackageEditor/Empty") { packageRoot in + fixture(name: "PackageEditor/OneProduct") { dependencyRoot in + _ = try execute(["add-dependency", dependencyRoot.pathString], packagePath: packageRoot) + let (dumpOutput, _) = try execute(["dump-package"], packagePath: packageRoot) + let json = try JSON(bytes: ByteString(encodingAsUTF8: dumpOutput)) + guard case .dictionary(let dict) = json else { XCTFail(); return } + guard case .array(let deps) = dict["dependencies"] else { XCTFail(); return } + guard case .dictionary(let depsMap) = deps[0] else { XCTFail(); return } + guard case .array(let localDeps) = depsMap["local"] else { XCTFail(); return } + XCTAssertEqual(localDeps.count, 1) + let dependency = localDeps[0] + XCTAssertEqual(dependency["name"], .string("MyPackage2")) + XCTAssertEqual(dependency["identity"], .string("packageeditor_oneproduct")) + + check(stderr: "error: 'MyPackage' already has a dependency on '\(dependencyRoot.pathString)'\n") { + try execute(["add-dependency", dependencyRoot.pathString], packagePath: packageRoot) + } + + check(stderr: "error: only one requirement is allowed when specifiying a dependency\n") { + try execute(["add-dependency", "http://www.githost.com/repo.git", + "--exact", "1.0.0", "--from", "1.0.0"], + packagePath: packageRoot) + } + + check(stderr: "error: '--to' and '--through' may only be used with '--from' to specify a range requirement\n") { + _ = try execute(["add-dependency", "http://www.githost.com/repo.git", + "--exact", "1.0.0", "--to", "2.0.0"], + packagePath: packageRoot) + } + + check(stderr: "error: '--to' and '--through' may not be used in the same requirement\n") { + _ = try execute(["add-dependency", "http://www.githost.com/repo.git", + "--from", "1.0.0", "--to", "2.0.0", "--through", "3.0.0"], + packagePath: packageRoot) + } + + } + } + } + + func testAddTarget() throws { + fixture(name: "PackageEditor/Empty") { packageRoot in + fixture(name: "PackageEditor/OneProduct") { dependencyRoot in + _ = try execute(["add-dependency", dependencyRoot.pathString], packagePath: packageRoot) + _ = try execute(["add-target", "MyLibrary", "--dependencies", "Library"], packagePath: packageRoot) + _ = try execute(["add-target", "MyExecutable", "--type", "executable", + "--dependencies", "MyLibrary"], packagePath: packageRoot) + _ = try execute(["add-target", "--type", "test", "IntegrationTests", + "--dependencies", "MyLibrary"], packagePath: packageRoot) + let contents = try localFileSystem.readFileContents(packageRoot.appending(component: Manifest.filename)).description + XCTAssertEqual(contents, """ + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "MyPackage", + dependencies: [ + .package(name: "MyPackage2", path: "\(dependencyRoot.pathString)"), + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [ + .product(name: "Library", package: "MyPackage2"), + ] + ), + .testTarget( + name: "MyLibraryTests", + dependencies: [ + "MyLibrary", + ] + ), + .target( + name: "MyExecutable", + dependencies: [ + "MyLibrary", + ] + ), + .testTarget( + name: "IntegrationTests", + dependencies: [ + "MyLibrary", + ] + ), + ] + ) + """) + + XCTAssertTrue(localFileSystem.exists(packageRoot.appending(components: "Sources", "MyLibrary", "MyLibrary.swift"))) + XCTAssertTrue(localFileSystem.exists(packageRoot.appending(components: "Tests", "MyLibraryTests", "MyLibraryTests.swift"))) + XCTAssertTrue(localFileSystem.exists(packageRoot.appending(components: "Sources", "MyExecutable", "main.swift"))) + XCTAssertTrue(localFileSystem.exists(packageRoot.appending(components: "Tests", "IntegrationTests", "IntegrationTests.swift"))) + + _ = try execute(["add-target", "MyLocalBinary", "--type", "binary", + "--path", "LocalBinary.xcframework"], + packagePath: packageRoot) + check(stderr: "error: a target named 'MyLibrary' already exists in 'MyPackage'\n") { + _ = try execute(["add-target", "MyLibrary"], packagePath: packageRoot) + } + check(stderr: "error: binary targets must specify either a path or both a URL and a checksum\n") { + _ = try execute(["add-target", "MyLibrary", "--type", "binary"], packagePath: packageRoot) + } + check(stderr: "error: option '--checksum' is only supported for binary targets\n") { + _ = try execute(["add-target", "MyLibrary", "--checksum", "checksum"], packagePath: packageRoot) + } + check(stderr: "error: option '--dependencies' is not supported for binary targets\n") { + _ = try execute(["add-target", "MyLibrary", "--type", "binary", "--dependencies", "MyLibrary"], + packagePath: packageRoot) + } + check(stderr: "error: unsupported target type 'unsupported'; supported types are library, executable, test, and binary\n") { + _ = try execute(["add-target", "MyLibrary", "--type", "unsupported"], + packagePath: packageRoot) + } + + } + } + } + + func testAddProduct() throws { + fixture(name: "PackageEditor/Empty") { packageRoot in + _ = try execute(["add-target", "MyLibrary", "--no-test-target"], + packagePath: packageRoot) + _ = try execute(["add-target", "MyLibrary2", "--no-test-target"], + packagePath: packageRoot) + + _ = try execute(["add-product", "LibraryProduct", + "--targets", "MyLibrary", "MyLibrary2"], + packagePath: packageRoot) + _ = try execute(["add-product", "DynamicLibraryProduct", + "--type", "dynamic-library", + "--targets", "MyLibrary"], + packagePath: packageRoot) + _ = try execute(["add-product", "StaticLibraryProduct", + "--type", "static-library", + "--targets", "MyLibrary"], + packagePath: packageRoot) + _ = try execute(["add-product", "ExecutableProduct", + "--type", "executable", + "--targets", "MyLibrary2"], + packagePath: packageRoot) + let contents = try localFileSystem.readFileContents( + packageRoot.appending(component: Manifest.filename)).description + XCTAssertEqual(contents, """ + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "MyPackage", + products: [ + .library( + name: "LibraryProduct", + targets: [ + "MyLibrary", + "MyLibrary2", + ] + ), + .library( + name: "DynamicLibraryProduct", + type: .dynamic, + targets: [ + "MyLibrary", + ] + ), + .library( + name: "StaticLibraryProduct", + type: .static, + targets: [ + "MyLibrary", + ] + ), + .executable( + name: "ExecutableProduct", + targets: [ + "MyLibrary2", + ] + ), + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [] + ), + .target( + name: "MyLibrary2", + dependencies: [] + ), + ] + ) + """) + + check(stderr: "error: a product named 'LibraryProduct' already exists in 'MyPackage'\n") { + _ = try execute(["add-product", "LibraryProduct", + "--targets", "MyLibrary,MyLibrary2"], + packagePath: packageRoot) + } + } + } + #endif } diff --git a/Tests/PackageSyntaxTests/AddPackageDependencyTests.swift b/Tests/PackageSyntaxTests/AddPackageDependencyTests.swift new file mode 100644 index 00000000000..648a89d33a3 --- /dev/null +++ b/Tests/PackageSyntaxTests/AddPackageDependencyTests.swift @@ -0,0 +1,579 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +import XCTest + +import PackageSyntax + +final class AddPackageDependencyTests: XCTestCase { + func testAddPackageDependency() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + ], + targets: [ + .target( + name: "exec", + dependencies: []), + .testTarget( + name: "execTests", + dependencies: ["exec"]), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target( + name: "exec", + dependencies: []), + .testTarget( + name: "execTests", + dependencies: ["exec"]), + ] + ) + """) + } + + func testAddPackageDependency2() throws { + let manifest = """ + let package = Package( + name: "exec", + dependencies: [], + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency3() throws { + let manifest = """ + let package = Package( + name: "exec", + dependencies: [ + // Here is a comment. + .package(url: "https://github.com/foo/bar", .branch("master")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + // FIXME: preserve comment + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/bar", .branch("master")), + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency4() throws { + let manifest = """ + let package = Package( + name: "exec", + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency5() throws { + // FIXME: This is broken, we end up removing the comment. + let manifest = """ + let package = Package( + name: "exec", + dependencies: [ + // Here is a comment. + ], + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency6() throws { + let manifest = """ + let myDeps = [ + .package(url: "https://github.com/foo/foo", from: "1.0.2"), + ] + + let package = Package( + name: "exec", + dependencies: myDeps + [ + .package(url: "https://github.com/foo/bar", from: "1.0.3"), + ], + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let myDeps = [ + .package(url: "https://github.com/foo/foo", from: "1.0.2"), + ] + + let package = Package( + name: "exec", + dependencies: myDeps + [ + .package(url: "https://github.com/foo/bar", from: "1.0.3"), + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency7() throws { + let manifest = """ + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/bar", from: "1.0.3") + ], + targets: [ + .target(name: "exec") + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/bar", from: "1.0.3"), + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec") + ] + ) + """) + } + + func testAddPackageDependency8() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + targets: [ + .target(name: "exec"), + ] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + targets: [ + .target(name: "exec"), + ] + ) + """) + } + + func testAddPackageDependency9() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + swiftLanguageVersions: [] + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ], + swiftLanguageVersions: [] + ) + """) + } + + func testAddPackageDependency10() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMajor("1.0.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMajor(from: "1.0.1")), + ] + ) + """) + } + + func testAddPackageDependencyWithExactRequirement() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .exact("2.0.2"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .exact("2.0.2")), + ] + ) + """) + } + + func testAddPackageDependencyWithBranchRequirement() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .branch("main"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .branch("main")), + ] + ) + """) + } + + func testAddPackageDependencyWithRevisionRequirement() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .revision("abcde"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .revision("abcde")), + ] + ) + """) + } + + func testAddPackageDependencyWithBranchRequirementUsingConvenienceMethods() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .branch("main"), + branchAndRevisionConvenienceMethodsSupported: true + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", branch: "main"), + ] + ) + """) + } + + func testAddPackageDependencyWithRevisionRequirementUsingConvenienceMethods() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .revision("abcde"), + branchAndRevisionConvenienceMethodsSupported: true + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", revision: "abcde"), + ] + ) + """) + } + + func testAddPackageDependencyWithUpToNextMinorRequirement() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .upToNextMinor("1.1.1"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", .upToNextMinor(from: "1.1.1")), + ] + ) + """) + } + + func testAddPackageDependenciesWithRangeRequirements() throws { + let manifest = """ + let package = Package( + name: "exec", + platforms: [.iOS], + ) + """ + + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .range("1.1.1", "2.2.2"), + branchAndRevisionConvenienceMethodsSupported: false + ) + try editor.addPackageDependency( + name: "goo", + url: "https://github.com/foo/goo", + requirement: .closedRange("2.2.2", "3.3.3"), + branchAndRevisionConvenienceMethodsSupported: false + ) + + XCTAssertEqual(editor.editedManifest, """ + let package = Package( + name: "exec", + platforms: [.iOS], + dependencies: [ + .package(name: "goo", url: "https://github.com/foo/goo", "1.1.1"..<"2.2.2"), + .package(name: "goo", url: "https://github.com/foo/goo", "2.2.2"..."3.3.3"), + ] + ) + """) + } +} diff --git a/Tests/PackageSyntaxTests/AddProductTests.swift b/Tests/PackageSyntaxTests/AddProductTests.swift new file mode 100644 index 00000000000..17a8762b165 --- /dev/null +++ b/Tests/PackageSyntaxTests/AddProductTests.swift @@ -0,0 +1,198 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors + */ + +import XCTest + +import PackageSyntax + +final class AddProductTests: XCTestCase { + func testAddProduct() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + products: [ + .executable(name: "abc", targets: ["foo"]), + ] + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addProduct(name: "exec", type: .executable) + try editor.addProduct(name: "lib", type: .library(.automatic)) + try editor.addProduct(name: "staticLib", type: .library(.static)) + try editor.addProduct(name: "dynamicLib", type: .library(.dynamic)) + + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + products: [ + .executable(name: "abc", targets: ["foo"]), + .executable( + name: "exec", + targets: [] + ), + .library( + name: "lib", + targets: [] + ), + .library( + name: "staticLib", + type: .static, + targets: [] + ), + .library( + name: "dynamicLib", + type: .dynamic, + targets: [] + ), + ] + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """) + } + + func testAddProduct2() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addProduct(name: "exec", type: .executable) + try editor.addProduct(name: "lib", type: .library(.automatic)) + try editor.addProduct(name: "staticLib", type: .library(.static)) + try editor.addProduct(name: "dynamicLib", type: .library(.dynamic)) + + // FIXME: weird indentation + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + products: [ + .executable( + name: "exec", + targets: [] + ), + .library( + name: "lib", + targets: [] + ), + .library( + name: "staticLib", + type: .static, + targets: [] + ), + .library( + name: "dynamicLib", + type: .dynamic, + targets: [] + ), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """) + } + + func testAddProduct3() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + \tname: "exec", + \ttargets: [ + \t\t.target( + \t\t\tname: "foo", + \t\t\tdependencies: [] + \t\t), + \t] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addProduct(name: "exec", type: .executable) + + // FIXME: weird indentation + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + \tname: "exec", + \tproducts: [ + \t\t.executable( + \t\t\tname: "exec", + \t\t\ttargets: [] + \t\t), + \t], + \ttargets: [ + \t\t.target( + \t\t\tname: "foo", + \t\t\tdependencies: [] + \t\t), + \t] + ) + """) + } +} diff --git a/Tests/SPMPackageEditorTests/AddTargetDependencyTests.swift b/Tests/PackageSyntaxTests/AddTargetDependencyTests.swift similarity index 71% rename from Tests/SPMPackageEditorTests/AddTargetDependencyTests.swift rename to Tests/PackageSyntaxTests/AddTargetDependencyTests.swift index 9fb19c2f263..ff21a5a6a8a 100644 --- a/Tests/SPMPackageEditorTests/AddTargetDependencyTests.swift +++ b/Tests/PackageSyntaxTests/AddTargetDependencyTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2019 Apple Inc. and the Swift project authors + Copyright (c) 2021 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 @@ -10,12 +10,12 @@ import XCTest -@testable import SPMPackageEditor +import PackageSyntax final class AddTargetDependencyTests: XCTestCase { func testAddTargetDependency() throws { let manifest = """ - // swift-tools-version:5.0 + // swift-tools-version:5.2 import PackageDescription let package = Package( @@ -40,16 +40,16 @@ final class AddTargetDependencyTests: XCTestCase { ) """ - let editor = try ManifestRewriter(manifest) - try editor.addTargetDependency( + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addByNameTargetDependency( target: "exec", dependency: "foo") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "exec", dependency: "bar") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "execTests", dependency: "foo") XCTAssertEqual(editor.editedManifest, """ - // swift-tools-version:5.0 + // swift-tools-version:5.2 import PackageDescription let package = Package( @@ -63,13 +63,19 @@ final class AddTargetDependencyTests: XCTestCase { dependencies: []), .target( name: "exec", - dependencies: ["foo", "bar"]), + dependencies: [ + "foo", + "bar", + ]), .target( name: "c", dependencies: []), .testTarget( name: "execTests", - dependencies: ["exec", "foo"]), + dependencies: [ + "exec", + "foo", + ]), ] ) """) @@ -101,16 +107,16 @@ final class AddTargetDependencyTests: XCTestCase { ) """ - let editor = try ManifestRewriter(manifest) - try editor.addTargetDependency( + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addByNameTargetDependency( target: "foo", dependency: "dep") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "foo1", dependency: "dep") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "foo2", dependency: "dep") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "foo3", dependency: "dep") - try editor.addTargetDependency( + try editor.addByNameTargetDependency( target: "foo4", dependency: "dep") XCTAssertEqual(editor.editedManifest, """ @@ -119,20 +125,28 @@ final class AddTargetDependencyTests: XCTestCase { targets: [ .target( name: "foo", - dependencies: ["bar", "dep"]), + dependencies: [ + "bar", + "dep", + ]), .target( name: "foo1", - dependencies: ["bar", "dep"]), + dependencies: [ + "bar", + "dep", + ]), .target( name: "foo2", - dependencies: ["dep"]), + dependencies: [ + "dep", + ]), .target( name: "foo3", - dependencies: ["foo", "bar", "dep"]), + dependencies: ["foo", "bar", "dep",]), .target( name: "foo4", dependencies: [ - "foo", "bar", "dep" + "foo", "bar", "dep", ]), ] ) diff --git a/Tests/PackageSyntaxTests/AddTargetTests.swift b/Tests/PackageSyntaxTests/AddTargetTests.swift new file mode 100644 index 00000000000..dcb7041e781 --- /dev/null +++ b/Tests/PackageSyntaxTests/AddTargetTests.swift @@ -0,0 +1,148 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors + */ + +import XCTest + +import PackageSyntax + +final class AddTargetTests: XCTestCase { + func testAddTarget() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target") + try editor.addTarget(targetName: "NewTargetTests", factoryMethodName: "testTarget") + try editor.addByNameTargetDependency(target: "NewTargetTests", dependency: "NewTarget") + + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + .target( + name: "NewTarget", + dependencies: [] + ), + .testTarget( + name: "NewTargetTests", + dependencies: [ + "NewTarget", + ] + ), + ] + ) + """) + } + + func testAddTarget2() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target") + + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "NewTarget", + dependencies: [] + ), + ] + ) + """) + } + + func testAddTarget3() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + \tname: "exec", + \tdependencies: [ + \t\t.package(url: "https://github.com/foo/goo", from: "1.0.1"), + \t] + ) + """ + + let editor = try ManifestRewriter(manifest, diagnosticsEngine: .init()) + try editor.addTarget(targetName: "NewTarget", factoryMethodName: "target") + + XCTAssertEqual(editor.editedManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + \tname: "exec", + \tdependencies: [ + \t\t.package(url: "https://github.com/foo/goo", from: "1.0.1"), + \t], + \ttargets: [ + \t\t.target( + \t\t\tname: "NewTarget", + \t\t\tdependencies: [] + \t\t), + \t] + ) + """) + } +} diff --git a/Tests/PackageSyntaxTests/ArrayFormattingTests.swift b/Tests/PackageSyntaxTests/ArrayFormattingTests.swift new file mode 100644 index 00000000000..8b732874d69 --- /dev/null +++ b/Tests/PackageSyntaxTests/ArrayFormattingTests.swift @@ -0,0 +1,469 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors + */ + +import XCTest +import PackageSyntax +import SwiftSyntax + +final class ArrayFormattingTests: XCTestCase { + func assertAdding(string: String, to arrayLiteralCode: String, produces result: String) { + let sourceFileSyntax = try! SyntaxParser.parse(source: arrayLiteralCode) + let arrayExpr = sourceFileSyntax.statements.first?.item.as(ArrayExprSyntax.self)! + let outputSyntax = arrayExpr?.withAdditionalElementExpr(ExprSyntax(SyntaxFactory.makeStringLiteralExpr(string))) + XCTAssertEqual(outputSyntax!.description, result) + } + + func assertAdding(string: String, toFunctionCallArg functionCallCode: String, produces result: String) { + let sourceFileSyntax = try! SyntaxParser.parse(source: functionCallCode) + let funcExpr = sourceFileSyntax.statements.first!.item.as(FunctionCallExprSyntax.self)! + let arg = funcExpr.argumentList.first! + let arrayExpr = arg.expression.as(ArrayExprSyntax.self)! + let newExpr = arrayExpr.withAdditionalElementExpr(ExprSyntax(SyntaxFactory.makeStringLiteralExpr(string))) + let outputSyntax = funcExpr.withArgumentList( + funcExpr.argumentList.replacing(childAt: 0, + with: arg.withExpression(ExprSyntax(newExpr))) + ) + XCTAssertEqual(outputSyntax.description, result) + } + + func testInsertingIntoArrayExprWith2PlusElements() throws { + assertAdding(string: "c", to: #"["a", "b"]"#, produces: #"["a", "b", "c",]"#) + assertAdding(string: "c", to: #"["a", "b",]"#, produces: #"["a", "b", "c",]"#) + assertAdding(string: "c", to: #"["a","b"]"#, produces: #"["a","b","c",]"#) + assertAdding(string: "c", + to: #"["a", /*hello*/"b"/*world!*/]"#, + produces: #"["a", /*hello*/"b",/*world!*/ "c",]"#) + assertAdding(string: "c", to: """ + [ + "a", + "b" + ] + """, produces: """ + [ + "a", + "b", + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + "a", + "b", + ] + """, produces: """ + [ + "a", + "b", + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + "a", "b" + ] + """, produces: """ + [ + "a", "b", "c", + ] + """) + assertAdding(string: "e", to: """ + [ + "a", "b", + "c", "d", + ] + """, produces: """ + [ + "a", "b", + "c", "d", "e", + ] + """) + assertAdding(string: "e", to: """ + ["a", "b", + "c", "d"] + """, produces: """ + ["a", "b", + "c", "d", "e",] + """) + assertAdding(string: "c", to: """ + \t[ + \t\t"a", + \t\t"b", + \t] + """, produces: """ + \t[ + \t\t"a", + \t\t"b", + \t\t"c", + \t] + """) + assertAdding(string: "c", to: """ + [ + "a", // Comment about a + "b", + ] + """, produces: """ + [ + "a", // Comment about a + "b", + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + "a", + "b", // Comment about b + ] + """, produces: """ + [ + "a", + "b", // Comment about b + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + "a", + "b", + /*comment*/ + ] + """, produces: """ + [ + "a", + "b", + /*comment*/ + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + /* + 1 + */ + "a", + /* + 2 + */ + "b", + /* + 3 + */ + ] + """, produces: """ + [ + /* + 1 + */ + "a", + /* + 2 + */ + "b", + /* + 3 + */ + "c", + ] + """) + assertAdding(string: "c", to: """ + [ + /// Comment + + "a", + + + "b", + ] + """, produces: """ + [ + /// Comment + + "a", + + + "b", + + + "c", + ] + """) + assertAdding(string: "3", toFunctionCallArg: """ + foo(someArg: ["1", "2"]) + """, produces: """ + foo(someArg: ["1", "2", "3",]) + """) + assertAdding(string: "3", toFunctionCallArg: """ + foo(someArg: ["1", + "2"]) + """, produces: """ + foo(someArg: ["1", + "2", + "3",]) + """) + assertAdding(string: "3", toFunctionCallArg: """ + foo( + arg1: ["1", "2"], arg2: [] + ) + """, produces: """ + foo( + arg1: ["1", "2", "3",], arg2: [] + ) + """) + assertAdding(string: "3", toFunctionCallArg: """ + foo(someArg: [ + "1", + "2", + ]) + """, produces: """ + foo(someArg: [ + "1", + "2", + "3", + ]) + """) + assertAdding(string: "3", toFunctionCallArg: """ + foo( + arg1: [ + "1", + "2", + ], arg2: [] + ) + """, produces: """ + foo( + arg1: [ + "1", + "2", + "3", + ], arg2: [] + ) + """) + } + + func testInsertingIntoEmptyArrayExpr() { + assertAdding(string: "1", to: #"[]"#, produces: """ + [ + "1", + ] + """) + assertAdding(string: "1", to: """ + [ + + ] + """, produces: """ + [ + "1", + ] + """) + assertAdding(string: "1", to: """ + [ + ] + """, produces: """ + [ + "1", + ] + """) + assertAdding(string: "1", to: """ + [ + + ] + """, produces: """ + [ + "1", + ] + """) + assertAdding(string: "1", to: """ + \t[ + + \t] + """, produces: """ + \t[ + \t\t"1", + \t] + """) + assertAdding(string: "1", toFunctionCallArg: """ + foo(someArg: []) + """, produces: """ + foo(someArg: [ + "1", + ]) + """) + assertAdding(string: "1", toFunctionCallArg: """ + foo(someArg: []) + """, produces: """ + foo(someArg: [ + "1", + ]) + """) + assertAdding(string: "1", toFunctionCallArg: """ + foo( + arg1: [], arg2: [] + ) + """, produces: """ + foo( + arg1: [ + "1", + ], arg2: [] + ) + """) + assertAdding(string: "1", toFunctionCallArg: """ + \tfoo(someArg: []) + """, produces: """ + \tfoo(someArg: [ + \t\t"1", + \t]) + """) + } + + func testInsertingIntoSingleElementArrayExpr() { + assertAdding(string: "b", to: """ + ["a"] + """, produces: """ + [ + "a", + "b", + ] + """) + assertAdding(string: "b", to: """ + [ + "a" + ] + """, produces: """ + [ + "a", + "b", + ] + """) + assertAdding(string: "b", to: """ + ["a",] + """, produces: """ + [ + "a", + "b", + ] + """) + assertAdding(string: "b", to: """ + [ + "a", + ] + """, produces: """ + [ + "a", + "b", + ] + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo(someArg: ["1"]) + """, produces: """ + foo(someArg: [ + "1", + "2", + ]) + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo(someArg: ["1"]) + """, produces: """ + foo(someArg: [ + "1", + "2", + ]) + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo( + arg1: ["1"], arg2: [] + ) + """, produces: """ + foo( + arg1: [ + "1", + "2", + ], arg2: [] + ) + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo(someArg: [ + "1" + ]) + """, produces: """ + foo(someArg: [ + "1", + "2", + ]) + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo(someArg: [ + "1" + ]) + """, produces: """ + foo(someArg: [ + "1", + "2", + ]) + """) + assertAdding(string: "2", toFunctionCallArg: """ + foo( + arg1: [ + "1" + ], arg2: [] + ) + """, produces: """ + foo( + arg1: [ + "1", + "2", + ], arg2: [] + ) + """) + } + + func assert(code: String, hasIndent indent: Trivia, forLine line: Int) { + let sourceFileSyntax = try! SyntaxParser.parse(source: code) + let converter = SourceLocationConverter(file: "test.swift", tree: sourceFileSyntax) + let visitor = DetermineLineIndentVisitor(lineNumber: line, sourceLocationConverter: converter) + visitor.walk(sourceFileSyntax) + XCTAssertEqual(visitor.lineIndent, indent) + } + + func testIndentVisitor() throws { + assert(code: """ + foo( + arg: [] + ) + """, hasIndent: [.spaces(4)], forLine: 2) + assert(code: """ + foo( + \targ: [] + ) + """, hasIndent: [.tabs(1)], forLine: 2) + assert(code: """ + foo( + arg1: [], arg2: [] + ) + """, hasIndent: [.spaces(4)], forLine: 2) + assert(code: """ + foo( + bar( + arg1: [], + arg2: [] + ) + ) + """, hasIndent: [.spaces(8)], forLine: 3) + assert(code: """ + foo( + bar(arg1: [], + arg2: []) + ) + """, hasIndent: [.spaces(4)], forLine: 2) + assert(code: """ + foo( + bar(arg1: [], + arg2: []) + ) + """, hasIndent: [.spaces(8)], forLine: 3) + } +} diff --git a/Tests/PackageSyntaxTests/PackageEditorTests.swift b/Tests/PackageSyntaxTests/PackageEditorTests.swift new file mode 100644 index 00000000000..34d18b00aaa --- /dev/null +++ b/Tests/PackageSyntaxTests/PackageEditorTests.swift @@ -0,0 +1,868 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 Swift project authors +*/ + +import XCTest +import TSCBasic +import TSCUtility +import SPMTestSupport +import SourceControl + +import PackageSyntax + +final class PackageEditorTests: XCTestCase { + + func testAddDependency5_2_to_5_4() throws { + for version in ["5.2", "5.3", "5.4"] { + let manifest = """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + try fs.createDirectory(.init("/pkg/repositories"), recursive: false) + try fs.createDirectory(.init("/pkg/repo"), recursive: false) + + + let provider = InMemoryGitRepositoryProvider() + let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs) + try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package(name: "repo") + """)) + try repo.commit() + try repo.tag(name: "1.1.1") + provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo) + + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: provider, fileSystem: fs), + toolchain: Resources.default.toolchain, + diagnosticsEngine: .init(), + fs: fs + ) + let editor = PackageEditor(context: context) + + try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1")) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + .package(name: "repo", url: "http://www.githost.com/repo.git", .exact("1.1.1")), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """) + } + } + + func testAddDependency5_5() throws { + let manifest = """ + // swift-tools-version:5.5 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + try fs.createDirectory(.init("/pkg/repositories"), recursive: false) + try fs.createDirectory(.init("/pkg/repo"), recursive: false) + + + let provider = InMemoryGitRepositoryProvider() + let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs) + try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package(name: "repo") + """)) + try repo.commit() + try repo.tag(name: "1.1.1") + provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo) + + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: provider, fileSystem: fs), + toolchain: Resources.default.toolchain, + diagnosticsEngine: .init(), + fs: fs + ) + let editor = PackageEditor(context: context) + + try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1")) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:5.5 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + .package(url: "http://www.githost.com/repo.git", .exact("1.1.1")), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """) + } + + func testAddTarget5_2() throws { + let manifest = """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"]) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'", + "could not find a product or target named 'NotFound'"]) + + try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]), + productPackageNameMapping: [:]) + try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]), + productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"]) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + .target( + name: "baz", + dependencies: [] + ), + .testTarget( + name: "bazTests", + dependencies: [ + "baz", + ] + ), + .target( + name: "qux", + dependencies: [ + "foo", + "baz", + ] + ), + .testTarget( + name: "IntegrationTests", + dependencies: [ + .product(name: "OtherProduct", package: "goo"), + "goo", + ] + ), + ] + ) + """) + + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift"))) + XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift"))) + } + + + func testAddTarget5_3() throws { + let manifest = """ + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"]) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'", + "could not find a product or target named 'NotFound'"]) + + try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]), + productPackageNameMapping: [:]) + try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]), + productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"]) + try editor.addTarget(.binary(name: "LocalBinary", + urlOrPath: "/some/local/binary/target.xcframework", + checksum: nil), + productPackageNameMapping: [:]) + try editor.addTarget(.binary(name: "RemoteBinary", + urlOrPath: "https://mybinaries.com/RemoteBinary.zip", + checksum: "totallylegitchecksum"), + productPackageNameMapping: [:]) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + .target( + name: "baz", + dependencies: [] + ), + .testTarget( + name: "bazTests", + dependencies: [ + "baz", + ] + ), + .target( + name: "qux", + dependencies: [ + "foo", + "baz", + ] + ), + .testTarget( + name: "IntegrationTests", + dependencies: [ + .product(name: "OtherProduct", package: "goo"), + "goo", + ] + ), + .binaryTarget( + name: "LocalBinary", + path: "/some/local/binary/target.xcframework" + ), + .binaryTarget( + name: "RemoteBinary", + url: "https://mybinaries.com/RemoteBinary.zip", + checksum: "totallylegitchecksum" + ), + ] + ) + """) + + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift"))) + XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift"))) + } + + func testAddTarget5_4_to_5_5() throws { + for version in ["5.4", "5.5"] { + let manifest = """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "foo", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'"]) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "Error", includeTestTarget: true, dependencyNames: ["NotFound"]), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a target named 'foo' already exists in 'exec'", + "could not find a product or target named 'NotFound'"]) + + try editor.addTarget(.library(name: "baz", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + try editor.addTarget(.executable(name: "qux", dependencyNames: ["foo", "baz"]), + productPackageNameMapping: [:]) + try editor.addTarget(.test(name: "IntegrationTests", dependencyNames: ["OtherProduct", "goo"]), + productPackageNameMapping: ["goo": "goo", "OtherProduct": "goo"]) + try editor.addTarget(.binary(name: "LocalBinary", + urlOrPath: "/some/local/binary/target.xcframework", + checksum: nil), + productPackageNameMapping: [:]) + try editor.addTarget(.binary(name: "RemoteBinary", + urlOrPath: "https://mybinaries.com/RemoteBinary.zip", + checksum: "totallylegitchecksum"), + productPackageNameMapping: [:]) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + .target( + name: "baz", + dependencies: [] + ), + .testTarget( + name: "bazTests", + dependencies: [ + "baz", + ] + ), + .executableTarget( + name: "qux", + dependencies: [ + "foo", + "baz", + ] + ), + .testTarget( + name: "IntegrationTests", + dependencies: [ + .product(name: "OtherProduct", package: "goo"), + "goo", + ] + ), + .binaryTarget( + name: "LocalBinary", + path: "/some/local/binary/target.xcframework" + ), + .binaryTarget( + name: "RemoteBinary", + url: "https://mybinaries.com/RemoteBinary.zip", + checksum: "totallylegitchecksum" + ), + ] + ) + """) + + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/qux/main.swift"))) + XCTAssertFalse(fs.exists(AbsolutePath("/pkg/Tests/quxTests"))) + XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/IntegrationTests/IntegrationTests.swift"))) + } + } + + func testAddProduct5_2_to_5_5() throws { + for version in ["5.2", "5.3", "5.4", "5.5"] { + let manifest = """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + products: [ + .executable(name: "abc", targets: ["foo"]), + ], + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "/pkg/Sources/bar/source.swift", + "/pkg/Tests/fooTests/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addProduct(name: "abc", type: .library(.automatic), targets: []) + } + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addProduct(name: "SomeProduct", type: .library(.automatic), targets: ["nonexistent"]) + } + + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["a product named 'abc' already exists in 'exec'", + "no target named 'nonexistent' in 'exec'"]) + + try editor.addProduct(name: "xyz", type: .executable, targets: ["bar"]) + try editor.addProduct(name: "libxyz", type: .library(.dynamic), targets: ["foo", "bar"]) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:\(version) + import PackageDescription + + let package = Package( + name: "exec", + products: [ + .executable(name: "abc", targets: ["foo"]), + .executable( + name: "xyz", + targets: [ + "bar", + ] + ), + .library( + name: "libxyz", + type: .dynamic, + targets: [ + "foo", + "bar", + ] + ), + ], + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + .target( + name: "bar", + dependencies: []), + .testTarget( + name: "fooTests", + dependencies: ["foo", "bar"]), + ] + ) + """) + } + } + + func testToolsVersionTest() throws { + let manifest = """ + // swift-tools-version:5.0 + import PackageDescription + + let package = Package( + name: "exec", + dependencies: [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: [ + .target( + name: "foo", + dependencies: []), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), provider: InMemoryGitRepositoryProvider()), + toolchain: Resources.default.toolchain, diagnosticsEngine: diags, fs: fs) + let editor = PackageEditor(context: context) + + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "bar", includeTestTarget: true, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), + ["command line editing of manifests is only supported for packages with a swift-tools-version of 5.2 and later"]) + } + + func testEditingManifestsWithComplexArgumentExpressions() throws { + let manifest = """ + // swift-tools-version:5.3 + import PackageDescription + + let flag = false + let extraDeps: [Package.Dependency] = [] + + let package = Package( + name: "exec", + products: [ + .library(name: "Library", targets: ["foo"]) + ].filter { _ in true }, + dependencies: extraDeps + [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + ], + targets: flag ? [] : [ + .target( + name: "foo", + dependencies: []), + ] + ) + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + try fs.createDirectory(.init("/pkg/repositories"), recursive: false) + try fs.createDirectory(.init("/pkg/repo"), recursive: false) + + + let provider = InMemoryGitRepositoryProvider() + let repo = InMemoryGitRepository(path: .init("/pkg/repo"), fs: fs) + try repo.writeFileContents(.init("/Package.swift"), bytes: .init(encodingAsUTF8: """ + // swift-tools-version:5.2 + import PackageDescription + + let package = Package(name: "repo") + """)) + try repo.commit() + try repo.tag(name: "1.1.1") + provider.add(specifier: .init(url: "http://www.githost.com/repo"), repository: repo) + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), + provider: provider, + fileSystem: fs), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + try editor.addPackageDependency(url: "http://www.githost.com/repo.git", requirement: .exact("1.1.1")) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "Library", includeTestTarget: false, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), + ["'targets' argument is not an array literal or concatenation of array literals"]) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addProduct(name: "Executable", type: .executable, targets: ["foo"]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), + ["'targets' argument is not an array literal or concatenation of array literals", + "'products' argument is not an array literal or concatenation of array literals"]) + + let newManifest = try fs.readFileContents(manifestPath).cString + XCTAssertEqual(newManifest, """ + // swift-tools-version:5.3 + import PackageDescription + + let flag = false + let extraDeps: [Package.Dependency] = [] + + let package = Package( + name: "exec", + products: [ + .library(name: "Library", targets: ["foo"]) + ].filter { _ in true }, + dependencies: extraDeps + [ + .package(url: "https://github.com/foo/goo", from: "1.0.1"), + .package(name: "repo", url: "http://www.githost.com/repo.git", .exact("1.1.1")), + ], + targets: flag ? [] : [ + .target( + name: "foo", + dependencies: []), + ] + ) + """) + } + + func testEditingConditionalPackageInit() throws { + let manifest = """ + // swift-tools-version:5.3 + import PackageDescription + + #if os(macOS) + let package = Package( + name: "macOSPackage" + ) + #else + let package = Package( + name: "otherPlatformsPackage" + ) + #endif + """ + + let fs = InMemoryFileSystem(emptyFiles: + "/pkg/Package.swift", + "/pkg/Sources/foo/source.swift", + "end") + + let manifestPath = AbsolutePath("/pkg/Package.swift") + try fs.writeFileContents(manifestPath) { $0 <<< manifest } + + try fs.createDirectory(.init("/pkg/repositories"), recursive: false) + + let diags = DiagnosticsEngine() + let context = try PackageEditorContext( + manifestPath: AbsolutePath("/pkg/Package.swift"), + repositoryManager: RepositoryManager(path: .init("/pkg/repositories"), + provider: InMemoryGitRepositoryProvider(), + fileSystem: fs), + toolchain: Resources.default.toolchain, + diagnosticsEngine: diags, + fs: fs) + let editor = PackageEditor(context: context) + XCTAssertThrows(Diagnostics.fatalError) { + try editor.addTarget(.library(name: "Library", includeTestTarget: false, dependencyNames: []), + productPackageNameMapping: [:]) + } + XCTAssertEqual(diags.diagnostics.map(\.message.text), ["found multiple Package initializers"]) + } +} diff --git a/Tests/SPMPackageEditorTests/AddPackageDependencyTests.swift b/Tests/SPMPackageEditorTests/AddPackageDependencyTests.swift deleted file mode 100644 index 631e413e9f9..00000000000 --- a/Tests/SPMPackageEditorTests/AddPackageDependencyTests.swift +++ /dev/null @@ -1,236 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import XCTest - -@testable import SPMPackageEditor - -final class AddPackageDependencyTests: XCTestCase { - func testAddPackageDependency() throws { - let manifest = """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - ], - targets: [ - .target( - name: "exec", - dependencies: []), - .testTarget( - name: "execTests", - dependencies: ["exec"]), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target( - name: "exec", - dependencies: []), - .testTarget( - name: "execTests", - dependencies: ["exec"]), - ] - ) - """) - } - - func testAddPackageDependency2() throws { - let manifest = """ - let package = Package( - name: "exec", - dependencies: [], - targets: [ - .target(name: "exec"), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """) - } - - func testAddPackageDependency3() throws { - let manifest = """ - let package = Package( - name: "exec", - dependencies: [ - // Here is a comment. - .package(url: "https://github.com/foo/bar", .branch("master")), - ], - targets: [ - .target(name: "exec"), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - let package = Package( - name: "exec", - dependencies: [ - // Here is a comment. - .package(url: "https://github.com/foo/bar", .branch("master")), - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """) - } - - func testAddPackageDependency4() throws { - let manifest = """ - let package = Package( - name: "exec", - targets: [ - .target(name: "exec"), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """) - } - - func testAddPackageDependency5() throws { - // FIXME: This is broken, we end up removing the comment. - let manifest = """ - let package = Package( - name: "exec", - dependencies: [ - // Here is a comment. - ], - targets: [ - .target(name: "exec"), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """) - } - - func testAddPackageDependency6() throws { - let manifest = """ - let myDeps = [ - .package(url: "https://github.com/foo/foo", from: "1.0.2"), - ] - - let package = Package( - name: "exec", - dependencies: myDeps + [ - .package(url: "https://github.com/foo/bar", from: "1.0.3"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """ - - - let editor = try ManifestRewriter(manifest) - try editor.addPackageDependency( - url: "https://github.com/foo/goo", - requirement: .upToNextMajor("1.0.1") - ) - - XCTAssertEqual(editor.editedManifest, """ - let myDeps = [ - .package(url: "https://github.com/foo/foo", from: "1.0.2"), - ] - - let package = Package( - name: "exec", - dependencies: myDeps + [ - .package(url: "https://github.com/foo/bar", from: "1.0.3"), - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target(name: "exec"), - ] - ) - """) - } -} diff --git a/Tests/SPMPackageEditorTests/AddTargetTests.swift b/Tests/SPMPackageEditorTests/AddTargetTests.swift deleted file mode 100644 index 0acedff7ffd..00000000000 --- a/Tests/SPMPackageEditorTests/AddTargetTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import XCTest - -@testable import SPMPackageEditor - -final class AddTargetTests: XCTestCase { - func testAddTarget() throws { - let manifest = """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target( - name: "foo", - dependencies: []), - .target( - name: "bar", - dependencies: []), - .testTarget( - name: "fooTests", - dependencies: ["foo", "bar"]), - ] - ) - """ - - let editor = try ManifestRewriter(manifest) - try editor.addTarget(targetName: "NewTarget") - try editor.addTarget(targetName: "NewTargetTests", type: .test) - try editor.addTargetDependency(target: "NewTargetTests", dependency: "NewTarget") - - XCTAssertEqual(editor.editedManifest, """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target( - name: "foo", - dependencies: []), - .target( - name: "bar", - dependencies: []), - .testTarget( - name: "fooTests", - dependencies: ["foo", "bar"]), - .target( - name: "NewTarget", - dependencies: []), - .testTarget( - name: "NewTargetTests", - dependencies: ["NewTarget"]), - ] - ) - """) - } -} diff --git a/Tests/SPMPackageEditorTests/PackageEditorTests.swift b/Tests/SPMPackageEditorTests/PackageEditorTests.swift deleted file mode 100644 index 5524479b9a9..00000000000 --- a/Tests/SPMPackageEditorTests/PackageEditorTests.swift +++ /dev/null @@ -1,97 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2019 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 Swift project authors -*/ - -import XCTest -import TSCBasic -import SPMTestSupport - -@testable import SPMPackageEditor - -final class PackageEditorTests: XCTestCase { - func testAddTarget() throws { - let manifest = """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target( - name: "foo", - dependencies: []), - .target( - name: "bar", - dependencies: []), - .testTarget( - name: "fooTests", - dependencies: ["foo", "bar"]), - ] - ) - """ - - let fs = InMemoryFileSystem(emptyFiles: - "/pkg/Package.swift", - "/pkg/Sources/foo/source.swift", - "/pkg/Sources/bar/source.swift", - "/pkg/Tests/fooTests/source.swift", - "end") - - let manifestPath = AbsolutePath("/pkg/Package.swift") - try fs.writeFileContents(manifestPath) { $0 <<< manifest } - - let context = try PackageEditorContext( - buildDir: AbsolutePath("/pkg/foo"), fs: fs) - let editor = PackageEditor(context: context) - - XCTAssertThrows(StringError("Already has a target named foo")) { - try editor.addTarget(options: - .init(manifestPath: manifestPath, targetName: "foo")) - } - - try editor.addTarget(options: - .init(manifestPath: manifestPath, targetName: "baz")) - - let newManifest = try fs.readFileContents(manifestPath).cString - XCTAssertEqual(newManifest, """ - // swift-tools-version:5.0 - import PackageDescription - - let package = Package( - name: "exec", - dependencies: [ - .package(url: "https://github.com/foo/goo", from: "1.0.1"), - ], - targets: [ - .target( - name: "foo", - dependencies: []), - .target( - name: "bar", - dependencies: []), - .testTarget( - name: "fooTests", - dependencies: ["foo", "bar"]), - .target( - name: "baz", - dependencies: []), - .testTarget( - name: "bazTests", - dependencies: ["baz"]), - ] - ) - """) - - XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Sources/baz/baz.swift"))) - XCTAssertTrue(fs.exists(AbsolutePath("/pkg/Tests/bazTests/bazTests.swift"))) - } -}