diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c2ed261..493868e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ ##### Enhancements -- None. +- Added support for Swift Testing. ##### Bug Fixes diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index 16a566d88..5cc503a1c 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -79,6 +79,7 @@ swift_library( "SourceGraph/Mutators/ResultBuilderRetainer.swift", "SourceGraph/Mutators/StringInterpolationAppendInterpolationRetainer.swift", "SourceGraph/Mutators/StructImplicitInitializerReferenceBuilder.swift", + "SourceGraph/Mutators/SwiftTestingRetainer.swift", "SourceGraph/Mutators/SwiftUIRetainer.swift", "SourceGraph/Mutators/UnusedImportMarker.swift", "SourceGraph/Mutators/UnusedParameterRetainer.swift", diff --git a/Sources/Indexer/SwiftIndexer.swift b/Sources/Indexer/SwiftIndexer.swift index 7adb442ff..8581cd413 100644 --- a/Sources/Indexer/SwiftIndexer.swift +++ b/Sources/Indexer/SwiftIndexer.swift @@ -229,6 +229,7 @@ final class SwiftIndexer: Indexer { multiplexingSyntaxVisitor.visit() sourceFile.importStatements = importSyntaxVisitor.importStatements + sourceFile.importsSwiftTesting = importSyntaxVisitor.importStatements.contains(where: { $0.module == "Testing" }) if !configuration.disableUnusedImportAnalysis { for stmt in sourceFile.importStatements where stmt.isExported { diff --git a/Sources/SourceGraph/Elements/SourceFile.swift b/Sources/SourceGraph/Elements/SourceFile.swift index 69e916000..e8944df84 100644 --- a/Sources/SourceGraph/Elements/SourceFile.swift +++ b/Sources/SourceGraph/Elements/SourceFile.swift @@ -5,6 +5,7 @@ public class SourceFile { public let path: FilePath public let modules: Set public var importStatements: [ImportStatement] = [] + public var importsSwiftTesting = false public init(path: FilePath, modules: Set) { self.path = path diff --git a/Sources/SourceGraph/Mutators/SwiftTestingRetainer.swift b/Sources/SourceGraph/Mutators/SwiftTestingRetainer.swift new file mode 100644 index 000000000..f8feb4886 --- /dev/null +++ b/Sources/SourceGraph/Mutators/SwiftTestingRetainer.swift @@ -0,0 +1,35 @@ +import Configuration +import Foundation +import Shared + +/// Retains Swift Testing declarations. +/// https://developer.apple.com/xcode/swift-testing/ +final class SwiftTestingRetainer: SourceGraphMutator { + private let graph: SourceGraph + + required init(graph: SourceGraph, configuration _: Configuration, swiftVersion _: SwiftVersion) { + self.graph = graph + } + + func mutate() { + for decl in graph.declarations(ofKinds: [.class, .struct]) { + guard decl.location.file.importsSwiftTesting else { continue } + + if decl.attributes.contains("Suite") { + graph.markRetained(decl) + } + } + + for decl in graph.declarations(ofKinds: [.functionFree, .functionMethodInstance, .functionMethodClass, .functionMethodStatic]) { + guard decl.location.file.importsSwiftTesting else { continue } + + if decl.attributes.contains("Test") { + graph.markRetained(decl) + + if let parent = decl.parent { + graph.markRetained(parent) + } + } + } + } +} diff --git a/Sources/SourceGraph/SourceGraphMutatorRunner.swift b/Sources/SourceGraph/SourceGraphMutatorRunner.swift index f67ddeb58..43ca4de8a 100644 --- a/Sources/SourceGraph/SourceGraphMutatorRunner.swift +++ b/Sources/SourceGraph/SourceGraphMutatorRunner.swift @@ -36,6 +36,7 @@ public final class SourceGraphMutatorRunner { EntryPointAttributeRetainer.self, PubliclyAccessibleRetainer.self, XCTestRetainer.self, + SwiftTestingRetainer.self, SwiftUIRetainer.self, StringInterpolationAppendInterpolationRetainer.self, PropertyWrapperRetainer.self, diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSwiftTestingDeclarations.swift b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSwiftTestingDeclarations.swift new file mode 100644 index 000000000..80051ae9d --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSwiftTestingDeclarations.swift @@ -0,0 +1,22 @@ +#if canImport(Testing) +import Testing + +@Test func swiftTestingFreeFunction() {} + +class SwiftTestingClass { + @Test("displayName") func instanceMethod() {} + @Test class func classMethod() {} + @Test static func staticMethod() {} +} + +@Suite struct SwiftTestingStructWithSuite { + @Test func instanceMethod() {} + @Test("displayName") static func staticMethod() {} +} + +@Suite("displayName") class SwiftTestingClassWithSuite { + @Test func instanceMethod() {} + @Test("displayName") class func classMethod() {} + @Test static func staticMethod() {} +} +#endif diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index 0b5d6d687..acc7a2d2d 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -1059,6 +1059,33 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } + // MARK: - Swift Testing + + #if canImport(Testing) + func testRetainsSwiftTestingDeclarations() { + analyze { + assertReferenced(.functionFree("swiftTestingFreeFunction()")) + + assertReferenced(.class("SwiftTestingClass")) { + self.assertReferenced(.functionMethodInstance("instanceMethod()")) + self.assertReferenced(.functionMethodClass("classMethod()")) + self.assertReferenced(.functionMethodStatic("staticMethod()")) + } + + assertReferenced(.struct("SwiftTestingStructWithSuite")) { + self.assertReferenced(.functionMethodInstance("instanceMethod()")) + self.assertReferenced(.functionMethodStatic("staticMethod()")) + } + + assertReferenced(.class("SwiftTestingClassWithSuite")) { + self.assertReferenced(.functionMethodInstance("instanceMethod()")) + self.assertReferenced(.functionMethodClass("classMethod()")) + self.assertReferenced(.functionMethodStatic("staticMethod()")) + } + } + } + #endif + // MARK: - Assign-only properties func testStructImplicitInitializer() {