diff --git a/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift b/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift index 523913633..6f26b1442 100644 --- a/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift +++ b/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift @@ -10,8 +10,11 @@ final class DynamicMemberRetainer: SourceGraphMutator { } func mutate() throws { + // Retain all subscript(dynamicMember:) declarations. This signature is specific to + // @dynamicMemberLookup and may be declared in extensions of external types where we + // cannot verify the attribute exists. for decl in graph.declarations(ofKind: .functionSubscript) { - if decl.name == "subscript(dynamicMember:)", decl.parent?.attributes.contains(where: { $0.name == "dynamicMemberLookup" }) ?? false { + if decl.name == "subscript(dynamicMember:)" { graph.markRetained(decl) } } diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testRetainsDynamicMemberLookupSubscriptInExternalTypeExtension.swift b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsDynamicMemberLookupSubscriptInExternalTypeExtension.swift new file mode 100644 index 000000000..922a736bd --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsDynamicMemberLookupSubscriptInExternalTypeExtension.swift @@ -0,0 +1,21 @@ +import Foundation + +// Extension on external @dynamicMemberLookup type (AttributeDynamicLookup) +public extension AttributeDynamicLookup { + subscript( + dynamicMember keyPath: KeyPath + ) -> T { + self[T.self] + } +} + +public extension AttributeScopes { + struct FixtureAttributes: AttributeScope { + let myAttribute: MyFixtureAttribute + } +} + +public enum MyFixtureAttribute: AttributedStringKey { + public typealias Value = String + public static let name = "MyFixtureAttribute" +} diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index 1d1031037..35c2d6778 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -946,6 +946,14 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } + func testRetainsDynamicMemberLookupSubscriptInExternalTypeExtension() { + analyze(retainPublic: true) { + assertReferenced(.extensionEnum("AttributeDynamicLookup")) { + self.assertReferenced(.functionSubscript("subscript(dynamicMember:)")) + } + } + } + func testRetainsCodableProperties() { analyze( retainPublic: true, diff --git a/Tests/Shared/DeclarationDescription.swift b/Tests/Shared/DeclarationDescription.swift index 487069331..ff0bc5218 100644 --- a/Tests/Shared/DeclarationDescription.swift +++ b/Tests/Shared/DeclarationDescription.swift @@ -105,4 +105,8 @@ struct DeclarationDescription: CustomStringConvertible { static func extensionClass(_ name: String, line: Int? = nil) -> Self { self.init(kind: .extensionClass, name: name, line: line) } + + static func extensionEnum(_ name: String, line: Int? = nil) -> Self { + self.init(kind: .extensionEnum, name: name, line: line) + } }