From 62de7b303c6b697e761c4ec3ed9027e6308ef937 Mon Sep 17 00:00:00 2001 From: Rob Napier Date: Wed, 4 Feb 2026 09:44:18 -0500 Subject: [PATCH] Fix false positive for subscript(dynamicMember:) in external type extensions Periphery incorrectly reported subscript(dynamicMember:) as unused when declared in an extension of an external type with @dynamicMemberLookup (e.g., Foundation's AttributeDynamicLookup). The fix retains all subscript(dynamicMember:) declarations unconditionally, since this signature is specific to Swift's @dynamicMemberLookup mechanism. Fixes #1066 --- .../Mutators/DynamicMemberRetainer.swift | 5 ++++- ...okupSubscriptInExternalTypeExtension.swift | 21 +++++++++++++++++++ Tests/PeripheryTests/RetentionTest.swift | 8 +++++++ Tests/Shared/DeclarationDescription.swift | 4 ++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Tests/Fixtures/Sources/RetentionFixtures/testRetainsDynamicMemberLookupSubscriptInExternalTypeExtension.swift diff --git a/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift b/Sources/SourceGraph/Mutators/DynamicMemberRetainer.swift index 5239136334..6f26b1442b 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 0000000000..922a736bdb --- /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 1d10310373..35c2d67780 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 487069331b..ff0bc5218e 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) + } }