diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b00cb570..156fbd5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ##### Bug Fixes -- None. +- Fix redundant public accessibility analysis for protocol members declared in extensions that are referenced cross-module where the protocol itself is not. ## 2.16.0 (2023-09-27) diff --git a/Sources/PeripheryKit/Indexer/Declaration.swift b/Sources/PeripheryKit/Indexer/Declaration.swift index 552feb5c9..c1b8db523 100644 --- a/Sources/PeripheryKit/Indexer/Declaration.swift +++ b/Sources/PeripheryKit/Indexer/Declaration.swift @@ -314,4 +314,8 @@ public struct DeclarationAccessibility { public func isExplicitly(_ testValue: Accessibility) -> Bool { isExplicit && value == testValue } + + var isAccessibleCrossModule: Bool { + value == .public || value == .open + } } diff --git a/Sources/PeripheryKit/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift b/Sources/PeripheryKit/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift index beac7e888..9e6853683 100644 --- a/Sources/PeripheryKit/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift +++ b/Sources/PeripheryKit/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift @@ -34,7 +34,11 @@ final class RedundantExplicitPublicAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { // Check if the declaration is public, and is referenced cross module. if decl.accessibility.isExplicitly(.public) { - if try !isReferencedCrossModule(decl) && !isExposedPubliclyByAnotherDeclaration(decl) && !graph.isRetained(decl) { + if !graph.isRetained(decl) && + !isReferencedCrossModule(decl) && + !isExposedPubliclyByAnotherDeclaration(decl) && + !isProtocolIndirectlyReferencedCrossModuleByExtensionMember(decl) + { // Public accessibility is redundant. mark(decl) markExplicitPublicDescendentDeclarations(from: decl) @@ -108,17 +112,50 @@ final class RedundantExplicitPublicAccessibilityMarker: SourceGraphMutator { return nil } - return referenceDecls.contains { refDecl in - refDecl.accessibility.value == .public || refDecl.accessibility.value == .open - } + return referenceDecls.contains { $0.accessibility.isAccessibleCrossModule } + } + + /// A public protocol that is not directly referenced cross-module may still be exposed by a public member declared + /// within an extension that is accessed on a conforming type. + /// + /// // TargetA + /// public protocol MyProtocol {} + /// public extension MyProtocol { + /// func someExtensionFunc() {} + /// } + /// public class MyClass: MyProtocol {} + /// + /// // TargetB + /// let cls = MyClass() + /// cls.someExtensionFunc() + /// + private func isProtocolIndirectlyReferencedCrossModuleByExtensionMember(_ decl: Declaration) -> Bool { + guard decl.kind == .protocol else { return false } + + return graph.references(to: decl) + .lazy + .compactMap { ref -> Declaration? in + guard let parent = ref.parent else { return nil } + + if parent.kind == .extensionProtocol && parent.name == decl.name { + return parent + } + + return nil + } + .contains { + $0.declarations.contains { + $0.accessibility.value == .public && isReferencedCrossModule($0) + } + } } - private func isReferencedCrossModule(_ decl: Declaration) throws -> Bool { - let referenceModules = try nonTestableModulesReferencing(decl) + private func isReferencedCrossModule(_ decl: Declaration) -> Bool { + let referenceModules = nonTestableModulesReferencing(decl) return !referenceModules.subtracting(decl.location.file.modules).isEmpty } - private func nonTestableModulesReferencing(_ decl: Declaration) throws -> Set { + private func nonTestableModulesReferencing(_ decl: Declaration) -> Set { let referenceFiles = graph.references(to: decl).map { $0.location.file } let referenceModules = referenceFiles.flatMapSet { file -> Set in diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index 2d44e240e..c4a8be4cd 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -31,6 +31,8 @@ _ = PublicTypeUsedAsPublicFunctionMetatypeParameterWithGenericReturnTypeRetainer _ = NotRedundantPublicTestableImportClass().testableProperty +ProtocolIndirectlyReferencedCrossModuleByExtensionMemberImpl().somePublicFunc() + // Typealias let _: PublicTypealiasWithClosureType? = nil @@ -56,4 +58,3 @@ _ = InternalProtocolRefiningPublicProtocolRetainer() // Closure let _ = PublicTypeUsedInPublicClosureRetainer().closure - diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolIndirectlyReferencedCrossModuleByExtensionMember.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolIndirectlyReferencedCrossModuleByExtensionMember.swift new file mode 100644 index 000000000..52d276236 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolIndirectlyReferencedCrossModuleByExtensionMember.swift @@ -0,0 +1,7 @@ +public protocol ProtocolIndirectlyReferencedCrossModuleByExtensionMember {} +public extension ProtocolIndirectlyReferencedCrossModuleByExtensionMember { + func somePublicFunc() {} +} +public class ProtocolIndirectlyReferencedCrossModuleByExtensionMemberImpl: ProtocolIndirectlyReferencedCrossModuleByExtensionMember { + public init() {} +} diff --git a/Tests/AccessibilityTests/RedundantPublicAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantPublicAccessibilityTest.swift index e86d03816..1ce19fbd0 100644 --- a/Tests/AccessibilityTests/RedundantPublicAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantPublicAccessibilityTest.swift @@ -229,4 +229,27 @@ class RedundantPublicAccessibilityTest: SourceGraphTestCase { // Destructured binding control. assertRedundantPublicAccessibility(.protocol("PublicTypeUsedAsPublicFunctionMetatypeParameterWithGenericReturnType4")) } + + /// A public protocol that is not directly referenced cross-module may still be exposed by a public member declared + /// within an extension that is accessed on a conforming type. + /// + /// // TargetA + /// public protocol MyProtocol {} + /// public extension MyProtocol { + /// func someExtensionFunc() {} + /// } + /// public class MyClass: MyProtocol {} + /// + /// // TargetB + /// let cls = MyClass() + /// cls.someExtensionFunc() + /// + func testPublicProtocolIndirectlyReferencedByExtensionMember() { + Self.index() + + assertNotRedundantPublicAccessibility(.protocol("ProtocolIndirectlyReferencedCrossModuleByExtensionMember")) + assertNotRedundantPublicAccessibility(.extensionProtocol("ProtocolIndirectlyReferencedCrossModuleByExtensionMember")) { + self.assertNotRedundantPublicAccessibility(.functionMethodInstance("somePublicFunc()")) + } + } }