diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 8883779..0df783f 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -24,7 +24,7 @@ jobs: - name: πŸ› οΈ Build with release configuration run: | - swift build --configuration release | xcpretty --utf --color && exit ${PIPESTATUS[0]} + swift build --configuration release -skipPackagePluginValidation | xcpretty --utf --color && exit ${PIPESTATUS[0]} - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f382878..5b84c23 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -25,4 +25,4 @@ jobs: - name: πŸ› οΈ Run All Tests run: | - xcodebuild test -scheme public-api-diff -destination "platform=iOS,name=Any iOS Device" | xcpretty --utf --color && exit ${PIPESTATUS[0]} + xcodebuild test -scheme public-api-diff -destination "platform=iOS,name=Any iOS Device" -skipPackagePluginValidation | xcpretty --utf --color && exit ${PIPESTATUS[0]} diff --git a/Package.resolved b/Package.resolved index 176ce7a..6aea85e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -44,6 +44,15 @@ "revision" : "86ed20990585f478c0daf309af645c2a528b59d8", "version" : "0.54.6" } + }, + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "7c80ce6f142164b0201871e580b021d1b2c69804", + "version" : "0.57.0" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 53030c0..eb64aed 100644 --- a/Package.swift +++ b/Package.swift @@ -34,12 +34,13 @@ let package = Package( .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1"), .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.6"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.3") + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.3"), + .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.57.0") ], targets: [ - + // MARK: - Executable Targets - + .executableTarget( name: "public-api-diff", dependencies: [ @@ -50,11 +51,14 @@ let package = Package( "PADSwiftInterfaceFileLocator", .product(name: "ArgumentParser", package: "swift-argument-parser") ], - path: "Sources/ExecutableTargets/CommandLineTool" + path: "Sources/ExecutableTargets/CommandLineTool", + plugins: [ + .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins") + ] ), - + // MARK: - Public Modules - + .target( name: "PADSwiftInterfaceDiff", dependencies: [ @@ -62,7 +66,7 @@ let package = Package( "PADLogging", "FileHandlingModule", .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax") ], path: "Sources/PublicModules/PADSwiftInterfaceDiff" ), @@ -94,9 +98,9 @@ let package = Package( dependencies: ["PADCore"], path: "Sources/PublicModules/PADOutputGenerator" ), - + // MARK: - Shared/Public - + .target( name: "PADCore", path: "Sources/Shared/Public/PADCore" @@ -111,9 +115,9 @@ let package = Package( dependencies: ["FileHandlingModule", "ShellModule", "PADLogging"], path: "Sources/Shared/Public/PADSwiftInterfaceFileLocator" ), - + // MARK: - Shared/Package - + .target( name: "FileHandlingModule", path: "Sources/Shared/Package/FileHandlingModule" @@ -127,9 +131,9 @@ let package = Package( dependencies: ["FileHandlingModule", "ShellModule", "PADLogging"], path: "Sources/Shared/Package/SwiftPackageFileHelperModule" ), - + // MARK: - Test Targets - + .testTarget( name: "UnitTests", dependencies: [ diff --git a/ReferencePackages/ReferencePackage/Sources/ReferencePackage/ReferencePackage.swift b/ReferencePackages/ReferencePackage/Sources/ReferencePackage/ReferencePackage.swift index 2a8e50c..86f3687 100644 --- a/ReferencePackages/ReferencePackage/Sources/ReferencePackage/ReferencePackage.swift +++ b/ReferencePackages/ReferencePackage/Sources/ReferencePackage/ReferencePackage.swift @@ -24,7 +24,7 @@ import Foundation public protocol CustomProtocol { typealias CustomAssociatedType = Equatable - + var getSetVar: any CustomAssociatedType { get set } var getVar: any CustomAssociatedType { get } func function() -> any CustomAssociatedType @@ -40,7 +40,7 @@ public struct CustomStruct: CustomProtocol { // MARK: - Generic public class public class CustomClass { - + public weak var weakObject: CustomClass? lazy var lazyVar: String = { "I am a lazy" }() @_spi(SomeSpi) @@ -48,18 +48,18 @@ public class CustomClass { open var computedVar: String { "I am computed" } package let constantLet: String = "I'm a let" public var optionalVar: T? - + @MainActor public func asyncThrowingFunc() async throws {} public func rethrowingFunc(throwingArg: @escaping () throws -> String) rethrows {} - + public init(weakObject: CustomClass? = nil, optionalVar: T? = nil) { self.weakObject = weakObject self.optionalVar = optionalVar } - + public init() {} - + public convenience init(value: T) { self.init(optionalVar: value) } @@ -70,12 +70,12 @@ public class CustomClass { @_spi(SystemProgrammingInterface) open class OpenSpiConformingClass: CustomProtocol { public typealias CustomAssociatedType = any Equatable - + public var getSetVar: CustomAssociatedType public var getVar: CustomAssociatedType @inlinable public func function() -> CustomAssociatedType { fatalError() } - + public init(getSetVar: CustomAssociatedType, getVar: CustomAssociatedType) { self.getSetVar = getSetVar self.getVar = getVar @@ -106,11 +106,11 @@ public actor CustomActor {} public enum OperatorNamespace: String { case someValue = "1" - + public static prefix func ++ (_ counter: OperatorNamespace) -> String { counter.rawValue } - + public static postfix func ++ (_ counter: OperatorNamespace) -> String { counter.rawValue } @@ -137,6 +137,6 @@ public enum CustomEnum { case caseWithString(String) case caseWithTuple(String, Int) case caseWithBlock((Int) throws -> Void) - + indirect case recursive(CustomEnum) } diff --git a/ReferencePackages/UpdatedPackage/Sources/ReferencePackage/ReferencePackage.swift b/ReferencePackages/UpdatedPackage/Sources/ReferencePackage/ReferencePackage.swift index 13cd1c2..a2ca7e7 100644 --- a/ReferencePackages/UpdatedPackage/Sources/ReferencePackage/ReferencePackage.swift +++ b/ReferencePackages/UpdatedPackage/Sources/ReferencePackage/ReferencePackage.swift @@ -32,7 +32,7 @@ public protocol ParentProtocol { public protocol CustomProtocol: ParentProtocol { associatedtype CustomAssociatedType: Equatable associatedtype AnotherAssociatedType: Strideable - + var getSetVar: AnotherAssociatedType { get set } var getVar: CustomAssociatedType { get } func function() -> CustomAssociatedType @@ -41,8 +41,8 @@ public protocol CustomProtocol: Par public struct CustomStruct: CustomProtocol { public typealias CustomAssociatedType = Int public typealias AnotherAssociatedType = Double - public typealias Iterator = Array - + public typealias Iterator = [AnotherAssociatedType] + @available(macOS, unavailable, message: "Unavailable on macOS") public struct NestedStruct { @available(*, deprecated, renamed: "nestedVar") @@ -50,7 +50,7 @@ public struct CustomStruct: CustomProtocol { @available(swift, introduced: 5.9) public let nestedVar: String = "var" } - + public var getSetVar: Double public var getVar: Int @discardableResult @@ -60,7 +60,7 @@ public struct CustomStruct: CustomProtocol { // MARK: - Generic public class public class CustomClass { - + public weak var weakObject: CustomClass? public lazy var lazyVar: String = { "I am a lazy" }() @_spi(SomeSpi) @@ -68,26 +68,26 @@ public class CustomClass { open var computedVar: String { "I am computed" } package let constantLet: String = "I'm a let" public var optionalVar: T? - + public let a = 0, b = 0, c = 0, d: Double = 5.0 - + @MainActor - public func asyncThrowingFunc(_ element: Element) async throws -> Void where Element: Strideable {} + public func asyncThrowingFunc(_ element: Element) async throws where Element: Strideable {} public func rethrowingFunc(throwingArg: @escaping () throws -> String) rethrows {} - + public init(weakObject: CustomClass? = nil, optionalVar: T? = nil) { self.weakObject = weakObject self.optionalVar = optionalVar - + lazyVar = "Great!" } - + public init?() {} - + public convenience init!(value: T) { self.init(optionalVar: value) } - + public subscript(index: Int) -> T? { get { optionalVar } set { optionalVar = newValue } @@ -107,13 +107,13 @@ extension Array { open class OpenSpiConformingClass: CustomProtocol { public typealias CustomAssociatedType = T public typealias AnotherAssociatedType = T - public typealias Iterator = Array - + public typealias Iterator = [Double] + public var getSetVar: T public var getVar: T @inlinable public func function() -> T where T: Equatable { getVar } - + public init(getSetVar: T, getVar: T) { self.getSetVar = getSetVar self.getVar = getVar @@ -144,11 +144,11 @@ public actor CustomActor: SimpleProtocol {} public enum OperatorNamespace: String { case someValue = "1" - + public static prefix func ++ (_ counter: OperatorNamespace) -> String { counter.rawValue } - + public static postfix func ++ (_ counter: OperatorNamespace) -> String { counter.rawValue } @@ -176,7 +176,7 @@ public enum CustomEnum { case caseWithTuple(_ foo: String, bar: Int) case caseWithBlock((Int) throws -> Void) case a, b, c, d, e(NestedStructInExtension) - + indirect case recursive(CustomEnum) } @@ -186,7 +186,7 @@ public enum RawValueEnum: String { } extension CustomEnum: SimpleProtocol { - + public struct NestedStructInExtension { public let string: String public init(string: String = "Hello") { @@ -196,14 +196,14 @@ extension CustomEnum: SimpleProtocol { } extension CustomEnum.NestedStructInExtension { - + var description: String { return string } } public extension CustomEnum where T == String { - + var titleOfCaseWithNamedString: String? { if case let .caseWithNamedString(title) = self { return title diff --git a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift index 0d039ba..dfe624c 100644 --- a/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift +++ b/Sources/ExecutableTargets/CommandLineTool/CommandLineTool.swift @@ -17,7 +17,7 @@ import PADSwiftInterfaceDiff @main struct PublicApiDiff: AsyncParsableCommand { - + static var configuration: CommandConfiguration = .init( commandName: "public-api-diff", subcommands: [ @@ -26,14 +26,14 @@ struct PublicApiDiff: AsyncParsableCommand { FrameworkToOutputCommand.self ] ) - + public func run() async throws { fatalError("No sub command provided") } } extension PublicApiDiff { - + static func logger( with logLevel: LogLevel, logOutputFilePath: String? @@ -43,7 +43,7 @@ extension PublicApiDiff { loggers += [LogFileLogger(outputFilePath: logOutputFilePath)] } loggers += [SystemLogger().withLogLevel(logLevel)] - + return LoggingGroup(with: loggers) } } diff --git a/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift index 5e7f11b..3ba8880 100644 --- a/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/FrameworkToOutputCommand.swift @@ -17,51 +17,51 @@ import PADSwiftInterfaceFileLocator /// Command that analyzes the differences between an old and new project and produces a human readable output struct FrameworkToOutputCommand: AsyncParsableCommand { - + static var configuration: CommandConfiguration = .init(commandName: "framework") - + /// The path to the new/updated xcframework @Option(help: "Specify the updated .framework to compare to") public var new: String - + /// The path to the old/reference xcframework @Option(help: "Specify the old .framework to compare to") public var old: String - + /// The name of the target/module to show in the output @Option(help: "The name of your target/module to show in the output") public var targetName: String - + @Option(help: "[Optional] Specify the type of .swiftinterface you want to compare (public/private)") public var swiftInterfaceType: SwiftInterfaceType = .public - + @Option(help: "[Optional] The name of your old version (e.g. v1.0 / main) to show in the output") public var oldVersionName: String? - + @Option(help: "[Optional] The name of your new version (e.g. v2.0 / develop) to show in the output") public var newVersionName: String? - + /// The (optional) output file path /// /// If not defined the output will be printed to the console @Option(help: "[Optional] Where to output the result (File path)") public var output: String? - + /// The (optional) path to the log output file @Option(help: "[Optional] Where to output the logs (File path)") public var logOutput: String? - + @Option(help: "[Optional] The log level to use during execution") public var logLevel: LogLevel = .default - + /// Entry point of the command line tool public func run() async throws { - + let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput) - + do { // MARK: - Locating .swiftinterface files - + let swiftInterfaceFiles = try Self.locateSwiftInterfaceFiles( targetName: targetName, oldPath: old, @@ -69,16 +69,16 @@ struct FrameworkToOutputCommand: AsyncParsableCommand { swiftInterfaceType: swiftInterfaceType, logger: logger ) - + // MARK: - Analyzing .swiftinterface files - + let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles( swiftInterfaceFiles: swiftInterfaceFiles, logger: logger ) - + // MARK: - Generate Output - + let generatedOutput = try Self.generateOutput( for: swiftInterfaceChanges, warnings: [], @@ -86,16 +86,16 @@ struct FrameworkToOutputCommand: AsyncParsableCommand { oldVersionName: oldVersionName, newVersionName: newVersionName ) - + // MARK: - - + if let output { try FileManager.default.write(generatedOutput, to: output) } else { // We're not using a logger here as we always want to have it printed if no output was specified print(generatedOutput) } - + logger.log("βœ… Success", from: "Main") } catch { logger.log("πŸ’₯ \(error.localizedDescription)", from: "Main") @@ -106,7 +106,7 @@ struct FrameworkToOutputCommand: AsyncParsableCommand { // MARK: - Privates private extension FrameworkToOutputCommand { - + static func locateSwiftInterfaceFiles( targetName: String, oldPath: String, @@ -115,7 +115,7 @@ private extension FrameworkToOutputCommand { logger: any Logging ) throws -> [SwiftInterfaceFile] { let locator = SwiftInterfaceFileLocator(logger: logger) - + let oldSwiftInterfaceFileUrl = try locator.locate( for: targetName, derivedDataPath: oldPath, @@ -126,25 +126,25 @@ private extension FrameworkToOutputCommand { derivedDataPath: newPath, type: swiftInterfaceType ) - + return [.init( name: targetName, oldFilePath: oldSwiftInterfaceFileUrl.path(), newFilePath: newSwiftInterfaceFileUrl.path() )] } - + static func analyzeSwiftInterfaceFiles( swiftInterfaceFiles: [SwiftInterfaceFile], logger: any Logging ) async throws -> [String: [Change]] { let swiftInterfaceDiff = SwiftInterfaceDiff(logger: logger) - + return try await swiftInterfaceDiff.run( with: swiftInterfaceFiles ) } - + static func generateOutput( for changes: [String: [Change]], warnings: [String], @@ -153,7 +153,7 @@ private extension FrameworkToOutputCommand { newVersionName: String? ) throws -> String { let outputGenerator: any OutputGenerating = MarkdownOutputGenerator() - + return try outputGenerator.generate( from: changes, allTargets: allTargets, diff --git a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift index a2325ad..94179ca 100644 --- a/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/ProjectToOutputCommand.swift @@ -18,58 +18,58 @@ import PADSwiftInterfaceDiff /// Command that analyzes the differences between an old and new project and produces a human readable output struct ProjectToOutputCommand: AsyncParsableCommand { - + static var configuration: CommandConfiguration = .init(commandName: "project") - + /// The representation of the new/updated project source @Option(help: "Specify the updated version to compare to") public var new: String - + /// The representation of the old/reference project source @Option(help: "Specify the old version to compare to") public var old: String - + /// The (optional) scheme to build /// /// Needed when comparing 2 xcode projects @Option(help: "[Optional] Which scheme to build (Needed when comparing 2 xcode projects)") public var scheme: String? - + @Option(help: "[Optional] Specify the type of .swiftinterface you want to compare (public/private)") public var swiftInterfaceType: SwiftInterfaceType = .public - + /// The (optional) output file path /// /// If not defined the output will be printed to the console @Option(help: "[Optional] Where to output the result (File path)") public var output: String? - + /// The (optional) path to the log output file @Option(help: "[Optional] Where to output the logs (File path)") public var logOutput: String? - + @Option(help: "[Optional] The log level to use during execution") public var logLevel: LogLevel = .default - + /// Entry point of the command line tool public func run() async throws { - + let projectType: ProjectType = { if let scheme { return .xcodeProject(scheme: scheme) } return .swiftPackage }() - + let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput) - + do { var warnings = [String]() var projectChanges = [Change]() - + let oldSource: ProjectSource = try .from(old) let newSource: ProjectSource = try .from(new) - + // MARK: - Producing .swiftinterface files - + let projectBuilderResult = try await Self.buildProject( oldSource: oldSource, newSource: newSource, @@ -77,16 +77,16 @@ struct ProjectToOutputCommand: AsyncParsableCommand { swiftInterfaceType: swiftInterfaceType, logger: logger ) - + // MARK: - Analyzing .swiftinterface files - + let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles( swiftInterfaceFiles: projectBuilderResult.swiftInterfaceFiles, logger: logger ) - + // MARK: - Analyzing Package.swift - + try Self.analyzeProject( ofType: projectType, projectDirectories: projectBuilderResult.projectDirectories, @@ -94,16 +94,16 @@ struct ProjectToOutputCommand: AsyncParsableCommand { warnings: &warnings, logger: logger ) - + // MARK: - Merging Changes - + var changes = swiftInterfaceChanges if !projectChanges.isEmpty { changes["Package.swift"] = projectChanges } - + // MARK: - Generate Output - + let generatedOutput = try Self.generateOutput( for: changes, warnings: warnings, @@ -111,16 +111,16 @@ struct ProjectToOutputCommand: AsyncParsableCommand { oldVersionName: oldSource.title, newVersionName: newSource.title ) - + // MARK: - - + if let output { try FileManager.default.write(generatedOutput, to: output) } else { // We're not using a logger here as we always want to have it printed if no output was specified print(generatedOutput) } - + logger.log("βœ… Success", from: "Main") } catch { logger.log("πŸ’₯ \(error.localizedDescription)", from: "Main") @@ -131,7 +131,7 @@ struct ProjectToOutputCommand: AsyncParsableCommand { // MARK: - Privates private extension ProjectToOutputCommand { - + static func buildProject( oldSource: ProjectSource, newSource: ProjectSource, @@ -139,19 +139,19 @@ private extension ProjectToOutputCommand { swiftInterfaceType: SwiftInterfaceType, logger: any Logging ) async throws -> ProjectBuilder.Result { - + let projectBuilder = ProjectBuilder( projectType: projectType, swiftInterfaceType: swiftInterfaceType, logger: logger ) - + return try await projectBuilder.build( oldSource: oldSource, newSource: newSource ) } - + static func analyzeProject( ofType projectType: ProjectType, projectDirectories: (old: URL, new: URL), @@ -168,7 +168,7 @@ private extension ProjectToOutputCommand { oldProjectUrl: projectDirectories.old, newProjectUrl: projectDirectories.new ) - + warnings = swiftPackageAnalysis.warnings changes = swiftPackageAnalysis.changes case .xcodeProject: @@ -177,18 +177,18 @@ private extension ProjectToOutputCommand { // Nothing to do } } - + static func analyzeSwiftInterfaceFiles( swiftInterfaceFiles: [SwiftInterfaceFile], logger: any Logging ) async throws -> [String: [Change]] { let swiftInterfaceDiff = SwiftInterfaceDiff(logger: logger) - + return try await swiftInterfaceDiff.run( with: swiftInterfaceFiles ) } - + static func generateOutput( for changes: [String: [Change]], warnings: [String], @@ -197,7 +197,7 @@ private extension ProjectToOutputCommand { newVersionName: String ) throws -> String { let outputGenerator: any OutputGenerating = MarkdownOutputGenerator() - + return try outputGenerator.generate( from: changes, allTargets: allTargets, diff --git a/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift b/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift index fa63faa..154a499 100644 --- a/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift +++ b/Sources/ExecutableTargets/CommandLineTool/SwiftInterfaceToOutputCommand.swift @@ -15,57 +15,58 @@ import PADPackageFileAnalyzer import PADProjectBuilder import PADSwiftInterfaceDiff -/// Command that analyzes the differences between an old and new `.swiftinterface` file and produces a human readable output +/// Command that analyzes the differences between an old and new `.swiftinterface` +/// file and produces a human readable output struct SwiftInterfaceToOutputCommand: AsyncParsableCommand { - + static var configuration: CommandConfiguration = .init(commandName: "swift-interface") - + /// The path to the new/updated .swiftinterface file @Option(help: "Specify the updated .swiftinterface file to compare to") public var new: String - + /// The path to the old/reference .swiftinterface file @Option(help: "Specify the old .swiftinterface file to compare to") public var old: String - + /// The name of the target/module to show in the output @Option(help: "[Optional] The name of your target/module to show in the output") public var targetName: String? - + @Option(help: "[Optional] The name of your old version (e.g. v1.0 / main) to show in the output") public var oldVersionName: String? - + @Option(help: "[Optional] The name of your new version (e.g. v2.0 / develop) to show in the output") public var newVersionName: String? - + /// The (optional) output file path /// /// If not defined the output will be printed to the console @Option(help: "[Optional] Where to output the result (File path)") public var output: String? - + /// The (optional) path to the log output file @Option(help: "[Optional] Where to output the logs (File path)") public var logOutput: String? - + @Option(help: "[Optional] The log level to use during execution") public var logLevel: LogLevel = .default - + /// Entry point of the command line tool public func run() async throws { - + let logger = PublicApiDiff.logger(with: logLevel, logOutputFilePath: logOutput) - + do { // MARK: - Analyzing .swiftinterface files - + let swiftInterfaceChanges = try await Self.analyzeSwiftInterfaceFiles( swiftInterfaceFiles: [.init(name: targetName ?? "", oldFilePath: old, newFilePath: new)], logger: logger ) - + // MARK: - Generate Output - + let generatedOutput = try Self.generateOutput( for: swiftInterfaceChanges, warnings: [], @@ -73,16 +74,16 @@ struct SwiftInterfaceToOutputCommand: AsyncParsableCommand { oldVersionName: oldVersionName, newVersionName: newVersionName ) - + // MARK: - - + if let output { try FileManager.default.write(generatedOutput, to: output) } else { // We're not using a logger here as we always want to have it printed if no output was specified print(generatedOutput) } - + logger.log("βœ… Success", from: "Main") } catch { logger.log("πŸ’₯ \(error.localizedDescription)", from: "Main") @@ -91,18 +92,18 @@ struct SwiftInterfaceToOutputCommand: AsyncParsableCommand { } private extension SwiftInterfaceToOutputCommand { - + static func analyzeSwiftInterfaceFiles( swiftInterfaceFiles: [SwiftInterfaceFile], logger: any Logging ) async throws -> [String: [Change]] { let swiftInterfaceDiff = SwiftInterfaceDiff(logger: logger) - + return try await swiftInterfaceDiff.run( with: swiftInterfaceFiles ) } - + static func generateOutput( for changes: [String: [Change]], warnings: [String], @@ -111,7 +112,7 @@ private extension SwiftInterfaceToOutputCommand { newVersionName: String? ) throws -> String { let outputGenerator: any OutputGenerating = MarkdownOutputGenerator() - + return try outputGenerator.generate( from: changes, allTargets: allTargets, diff --git a/Sources/PublicModules/PADOutputGenerator/MarkdownOutputGenerator.swift b/Sources/PublicModules/PADOutputGenerator/MarkdownOutputGenerator.swift index c604dda..6909e18 100644 --- a/Sources/PublicModules/PADOutputGenerator/MarkdownOutputGenerator.swift +++ b/Sources/PublicModules/PADOutputGenerator/MarkdownOutputGenerator.swift @@ -9,9 +9,9 @@ import PADCore /// Allows generation of human readable output from the provided information public struct MarkdownOutputGenerator: OutputGenerating { - + public init() {} - + /// Generates human readable output from the provided information public func generate( from changesPerTarget: [String: [Change]], @@ -20,34 +20,34 @@ public struct MarkdownOutputGenerator: OutputGenerating { newVersionName: String?, warnings: [String] ) -> String { - + let separator = "\n---" let changes = Self.changeLines(changesPerModule: changesPerTarget) - + var lines = [ Self.title(changesPerTarget: changesPerTarget) ] - + if let oldVersionName, let newVersionName { lines += [Self.repoInfo(oldVersionName: oldVersionName, newVersionName: newVersionName)] } - + lines += [separator] - + if !warnings.isEmpty { lines += Self.warningInfo(for: warnings) + [separator] } - + if !changes.isEmpty { lines += changes + [separator] } - + if let allTargets { lines += [ Self.analyzedModulesInfo(allTargets: allTargets) ] } - + return lines.joined(separator: "\n") } } @@ -55,52 +55,52 @@ public struct MarkdownOutputGenerator: OutputGenerating { // MARK: - Privates private extension MarkdownOutputGenerator { - + static func title(changesPerTarget: [String: [Change]]) -> String { - + if changesPerTarget.keys.isEmpty { return "# βœ… No changes detected" } - + let totalChangeCount = changesPerTarget.totalChangeCount return "# πŸ‘€ \(totalChangeCount) public \(totalChangeCount == 1 ? "change" : "changes") detected" } - + static func repoInfo(oldVersionName: String, newVersionName: String) -> String { "_Comparing `\(newVersionName)` to `\(oldVersionName)`_" } - + static func analyzedModulesInfo(allTargets: [String]) -> String { "**Analyzed targets:** \(allTargets.joined(separator: ", "))" } - + static func warningInfo(for warnings: [String]) -> [String] { warnings.map { "> [!WARNING]\n> \($0)" } } - + static func changeLines(changesPerModule: [String: [Change]]) -> [String] { var lines = [String]() - + changesPerModule.keys.sorted().forEach { targetName in guard let changesForTarget = changesPerModule[targetName], !changesPerModule.isEmpty else { return } - + if !targetName.isEmpty { lines.append("## `\(targetName)`") } - + var groupedChanges = [String: [Change]]() - + changesForTarget.forEach { groupedChanges[$0.parentPath ?? ""] = (groupedChanges[$0.parentPath ?? ""] ?? []) + [$0] } - + groupedChanges.keys.sorted().forEach { parent in guard let changes = groupedChanges[parent], !changes.isEmpty else { return } - + if !parent.isEmpty { lines.append("### `\(parent)`") } - + let additionLines = changeSectionLines( title: "#### ❇️ Added", changes: changes.filter(\.changeType.isAddition) @@ -113,19 +113,19 @@ private extension MarkdownOutputGenerator { title: "#### πŸ˜Άβ€πŸŒ«οΈ Removed", changes: changes.filter(\.changeType.isRemoval) ) - + if !additionLines.isEmpty { lines += additionLines } if !changeLines.isEmpty { lines += changeLines } if !removalLines.isEmpty { lines += removalLines } } } - + return lines } } private extension MarkdownOutputGenerator { - + static func changeSectionLines(title: String, changes: [Change]) -> [String] { if changes.isEmpty { return [] } @@ -150,7 +150,7 @@ private extension MarkdownOutputGenerator { } return lines } - + static func description(for change: Change) -> String { switch change.changeType { case let .addition(description): @@ -164,7 +164,7 @@ private extension MarkdownOutputGenerator { } private extension [String: [Change]] { - + var totalChangeCount: Int { var totalChangeCount = 0 keys.forEach { targetName in diff --git a/Sources/PublicModules/PADOutputGenerator/OutputGenerating.swift b/Sources/PublicModules/PADOutputGenerator/OutputGenerating.swift index baf9e8c..2106f1e 100644 --- a/Sources/PublicModules/PADOutputGenerator/OutputGenerating.swift +++ b/Sources/PublicModules/PADOutputGenerator/OutputGenerating.swift @@ -9,9 +9,9 @@ import PADCore /// Interface definition for an output generator public protocol OutputGenerating { - + associatedtype OutputType - + /// Generates an output from input parameters /// - Parameters: /// - changesPerTarget: A list of changes per target/module diff --git a/Sources/PublicModules/PADPackageFileAnalyzer/SwiftPackageFileAnalyzer.swift b/Sources/PublicModules/PADPackageFileAnalyzer/SwiftPackageFileAnalyzer.swift index 020bd86..9d69162 100644 --- a/Sources/PublicModules/PADPackageFileAnalyzer/SwiftPackageFileAnalyzer.swift +++ b/Sources/PublicModules/PADPackageFileAnalyzer/SwiftPackageFileAnalyzer.swift @@ -15,11 +15,11 @@ import SwiftPackageFileHelperModule /// Analyzes 2 versions of a `Package.swift` public struct SwiftPackageFileAnalyzer: SwiftPackageFileAnalyzing { - + private let fileHandler: any FileHandling private let shell: any ShellHandling private let logger: (any Logging)? - + private enum Constants { static let packageFileName = "Package.swift" static func packageFileName(child: String) -> String { @@ -34,7 +34,7 @@ public struct SwiftPackageFileAnalyzer: SwiftPackageFileAnalyzing { logger: logger ) } - + package init( fileHandler: FileHandling = FileManager.default, shell: ShellHandling = Shell(), @@ -44,25 +44,25 @@ public struct SwiftPackageFileAnalyzer: SwiftPackageFileAnalyzing { self.logger = logger self.shell = shell } - + public func analyze( oldProjectUrl: URL, newProjectUrl: URL ) throws -> SwiftPackageFileAnalyzingResult { - + let oldProjectPath = oldProjectUrl.path() let newProjectPath = newProjectUrl.path() - + let oldPackagePath = SwiftPackageFileHelper.packagePath(for: oldProjectPath) let newPackagePath = SwiftPackageFileHelper.packagePath(for: newProjectPath) - + if fileHandler.fileExists(atPath: oldPackagePath), fileHandler.fileExists(atPath: newPackagePath) { let packageHelper = SwiftPackageFileHelper( fileHandler: fileHandler, shell: shell, logger: logger ) - + return try analyze( old: packageHelper.packageDescription(at: oldProjectPath), new: packageHelper.packageDescription(at: newProjectPath) @@ -74,55 +74,55 @@ public struct SwiftPackageFileAnalyzer: SwiftPackageFileAnalyzing { } private extension SwiftPackageFileAnalyzer { - + /// Compiles all changes between 2 `SwiftPackageDescription`s private func analyze( old: SwiftPackageDescription, new: SwiftPackageDescription ) throws -> SwiftPackageFileAnalyzingResult { - + guard old != new else { return .init(changes: [], warnings: []) } - + var changes = [Change]() changes += try analyzeToolsVersion(old: old.toolsVersion, new: new.toolsVersion) - + changes += try analyzeDefaultLocalization(old: old.defaultLocalization, new: new.defaultLocalization) changes += try analyzeName(old: old.name, new: new.name) - + changes += try analyzePlatforms(old: old.platforms, new: new.platforms) changes += try analyzeProducts(old: old.products, new: new.products) changes += try analyzeTargets(old: old.targets, new: new.targets) changes += try analyzeDependencies(old: old.dependencies, new: new.dependencies) - + return .init(changes: changes, warnings: new.warnings) } - + // MARK: - Default Localization - + private func analyzeDefaultLocalization( old: String?, new: String? ) throws -> [Change] { guard old != new else { return [] } - + let keyName = "defaultLocalization" - + if let old, new == nil { return [.init( changeType: .removal(description: "\(keyName): \"\(old)\""), parentPath: Constants.packageFileName )] } - + if let new, old == nil { return [.init( changeType: .addition(description: "\(keyName): \"\(new)\""), parentPath: Constants.packageFileName )] } - + guard let new, let old else { return [] } - + return [.init( changeType: .change( oldDescription: "\(keyName): \"\(old)\"", @@ -131,17 +131,17 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName )] } - + // MARK: - Name - + private func analyzeName( old: String, new: String ) throws -> [Change] { guard old != new else { return [] } - + let keyName = "name" - + return [.init( changeType: .change( oldDescription: "\(keyName): \"\(old)\"", @@ -150,46 +150,46 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName )] } - + // MARK: - Platforms - + private func analyzePlatforms( old: [SwiftPackageDescription.Platform], new: [SwiftPackageDescription.Platform] ) throws -> [Change] { guard old != new else { return [] } - + let oldPlatformNames = Set(old.map(\.name)) let newPlatformNames = Set(new.map(\.name)) - + var listOfChanges = [String]() - + let added = newPlatformNames.subtracting(oldPlatformNames) let removed = oldPlatformNames.subtracting(newPlatformNames) let consistent = oldPlatformNames.intersection(newPlatformNames) - + listOfChanges += added.compactMap { platformName in guard let addedPlatform = new.first(where: { $0.name == platformName }) else { return nil } return "Added \(addedPlatform.description)" } - + listOfChanges += consistent.compactMap { platformName in guard let newPlatform = new.first(where: { $0.name == platformName }), let oldPlatform = old.first(where: { $0.name == platformName }) else { return nil } - + return "Changed from \(oldPlatform.description) to \(newPlatform.description)" } - + listOfChanges += removed.compactMap { platformName in guard let removedPlatform = old.first(where: { $0.name == platformName }) else { return nil } return "Removed \(removedPlatform.description)" } - + let oldPlatformsString = old.map { "\($0.description)" }.joined(separator: ", ") let newPlatformsString = new.map { "\($0.description)" }.joined(separator: ", ") - + return [.init( changeType: .change( oldDescription: "platforms: [\(oldPlatformsString)]", @@ -199,24 +199,24 @@ private extension SwiftPackageFileAnalyzer { listOfChanges: listOfChanges )] } - + // MARK: - Products - + private func analyzeProducts( old: [SwiftPackageDescription.Product], new: [SwiftPackageDescription.Product] ) throws -> [Change] { guard old != new else { return [] } - + let oldProductNames = Set(old.map(\.name)).filter { $0 != "_AllTargets" } let newProductNames = Set(new.map(\.name)).filter { $0 != "_AllTargets" } - + let added = newProductNames.subtracting(oldProductNames) let removed = oldProductNames.subtracting(newProductNames) let consistent = Set(oldProductNames).intersection(Set(newProductNames)) - + var changes = [Change]() - + changes += added.compactMap { addition in guard let addedProduct = new.first(where: { $0.name == addition }) else { return nil } return .init( @@ -224,19 +224,19 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "products") ) } - + try consistent.forEach { productName in guard let oldProduct = old.first(where: { $0.name == productName }), let newProduct = new.first(where: { $0.name == productName }) else { return } - + changes += try analyzeProduct( old: oldProduct, new: newProduct ) } - + changes += removed.compactMap { removal in guard let removedProduct = old.first(where: { $0.name == removal }) else { return nil } return .init( @@ -244,26 +244,26 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "products") ) } - + return changes } - + private func analyzeProduct( old oldProduct: SwiftPackageDescription.Product, new newProduct: SwiftPackageDescription.Product ) throws -> [Change] { guard oldProduct != newProduct else { return [] } - + let oldTargetNames = Set(oldProduct.targets) let newTargetNames = Set(newProduct.targets) - + let added = newTargetNames.subtracting(oldTargetNames) let removed = oldTargetNames.subtracting(newTargetNames) - + var listOfChanges = [String]() listOfChanges += added.map { "Added target \"\($0)\"" } listOfChanges += removed.map { "Removed target \"\($0)\"" } - + return [.init( changeType: .change( oldDescription: oldProduct.description, @@ -273,24 +273,24 @@ private extension SwiftPackageFileAnalyzer { listOfChanges: listOfChanges )] } - + // MARK: - Targets - + private func analyzeTargets( old: [SwiftPackageDescription.Target], new: [SwiftPackageDescription.Target] ) throws -> [Change] { guard old != new else { return [] } - + let oldTargetNames = Set(old.map(\.name)) let newTargetNames = Set(new.map(\.name)) - + let added = newTargetNames.subtracting(oldTargetNames) let removed = oldTargetNames.subtracting(newTargetNames) let consistent = Set(oldTargetNames).intersection(Set(newTargetNames)) - + var changes = [Change]() - + changes += added.compactMap { addition in guard let addedTarget = new.first(where: { $0.name == addition }) else { return nil } return .init( @@ -298,19 +298,19 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "targets") ) } - + try consistent.forEach { productName in guard let oldTarget = old.first(where: { $0.name == productName }), let newTarget = new.first(where: { $0.name == productName }) else { return } - + changes += try analyzeTarget( oldTarget: oldTarget, newTarget: newTarget ) } - + changes += removed.compactMap { removal in guard let removedTarget = old.first(where: { $0.name == removal }) else { return nil } return .init( @@ -318,47 +318,47 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "targets") ) } - + return changes } - + private func analyzeTarget( oldTarget: SwiftPackageDescription.Target, newTarget: SwiftPackageDescription.Target ) throws -> [Change] { guard oldTarget != newTarget else { return [] } - + // MARK: Target Dependencies - + let oldTargetDependencies = Set(oldTarget.targetDependencies ?? []) let newTargetDependencies = Set(newTarget.targetDependencies ?? []) - + let addedTargetDependencies = newTargetDependencies.subtracting(oldTargetDependencies) let removedTargetDependencies = oldTargetDependencies.subtracting(newTargetDependencies) - + // MARK: Product Dependencies - + let oldProductDependencies = Set(oldTarget.productDependencies ?? []) let newProductDependencies = Set(newTarget.productDependencies ?? []) - + let addedProductDependencies = newProductDependencies.subtracting(oldProductDependencies) let removedProductDependencies = oldProductDependencies.subtracting(newProductDependencies) - + var listOfChanges = [String]() listOfChanges += addedTargetDependencies.map { "Added dependency .target(name: \"\($0)\")" } listOfChanges += addedProductDependencies.map { "Added dependency .product(name: \"\($0)\", ...)" } - + if oldTarget.path != newTarget.path { listOfChanges += ["Changed path from \"\(oldTarget.path)\" to \"\(newTarget.path)\""] } - + if oldTarget.type != newTarget.type { listOfChanges += ["Changed type from `.\(oldTarget.type.description)` to `.\(newTarget.type.description)`"] } - + listOfChanges += removedTargetDependencies.map { "Removed dependency .target(name: \"\($0)\")" } listOfChanges += removedProductDependencies.map { "Removed dependency .product(name: \"\($0)\", ...)" } - + return [.init( changeType: .change( oldDescription: oldTarget.description, @@ -367,26 +367,26 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "targets"), listOfChanges: listOfChanges )] - + } - + // MARK: - Dependencies - + private func analyzeDependencies( old: [SwiftPackageDescription.Dependency], new: [SwiftPackageDescription.Dependency] ) throws -> [Change] { guard old != new else { return [] } - + let oldDependencies = Set(old.map(\.identity)) let newDependencies = Set(new.map(\.identity)) - + let addedDependencies = newDependencies.subtracting(oldDependencies) let removedDependencies = oldDependencies.subtracting(newDependencies) let consistentDependencies = oldDependencies.intersection(newDependencies) - + var changes = [Change]() - + changes += addedDependencies.compactMap { addition in guard let addedDependency = new.first(where: { $0.identity == addition }) else { return nil } return .init( @@ -394,19 +394,19 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "dependencies") ) } - + try consistentDependencies.forEach { dependencyIdentity in guard let oldDependency = old.first(where: { $0.identity == dependencyIdentity }), let newDependency = new.first(where: { $0.identity == dependencyIdentity }) else { return } - + changes += try analyzeDependency( oldDependency: oldDependency, newDependency: newDependency ) } - + changes += removedDependencies.compactMap { addition in guard let removedDependency = old.first(where: { $0.identity == addition }) else { return nil } return .init( @@ -414,16 +414,16 @@ private extension SwiftPackageFileAnalyzer { parentPath: Constants.packageFileName(child: "dependencies") ) } - + return changes } - + private func analyzeDependency( oldDependency: SwiftPackageDescription.Dependency, newDependency: SwiftPackageDescription.Dependency ) throws -> [Change] { guard oldDependency != newDependency else { return [] } - + return [.init( changeType: .change( oldDescription: oldDependency.description, @@ -433,15 +433,15 @@ private extension SwiftPackageFileAnalyzer { listOfChanges: [] // TODO: Improvement: Provide a `listOfChanges` )] } - + // MARK: - Tools Version - + private func analyzeToolsVersion( old: String, new: String ) throws -> [Change] { guard old != new else { return [] } - + return [.init( changeType: .change( oldDescription: "// swift-tools-version: \(old)", diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift b/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift index be3a8bb..7381ab6 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectBuilder.swift @@ -23,7 +23,7 @@ import ShellModule /// - Inspecting `Package.swift` for any changes between versions (if applicable / if ``ProjectType/swiftPackage``) /// - Returning a ``PADProjectBuilder/ProjectBuilder/Result`` containing package file changes, warnings + the found ``PADCore/SwiftInterfaceFile``s public struct ProjectBuilder { - + /// The result returned by the build function of ``PADProjectBuilder/ProjectBuilder`` public struct Result { /// The `.swiftinterface` file references found @@ -31,13 +31,13 @@ public struct ProjectBuilder { /// The project directories for the setup projects public let projectDirectories: (old: URL, new: URL) } - + private let projectType: ProjectType private let swiftInterfaceType: SwiftInterfaceType private let fileHandler: any FileHandling private let shell: any ShellHandling private let logger: (any Logging)? - + public init( projectType: ProjectType, swiftInterfaceType: SwiftInterfaceType, @@ -51,7 +51,7 @@ public struct ProjectBuilder { logger: logger ) } - + init( projectType: ProjectType, swiftInterfaceType: SwiftInterfaceType, @@ -65,37 +65,37 @@ public struct ProjectBuilder { self.shell = shell self.logger = logger } - + public func build( oldSource: ProjectSource, newSource: ProjectSource ) async throws -> Result { - + let oldVersionName = oldSource.description let newVersionName = newSource.description - + logger?.log("Comparing `\(newVersionName)` to `\(oldVersionName)`", from: "Main") - + let currentDirectory = fileHandler.currentDirectoryPath let workingDirectoryPath = currentDirectory.appending("/tmp-public-api-diff") - + // MARK: - Setup projects - + let projectSetupHelper = ProjectSetupHelper( workingDirectoryPath: workingDirectoryPath, shell: shell, fileHandler: fileHandler, logger: logger ) - + let projectDirectories = try await projectSetupHelper.setupProjects( oldSource: oldSource, newSource: newSource, projectType: projectType ) - + // MARK: - Produce .swiftinterface files - + let producer = SwiftInterfaceProducer( workingDirectoryPath: workingDirectoryPath, projectType: projectType, @@ -104,12 +104,12 @@ public struct ProjectBuilder { shell: shell, logger: logger ) - + let swiftInterfaceFiles = try await producer.produceInterfaceFiles( oldProjectDirectory: projectDirectories.old, newProjectDirectory: projectDirectories.new ) - + return .init( swiftInterfaceFiles: swiftInterfaceFiles, projectDirectories: projectDirectories diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift index 310273e..be8122b 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/Git.swift @@ -14,7 +14,7 @@ import ShellModule internal enum GitError: LocalizedError, Equatable { case couldNotClone(branchOrTag: String, repository: String) - + var errorDescription: String? { switch self { case let .couldNotClone(branchOrTag, repository): @@ -24,11 +24,11 @@ internal enum GitError: LocalizedError, Equatable { } internal struct Git { - + private let shell: ShellHandling private let fileHandler: FileHandling private let logger: Logging? - + init( shell: ShellHandling, fileHandler: FileHandling, @@ -38,7 +38,7 @@ internal struct Git { self.fileHandler = fileHandler self.logger = logger } - + /// Clones a repository at a specific branch or tag into the current directory /// /// - Parameters: @@ -51,7 +51,7 @@ internal struct Git { logger?.log("🐱 Cloning \(repository) @ \(branchOrTag) into \(targetDirectoryPath)", from: String(describing: Self.self)) let command = "git clone -b \(branchOrTag) \(repository) \(targetDirectoryPath)" shell.execute(command) - + guard fileHandler.fileExists(atPath: targetDirectoryPath) else { throw GitError.couldNotClone(branchOrTag: branchOrTag, repository: repository) } diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/ProjectSetupHelper.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/ProjectSetupHelper.swift index d9ed69a..df8c9b0 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/ProjectSetupHelper.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/ProjectSetupHelper.swift @@ -16,13 +16,13 @@ import ShellModule /// - Copying project files into a working directory (and skipping unwanted files) /// - Fetching a remote project (if applicable) struct ProjectSetupHelper: ProjectSetupHelping { - + let workingDirectoryPath: String let shell: any ShellHandling let randomStringGenerator: any RandomStringGenerating let fileHandler: any FileHandling let logger: (any Logging)? - + init( workingDirectoryPath: String, randomStringGenerator: any RandomStringGenerating = RandomStringGenerator(), @@ -36,7 +36,7 @@ struct ProjectSetupHelper: ProjectSetupHelping { self.fileHandler = fileHandler self.logger = logger } - + func setup( _ projectSource: ProjectSource, projectType: ProjectType @@ -50,12 +50,12 @@ struct ProjectSetupHelper: ProjectSetupHelping { let git = Git(shell: shell, fileHandler: fileHandler, logger: logger) try git.clone(repository, at: branch, targetDirectoryPath: checkoutPath) } - + filterProjectFiles(at: checkoutPath, for: projectType) return URL(filePath: checkoutPath) }.value } - + func filterProjectFiles(at checkoutPath: String, for projectType: ProjectType) { try? fileHandler.contentsOfDirectory(atPath: checkoutPath) .filter { !projectType.fileIsIncluded(filePath: $0) } @@ -66,7 +66,7 @@ struct ProjectSetupHelper: ProjectSetupHelping { } extension ProjectSetupHelper { - + /// Convenience method that calls into `setup(_:projectType:)` for the old and new source func setupProjects( oldSource: ProjectSource, @@ -77,17 +77,17 @@ extension ProjectSetupHelper { workingDirectoryPath: workingDirectoryPath, logger: logger ) - + // async let to make them perform in parallel async let newProjectDirectoryPath = try projectSetupHelper.setup(newSource, projectType: projectType) async let oldProjectDirectoryPath = try projectSetupHelper.setup(oldSource, projectType: projectType) - + return try await (oldProjectDirectoryPath, newProjectDirectoryPath) } } private extension ProjectType { - + var excludedFileSuffixes: [String] { switch self { case .swiftPackage: @@ -96,7 +96,7 @@ private extension ProjectType { ["Package.swift"] } } - + func fileIsIncluded(filePath: String) -> Bool { for excludedFileSuffix in excludedFileSuffixes { if filePath.hasSuffix(excludedFileSuffix) { return false } diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/RandomStringGenerating.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/RandomStringGenerating.swift index 81dc358..c28e925 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSetup/RandomStringGenerating.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSetup/RandomStringGenerating.swift @@ -7,14 +7,14 @@ import Foundation package protocol RandomStringGenerating { - + func generateRandomString() -> String } package struct RandomStringGenerator: RandomStringGenerating { - + package init() {} - + package func generateRandomString() -> String { UUID().uuidString } diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSource+Error.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSource+Error.swift index 857b997..0475609 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSource+Error.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSource+Error.swift @@ -9,7 +9,7 @@ import Foundation internal extension ProjectSource { enum Error: LocalizedError, Equatable { case invalidSourceValue(value: String) - + var errorDescription: String? { switch self { case let .invalidSourceValue(value): diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectSource.swift b/Sources/PublicModules/PADProjectBuilder/ProjectSource.swift index 7c472ad..80ca44f 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectSource.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectSource.swift @@ -9,15 +9,15 @@ import Foundation /// The source type of the project (local/remote) public enum ProjectSource: Equatable, CustomStringConvertible { - + /// The separator used to join branch & repository static var gitSourceSeparator: String { "~" } - + /// Representing a local `path` case local(path: String) /// Representing a `branch` of a **git** `repository` case git(branch: String, repository: String) - + /// Creates a ``ProjectSource`` from a rawValue /// - Parameters: /// - rawValue: The rawValue presentation of a ``ProjectSource`` @@ -26,20 +26,20 @@ public enum ProjectSource: Equatable, CustomStringConvertible { public static func from(_ rawValue: String) throws -> Self { try from(rawValue, fileHandler: FileManager.default) } - + package static func from(_ rawValue: String, fileHandler: FileHandling) throws -> Self { if fileHandler.fileExists(atPath: rawValue) { return .local(path: rawValue) } - + let remoteComponents = rawValue.components(separatedBy: gitSourceSeparator) if remoteComponents.count == 2, let branch = remoteComponents.first, let repository = remoteComponents.last, URL(string: repository) != nil { return .git(branch: branch, repository: repository) } - + throw Error.invalidSourceValue(value: rawValue) } - + public var description: String { switch self { case let .local(path): @@ -48,7 +48,7 @@ public enum ProjectSource: Equatable, CustomStringConvertible { return "\(repository) @ \(branch)" } } - + public var title: String { switch self { case let .local(path): diff --git a/Sources/PublicModules/PADProjectBuilder/ProjectType.swift b/Sources/PublicModules/PADProjectBuilder/ProjectType.swift index 9af1d5b..e92d009 100644 --- a/Sources/PublicModules/PADProjectBuilder/ProjectType.swift +++ b/Sources/PublicModules/PADProjectBuilder/ProjectType.swift @@ -8,11 +8,11 @@ import Foundation /// The type of project to build public enum ProjectType { - + /// The project is a `Package.swift` /// When using this project type all targets get built case swiftPackage - + /// The project is an Xcode project/workspace /// When using this project type the specified scheme get built case xcodeProject(scheme: String) diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer+Error.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer+Error.swift index fa9a066..72f0ede 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer+Error.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer+Error.swift @@ -9,7 +9,7 @@ import Foundation extension SwiftInterfaceProducer { enum Error: LocalizedError { case noTargetFound - + var errorDescription: String? { switch self { case .noTargetFound: "No targets found to analyze" diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift index 15e28bc..43c0511 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/SwiftInterfaceProducer.swift @@ -16,53 +16,37 @@ import SwiftPackageFileHelperModule /// Allows building of the old & new project and returns the `.swiftinterface` files struct SwiftInterfaceProducer { - + private typealias ProjectPreparationResult = (archiveScheme: String, schemesToCompare: [String]) private typealias DerivedDataPaths = (new: String, old: String) - + let workingDirectoryPath: String let projectType: ProjectType let swiftInterfaceType: SwiftInterfaceType let fileHandler: any FileHandling let shell: any ShellHandling let logger: (any Logging)? - - init( - workingDirectoryPath: String, - projectType: ProjectType, - swiftInterfaceType: SwiftInterfaceType, - fileHandler: any FileHandling, - shell: any ShellHandling, - logger: (any Logging)? - ) { - self.workingDirectoryPath = workingDirectoryPath - self.projectType = projectType - self.swiftInterfaceType = swiftInterfaceType - self.fileHandler = fileHandler - self.shell = shell - self.logger = logger - } - + /// Builds the projects and returns the `.swiftinterface` files func produceInterfaceFiles( oldProjectDirectory: URL, newProjectDirectory: URL ) async throws -> [SwiftInterfaceFile] { - + let newProjectDirectoryPath = newProjectDirectory.path() let oldProjectDirectoryPath = oldProjectDirectory.path() - + let projectPreparationResult = try prepareProjectsForArchiving( newProjectDirectoryPath: newProjectDirectoryPath, oldProjectDirectoryPath: oldProjectDirectoryPath ) - + let derivedDataPaths = try await archiveProjects( newProjectDirectoryPath: newProjectDirectoryPath, oldProjectDirectoryPath: oldProjectDirectoryPath, scheme: projectPreparationResult.archiveScheme ) - + return try locateInterfaceFiles( newDerivedDataPath: derivedDataPaths.new, oldDerivedDataPath: derivedDataPaths.old, @@ -72,7 +56,7 @@ struct SwiftInterfaceProducer { } extension SwiftInterfaceProducer { - + /// Prepares the projects for archiving considering the project type /// /// - .swiftPackage @@ -88,7 +72,7 @@ extension SwiftInterfaceProducer { ) throws -> ProjectPreparationResult { let archiveScheme: String let schemesToCompare: [String] - + switch projectType { case .swiftPackage: archiveScheme = "_AllTargets" @@ -97,24 +81,24 @@ extension SwiftInterfaceProducer { .preparePackageWithConsolidatedLibrary(named: archiveScheme, at: newProjectDirectoryPath) try packageFileHelper .preparePackageWithConsolidatedLibrary(named: archiveScheme, at: oldProjectDirectoryPath) - + let newTargets = try Set(packageFileHelper.availableTargets(at: newProjectDirectoryPath)) let oldTargets = try Set(packageFileHelper.availableTargets(at: oldProjectDirectoryPath)) - + schemesToCompare = newTargets.intersection(oldTargets).sorted() - + if schemesToCompare.isEmpty { throw Error.noTargetFound } - + case let .xcodeProject(scheme): archiveScheme = scheme schemesToCompare = [scheme] } - + return (archiveScheme, schemesToCompare) } - + /// Archives the projects to produce `.swiftinterface` files /// - Parameters: /// - newProjectDirectoryPath: The path to the "new" project directory @@ -126,15 +110,15 @@ extension SwiftInterfaceProducer { oldProjectDirectoryPath: String, scheme: String ) async throws -> DerivedDataPaths { - + // We don't run them in parallel to not conflict with resolving dependencies concurrently - + let xcodeTools = XcodeTools( shell: shell, fileHandler: fileHandler, logger: logger ) - + let newDerivedDataPath = try await xcodeTools.archive( projectDirectoryPath: newProjectDirectoryPath, scheme: scheme, @@ -145,10 +129,10 @@ extension SwiftInterfaceProducer { scheme: scheme, projectType: projectType ) - + return (newDerivedDataPath, oldDerivedDataPath) } - + /// Locates the `.swiftinterface` files for the provided schemes within the derived data directories /// - Parameters: /// - newDerivedDataPath: The "new" derived data directory path @@ -161,7 +145,7 @@ extension SwiftInterfaceProducer { schemes schemesToCompare: [String] ) throws -> [SwiftInterfaceFile] { logger?.log("πŸ”Ž Locating interface files for \(schemesToCompare.joined(separator: ", "))", from: String(describing: Self.self)) - + let interfaceFileLocator = SwiftInterfaceFileLocator(fileHandler: fileHandler, shell: shell, logger: logger) return schemesToCompare.compactMap { scheme in do { diff --git a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift index e170fe2..005ecf7 100644 --- a/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift +++ b/Sources/PublicModules/PADProjectBuilder/SwiftInterfaceProducer/XcodeTools.swift @@ -14,22 +14,22 @@ import ShellModule struct XcodeToolsError: LocalizedError, CustomDebugStringConvertible { var errorDescription: String var underlyingError: String - + var debugDescription: String { errorDescription } } /// A helper that provides tools to build a project struct XcodeTools { - + internal enum Constants { static let derivedDataPath: String = ".build" static let simulatorSdkCommand = "xcrun --sdk iphonesimulator --show-sdk-path" } - + private let shell: ShellHandling private let fileHandler: FileHandling private let logger: Logging? - + init( shell: ShellHandling = Shell(), fileHandler: FileHandling = FileManager.default, @@ -39,7 +39,7 @@ struct XcodeTools { self.fileHandler = fileHandler self.logger = logger } - + /// Archives a project at the specified path / scheme by building for library evolution /// - Parameters: /// - projectDirectoryPath: The path to the project root directory @@ -59,37 +59,37 @@ struct XcodeTools { "-sdk `\(Constants.simulatorSdkCommand)`", "BUILD_LIBRARY_FOR_DISTRIBUTION=YES" ] - + switch projectType { case .swiftPackage: commandComponents += ["-skipPackagePluginValidation"] case .xcodeProject: break // Nothing to add } - + let command = commandComponents.joined(separator: " ") - + return try await Task { logger?.log("πŸ“¦ Archiving \(scheme) from \(projectDirectoryPath)", from: String(describing: Self.self)) - + let result = shell.execute(command) let derivedDataPath = "\(projectDirectoryPath)/\(Constants.derivedDataPath)" - + logger?.debug(result, from: String(describing: Self.self)) - + // It might be that the archive failed but the .swiftinterface files are still created // so we have to check outside if they exist. // // Also see: https://github.com/swiftlang/swift/issues/56573 guard fileHandler.fileExists(atPath: derivedDataPath) else { print(result) - + throw XcodeToolsError( errorDescription: "πŸ’₯ Building project failed", underlyingError: result ) } - + return derivedDataPath }.value } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/IndependentSwiftInterfaceChange.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/IndependentSwiftInterfaceChange.swift index 3d9f589..9589970 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/IndependentSwiftInterfaceChange.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/IndependentSwiftInterfaceChange.swift @@ -12,7 +12,7 @@ import PADCore /// This intermediate structure helps gathering a list of additions and removals /// that are later consolidated to a ``Change`` struct IndependentSwiftInterfaceChange: Equatable { - + enum ChangeType: Equatable { case addition(_ description: String) case removal(_ description: String) @@ -30,14 +30,14 @@ struct IndependentSwiftInterfaceChange: Equatable { let oldFirst: Bool var parentPath: String? { element.parentPath } - + static func == (lhs: IndependentSwiftInterfaceChange, rhs: IndependentSwiftInterfaceChange) -> Bool { lhs.changeType == rhs.changeType && lhs.element.description == rhs.element.description && lhs.oldFirst == rhs.oldFirst && lhs.parentPath == rhs.parentPath } - + func differences(to otherIndependentChange: IndependentSwiftInterfaceChange) -> [String] { element.differences(to: otherIndependentChange.element).sorted() } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceAnalyzer.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceAnalyzer.swift index 4a6a67f..8cf9481 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceAnalyzer.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceAnalyzer.swift @@ -8,21 +8,21 @@ import Foundation import PADCore struct SwiftInterfaceAnalyzer: SwiftInterfaceAnalyzing { - + let changeConsolidator: SwiftInterfaceChangeConsolidating - + init(changeConsolidator: SwiftInterfaceChangeConsolidating = SwiftInterfaceChangeConsolidator()) { self.changeConsolidator = changeConsolidator } - + func analyze( old: some SwiftInterfaceElement, new: some SwiftInterfaceElement ) -> [Change] { - + // Very naive diff from both sides // There is room for improvement here but it's "performant enough" for now - + let individualChanges = Self.recursiveCompare( element: old, to: new, @@ -34,30 +34,30 @@ struct SwiftInterfaceAnalyzer: SwiftInterfaceAnalyzing { oldFirst: false, isRoot: true ) - + // Matching removals/additions to changes when applicable return changeConsolidator.consolidate(individualChanges) } - + private static func recursiveCompare( element lhs: some SwiftInterfaceElement, to rhs: some SwiftInterfaceElement, oldFirst: Bool, isRoot: Bool = false ) -> [IndependentSwiftInterfaceChange] { - + var changes = [IndependentSwiftInterfaceChange]() - + if lhs.recursiveDescription() == rhs.recursiveDescription() { return changes } - + if !isRoot, oldFirst, lhs.description != rhs.description { changes += independentChanges(from: lhs, and: rhs, oldFirst: oldFirst) } - + changes += lhs.children.flatMap { lhsElement in - + // Trying to find a matching element - + // First checking if we found an exact match based on the recursive description // as we don't want to match a non-change with a change // @@ -66,19 +66,19 @@ struct SwiftInterfaceAnalyzer: SwiftInterfaceAnalyzing { if rhs.children.first(where: { $0.recursiveDescription() == lhsElement.recursiveDescription() }) != nil { return [IndependentSwiftInterfaceChange]() } - + // First checking if we found a match based on the description if let descriptionMatch = rhs.children.first(where: { $0.description == lhsElement.description }) { // so we check if the children changed return Self.recursiveCompare(element: lhsElement, to: descriptionMatch, oldFirst: oldFirst) } - + // ... then losening the criteria to find a comparable element if let rhsChildForName = rhs.children.first(where: { $0.isDiffable(with: lhsElement) }) { // We found a comparable element so we check if the children changed return Self.recursiveCompare(element: lhsElement, to: rhsChildForName, oldFirst: oldFirst) } - + // No matching element was found so either it was removed or added let changeType: IndependentSwiftInterfaceChange.ChangeType = oldFirst ? .removal(lhsElement.description) : @@ -92,10 +92,10 @@ struct SwiftInterfaceAnalyzer: SwiftInterfaceAnalyzing { ) ] } - + return changes } - + private static func independentChanges( from lhs: any SwiftInterfaceElement, and rhs: any SwiftInterfaceElement, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceChangeConsolidator.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceChangeConsolidator.swift index 9fd3685..439fab7 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceChangeConsolidator.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceAnalyzer/SwiftInterfaceChangeConsolidator.swift @@ -9,7 +9,7 @@ import PADCore /// A helper to consolidate a `removal` and `addition` to `change` protocol SwiftInterfaceChangeConsolidating { - + /// Tries to match a `removal` and `addition` to a `change` /// /// - Parameters: @@ -18,7 +18,7 @@ protocol SwiftInterfaceChangeConsolidating { } struct SwiftInterfaceChangeConsolidator: SwiftInterfaceChangeConsolidating { - + /// Tries to match a `removal` and `addition` to a `change` /// /// - Parameters: @@ -50,12 +50,12 @@ struct SwiftInterfaceChangeConsolidator: SwiftInterfaceChangeConsolidating { let oldDescription = change.oldFirst ? change.element.description : match.element.description let newDescription = change.oldFirst ? match.element.description : change.element.description let listOfChanges = listOfChanges(between: change, and: match) - + if listOfChanges.isEmpty { assertionFailure("We should not end up here - investigate how this happened") break } - + consolidatedChanges.append( .init( changeType: .change( diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceDiff.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceDiff.swift index a3552d4..2bdc70a 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceDiff.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceDiff.swift @@ -12,14 +12,14 @@ import PADLogging /// Takes a list of ``PADCore/SwiftInterfaceFile``s and detects changes between the old and new version public struct SwiftInterfaceDiff { - + public typealias ModuleName = String - + let fileHandler: any FileHandling let swiftInterfaceParser: any SwiftInterfaceParsing let swiftInterfaceAnalyzer: any SwiftInterfaceAnalyzing let logger: (any Logging)? - + /// Creates a new instance of ``SwiftInterfaceDiff`` /// - Parameter logger: The (optional) logger public init( @@ -32,7 +32,7 @@ public struct SwiftInterfaceDiff { logger: logger ) } - + init( fileHandler: FileHandling = FileManager.default, swiftInterfaceParser: any SwiftInterfaceParsing = SwiftInterfaceParser(), @@ -44,31 +44,31 @@ public struct SwiftInterfaceDiff { self.swiftInterfaceAnalyzer = swiftInterfaceAnalyzer self.logger = logger } - + /// Analyzes the passed ``PADCore/SwiftInterfaceFile``s and returns a list of changes grouped by scheme/target /// - Parameter swiftInterfaceFiles: The ``PADCore/SwiftInterfaceFile``s to analyze /// - Returns: A list of changes grouped by scheme/target public func run(with swiftInterfaceFiles: [SwiftInterfaceFile]) async throws -> [ModuleName: [Change]] { - + var changes = [String: [Change]]() - + try swiftInterfaceFiles.forEach { file in logger?.log("πŸ§‘β€πŸ”¬ Analyzing \(file.name)", from: String(describing: Self.self)) let newContent = try fileHandler.loadString(from: file.newFilePath) let oldContent = try fileHandler.loadString(from: file.oldFilePath) let newParsed = swiftInterfaceParser.parse(source: newContent, moduleName: file.name) let oldParsed = swiftInterfaceParser.parse(source: oldContent, moduleName: file.name) - + let moduleChanges = try swiftInterfaceAnalyzer.analyze( old: oldParsed, new: newParsed ) - + if !moduleChanges.isEmpty { changes[file.name] = moduleChanges } } - + return changes } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ActorDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ActorDeclSyntax+SwiftInterface.swift index b53c914..ddf2f36 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ActorDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ActorDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/actordeclsyntax extension ActorDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceActor { SwiftInterfaceActor( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/AssociatedTypeDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/AssociatedTypeDeclSyntax+SwiftInterface.swift index ec6b1e0..307e7c2 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/AssociatedTypeDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/AssociatedTypeDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/associatedtypedeclsyntax-swift.struct extension AssociatedTypeDeclSyntax { - + func toInterfaceElement() -> SwiftInterfaceAssociatedType { SwiftInterfaceAssociatedType( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ClassDeclSyntac+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ClassDeclSyntac+SwiftInterface.swift index af3766e..31af6d7 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ClassDeclSyntac+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ClassDeclSyntac+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/classdeclsyntax extension ClassDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceClass { SwiftInterfaceClass( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/DeclSyntax+Convenience.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/DeclSyntax+Convenience.swift index d596a50..349a9f1 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/DeclSyntax+Convenience.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/DeclSyntax+Convenience.swift @@ -7,7 +7,7 @@ import SwiftSyntax extension SyntaxCollection { - + /// Produces a description where all elements in the list are mapped to their `trimmedDescription` var sanitizedList: [String] { self.map(\.trimmedDescription) @@ -15,14 +15,14 @@ extension SyntaxCollection { } extension AttributeListSyntax { - + private var excludedAttributes: Set { [ "@_hasMissingDesignatedInitializers", "@_inheritsConvenienceInitializers" ] } - + /// Produces a description where all elements in the list are mapped to their `trimmedDescription` var sanitizedList: [String] { self.compactMap { @@ -34,7 +34,7 @@ extension AttributeListSyntax { } extension InheritedTypeListSyntax { - + /// Produces a description where all elements in the list are mapped to their type's `trimmedDescription` var sanitizedList: [String] { self.map(\.type.trimmedDescription) @@ -42,7 +42,7 @@ extension InheritedTypeListSyntax { } extension AccessorBlockSyntax { - + /// Produces a description where all newlines and spaces are replaced by a single space /// /// e.g. "get\n set\n" -> "get set" @@ -52,7 +52,7 @@ extension AccessorBlockSyntax { } extension String { - + /// Produces a string where all newlines and spaces are replaced by a single space /// /// e.g. "get\n set\n" -> "get set" diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumCaseDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumCaseDeclSyntax+SwiftInterface.swift index 188064a..aff5b78 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumCaseDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumCaseDeclSyntax+SwiftInterface.swift @@ -9,12 +9,12 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/enumcasedeclsyntax extension EnumCaseDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> [SwiftInterfaceEnumCase] { let attributes = self.attributes.sanitizedList let modifiers = self.modifiers.sanitizedList - + return elements.map { SwiftInterfaceEnumCase( attributes: attributes, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumDeclSyntax+SwiftInterface.swift index 4688ff5..b2bcc37 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/EnumDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/enumdeclsyntax extension EnumDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceEnum { SwiftInterfaceEnum( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ExtensionDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ExtensionDeclSyntax+SwiftInterface.swift index 5e95358..e260b09 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ExtensionDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ExtensionDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/extensiondeclsyntax extension ExtensionDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceExtension { SwiftInterfaceExtension( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/FunctionDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/FunctionDeclSyntax+SwiftInterface.swift index 6ed9285..a0286a9 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/FunctionDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/FunctionDeclSyntax+SwiftInterface.swift @@ -9,11 +9,11 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/functiondeclsyntax extension FunctionDeclSyntax { - + func toInterfaceElement() -> SwiftInterfaceFunction { - + var effectSpecifiers = [String]() - + if let effects = signature.effectSpecifiers { if let asyncSpecifier = effects.asyncSpecifier { effectSpecifiers.append(asyncSpecifier.trimmedDescription) @@ -22,7 +22,7 @@ extension FunctionDeclSyntax { effectSpecifiers.append(throwsClause.trimmedDescription) } } - + let parameters: [SwiftInterfaceFunction.Parameter] = self.signature.parameterClause.parameters.map { .init( firstName: $0.firstName.trimmedDescription, @@ -31,7 +31,7 @@ extension FunctionDeclSyntax { defaultValue: $0.defaultValue?.value.trimmedDescription.sanitizingNewlinesAndSpaces ) } - + return SwiftInterfaceFunction( attributes: self.attributes.sanitizedList, modifiers: self.modifiers.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/InitializerDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/InitializerDeclSyntax+SwiftInterface.swift index 9c40821..57208a8 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/InitializerDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/InitializerDeclSyntax+SwiftInterface.swift @@ -9,11 +9,11 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/initializerdeclsyntax extension InitializerDeclSyntax { - + func toInterfaceElement() -> SwiftInterfaceInitializer { - + var effectSpecifiers = [String]() - + if let effects = signature.effectSpecifiers { if let asyncSpecifier = effects.asyncSpecifier { effectSpecifiers.append(asyncSpecifier.trimmedDescription) @@ -22,7 +22,7 @@ extension InitializerDeclSyntax { effectSpecifiers.append(throwsClause.trimmedDescription) } } - + let parameters: [SwiftInterfaceInitializer.Parameter] = self.signature.parameterClause.parameters.map { .init( firstName: $0.firstName.trimmedDescription, @@ -31,7 +31,7 @@ extension InitializerDeclSyntax { defaultValue: $0.defaultValue?.value.trimmedDescription.sanitizingNewlinesAndSpaces ) } - + return SwiftInterfaceInitializer( attributes: self.attributes.sanitizedList, modifiers: self.modifiers.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ProtocolDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ProtocolDeclSyntax+SwiftInterface.swift index 18390a4..39e03f1 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ProtocolDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/ProtocolDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/protocoldeclsyntax extension ProtocolDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceProtocol { SwiftInterfaceProtocol( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/StructDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/StructDeclSyntax+SwiftInterface.swift index ef18253..3dbfa09 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/StructDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/StructDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/structdeclsyntax extension StructDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceStruct { SwiftInterfaceStruct( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/SubscriptDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/SubscriptDeclSyntax+SwiftInterface.swift index 0887b56..8c766f2 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/SubscriptDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/SubscriptDeclSyntax+SwiftInterface.swift @@ -9,9 +9,9 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/subscriptdeclsyntax extension SubscriptDeclSyntax { - + func toInterfaceElement() -> SwiftInterfaceSubscript { - + let parameters: [SwiftInterfaceSubscript.Parameter] = self.parameterClause.parameters.map { .init( firstName: $0.firstName.trimmedDescription, @@ -20,7 +20,7 @@ extension SubscriptDeclSyntax { defaultValue: $0.defaultValue?.value.trimmedDescription.sanitizingNewlinesAndSpaces ) } - + return SwiftInterfaceSubscript( attributes: self.attributes.sanitizedList, modifiers: self.modifiers.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/TypeAliasDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/TypeAliasDeclSyntax+SwiftInterface.swift index 8f1495a..6c5b4de 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/TypeAliasDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/TypeAliasDeclSyntax+SwiftInterface.swift @@ -9,7 +9,7 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/typealiasdeclsyntax-swift.struct extension TypeAliasDeclSyntax { - + func toInterfaceElement(children: [any SwiftInterfaceElement]) -> SwiftInterfaceTypeAlias { SwiftInterfaceTypeAlias( attributes: self.attributes.sanitizedList, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/VarDeclSyntax+SwiftInterface.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/VarDeclSyntax+SwiftInterface.swift index 57d9bb4..108ffbb 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/VarDeclSyntax+SwiftInterface.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/DeclSyntax+SwiftInterface/VarDeclSyntax+SwiftInterface.swift @@ -9,13 +9,13 @@ import SwiftSyntax /// See: https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/structdeclsyntax extension VariableDeclSyntax { - + func toInterfaceElement() -> [SwiftInterfaceVar] { - + let declarationAttributes = self.attributes.sanitizedList let modifiers = self.modifiers.sanitizedList let bindingSpecifier = self.bindingSpecifier.trimmedDescription - + // Transforming: // - final public let a = 0, b = 1, c: Double = 5.0 // Into: @@ -28,7 +28,7 @@ extension VariableDeclSyntax { // If the accessors are missing and we have a let we can assume it's get only accessors = "get" } - + return SwiftInterfaceVar( attributes: declarationAttributes, modifiers: modifiers, diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Actor.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Actor.swift index 7dbeee8..598bb40 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Actor.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Actor.swift @@ -7,39 +7,39 @@ import Foundation class SwiftInterfaceActor: SwiftInterfaceExtendableElement { - + var pathComponentName: String { name } - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// The name of the element let name: String - + /// e.g. let genericParameterDescription: String? - + /// Types/Protocols the element inherits from var inheritance: [String]? - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + var typeName: String { name } - + init( attributes: [String], modifiers: [String], @@ -60,7 +60,7 @@ class SwiftInterfaceActor: SwiftInterfaceExtendableElement { } extension SwiftInterfaceActor { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -74,30 +74,30 @@ extension SwiftInterfaceActor { } private extension SwiftInterfaceActor { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["actor"] - + components += [{ var components = [ name, genericParameterDescription ].compactMap { $0 }.joined() - + if let inheritance, !inheritance.isEmpty { components += ": \(inheritance.joined(separator: ", "))" } - + return components }()] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+AssociatedType.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+AssociatedType.swift index 5220edc..12001f4 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+AssociatedType.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+AssociatedType.swift @@ -7,38 +7,38 @@ import Foundation class SwiftInterfaceAssociatedType: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// The name of the element let name: String - + /// Types/Protocols the element inherits from let inheritance: [String]? - + /// e.g. any Swift.Equatable let initializerValue: String? - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + /// A associatedtype does not have children let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -57,7 +57,7 @@ class SwiftInterfaceAssociatedType: SwiftInterfaceElement { } extension SwiftInterfaceAssociatedType { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -71,25 +71,25 @@ extension SwiftInterfaceAssociatedType { } private extension SwiftInterfaceAssociatedType { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["associatedtype"] - + if let inheritance, !inheritance.isEmpty { components += ["\(name): \(inheritance.joined(separator: ", "))"] } else { components += [name] } - + initializerValue.map { components += ["= \($0)"] } - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Class.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Class.swift index 2aae8e3..fdedee2 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Class.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Class.swift @@ -7,39 +7,39 @@ import Foundation class SwiftInterfaceClass: SwiftInterfaceExtendableElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// The name of the element let name: String - + /// e.g. let genericParameterDescription: String? - + /// Types/Protocols the element inherits from var inheritance: [String]? - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + var typeName: String { name } - + init( attributes: [String], modifiers: [String], @@ -60,7 +60,7 @@ class SwiftInterfaceClass: SwiftInterfaceExtendableElement { } extension SwiftInterfaceClass { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -74,30 +74,30 @@ extension SwiftInterfaceClass { } private extension SwiftInterfaceClass { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["class"] - + components += [{ var components = [ name, genericParameterDescription ].compactMap { $0 }.joined() - + if let inheritance, !inheritance.isEmpty { components += ": \(inheritance.joined(separator: ", "))" } - + return components }()] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Enum.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Enum.swift index 718123e..e230d4f 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Enum.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Enum.swift @@ -7,37 +7,37 @@ import Foundation class SwiftInterfaceEnum: SwiftInterfaceExtendableElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + let name: String - + /// e.g. let genericParameterDescription: String? - + var inheritance: [String]? - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + var typeName: String { name } - + init( attributes: [String], modifiers: [String], @@ -58,7 +58,7 @@ class SwiftInterfaceEnum: SwiftInterfaceExtendableElement { } extension SwiftInterfaceEnum { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -72,30 +72,30 @@ extension SwiftInterfaceEnum { } private extension SwiftInterfaceEnum { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["enum"] - + components += [{ var components = [ name, genericParameterDescription ].compactMap { $0 }.joined() - + if let inheritance, !inheritance.isEmpty { components += ": \(inheritance.joined(separator: ", "))" } - + return components }()] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+EnumCase.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+EnumCase.swift index 4ad89b4..763b0c8 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+EnumCase.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+EnumCase.swift @@ -7,65 +7,65 @@ import Foundation extension SwiftInterfaceEnumCase { - + struct Parameter { - + let firstName: String? - + let secondName: String? - + let type: String - + let defaultValue: String? - + var description: String { var description = [ firstName, secondName ].compactMap { $0 }.joined(separator: " ") - + if description.isEmpty { description += "\(type)" } else { description += ": \(type)" } - + if let defaultValue { description += " = \(defaultValue)" } - + return description } } } class SwiftInterfaceEnumCase: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + let name: String - + let parameters: [Parameter]? - + let rawValue: String? - + var pathComponentName: String { "" } // Not relevant as / no children - + /// An enum case does not have children let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -82,7 +82,7 @@ class SwiftInterfaceEnumCase: SwiftInterfaceElement { } extension SwiftInterfaceEnumCase { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -95,21 +95,21 @@ extension SwiftInterfaceEnumCase { } private extension SwiftInterfaceEnumCase { - + func compileDescription() -> String { var components = [String]() - + components += attributes components += modifiers components += ["case"] - + if let parameters { components += ["\(name)(\(parameters.map(\.description).joined(separator: ", ")))"] } else { components += [name] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Extension.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Extension.swift index 8508643..7313bc8 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Extension.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Extension.swift @@ -7,37 +7,37 @@ import Foundation class SwiftInterfaceExtension: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + let extendedType: String - + let inheritance: [String]? - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { [extendedType, genericWhereClauseDescription.map { "[\($0)]" }].compactMap { $0 }.joined() } - + /// The members, declarations, ... inside of the body of the struct var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { extendedType } - + var consolidatableName: String { extendedType } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -56,7 +56,7 @@ class SwiftInterfaceExtension: SwiftInterfaceElement { } extension SwiftInterfaceExtension { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -69,23 +69,23 @@ extension SwiftInterfaceExtension { } private extension SwiftInterfaceExtension { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["extension"] - + if let inheritance, !inheritance.isEmpty { components += ["\(extendedType): \(inheritance.joined(separator: ", "))"] } else { components += [extendedType] } - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Function.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Function.swift index 05d533d..456f51a 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Function.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Function.swift @@ -7,77 +7,77 @@ import Foundation extension SwiftInterfaceFunction { - + struct Parameter { - + let firstName: String - + /// optional second "internal" name - can be ignored let secondName: String? - + let type: String - + let defaultValue: String? - + var description: String { var description = [ firstName, secondName ].compactMap { $0 }.joined(separator: " ") - + if description.isEmpty { description += "\(type)" } else { description += ": \(type)" } - + if let defaultValue { description += " = \(defaultValue)" } - + return description } } } class SwiftInterfaceFunction: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + let name: String - + /// e.g. let genericParameterDescription: String? - + let parameters: [Parameter] - + /// e.g. async, throws, rethrows let effectSpecifiers: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + let returnType: String - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + /// A function does not have children let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { "\(name)(\(parameters.map { "\($0.firstName):" }.joined()))" } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -100,7 +100,7 @@ class SwiftInterfaceFunction: SwiftInterfaceElement { } extension SwiftInterfaceFunction { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -116,14 +116,14 @@ extension SwiftInterfaceFunction { } private extension SwiftInterfaceFunction { - + func compileDescription() -> String { var components = [String]() - + components += attributes components += modifiers components += ["func"] - + components += [ [ name, @@ -131,12 +131,12 @@ private extension SwiftInterfaceFunction { "(\(parameters.map(\.description).joined(separator: ", ")))" ].compactMap { $0 }.joined() ] - + components += effectSpecifiers components += ["-> \(returnType)"] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Initializer.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Initializer.swift index 3a05143..8a1dc59 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Initializer.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Initializer.swift @@ -7,77 +7,77 @@ import Foundation extension SwiftInterfaceInitializer { - + struct Parameter { - + let firstName: String - + /// optional second "internal" name - can be ignored let secondName: String? - + let type: String - + let defaultValue: String? - + var description: String { var description = [ firstName, secondName ].compactMap { $0 }.joined(separator: " ") - + if description.isEmpty { description += "\(type)" } else { description += ": \(type)" } - + if let defaultValue { description += " = \(defaultValue)" } - + return description } } } class SwiftInterfaceInitializer: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + let optionalMark: String? - + /// e.g. let genericParameterDescription: String? - + let parameters: [Parameter] - + /// e.g. async, throws, rethrows let effectSpecifiers: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { "" } // Not relevant as / no children - + /// A initializer does not have children let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { "init(\(parameters.map { "\($0.firstName):" }.joined()))" } - + var consolidatableName: String { "init" } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -98,7 +98,7 @@ class SwiftInterfaceInitializer: SwiftInterfaceElement { } extension SwiftInterfaceInitializer { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -114,13 +114,13 @@ extension SwiftInterfaceInitializer { } private extension SwiftInterfaceInitializer { - + func compileDescription() -> String { var components = [String]() - + components += attributes components += modifiers - + components += [ [ "init", @@ -129,11 +129,11 @@ private extension SwiftInterfaceInitializer { "(\(parameters.map(\.description).joined(separator: ", ")))" ].compactMap { $0 }.joined() ] - + components += effectSpecifiers - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Protocol.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Protocol.swift index f6a605f..0f1fef0 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Protocol.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Protocol.swift @@ -7,37 +7,37 @@ import Foundation class SwiftInterfaceProtocol: SwiftInterfaceExtendableElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + let name: String - + let primaryAssociatedTypes: [String]? - + var inheritance: [String]? - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + /// The members, declarations, ... inside of the body of the struct var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + var typeName: String { name } - + init( attributes: [String], modifiers: [String], @@ -58,7 +58,7 @@ class SwiftInterfaceProtocol: SwiftInterfaceExtendableElement { } extension SwiftInterfaceProtocol { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -72,30 +72,30 @@ extension SwiftInterfaceProtocol { } private extension SwiftInterfaceProtocol { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["protocol"] - + components += [{ var components = [ name, primaryAssociatedTypes.map { "<\($0.joined(separator: ", "))>" } ].compactMap { $0 }.joined() - + if let inheritance, !inheritance.isEmpty { components += ": \(inheritance.joined(separator: ", "))" } - + return components }()] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Struct.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Struct.swift index 728868d..75d1c98 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Struct.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Struct.swift @@ -7,37 +7,37 @@ import Foundation class SwiftInterfaceStruct: SwiftInterfaceExtendableElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + let name: String - + /// e.g. let genericParameterDescription: String? - + var inheritance: [String]? - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { name } - + var children: [any SwiftInterfaceElement] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + var typeName: String { name } - + init( attributes: [String], modifiers: [String], @@ -58,7 +58,7 @@ class SwiftInterfaceStruct: SwiftInterfaceExtendableElement { } extension SwiftInterfaceStruct { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -72,30 +72,30 @@ extension SwiftInterfaceStruct { } private extension SwiftInterfaceStruct { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += ["struct"] - + components += [{ var components = [ name, genericParameterDescription ].compactMap { $0 }.joined() - + if let inheritance, !inheritance.isEmpty { components += ": \(inheritance.joined(separator: ", "))" } - + return components }()] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Subscript.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Subscript.swift index af8982c..122db4c 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Subscript.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Subscript.swift @@ -7,75 +7,75 @@ import Foundation extension SwiftInterfaceSubscript { - + struct Parameter { - + let firstName: String - + /// optional second "internal" name - can be ignored let secondName: String? - + let type: String - + let defaultValue: String? - + var description: String { var description = [ firstName, secondName ].compactMap { $0 }.joined(separator: " ") - + if description.isEmpty { description += "\(type)" } else { description += ": \(type)" } - + if let defaultValue { description += " = \(defaultValue)" } - + return description } } } class SwiftInterfaceSubscript: SwiftInterfaceElement { - + let name: String = "subscript" - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. let genericParameterDescription: String? - + let parameters: [Parameter] - + let returnType: String - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + let accessors: String? - + var pathComponentName: String { "" } // Not relevant as / no children - + let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { "\(name)(\(parameters.map { "\($0.firstName):" }.joined()))" } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -96,7 +96,7 @@ class SwiftInterfaceSubscript: SwiftInterfaceElement { } extension SwiftInterfaceSubscript { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -112,13 +112,13 @@ extension SwiftInterfaceSubscript { } private extension SwiftInterfaceSubscript { - + func compileDescription() -> String { var components = [String]() - + components += attributes components += modifiers - + components += [ [ "subscript", @@ -126,13 +126,13 @@ private extension SwiftInterfaceSubscript { "(\(parameters.map(\.description).joined(separator: ", ")))" ].compactMap { $0 }.joined() ] - + components += ["-> \(returnType)"] - + genericWhereClauseDescription.map { components += [$0] } - + accessors.map { components += ["{ \($0) }"] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+TypeAlias.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+TypeAlias.swift index 2ca6b86..c95766a 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+TypeAlias.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+TypeAlias.swift @@ -7,38 +7,38 @@ import Foundation class SwiftInterfaceTypeAlias: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + let name: String - + /// e.g. let genericParameterDescription: String? - + /// e.g. any Swift.Equatable let initializerValue: String - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. where T : Equatable let genericWhereClauseDescription: String? - + var pathComponentName: String { "" } // Not relevant as / no children - + let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -57,7 +57,7 @@ class SwiftInterfaceTypeAlias: SwiftInterfaceElement { } extension SwiftInterfaceTypeAlias { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -71,26 +71,26 @@ extension SwiftInterfaceTypeAlias { } private extension SwiftInterfaceTypeAlias { - + func compileDescription() -> String { var components = [String]() - + components += attributes components += modifiers components += ["typealias"] - + components += [ [ name, genericParameterDescription ].compactMap { $0 }.joined() ] - + components += ["= \(initializerValue)"] - + genericWhereClauseDescription.map { components += [$0] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Var.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Var.swift index 1552113..205d3ab 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Var.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement+Declaration/SwiftInterfaceElement+Var.swift @@ -7,38 +7,38 @@ import Foundation class SwiftInterfaceVar: SwiftInterfaceElement { - + /// e.g. @discardableResult, @MainActor, @objc, @_spi(...), ... let attributes: [String] - + /// e.g. public, private, package, open, internal let modifiers: [String] - + /// e.g. let | var | inout | _mutating | _borrowing | _consuming let bindingSpecifier: String - + let name: String - + let typeAnnotation: String - + let initializerValue: String? - + let accessors: String? - + var pathComponentName: String { "" } // Not relevant as / no children - + let children: [any SwiftInterfaceElement] = [] - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { name } - + var consolidatableName: String { name } - + var description: String { compileDescription() } - + init( attributes: [String], modifiers: [String], @@ -59,7 +59,7 @@ class SwiftInterfaceVar: SwiftInterfaceElement { } extension SwiftInterfaceVar { - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { var changes = [String?]() guard let other = otherElement as? Self else { return [] } @@ -74,11 +74,11 @@ extension SwiftInterfaceVar { } private extension SwiftInterfaceVar { - + func compileDescription() -> String { - + var components = [String]() - + components += attributes components += modifiers components += [bindingSpecifier] @@ -86,7 +86,7 @@ private extension SwiftInterfaceVar { initializerValue.map { components += ["= \($0)"] } accessors.map { components += ["{ \($0) }"] } - + return components.joined(separator: " ") } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement+DiffHelper.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement+DiffHelper.swift index 874bc24..937ceac 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement+DiffHelper.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement+DiffHelper.swift @@ -8,7 +8,7 @@ import Foundation import PADCore extension SwiftInterfaceElement { - + /// Returns a description for a change between an old and new value /// - Parameters: /// - propertyType: The (optional) property type name (e.g. "accessor", "modifier", "generic where clause", ...) for additional information @@ -20,16 +20,16 @@ extension SwiftInterfaceElement { oldValue: String?, newValue: String? ) -> [String] { - + guard let changeType: ChangeType = .for(oldValue: oldValue, newValue: newValue) else { return [] } - + var diffDescription: String if let propertyType { diffDescription = "\(changeType.title) \(propertyType)" } else { diffDescription = "\(changeType.title)" } - + switch changeType { case let .change(old, new): diffDescription += " from `\(old)` to `\(new)`" @@ -38,10 +38,10 @@ extension SwiftInterfaceElement { case let .addition(string): diffDescription += " `\(string)`" } - + return [diffDescription] } - + /// Returns a list of change descriptions for changes between the old and new values /// - Parameters: /// - propertyType: The (optional) property type name (e.g. "accessor", "modifier", "generic where clause", ...) for additional information @@ -49,7 +49,7 @@ extension SwiftInterfaceElement { /// - newValue: The (optional) new values /// - Returns: A list of change descriptions caused by a value change func diffDescription(propertyType: String, oldValues: [String]?, newValues: [String]?) -> [String] { - + if let oldValues, let newValues { let old = Set(oldValues) let new = Set(newValues) @@ -57,15 +57,15 @@ extension SwiftInterfaceElement { "\(new.contains($0) ? "Added" : "Removed") \(propertyType) `\($0)`" } } - + if let oldValues { return oldValues.map { "Removed \(propertyType) `\($0)`" } } - + if let newValues { return newValues.map { "Added \(propertyType) `\($0)`" } } - + return [] } } @@ -77,7 +77,7 @@ private enum ChangeType { case change(old: String, new: String) case removal(String) case addition(String) - + var title: String { switch self { case .change: "Changed" @@ -85,7 +85,7 @@ private enum ChangeType { case .addition: "Added" } } - + static func `for`(oldValue: String?, newValue: String?) -> Self? { if oldValue == newValue { return nil } if let oldValue, let newValue { return .change(old: oldValue, new: newValue) } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement.swift index 5abc123..fe0cf98 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceElement/SwiftInterfaceElement.swift @@ -7,56 +7,56 @@ import Foundation protocol SwiftInterfaceExtendableElement: SwiftInterfaceElement { - + /// Name of the type /// /// Is used to match an extension's `extendedType` to the element it extends var typeName: String { get } - + /// Types/Protocols the element inherits from var inheritance: [String]? { get set } - + var children: [any SwiftInterfaceElement] { get set } } protocol SwiftInterfaceElement: CustomStringConvertible, AnyObject { - + /// The name of the element used to construct the parent path for its children var pathComponentName: String { get } - + /// The full description of the element (without children) var description: String { get } - + /// The cildren of the element (e.g. properties/functions of a struct/class/...) var children: [any SwiftInterfaceElement] { get } - + /// A reduced signature of the element to be used to find 2 versions of the same element in a diff /// by deliberately omitting specifics like types and other decorators. /// /// e.g. `func foo(bar: Int = 0, baz: String)` would have a diffable signature of `foo(bar:baz)` var diffableSignature: String { get } - + /// A very reduced signature that allows consolidating changes /// /// e.g. `func foo(bar: Int = 0, baz: String)` would have a consolidatable name of `foo` var consolidatableName: String { get } - + /// The parent of the element (setup by using ``setupParentRelationships(parent:)`` var parent: (any SwiftInterfaceElement)? { get set } - + /// Produces a list of differences between one and another element func differences(to otherElement: T) -> [String] } extension SwiftInterfaceElement { - + func setupParentRelationships(parent: (any SwiftInterfaceElement)? = nil) { self.parent = parent children.forEach { $0.setupParentRelationships(parent: self) } } - + /// The path to the parent based on the `pathComponentName` /// /// The path does not including the own `pathComponentName` @@ -71,24 +71,24 @@ extension SwiftInterfaceElement { parentPath: extensionElement.extendedType ) } - + var parent = self.parent var path = [parent?.pathComponentName] - + while parent != nil { parent = parent?.parent path += [parent?.pathComponentName] } - + return sanitized( parentPath: path.compactMap { $0 }.filter { !$0.isEmpty }.reversed().joined(separator: ".") ) } - + /// Removing module name prefix for nicer readability private func sanitized(parentPath: String) -> String { var sanitizedPathComponents = parentPath.components(separatedBy: ".") - + // The first path component is always the module name so it's safe to remove all prefixes if let moduleName = sanitizedPathComponents.first { while sanitizedPathComponents.first == moduleName { @@ -116,7 +116,7 @@ extension SwiftInterfaceElement { } extension SwiftInterfaceElement { - + /// Produces the complete recursive description of the element func recursiveDescription(indentation: Int = 0) -> String { let spacer = " " @@ -128,11 +128,11 @@ extension SwiftInterfaceElement { } recursiveDescription.append("\n\(String(repeating: spacer, count: indentation))}") } - + if indentation == 0 { recursiveDescription.append("\n") } - + return recursiveDescription } } diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser+Root.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser+Root.swift index 8c05332..107e151 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser+Root.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser+Root.swift @@ -7,16 +7,16 @@ import Foundation extension SwiftInterfaceParser { - + /// The root element returned as the result of parsing the interface class Root: SwiftInterfaceElement { - - var parent: (any SwiftInterfaceElement)? = nil - + + var parent: (any SwiftInterfaceElement)? + var diffableSignature: String { "" } - + var consolidatableName: String { "" } - + /// Produces the complete recursive description of the interface var description: String { var description = "" @@ -26,20 +26,20 @@ extension SwiftInterfaceParser { } return description } - + var pathComponentName: String { moduleName } - + private let moduleName: String private(set) var children: [any SwiftInterfaceElement] - + init(moduleName: String, elements: [any SwiftInterfaceElement]) { self.moduleName = moduleName self.children = elements - + self.children = Self.mergeExtensions(for: self.children, moduleName: moduleName) self.children.forEach { $0.setupParentRelationships(parent: self) } } - + func differences(to otherElement: some SwiftInterfaceElement) -> [String] { [] } @@ -49,7 +49,7 @@ extension SwiftInterfaceParser { // MARK: - Convenience methods private extension SwiftInterfaceParser.Root { - + /// Attempting to merge extensions into their extended type to allow for better diffing /// /// Independent extensions (without a where clause) are very hard to diff as the only information we have @@ -74,32 +74,32 @@ private extension SwiftInterfaceParser.Root { /// } /// ``` static func mergeExtensions(for elements: [any SwiftInterfaceElement], moduleName: String) -> [any SwiftInterfaceElement] { - + let extensions = elements.compactMap { $0 as? SwiftInterfaceExtension } let extendableElements = elements.compactMap { $0 as? SwiftInterfaceExtendableElement } let nonExtensions = elements.filter { !($0 is SwiftInterfaceExtension) } - + var adjustedElements: [any SwiftInterfaceElement] = nonExtensions - + extensions.forEach { extensionElement in - + // We want to merge all extensions that don't have a where clause into the extended type guard extensionElement.genericWhereClauseDescription == nil else { adjustedElements.append(extensionElement) return } - + if merge(extensionElement: extensionElement, with: extendableElements, prefix: moduleName) { return // We found the matching extended element } - + // We could not find the extended type so we add the extension to the list adjustedElements.append(extensionElement) } - + return adjustedElements } - + /// Attempting to recursively merge an extension element with potential matches of extendable elements /// The prefix provides the parent path as the types don't include it but the `extension.extendedType` does /// @@ -131,21 +131,21 @@ private extension SwiftInterfaceParser.Root { with extendableElements: [any SwiftInterfaceExtendableElement], prefix: String ) -> Bool { - + // Finding the first extendable element that has the same prefix as the extension guard let extendedElement = extendableElements.first(where: { extensionElement.extendedType.hasPrefix("\(prefix).\($0.typeName)") }) else { return false } - + let extendedElementPrefix = "\(prefix).\(extendedElement.typeName)" - + // We found the extended type if extendedElementPrefix == extensionElement.extendedType { extendedElement.inheritance = (extendedElement.inheritance ?? []) + (extensionElement.inheritance ?? []) extendedElement.children += extensionElement.children return true } - + // We're looking for the extended type inside of the children let extendableChildren = extendedElement.children.compactMap { $0 as? SwiftInterfaceExtendableElement } return merge(extensionElement: extensionElement, with: extendableChildren, prefix: extendedElementPrefix) diff --git a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser.swift b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser.swift index 4665536..828bf67 100644 --- a/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser.swift +++ b/Sources/PublicModules/PADSwiftInterfaceDiff/SwiftInterfaceParser/SwiftInterfaceParser.swift @@ -13,16 +13,16 @@ import SwiftSyntax /// See: /// - [DeclSyntax](https://swiftpackageindex.com/swiftlang/swift-syntax/documentation/swiftsyntax/declsyntax) class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { - + // TODO: Handle (Nice to have) // - DeinitializerDeclSyntax // - PrecedenceGroupDeclSyntax // - OperatorDeclSyntax // - IfConfigClauseListSyntax // - ... (There are more but not important right now) - + private var scope: Scope = .root(elements: []) - + func parse(source: String, moduleName: String) -> any SwiftInterfaceElement { let visitor = Self() visitor.walk(Parser.parse(source: source)) @@ -31,18 +31,18 @@ class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { elements: visitor.scope.elements ) } - + /// Designated initializer required init() { super.init(viewMode: .sourceAccurate) } - + /// Starts a new scope which can contain zero or more nested symbols func startScope() -> SyntaxVisitorContinueKind { scope.start() return .visitChildren } - + /// Ends the current scope and adds the symbol returned by the closure to the symbol tree /// - Parameter makeSymbolWithChildrenInScope: Closure that return a new ``Symbol`` /// @@ -50,9 +50,9 @@ class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { func endScopeAndAddSymbol(makeElementsWithChildrenInScope: (_ children: [any SwiftInterfaceElement]) -> [any SwiftInterfaceElement]) { scope.end(makeElementsWithChildrenInScope: makeElementsWithChildrenInScope) } - + // MARK: - Class - + override open func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } @@ -60,9 +60,9 @@ class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { override open func visitPost(_ node: ClassDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - Struct - + override open func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } @@ -70,29 +70,29 @@ class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { override open func visitPost(_ node: StructDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - TypeAlias - + override open func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: TypeAliasDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - Function - + override open func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: FunctionDeclSyntax) { endScopeAndAddSymbol { _ in [node.toInterfaceElement()] } } - + // MARK: - Var - + override open func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } @@ -100,83 +100,83 @@ class SwiftInterfaceParser: SyntaxVisitor, SwiftInterfaceParsing { override open func visitPost(_ node: VariableDeclSyntax) { endScopeAndAddSymbol { _ in node.toInterfaceElement() } } - + // MARK: - AssociatedType - + override open func visit(_ node: AssociatedTypeDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: AssociatedTypeDeclSyntax) { endScopeAndAddSymbol { _ in [node.toInterfaceElement()] } } - + // MARK: - Protocol - + override open func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: ProtocolDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - Enum - + override open func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: EnumDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - EnumCase - + override open func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: EnumCaseDeclSyntax) { endScopeAndAddSymbol { node.toInterfaceElement(children: $0) } } - + // MARK: - Extension - + override open func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: ExtensionDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - Initializer - + override open func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: InitializerDeclSyntax) { endScopeAndAddSymbol { _ in [node.toInterfaceElement()] } } - + // MARK: - Actor - + override open func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: ActorDeclSyntax) { endScopeAndAddSymbol { [node.toInterfaceElement(children: $0)] } } - + // MARK: - Subscript - + override open func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { startScope() } - + override open func visitPost(_ node: SubscriptDeclSyntax) { endScopeAndAddSymbol { _ in [node.toInterfaceElement()] } } @@ -188,7 +188,7 @@ private indirect enum Scope { /// The root scope of a file case root(elements: [any SwiftInterfaceElement]) - + /// A nested scope, within a parent scope case nested(parent: Scope, elements: [any SwiftInterfaceElement]) diff --git a/Sources/Shared/Package/FileHandlingModule/FileHandling+Convenience.swift b/Sources/Shared/Package/FileHandlingModule/FileHandling+Convenience.swift index 634e0d3..b5440e5 100644 --- a/Sources/Shared/Package/FileHandlingModule/FileHandling+Convenience.swift +++ b/Sources/Shared/Package/FileHandlingModule/FileHandling+Convenience.swift @@ -15,7 +15,7 @@ package enum FileHandlerError: LocalizedError, Equatable { case couldNotLoadFile(filePath: String) /// File/Directory does not exist at `path` case pathDoesNotExist(path: String) - + public var errorDescription: String? { switch self { case .couldNotEncodeOutput: @@ -33,7 +33,7 @@ package enum FileHandlerError: LocalizedError, Equatable { // MARK: - Convenience package extension FileHandling { - + /// Creates a directory at the specified path and deletes any old directory if existing /// /// - Parameters: @@ -45,25 +45,25 @@ package extension FileHandling { try? removeItem(atPath: cleanDirectoryPath) try createDirectory(atPath: cleanDirectoryPath) } - + /// Persists an output string to a file /// /// - Parameters: /// - output: The output string to persist /// - outputFilePath: The file path to persist the output to func write(_ output: String, to outputFilePath: String) throws { - + guard let data = output.data(using: String.Encoding.utf8) else { throw FileHandlerError.couldNotEncodeOutput } - + // Remove existing directory if it exists try? removeItem(atPath: outputFilePath) if !createFile(atPath: outputFilePath, contents: data) { throw FileHandlerError.couldNotCreateFile(outputFilePath: outputFilePath) } } - + /// Get the string contents from a file at a specified path /// /// - Parameters: diff --git a/Sources/Shared/Package/FileHandlingModule/FileHandling.swift b/Sources/Shared/Package/FileHandlingModule/FileHandling.swift index 330d6c5..ae50e30 100644 --- a/Sources/Shared/Package/FileHandlingModule/FileHandling.swift +++ b/Sources/Shared/Package/FileHandlingModule/FileHandling.swift @@ -7,18 +7,18 @@ import Foundation package protocol FileHandling { - + var currentDirectoryPath: String { get } - + func loadData(from path: String) throws -> Data - + func removeItem(atPath path: String) throws - + func contentsOfDirectory(atPath path: String) throws -> [String] - + func createDirectory(atPath path: String) throws - + func createFile(atPath path: String, contents data: Data) -> Bool - + func fileExists(atPath path: String) -> Bool } diff --git a/Sources/Shared/Package/FileHandlingModule/FileManager+FileHandling.swift b/Sources/Shared/Package/FileHandlingModule/FileManager+FileHandling.swift index 109ae07..70ffea3 100644 --- a/Sources/Shared/Package/FileHandlingModule/FileManager+FileHandling.swift +++ b/Sources/Shared/Package/FileHandlingModule/FileManager+FileHandling.swift @@ -7,22 +7,22 @@ import Foundation extension FileManager: FileHandling { - + /// Creates a directory at the specified path package func createDirectory(atPath path: String) throws { try createDirectory(atPath: path, withIntermediateDirectories: true) } - + /// Creates a file at the specified path with the provided content package func createFile(atPath path: String, contents data: Data) -> Bool { createFile(atPath: path, contents: data, attributes: nil) } - + package func loadData(from filePath: String) throws -> Data { guard let data = self.contents(atPath: filePath) else { throw FileHandlerError.couldNotLoadFile(filePath: filePath) } - + return data } } diff --git a/Sources/Shared/Package/ShellModule/Shell.swift b/Sources/Shared/Package/ShellModule/Shell.swift index 0217380..b45d16c 100644 --- a/Sources/Shared/Package/ShellModule/Shell.swift +++ b/Sources/Shared/Package/ShellModule/Shell.swift @@ -7,28 +7,28 @@ import Foundation package protocol ShellHandling { - + @discardableResult func execute(_ command: String) -> String } package struct Shell: ShellHandling { - + package init() {} - + @discardableResult package func execute(_ command: String) -> String { - + let task = Process() let pipe = Pipe() - + task.standardOutput = pipe task.standardError = pipe task.arguments = ["-c", command] task.launchPath = "/bin/zsh" task.standardInput = nil task.launch() - + let data = pipe.fileHandleForReading.readDataToEndOfFile() return String(data: data, encoding: .utf8) ?? "" } diff --git a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift index cf4ba73..360af4e 100644 --- a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift +++ b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageDescription.swift @@ -10,19 +10,19 @@ import Foundation /// /// See: [PackageDescription](https://docs.swift.org/package-manager/PackageDescription/index.html) & [PackageDescriptionSerialization](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageDescription/PackageDescriptionSerialization.swift) package struct SwiftPackageDescription: Codable, Equatable { - + package let name: String package let platforms: [Platform] package let defaultLocalization: String? - + package let targets: [Target] package let products: [Product] package let dependencies: [Dependency] - + package let toolsVersion: String - + package var warnings = [String]() - + init( defaultLocalization: String?, name: String, @@ -42,7 +42,7 @@ package struct SwiftPackageDescription: Codable, Equatable { self.toolsVersion = toolsVersion self.warnings = warnings } - + enum CodingKeys: String, CodingKey { case defaultLocalization = "default_localization" case name @@ -55,34 +55,34 @@ package struct SwiftPackageDescription: Codable, Equatable { } extension SwiftPackageDescription { - + package struct Platform: Codable, Equatable, Hashable { - + package let name: String package let version: String } } extension SwiftPackageDescription.Platform: CustomStringConvertible { - + package var description: String { "\(name)(\(version))" } } package extension SwiftPackageDescription { - + struct Product: Codable, Equatable, Hashable { - + // TODO: Add `rule` property - + package let name: String package let targets: [String] } } extension SwiftPackageDescription.Product: CustomStringConvertible { - + package var description: String { let targetsDescription = targets.map { "\"\($0)\"" }.joined(separator: ", ") return ".library(name: \"\(name)\", targets: [\(targetsDescription)])" @@ -90,9 +90,9 @@ extension SwiftPackageDescription.Product: CustomStringConvertible { } package extension SwiftPackageDescription { - + struct Dependency: Codable, Equatable { - + package let identity: String package let requirement: Requirement package let type: String @@ -101,79 +101,79 @@ package extension SwiftPackageDescription { } extension SwiftPackageDescription.Dependency: CustomStringConvertible { - + package var description: String { var description = ".package(" - + var fields = [String]() - + if let url { fields += ["url: \"\(url)\""] } - + fields += [requirement.description] - + description += fields.joined(separator: ", ") - + description += ")" return description } } package extension SwiftPackageDescription.Dependency { - + struct Requirement: Codable, Equatable { - + // TODO: Which other requirements exist? - + package let exact: [String]? } } extension SwiftPackageDescription.Dependency.Requirement: CustomStringConvertible { - + package var description: String { if let exactVersion = exact?.first { return "exact: \"\(exactVersion)\"" } - + return "UNKNOWN_REQUIREMENT" } } package extension SwiftPackageDescription { - + struct Target: Codable, Equatable { - + package enum ModuleType: String, Codable, Equatable { case swiftTarget = "SwiftTarget" case binaryTarget = "BinaryTarget" case clangTarget = "ClangTarget" } - + package enum TargetType: String, Codable, Equatable { case library = "library" case binary = "binary" case test = "test" } - + package let name: String package let type: TargetType package let path: String package let moduleType: ModuleType - + /// `.product(name: ...)` dependency package let productDependencies: [String]? /// `.target(name: ...) dependency package let targetDependencies: [String]? - + // Ignoring following properties for now as they are not handled in the `PackageAnalyzer` // and thus would produce changes that are not visible // // let productMemberships: [String]? // let sources: [String] // let resources: [Resource]? - + init( name: String, type: TargetType, @@ -189,7 +189,7 @@ package extension SwiftPackageDescription { self.productDependencies = productDependencies self.targetDependencies = targetDependencies } - + enum CodingKeys: String, CodingKey { case moduleType = "module_type" case name @@ -202,7 +202,7 @@ package extension SwiftPackageDescription { } extension SwiftPackageDescription.Target.TargetType: CustomStringConvertible { - + package var description: String { switch self { case .binary: "binaryTarget" @@ -213,39 +213,39 @@ extension SwiftPackageDescription.Target.TargetType: CustomStringConvertible { } extension SwiftPackageDescription.Target: CustomStringConvertible { - + package var description: String { var description = ".\(type.description)(name: \"\(name)\"" - + var dependencyDescriptions = [String]() - + if let targetDependenciesDescriptions = targetDependencies?.map({ ".target(name: \"\($0)\")" }) { dependencyDescriptions += targetDependenciesDescriptions } - + if let productDependenciesDescriptions = productDependencies?.map({ ".product(name: \"\($0)\", ...)" }) { dependencyDescriptions += productDependenciesDescriptions } - + if !dependencyDescriptions.isEmpty { // `, dependencies: [.target(name: ...), .target(name: ...), .product(name: ...), ...]` description += ", dependencies: [\(dependencyDescriptions.joined(separator: ", "))]" } - + description += ", path: \"\(path)\"" - + description += ")" - + return description } } package extension SwiftPackageDescription.Target { - + struct Resource: Codable, Equatable { - + // TODO: Add `rule` property - + package let path: String } } diff --git a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift index f935dab..9b6d06b 100644 --- a/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift +++ b/Sources/Shared/Package/SwiftPackageFileHelperModule/SwiftPackageFileHelper.swift @@ -15,7 +15,7 @@ enum SwiftPackageFileHelperError: LocalizedError { case packageDescriptionError(_ description: String) case couldNotGeneratePackageDescription case couldNotConsolidateTargetsInPackageFile - + var errorDescription: String? { switch self { case .couldNotGeneratePackageDescription: @@ -29,11 +29,11 @@ enum SwiftPackageFileHelperError: LocalizedError { } package struct SwiftPackageFileHelper { - + private let fileHandler: FileHandling private let shell: any ShellHandling private let logger: (any Logging)? - + package init( fileHandler: FileHandling, shell: any ShellHandling, @@ -43,40 +43,40 @@ package struct SwiftPackageFileHelper { self.shell = shell self.logger = logger } - + package static func packagePath(for projectDirectoryPath: String) -> String { projectDirectoryPath.appending("/Package.swift") } - + package func availableTargets( at projectDirectoryPath: String, moduleType: SwiftPackageDescription.Target.ModuleType? = nil, targetType: SwiftPackageDescription.Target.TargetType? = nil ) throws -> Set { - + var targets = try packageDescription(at: projectDirectoryPath).targets - + if let moduleType { targets = targets.filter { $0.moduleType == moduleType } } - + if let targetType { targets = targets.filter { $0.type == targetType } } - + return Set(targets.map(\.name)) } - + package func packageDescription(at projectDirectoryPath: String) throws -> SwiftPackageDescription { try generatePackageDescription(at: projectDirectoryPath) } - + /// Inserts a new library into the targets section containing all targets from the target section package func preparePackageWithConsolidatedLibrary( named consolidatedLibraryName: String, at projectDirectoryPath: String ) throws { - + let packagePath = Self.packagePath(for: projectDirectoryPath) let packageContent = try fileHandler.loadString(from: packagePath) let targets = try availableTargets( @@ -84,10 +84,10 @@ package struct SwiftPackageFileHelper { moduleType: .swiftTarget, targetType: .library ) - + let consolidatedEntry = consolidatedLibraryEntry(consolidatedLibraryName, from: targets.sorted()) let updatedPackageContent = try updatedContent(packageContent, with: consolidatedEntry) - + // Write the updated content back to the file try fileHandler.write(updatedPackageContent, to: packagePath) } @@ -98,28 +98,28 @@ package struct SwiftPackageFileHelper { // MARK: Generate Package Description private extension SwiftPackageFileHelper { - + func generatePackageDescription(at projectDirectoryPath: String) throws -> SwiftPackageDescription { - + let result = try loadPackageDescription(projectDirectoryPath: projectDirectoryPath) - + let newLine = "\n" let errorTag = "error: " let warningTag = "warning: " - + var packageDescriptionLines = result.components(separatedBy: newLine) var warnings = [String]() - + while let firstLine = packageDescriptionLines.first { - + // If there are warnings/errors when generating the description // there are non-json lines added on the top of the result // That we have to get rid of first to generate the description object - + if firstLine.starts(with: errorTag) { throw SwiftPackageFileHelperError.packageDescriptionError(result) } - + if firstLine.starts(with: warningTag) { let directoryTag = "'\(URL(filePath: projectDirectoryPath).lastPathComponent)': " let warning = firstLine @@ -127,19 +127,19 @@ private extension SwiftPackageFileHelper { .replacingOccurrences(of: directoryTag, with: "", options: .caseInsensitive) warnings += [warning] } - + if firstLine.starts(with: "{"), let packageDescriptionData = packageDescriptionLines.joined(separator: newLine).data(using: .utf8) { return try decodePackageDescription(from: packageDescriptionData, warnings: warnings) } - + packageDescriptionLines.removeFirst() } - + throw SwiftPackageFileHelperError.couldNotGeneratePackageDescription } - + func loadPackageDescription( projectDirectoryPath: String ) throws -> String { @@ -147,10 +147,10 @@ private extension SwiftPackageFileHelper { "cd \(projectDirectoryPath);", "swift package describe --type json" ] - + return shell.execute(command.joined(separator: " ")) } - + func decodePackageDescription(from packageDescriptionData: Data, warnings: [String]) throws -> SwiftPackageDescription { var packageDescription = try JSONDecoder().decode(SwiftPackageDescription.self, from: packageDescriptionData) packageDescription.warnings = warnings @@ -161,21 +161,21 @@ private extension SwiftPackageFileHelper { // MARK: Update Package Content private extension SwiftPackageFileHelper { - + /// Generates a library entry from the name and available target names to be inserted into the `Package.swift` file func consolidatedLibraryEntry( _ name: String, from availableTargets: [String] ) -> String { """ - + .library( name: "\(name)", targets: [\(availableTargets.map { "\"\($0)\"" }.joined(separator: ", "))] ), """ } - + /// Generates the updated content for the `Package.swift` adding the consolidated library entry (containing all targets) in the products section func updatedContent( _ packageContent: String, diff --git a/Sources/Shared/Public/PADCore/Change.swift b/Sources/Shared/Public/PADCore/Change.swift index 5b8d885..0d18af1 100644 --- a/Sources/Shared/Public/PADCore/Change.swift +++ b/Sources/Shared/Public/PADCore/Change.swift @@ -13,12 +13,12 @@ public struct Change: Equatable { case removal(description: String) case change(oldDescription: String, newDescription: String) } - + public private(set) var changeType: ChangeType public private(set) var parentPath: String? public private(set) var listOfChanges: [String] = [] - + public init( changeType: ChangeType, parentPath: String? = nil, @@ -53,7 +53,7 @@ extension Change.ChangeType { return false } } - + public var isChange: Bool { switch self { case .addition: diff --git a/Sources/Shared/Public/PADCore/SwiftInterfaceFile.swift b/Sources/Shared/Public/PADCore/SwiftInterfaceFile.swift index 39b31fc..cd2fba1 100644 --- a/Sources/Shared/Public/PADCore/SwiftInterfaceFile.swift +++ b/Sources/Shared/Public/PADCore/SwiftInterfaceFile.swift @@ -14,7 +14,7 @@ public struct SwiftInterfaceFile { public let oldFilePath: String /// The file path to the new/updated `.swiftinterface` public let newFilePath: String - + /// Creates a new instance of a ``SwiftInterfaceFile`` public init( name: String, diff --git a/Sources/Shared/Public/PADLogging/LogFileLogger.swift b/Sources/Shared/Public/PADLogging/LogFileLogger.swift index b1df365..666a790 100644 --- a/Sources/Shared/Public/PADLogging/LogFileLogger.swift +++ b/Sources/Shared/Public/PADLogging/LogFileLogger.swift @@ -9,22 +9,22 @@ import Foundation /// Logs into a log file at a specified output path public class LogFileLogger: Logging { - + private let fileHandler: any FileHandling private let outputFilePath: String - + @MainActor private var output: [String] = [] { didSet { try? fileHandler.write(output.joined(separator: "\n"), to: outputFilePath) } } - + /// Creates a new instance that targets the specified output path public convenience init(outputFilePath: String) { self.init(fileHandler: FileManager.default, outputFilePath: outputFilePath) } - + init( fileHandler: any FileHandling, outputFilePath: String @@ -32,13 +32,13 @@ public class LogFileLogger: Logging { self.fileHandler = fileHandler self.outputFilePath = outputFilePath } - + public func log(_ message: String, from subsystem: String) { Task { @MainActor in output += ["πŸͺ΅ [\(subsystem)] \(message)\n"] } } - + public func debug(_ message: String, from subsystem: String) { Task { @MainActor in output += ["🐞 [\(subsystem)] \(message)\n"] diff --git a/Sources/Shared/Public/PADLogging/LogLevelLogger.swift b/Sources/Shared/Public/PADLogging/LogLevelLogger.swift index 7424bd4..cd78cd4 100644 --- a/Sources/Shared/Public/PADLogging/LogLevelLogger.swift +++ b/Sources/Shared/Public/PADLogging/LogLevelLogger.swift @@ -15,7 +15,7 @@ public enum LogLevel { case `default` /// All logs case debug - + var shouldLog: Bool { switch self { case .quiet: @@ -26,7 +26,7 @@ public enum LogLevel { return true } } - + var shouldDebugLog: Bool { switch self { case .quiet: @@ -43,20 +43,20 @@ public enum LogLevel { /// Logger that respects a ``LogLevel`` public struct LogLevelLogger: Logging { - + private let logLevel: LogLevel internal let wrappedLogger: LoggerType - + init(with logger: LoggerType, logLevel: LogLevel) { self.wrappedLogger = logger self.logLevel = logLevel } - + public func log(_ message: String, from subsystem: String) { guard logLevel.shouldLog else { return } wrappedLogger.log("\(message)", from: subsystem) } - + public func debug(_ message: String, from subsystem: String) { guard logLevel.shouldDebugLog else { return } wrappedLogger.debug("\(message)", from: subsystem) diff --git a/Sources/Shared/Public/PADLogging/LoggingGroup.swift b/Sources/Shared/Public/PADLogging/LoggingGroup.swift index fa176db..582a226 100644 --- a/Sources/Shared/Public/PADLogging/LoggingGroup.swift +++ b/Sources/Shared/Public/PADLogging/LoggingGroup.swift @@ -8,17 +8,17 @@ import Foundation /// A group of loggers public struct LoggingGroup: Logging { - + let logger: [any Logging] - + public init(with logger: [any Logging]) { self.logger = logger } - + public func log(_ message: String, from subsystem: String) { logger.forEach { $0.log(message, from: subsystem) } } - + public func debug(_ message: String, from subsystem: String) { logger.forEach { $0.debug(message, from: subsystem) } } diff --git a/Sources/Shared/Public/PADLogging/SystemLogger.swift b/Sources/Shared/Public/PADLogging/SystemLogger.swift index 4797e90..fdea2fc 100644 --- a/Sources/Shared/Public/PADLogging/SystemLogger.swift +++ b/Sources/Shared/Public/PADLogging/SystemLogger.swift @@ -9,20 +9,20 @@ import OSLog /// A logger that outputs logs to the console public struct SystemLogger: Logging { - + public init() {} - + public func log(_ message: String, from subsystem: String) { logger(for: subsystem).log("\(message)") } - + public func debug(_ message: String, from subsystem: String) { logger(for: subsystem).debug("\(message)") } } private extension SystemLogger { - + func logger(for subsystem: String) -> Logger { Logger( subsystem: subsystem, diff --git a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift index edd4173..fa961d1 100644 --- a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift +++ b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceFileLocator.swift @@ -12,11 +12,11 @@ import ShellModule /// A helper to locate `.swiftinterface` files public struct SwiftInterfaceFileLocator { - + let fileHandler: any FileHandling let shell: any ShellHandling let logger: (any Logging)? - + package init( fileHandler: any FileHandling, shell: any ShellHandling, @@ -26,7 +26,7 @@ public struct SwiftInterfaceFileLocator { self.shell = shell self.logger = logger } - + public init( logger: (any Logging)? = nil ) { @@ -36,7 +36,7 @@ public struct SwiftInterfaceFileLocator { logger: logger ) } - + /// Tries to locate a `.swiftinterface` files in the derivedData folder for a specific scheme /// - Parameters: /// - scheme: The scheme to find the `.swiftinterface` file for @@ -46,7 +46,7 @@ public struct SwiftInterfaceFileLocator { /// - Throws: An error if no `.swiftinterface` file can be found for the given scheme + derived data path public func locate(for scheme: String, derivedDataPath: String, type: SwiftInterfaceType) throws -> URL { let schemeSwiftModuleName = "\(scheme).swiftmodule" - + let swiftModulePathsForScheme = shell.execute("cd '\(derivedDataPath)'; find . -type d -name '\(schemeSwiftModuleName)'") .components(separatedBy: .newlines) .map { URL(filePath: $0) } @@ -54,11 +54,11 @@ public struct SwiftInterfaceFileLocator { guard let swiftModulePath = swiftModulePathsForScheme.first?.path() else { throw FileHandlerError.pathDoesNotExist(path: "find . -type d -name '\(schemeSwiftModuleName)'") } - + let completeSwiftModulePath = derivedDataPath + "/" + swiftModulePath - + let swiftModuleContent = try fileHandler.contentsOfDirectory(atPath: completeSwiftModulePath) - + let swiftInterfacePaths: [String] switch type { case .private: @@ -66,7 +66,7 @@ public struct SwiftInterfaceFileLocator { case .public: swiftInterfacePaths = swiftModuleContent.filter { $0.hasSuffix(".swiftinterface") && !$0.hasSuffix(".private.swiftinterface") } } - + guard let swiftInterfacePath = swiftInterfacePaths.first else { switch type { case .private: @@ -75,7 +75,7 @@ public struct SwiftInterfaceFileLocator { throw FileHandlerError.pathDoesNotExist(path: "'\(completeSwiftModulePath)/\(scheme).swiftinterface'") } } - + return URL(filePath: "\(completeSwiftModulePath)/\(swiftInterfacePath)") } } diff --git a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift index fc6b5b1..407c997 100644 --- a/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift +++ b/Sources/Shared/Public/PADSwiftInterfaceFileLocator/SwiftInterfaceType.swift @@ -10,7 +10,7 @@ import Foundation public enum SwiftInterfaceType { case `private` case `public` - + var name: String { switch self { case .private: "private" diff --git a/Tests/IntegrationTests/ReferencePackageTests.swift b/Tests/IntegrationTests/ReferencePackageTests.swift index 76e3021..c794fb8 100644 --- a/Tests/IntegrationTests/ReferencePackageTests.swift +++ b/Tests/IntegrationTests/ReferencePackageTests.swift @@ -11,42 +11,42 @@ import XCTest class ReferencePackageTests: XCTestCase { - + override func setUp() async throws { - + let referencePackagesRoot = try Self.referencePackagesPath() let oldReferencePackageDirectory = referencePackagesRoot.appending(path: "ReferencePackage") let newReferencePackageDirectory = referencePackagesRoot.appending(path: "UpdatedPackage") - + if FileManager.default.fileExists(atPath: oldReferencePackageDirectory.appending(path: XcodeTools.Constants.derivedDataPath).path()), FileManager.default.fileExists(atPath: newReferencePackageDirectory.appending(path: XcodeTools.Constants.derivedDataPath).path()) { return // Nothing to build } - + let xcodeTools = XcodeTools(logger: nil) - + _ = try await xcodeTools.archive(projectDirectoryPath: oldReferencePackageDirectory.path(), scheme: "ReferencePackage", projectType: .swiftPackage) _ = try await xcodeTools.archive(projectDirectoryPath: newReferencePackageDirectory.path(), scheme: "ReferencePackage", projectType: .swiftPackage) } - + override static func tearDown() { - + guard let referencePackagesRoot = try? referencePackagesPath() else { return } let oldReferencePackageDirectory = referencePackagesRoot.appending(path: "ReferencePackage").appending(path: XcodeTools.Constants.derivedDataPath) let newReferencePackageDirectory = referencePackagesRoot.appending(path: "UpdatedPackage").appending(path: XcodeTools.Constants.derivedDataPath) - + try? FileManager.default.removeItem(at: oldReferencePackageDirectory) try? FileManager.default.removeItem(at: newReferencePackageDirectory) } - + func test_swiftInterface_public() async throws { - + let interfaceType: InterfaceType = .public - + let expectedOutput = try expectedOutput(for: interfaceType) let pipelineOutput = try await runPipeline(for: interfaceType) - + let markdownOutput = MarkdownOutputGenerator().generate( from: pipelineOutput, allTargets: ["ReferencePackage"], @@ -54,10 +54,10 @@ class ReferencePackageTests: XCTestCase { newVersionName: "new_public", warnings: [] ) - + let expectedLines = sanitizeOutput(expectedOutput).components(separatedBy: "\n") let markdownOutputLines = sanitizeOutput(markdownOutput).components(separatedBy: "\n") - + for i in 0.. URL { // Unfortunately we can't use packages as Test Resources, so we put it in a `ReferencePackages` directory on root guard let projectRoot = #file.replacingOccurrences(of: "relatve/path/to/file", with: "").split(separator: "/Tests/").first else { struct CannotFindRootDirectoryError: Error {} throw CannotFindRootDirectoryError() } - + return URL(filePath: String(projectRoot)).appending(path: "ReferencePackages") } - + enum InterfaceType { case `public` case `private` - + var expectedOutputFileName: String { switch self { case .public: @@ -117,7 +117,7 @@ private extension ReferencePackageTests { "expected-reference-changes-swift-interface-private" } } - + var interfaceFilePath: String { switch self { case .public: @@ -127,35 +127,35 @@ private extension ReferencePackageTests { } } } - + func expectedOutput(for source: InterfaceType) throws -> String { let expectedOutputFilePath = try XCTUnwrap(Bundle.module.path(forResource: source.expectedOutputFileName, ofType: "md")) let expectedOutputData = try XCTUnwrap(FileManager.default.contents(atPath: expectedOutputFilePath)) return try XCTUnwrap(String(data: expectedOutputData, encoding: .utf8)) } - + func swiftInterfaceFilePath(for referencePackagesRoot: URL, packageName: String, interfaceType: InterfaceType) throws -> String { let oldReferencePackageDirectory = referencePackagesRoot.appending(path: packageName) let interfaceFilePath = try XCTUnwrap(oldReferencePackageDirectory.appending(path: interfaceType.interfaceFilePath)) return interfaceFilePath.path() } - + func runPipeline(for interfaceType: InterfaceType) async throws -> [String: [Change]] { let referencePackagesRoot = try Self.referencePackagesPath() - + let oldPrivateSwiftInterfaceFilePath = try swiftInterfaceFilePath( for: referencePackagesRoot, packageName: "ReferencePackage", interfaceType: interfaceType ) - + let newPrivateSwiftInterfaceFilePath = try swiftInterfaceFilePath( for: referencePackagesRoot, packageName: "UpdatedPackage", interfaceType: interfaceType ) - + let interfaceFiles = [ SwiftInterfaceFile( name: "ReferencePackage", @@ -163,7 +163,7 @@ private extension ReferencePackageTests { newFilePath: newPrivateSwiftInterfaceFilePath ) ] - + return try await SwiftInterfaceDiff( fileHandler: FileManager.default, swiftInterfaceParser: SwiftInterfaceParser(), @@ -171,7 +171,7 @@ private extension ReferencePackageTests { logger: nil ).run(with: interfaceFiles) } - + /// Removes the 2nd line that contains local file paths + empty newline at the end of the content if it exists func sanitizeOutput(_ output: String) -> String { var lines = output.components(separatedBy: "\n") diff --git a/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-private.md b/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-private.md index 108e833..383800a 100644 --- a/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-private.md +++ b/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-private.md @@ -328,7 +328,7 @@ public typealias CustomAssociatedType = Swift.Int ``` ```javascript -public typealias Iterator = Swift.Array.AnotherAssociatedType> +public typealias Iterator = [ReferencePackage.CustomStruct.AnotherAssociatedType] ``` ```javascript @@ -379,7 +379,7 @@ Changes: ``` ```javascript -@_spi(SystemProgrammingInterface) public typealias Iterator = Swift.Array +@_spi(SystemProgrammingInterface) public typealias Iterator = [Swift.Double] ``` ```javascript diff --git a/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-public.md b/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-public.md index c3989da..eb5c51e 100644 --- a/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-public.md +++ b/Tests/IntegrationTests/Resources/expected-reference-changes-swift-interface-public.md @@ -315,7 +315,7 @@ public typealias CustomAssociatedType = Swift.Int ``` ```javascript -public typealias Iterator = Swift.Array.AnotherAssociatedType> +public typealias Iterator = [ReferencePackage.CustomStruct.AnotherAssociatedType] ``` ```javascript diff --git a/Tests/UnitTests/FileHandlingTests.swift b/Tests/UnitTests/FileHandlingTests.swift index 8a067ca..9465833 100644 --- a/Tests/UnitTests/FileHandlingTests.swift +++ b/Tests/UnitTests/FileHandlingTests.swift @@ -8,33 +8,33 @@ import XCTest class FileHandlingTests: XCTestCase { - + func test_write() throws { - + let output = "output" let filePath = "output/file/path.txt" - + var fileHandler = MockFileHandler() fileHandler.handleRemoveItem = { path in XCTAssertEqual(path, filePath) } - + // Success scenario - + fileHandler.handleCreateFile = { path, data in XCTAssertEqual(path, filePath) XCTAssertEqual(String(data: data, encoding: .utf8), output) return true } - + try fileHandler.write(output, to: filePath) - + // Fail scenario - + fileHandler.handleCreateFile = { _, _ in false } - + do { try fileHandler.write(output, to: filePath) XCTFail("Write should have thrown an error") @@ -43,11 +43,11 @@ class FileHandlingTests: XCTestCase { XCTAssertEqual(fileHandlerError, FileHandlerError.couldNotCreateFile(outputFilePath: filePath)) } } - + func test_createCleanRepository_success() throws { - + let filePath = "directory/path" - + var fileHandler = MockFileHandler() fileHandler.handleRemoveItem = { path in XCTAssertEqual(path, filePath) @@ -56,14 +56,14 @@ class FileHandlingTests: XCTestCase { fileHandler.handleCreateDirectory = { path in XCTAssertEqual(path, filePath) } - + try fileHandler.createCleanDirectory(atPath: filePath) } - + func test_createCleanRepository_failure() throws { - + let filePath = "directory/path" - + var fileHandler = MockFileHandler() fileHandler.handleRemoveItem = { path in XCTAssertEqual(path, filePath) @@ -73,7 +73,7 @@ class FileHandlingTests: XCTestCase { XCTAssertEqual(path, filePath) throw FileHandlerError.couldNotCreateFile(outputFilePath: path) } - + do { try fileHandler.createCleanDirectory(atPath: filePath) XCTFail("createCleanDirectory should have thrown an error") @@ -81,32 +81,32 @@ class FileHandlingTests: XCTestCase { let fileHandlerError = try XCTUnwrap(error as? FileHandlerError) XCTAssertEqual(fileHandlerError, .couldNotCreateFile(outputFilePath: filePath)) } - + } - + func test_load() throws { - + let filePath = "input/file/path.txt" let expectedContent = "content" - + var fileHandler = MockFileHandler() - + // Success scenario - + fileHandler.handleLoadData = { path in XCTAssertEqual(path, filePath) return try XCTUnwrap(expectedContent.data(using: .utf8)) } - + let content = try fileHandler.loadString(from: filePath) XCTAssertEqual(expectedContent, content) - + // Fail scenario - + fileHandler.handleLoadData = { path in throw FileHandlerError.couldNotLoadFile(filePath: path) } - + do { _ = try fileHandler.loadString(from: filePath) XCTFail("Load should have thrown an error") diff --git a/Tests/UnitTests/GitTests.swift b/Tests/UnitTests/GitTests.swift index 4fda4d8..2aa6e8a 100644 --- a/Tests/UnitTests/GitTests.swift +++ b/Tests/UnitTests/GitTests.swift @@ -8,49 +8,49 @@ import XCTest class GitTests: XCTestCase { - + func test_clone_success() throws { - + let repository = "repository" let branch = "branch" let targetDirectoryPath = "targetDirectoryPath" - + let mockShell = MockShell { command in XCTAssertEqual(command, "git clone -b \(branch) \(repository) \(targetDirectoryPath)") return "" } - + let mockFileHandler = MockFileHandler(handleFileExists: { _ in true }) var mockLogger = MockLogger() mockLogger.handleLog = { message, subsystem in XCTAssertEqual(message, "🐱 Cloning repository @ branch into targetDirectoryPath") XCTAssertEqual(subsystem, "Git") } - + let git = Git(shell: mockShell, fileHandler: mockFileHandler, logger: mockLogger) try git.clone(repository, at: branch, targetDirectoryPath: targetDirectoryPath) } - + func test_clone_fail() throws { - + let repository = "repository" let branch = "branch" let targetDirectoryPath = "targetDirectoryPath" - + let mockShell = MockShell { command in XCTAssertEqual(command, "git clone -b \(branch) \(repository) \(targetDirectoryPath)") return "" } - + let mockFileHandler = MockFileHandler(handleFileExists: { _ in false }) var mockLogger = MockLogger() mockLogger.handleLog = { message, subsystem in XCTAssertEqual(message, "🐱 Cloning repository @ branch into targetDirectoryPath") XCTAssertEqual(subsystem, "Git") } - + let git = Git(shell: mockShell, fileHandler: mockFileHandler, logger: mockLogger) - + do { try git.clone(repository, at: branch, targetDirectoryPath: targetDirectoryPath) XCTFail("Clone should have thrown an error") diff --git a/Tests/UnitTests/LoggerTests.swift b/Tests/UnitTests/LoggerTests.swift index b997ca7..bfaffaf 100644 --- a/Tests/UnitTests/LoggerTests.swift +++ b/Tests/UnitTests/LoggerTests.swift @@ -8,20 +8,20 @@ import XCTest class LoggerTests: XCTestCase { - + func test_logLevels() async throws { - + var logger = MockLogger() - + // .quiet - + logger.handleLog = { _, _ in XCTFail(".handleLog should not be called") } logger.handleDebug = { _, _ in XCTFail(".handleDebug should not be called") } logger.withLogLevel(.quiet).log("log", from: "quiet") logger.withLogLevel(.quiet).debug("debug", from: "quiet") - + // .default - + logger.handleLog = { message, subsystem in XCTAssertEqual(message, "log") XCTAssertEqual(subsystem, "default") @@ -29,9 +29,9 @@ class LoggerTests: XCTestCase { logger.handleDebug = { _, _ in XCTFail(".handleDebug should not be called") } logger.withLogLevel(.default).log("log", from: "default") logger.withLogLevel(.default).debug("debug", from: "default") - + // .debug - + logger.handleLog = { message, subsystem in XCTAssertEqual(message, "log") XCTAssertEqual(subsystem, "debug") @@ -43,19 +43,19 @@ class LoggerTests: XCTestCase { logger.withLogLevel(.debug).log("log", from: "debug") logger.withLogLevel(.debug).debug("debug", from: "debug") } - + func test_logFileLogger() async throws { - + let outputFilePath = "output_file_path" - + let removeExpectation = expectation(description: "remove was called twice") removeExpectation.expectedFulfillmentCount = 2 - + var expectedHandleCreateFileCalls = [ "πŸͺ΅ [test] log\n", "πŸͺ΅ [test] log\n\n🐞 [test] debug\n" ] - + var fileHandler = MockFileHandler() fileHandler.handleRemoveItem = { path in XCTAssertEqual(path, outputFilePath) @@ -67,14 +67,14 @@ class LoggerTests: XCTestCase { XCTAssertEqual(String(data: data, encoding: .utf8), expectedInput) return true } - + let logFileLogger = LogFileLogger(fileHandler: fileHandler, outputFilePath: outputFilePath) - + logFileLogger.log("log", from: "test") // Small sleep because the file manager calls are done on a detached Task and we want to guarantee the order try await Task.sleep(for: .milliseconds(10)) logFileLogger.debug("debug", from: "test") - + await fulfillment(of: [removeExpectation]) XCTAssertTrue(expectedHandleCreateFileCalls.isEmpty) } diff --git a/Tests/UnitTests/OutputGeneratorTests.swift b/Tests/UnitTests/OutputGeneratorTests.swift index 26e8bc4..566a145 100644 --- a/Tests/UnitTests/OutputGeneratorTests.swift +++ b/Tests/UnitTests/OutputGeneratorTests.swift @@ -8,9 +8,9 @@ import XCTest class OutputGeneratorTests: XCTestCase { - + func test_noChanges_singleModule() { - + let expectedOutput = """ # βœ… No changes detected _Comparing `new_source` to `old_source`_ @@ -18,7 +18,7 @@ class OutputGeneratorTests: XCTestCase { --- **Analyzed targets:** Target_1 """ - + let outputGenerator = MarkdownOutputGenerator() let output = outputGenerator.generate( from: [:], @@ -29,9 +29,9 @@ class OutputGeneratorTests: XCTestCase { ) XCTAssertEqual(output, expectedOutput) } - + func test_oneChange_singleModule() { - + let expectedOutput = """ # πŸ‘€ 1 public change detected _Comparing `new_source` to `old_source`_ @@ -46,9 +46,9 @@ class OutputGeneratorTests: XCTestCase { --- **Analyzed targets:** Target_1 """ - + let outputGenerator = MarkdownOutputGenerator() - + let output = outputGenerator.generate( from: ["Target_1": [.init(changeType: .addition(description: "Some Addition"), parentPath: "")]], allTargets: ["Target_1"], @@ -58,9 +58,9 @@ class OutputGeneratorTests: XCTestCase { ) XCTAssertEqual(output, expectedOutput) } - + func test_multipleChanges_multipleModules() { - + let expectedOutput = """ # πŸ‘€ 4 public changes detected _Comparing `new_source` to `old_repository @ old_branch`_ @@ -90,7 +90,7 @@ class OutputGeneratorTests: XCTestCase { """ let outputGenerator = MarkdownOutputGenerator() - + let output = outputGenerator.generate( from: [ "Target_1": [ diff --git a/Tests/UnitTests/PipelineTests.swift b/Tests/UnitTests/PipelineTests.swift index 7f9436f..6491c35 100644 --- a/Tests/UnitTests/PipelineTests.swift +++ b/Tests/UnitTests/PipelineTests.swift @@ -9,15 +9,15 @@ import XCTest class PipelineTests: XCTestCase { - + func test_pipeline() async throws { - + let swiftInterfaceFile = SwiftInterfaceFile( name: "MODULE_NAME", oldFilePath: "old_file_path", newFilePath: "new_file_path" ) - + let expectedChanges: [Change] = [.init(changeType: .addition(description: "addition"))] var expectedHandleLoadDataCalls = ["new_file_path", "old_file_path"] var expectedHandleParseSourceCalls: [(source: String, moduleName: String)] = [ @@ -31,16 +31,16 @@ class PipelineTests: XCTestCase { ("πŸ§‘β€πŸ”¬ Analyzing MODULE_NAME", "SwiftInterfaceDiff") ] let expectedPipelineOutput: [String: [Change]] = ["MODULE_NAME": expectedChanges] - + // Mock Setup - + var fileHandler = MockFileHandler() fileHandler.handleLoadData = { path in let expectedInput = expectedHandleLoadDataCalls.removeFirst() XCTAssertEqual(path, expectedInput) return try XCTUnwrap(String("content_for_\(path)").data(using: .utf8)) } - + var swiftInterfaceParser = MockSwiftInterfaceParser() swiftInterfaceParser.handleParseSource = { source, moduleName in let expectedInput = expectedHandleParseSourceCalls.removeFirst() @@ -48,7 +48,7 @@ class PipelineTests: XCTestCase { XCTAssertEqual(expectedInput.moduleName, moduleName) return SwiftInterfaceParser.Root(moduleName: moduleName, elements: []) } - + var swiftInterfaceAnalyzer = MockSwiftInterfaceAnalyzer() swiftInterfaceAnalyzer.handleAnalyze = { old, new in let expectedInput = expectedHandleAnalyzeCalls.removeFirst() @@ -56,27 +56,27 @@ class PipelineTests: XCTestCase { XCTAssertEqual(new.recursiveDescription(), expectedInput.new.recursiveDescription()) return expectedChanges } - + var logger = MockLogger() logger.handleLog = { message, subsystem in let expectedInput = expectedHandleLogCalls.removeFirst() XCTAssertEqual(message, expectedInput.message) XCTAssertEqual(subsystem, expectedInput.subsystem) } - + // Pipeline run - + let pipeline = SwiftInterfaceDiff( fileHandler: fileHandler, swiftInterfaceParser: swiftInterfaceParser, swiftInterfaceAnalyzer: swiftInterfaceAnalyzer, logger: logger ) - + let pipelineOutput = try await pipeline.run(with: [swiftInterfaceFile]) - + // Validation - + XCTAssertEqual(pipelineOutput, expectedPipelineOutput) XCTAssertTrue(expectedHandleLoadDataCalls.isEmpty) XCTAssertTrue(expectedHandleParseSourceCalls.isEmpty) diff --git a/Tests/UnitTests/ProjectSourceTests.swift b/Tests/UnitTests/ProjectSourceTests.swift index 4da7aee..c4858d4 100644 --- a/Tests/UnitTests/ProjectSourceTests.swift +++ b/Tests/UnitTests/ProjectSourceTests.swift @@ -8,42 +8,42 @@ import XCTest class ProjectSourceTests: XCTestCase { - + // MARK: - Remote - + func test_remote_validSource() throws { let repositoryUrl = "https://github.com/Adyen/adyen-ios.git" let separator = ProjectSource.gitSourceSeparator let branch = "develop" let rawProjectSourceValue = "\(branch)\(separator)\(repositoryUrl)" - + let mockFileHandler = MockFileHandler(handleFileExists: { XCTAssertEqual($0, rawProjectSourceValue) return false }) - + let projectSource = try ProjectSource.from( "\(branch)\(separator)\(repositoryUrl)", fileHandler: mockFileHandler ) - + XCTAssertEqual( projectSource, .git(branch: branch, repository: repositoryUrl) ) } - + func test_remote_invalidRepoUrl() throws { let repositoryUrl = "" let separator = ProjectSource.gitSourceSeparator let branch = "develop" let rawProjectSourceValue = "\(branch)\(separator)\(repositoryUrl)" - + let mockFileHandler = MockFileHandler(handleFileExists: { XCTAssertEqual($0, rawProjectSourceValue) return false }) - + do { let source = try ProjectSource.from( rawProjectSourceValue, @@ -52,25 +52,25 @@ class ProjectSourceTests: XCTestCase { XCTAssertNil(source) // Guard to make sure that we catch if it succeeds } catch { let projectSourceError = try XCTUnwrap(error as? ProjectSource.Error) - + XCTAssertEqual( projectSourceError, ProjectSource.Error.invalidSourceValue(value: rawProjectSourceValue) ) } } - + func test_remote_invalidSeparator() throws { let repositoryUrl = "https://github.com/Adyen/adyen-ios.git" let separator = "_" let branch = "develop" let rawProjectSourceValue = "\(branch)\(separator)\(repositoryUrl)" - + let mockFileHandler = MockFileHandler(handleFileExists: { XCTAssertEqual($0, rawProjectSourceValue) return false }) - + do { let source = try ProjectSource.from( rawProjectSourceValue, @@ -79,46 +79,46 @@ class ProjectSourceTests: XCTestCase { XCTAssertNil(source) // Guard to make sure that we catch if it succeeds } catch { let projectSourceError = try XCTUnwrap(error as? ProjectSource.Error) - + XCTAssertEqual( projectSourceError, ProjectSource.Error.invalidSourceValue(value: rawProjectSourceValue) ) } } - + // MARK: - Local - + func test_local_validSource() throws { let repositoryDirectoryPath = "/Some/Repository/Directory" let mockFileHandler = MockFileHandler(handleFileExists: { XCTAssertEqual($0, repositoryDirectoryPath) return true }) - + let projectSource = try ProjectSource.from( repositoryDirectoryPath, fileHandler: mockFileHandler ) - + XCTAssertEqual( projectSource, .local(path: repositoryDirectoryPath) ) } - + func test_local_nonExistentDirectory() throws { let repositoryDirectoryPath = "/Some/Repository/Directory" let mockFileHandler = MockFileHandler(handleFileExists: { XCTAssertEqual($0, repositoryDirectoryPath) return false }) - + let projectSource = try? ProjectSource.from( repositoryDirectoryPath, fileHandler: mockFileHandler ) - + XCTAssertNil(projectSource) } } diff --git a/Tests/UnitTests/SwiftPackageFileAnalyzerTests.swift b/Tests/UnitTests/SwiftPackageFileAnalyzerTests.swift index 599e656..8f21a4b 100644 --- a/Tests/UnitTests/SwiftPackageFileAnalyzerTests.swift +++ b/Tests/UnitTests/SwiftPackageFileAnalyzerTests.swift @@ -13,12 +13,12 @@ import XCTest class SwiftPackageFileAnalyzerTests: XCTestCase { - + func test_noPackageLibraryDifferences_causeNoChanges() throws { - + let handleFileExpectation = expectation(description: "handleFileExists is called twice") handleFileExpectation.expectedFulfillmentCount = 2 - + var fileHandler = MockFileHandler() fileHandler.handleFileExists = { _ in handleFileExpectation.fulfill() @@ -34,39 +34,39 @@ class SwiftPackageFileAnalyzerTests: XCTestCase { let encodedPackageDescription = try! JSONEncoder().encode(packageDescription) return String(data: encodedPackageDescription, encoding: .utf8)! } - + let projectAnalyzer = SwiftPackageFileAnalyzer( fileHandler: fileHandler, shell: shell, logger: nil ) - + let changes = try projectAnalyzer.analyze( oldProjectUrl: URL(filePath: "NewPackage"), newProjectUrl: URL(filePath: "NewPackage") ) - + let expectedChanges: [Change] = [] XCTAssertEqual(changes.changes, expectedChanges) - + waitForExpectations(timeout: 1) } - + func test_packageLibraryDifferences_causeChanges() throws { - + let handleFileExpectation = expectation(description: "handleFileExists is called twice") handleFileExpectation.expectedFulfillmentCount = 2 - + var fileHandler = MockFileHandler() fileHandler.handleFileExists = { _ in handleFileExpectation.fulfill() return true } - + var shell = MockShell() shell.handleExecute = { command in let packageDescription: SwiftPackageDescription - + if command.range(of: "NewPackage") != nil { packageDescription = SwiftPackageDescription( defaultLocalization: "en-us", @@ -112,22 +112,22 @@ class SwiftPackageFileAnalyzerTests: XCTestCase { toolsVersion: "2.0" ) } - + let encodedPackageDescription = try! JSONEncoder().encode(packageDescription) return String(data: encodedPackageDescription, encoding: .utf8)! } - + let projectAnalyzer = SwiftPackageFileAnalyzer( fileHandler: fileHandler, shell: shell, logger: nil ) - + let changes = try projectAnalyzer.analyze( oldProjectUrl: URL(filePath: "OldPackage"), newProjectUrl: URL(filePath: "NewPackage") ) - + let expectedChanges: [Change] = [ .init( changeType: .change( @@ -220,16 +220,16 @@ class SwiftPackageFileAnalyzerTests: XCTestCase { listOfChanges: [] ) ] - + XCTAssertEqual(changes.changes, expectedChanges) - + waitForExpectations(timeout: 1) } - + func test_project_causesNoChanges() throws { - + let handleFileExpectation = expectation(description: "handleFileExists is called once") - + var fileHandler = MockFileHandler() fileHandler.handleFileExists = { _ in handleFileExpectation.fulfill() @@ -239,15 +239,15 @@ class SwiftPackageFileAnalyzerTests: XCTestCase { fileHandler: fileHandler, logger: nil ) - + let changes = try projectAnalyzer.analyze( oldProjectUrl: URL(filePath: "OldProject"), newProjectUrl: URL(filePath: "NewProject") ) - + let expectedChanges: [Change] = [] XCTAssertEqual(changes.changes, expectedChanges) - + waitForExpectations(timeout: 1) } } diff --git a/Tests/UnitTests/Utilities/MockFileHandler.swift b/Tests/UnitTests/Utilities/MockFileHandler.swift index c773d79..923b6b9 100644 --- a/Tests/UnitTests/Utilities/MockFileHandler.swift +++ b/Tests/UnitTests/Utilities/MockFileHandler.swift @@ -8,57 +8,57 @@ import XCTest struct MockFileHandler: FileHandling { - + var currentDirectoryPath: String = "" - + var handleRemoveItem: (String) throws -> Void = { _ in XCTFail("Unexpectedly called `\(#function)`") } - + var handleContentsOfDirectory: (String) throws -> [String] = { _ in XCTFail("Unexpectedly called `\(#function)`") return [] } - + var handleCreateDirectory: (String) throws -> Void = { _ in XCTFail("Unexpectedly called `\(#function)`") } - + var handleCreateFile: (String, Data) -> Bool = { _, _ in XCTFail("Unexpectedly called `\(#function)`") return false } - + var handleFileExists: (String) -> Bool = { XCTFail("Unexpectedly called `fileExists(atPath: \($0))`") return false } - + var handleLoadData: (String) throws -> Data = { XCTFail("Unexpectedly called `contents(atPath: \($0))`") return .init() } - + func removeItem(atPath path: String) throws { try handleRemoveItem(path) } - + func contentsOfDirectory(atPath path: String) throws -> [String] { try handleContentsOfDirectory(path) } - + func createDirectory(atPath path: String) throws { try handleCreateDirectory(path) } - + func createFile(atPath path: String, contents data: Data) -> Bool { handleCreateFile(path, data) } - + func fileExists(atPath path: String) -> Bool { handleFileExists(path) } - + func loadData(from path: String) throws -> Data { try handleLoadData(path) } diff --git a/Tests/UnitTests/Utilities/MockLogger.swift b/Tests/UnitTests/Utilities/MockLogger.swift index f82c362..019ec0e 100644 --- a/Tests/UnitTests/Utilities/MockLogger.swift +++ b/Tests/UnitTests/Utilities/MockLogger.swift @@ -8,19 +8,19 @@ import XCTest struct MockLogger: Logging { - + var handleLog: (String, String) -> Void = { _, _ in XCTFail("Unexpectedly called `\(#function)`") } - + var handleDebug: (String, String) -> Void = { _, _ in XCTFail("Unexpectedly called `\(#function)`") } - + func log(_ message: String, from subsystem: String) { handleLog(message, subsystem) } - + func debug(_ message: String, from subsystem: String) { handleDebug(message, subsystem) } diff --git a/Tests/UnitTests/Utilities/MockPipelineModules/MockLibraryAnalyzer.swift b/Tests/UnitTests/Utilities/MockPipelineModules/MockLibraryAnalyzer.swift index cb1cd37..8414e54 100644 --- a/Tests/UnitTests/Utilities/MockPipelineModules/MockLibraryAnalyzer.swift +++ b/Tests/UnitTests/Utilities/MockPipelineModules/MockLibraryAnalyzer.swift @@ -9,9 +9,9 @@ import XCTest struct MockSwiftPackageFileAnalyzer: SwiftPackageFileAnalyzing { - + var onAnalyze: (URL, URL) throws -> SwiftPackageFileAnalyzingResult - + func analyze(oldProjectUrl: URL, newProjectUrl: URL) throws -> SwiftPackageFileAnalyzingResult { try onAnalyze(oldProjectUrl, newProjectUrl) } diff --git a/Tests/UnitTests/Utilities/MockPipelineModules/MockOutputGenerator.swift b/Tests/UnitTests/Utilities/MockPipelineModules/MockOutputGenerator.swift index 0b958b3..283cab0 100644 --- a/Tests/UnitTests/Utilities/MockPipelineModules/MockOutputGenerator.swift +++ b/Tests/UnitTests/Utilities/MockPipelineModules/MockOutputGenerator.swift @@ -11,7 +11,7 @@ import XCTest struct MockOutputGenerator: OutputGenerating { var onGenerate: ([String: [Change]], [String]?, String?, String?, [String]) throws -> String - + func generate( from changesPerTarget: [String: [Change]], allTargets: [String]?, diff --git a/Tests/UnitTests/Utilities/MockRandomStringGenerator.swift b/Tests/UnitTests/Utilities/MockRandomStringGenerator.swift index 0d0173c..cceec04 100644 --- a/Tests/UnitTests/Utilities/MockRandomStringGenerator.swift +++ b/Tests/UnitTests/Utilities/MockRandomStringGenerator.swift @@ -8,12 +8,12 @@ import XCTest struct MockRandomStringGenerator: RandomStringGenerating { - + var onGenerateRandomString: () -> String = { XCTFail("Unexpectedly called `\(#function)`") return "" } - + func generateRandomString() -> String { onGenerateRandomString() } diff --git a/Tests/UnitTests/Utilities/MockShell.swift b/Tests/UnitTests/Utilities/MockShell.swift index b2e102c..565aa0f 100644 --- a/Tests/UnitTests/Utilities/MockShell.swift +++ b/Tests/UnitTests/Utilities/MockShell.swift @@ -8,12 +8,12 @@ import XCTest struct MockShell: ShellHandling { - + var handleExecute: (String) -> String = { _ in XCTFail("Unexpectedly called `\(#function)`") return "" } - + @discardableResult func execute(_ command: String) -> String { handleExecute(command) diff --git a/Tests/UnitTests/Utilities/MockSwiftInterfaceAnalyzer.swift b/Tests/UnitTests/Utilities/MockSwiftInterfaceAnalyzer.swift index 91b6c0f..28cd345 100644 --- a/Tests/UnitTests/Utilities/MockSwiftInterfaceAnalyzer.swift +++ b/Tests/UnitTests/Utilities/MockSwiftInterfaceAnalyzer.swift @@ -9,12 +9,12 @@ import XCTest struct MockSwiftInterfaceAnalyzer: SwiftInterfaceAnalyzing { - + var handleAnalyze: (any SwiftInterfaceElement, any SwiftInterfaceElement) -> [Change] = { _, _ in XCTFail("Unexpectedly called `\(#function)`") return [] } - + func analyze(old: some SwiftInterfaceElement, new: some SwiftInterfaceElement) throws -> [Change] { handleAnalyze(old, new) } diff --git a/Tests/UnitTests/Utilities/MockSwiftInterfaceParser.swift b/Tests/UnitTests/Utilities/MockSwiftInterfaceParser.swift index 27485a8..d6e3b57 100644 --- a/Tests/UnitTests/Utilities/MockSwiftInterfaceParser.swift +++ b/Tests/UnitTests/Utilities/MockSwiftInterfaceParser.swift @@ -8,12 +8,12 @@ import XCTest struct MockSwiftInterfaceParser: SwiftInterfaceParsing { - + var handleParseSource: (String, String) -> any SwiftInterfaceElement = { _, _ in XCTFail("Unexpectedly called `\(#function)`") return SwiftInterfaceParser.Root(moduleName: "Module Name", elements: []) } - + func parse(source: String, moduleName: String) -> any SwiftInterfaceElement { handleParseSource(source, moduleName) } diff --git a/Tests/UnitTests/XcodeToolsTests.swift b/Tests/UnitTests/XcodeToolsTests.swift index 32b6238..d43a31a 100644 --- a/Tests/UnitTests/XcodeToolsTests.swift +++ b/Tests/UnitTests/XcodeToolsTests.swift @@ -8,24 +8,24 @@ import XCTest class XcodeToolsTests: XCTestCase { - + func test_archive_swiftPackage() async throws { - + let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" let scheme = "SCHEME" - + try await testArchiving( projectDirectoryPath: projectDirectoryPath, scheme: scheme, projectType: .swiftPackage ) } - + func test_archive_xcodeProject() async throws { - + let projectDirectoryPath = "PROJECT_DIRECTORY_PATH" let scheme = "SCHEME" - + try await testArchiving( projectDirectoryPath: projectDirectoryPath, scheme: scheme, @@ -35,13 +35,13 @@ class XcodeToolsTests: XCTestCase { } private extension XcodeToolsTests { - + func testArchiving( projectDirectoryPath: String, scheme: String, projectType: ProjectType ) async throws { - + let archiveResult = "ARCHIVE_RESULT" let expectedDerivedDataPath = "\(projectDirectoryPath)/.build" var expectedHandleExecuteCalls: [String] = { @@ -59,7 +59,7 @@ private extension XcodeToolsTests { (archiveResult, "XcodeTools") ] var expectedHandleFileExistsCalls = ["PROJECT_DIRECTORY_PATH/.build"] - + var shell = MockShell() shell.handleExecute = { command in let expectedInput = expectedHandleExecuteCalls.removeFirst() @@ -83,19 +83,19 @@ private extension XcodeToolsTests { XCTAssertEqual(message, expectedInput.message) XCTAssertEqual(subsystem, expectedInput.subsystem) } - + let xcodeTools = XcodeTools( shell: shell, fileHandler: fileHandler, logger: logger ) - + let derivedDataPath = try await xcodeTools.archive( projectDirectoryPath: projectDirectoryPath, scheme: scheme, projectType: projectType ) - + XCTAssertEqual(derivedDataPath, expectedDerivedDataPath) XCTAssertTrue(expectedHandleExecuteCalls.isEmpty) XCTAssertTrue(expectedHandleLogCalls.isEmpty)