From 2342c8f6eb22dfee7ffd48a276f1da701d04be46 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:17:01 -0700 Subject: [PATCH 01/31] Initial commit adding redundant internal and redundant fileprivate access --- Sources/BUILD.bazel | 2 + Sources/Configuration/Configuration.swift | 7 ++ Sources/Frontend/Commands/ScanCommand.swift | 8 ++ .../Results/OutputFormatter.swift | 10 +++ Sources/PeripheryKit/ScanResult.swift | 2 + Sources/PeripheryKit/ScanResultBuilder.swift | 10 +++ .../SourceGraph/Elements/Declaration.swift | 2 + .../Mutators/ExtensionReferenceBuilder.swift | 13 +++ ...undantFilePrivateAccessibilityMarker.swift | 70 ++++++++++++++++ ...RedundantInternalAccessibilityMarker.swift | 82 +++++++++++++++++++ Sources/SourceGraph/SourceGraph.swift | 21 +++++ .../SourceGraphMutatorRunner.swift | 4 + .../TargetA/InternalPropertyExtension.swift | 9 ++ .../TargetA/InternalPropertyOwner.swift | 4 + .../InternalPropertyUsedInExtension.swift | 8 ++ .../InternalPropertyUserExtension.swift | 6 ++ .../NotRedundantFilePrivateComponents.swift | 28 +++++++ .../NotRedundantInternalClassComponents.swift | 22 +++++ ...ndantInternalClassComponents_Support.swift | 16 ++++ .../RedundantFilePrivateComponents.swift | 19 +++++ .../TargetA/RedundantInternalComponents.swift | 19 +++++ ...edundantFilePrivateAccessibilityTest.swift | 22 +++++ .../RedundantInternalAccessibilityTest.swift | 51 ++++++++++++ Tests/Shared/SourceGraphTestCase.swift | 68 +++++++++++++++ 24 files changed, 503 insertions(+) create mode 100644 Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift create mode 100644 Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift create mode 100644 Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift create mode 100644 Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index 3631704b15..4ce93f1323 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -79,6 +79,8 @@ swift_library( "SourceGraph/Mutators/ProtocolExtensionReferenceBuilder.swift", "SourceGraph/Mutators/PubliclyAccessibleRetainer.swift", "SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift", + "SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift", + "SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift", "SourceGraph/Mutators/RedundantProtocolMarker.swift", "SourceGraph/Mutators/ResultBuilderRetainer.swift", "SourceGraph/Mutators/StringInterpolationAppendInterpolationRetainer.swift", diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index 88aa090874..82531c73d8 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -80,6 +80,12 @@ public final class Configuration { @Setting(key: "disable_redundant_public_analysis", defaultValue: false) public var disableRedundantPublicAnalysis: Bool + @Setting(key: "disable_redundant_internal_analysis", defaultValue: false) + public var disableRedundantInternalAnalysis: Bool + + @Setting(key: "disable_redundant_fileprivate_analysis", defaultValue: false) + public var disableRedundantFilePrivateAnalysis: Bool + @Setting(key: "disable_unused_import_analysis", defaultValue: false) public var disableUnusedImportAnalysis: Bool @@ -219,6 +225,7 @@ public final class Configuration { $project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat, $retainPublic, $noRetainSPI, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible, $retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis, + $disableRedundantInternalAnalysis, $disableRedundantFilePrivateAnalysis, $disableUnusedImportAnalysis, $superfluousIgnoreComments, $retainUnusedImportedModules, $externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $color, $disableUpdateCheck, $strict, $indexStorePath, diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index 8ac9efbb51..cd3d598940 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -63,6 +63,12 @@ struct ScanCommand: ParsableCommand { @Flag(help: "Disable identification of redundant public accessibility") var disableRedundantPublicAnalysis: Bool = defaultConfiguration.$disableRedundantPublicAnalysis.defaultValue + @Flag(help: "Disable identification of redundant internal accessibility") + var disableRedundantInternalAnalysis: Bool = defaultConfiguration.$disableRedundantInternalAnalysis.defaultValue + + @Flag(help: "Disable identification of redundant fileprivate accessibility") + var disableRedundantFilePrivateAnalysis: Bool = defaultConfiguration.$disableRedundantFilePrivateAnalysis.defaultValue + @Flag(help: "Disable identification of unused imports") var disableUnusedImportAnalysis: Bool = defaultConfiguration.$disableUnusedImportAnalysis.defaultValue @@ -193,6 +199,8 @@ struct ScanCommand: ParsableCommand { configuration.apply(\.$retainUnusedProtocolFuncParams, retainUnusedProtocolFuncParams) configuration.apply(\.$retainSwiftUIPreviews, retainSwiftUIPreviews) configuration.apply(\.$disableRedundantPublicAnalysis, disableRedundantPublicAnalysis) + configuration.apply(\.$disableRedundantInternalAnalysis, disableRedundantInternalAnalysis) + configuration.apply(\.$disableRedundantFilePrivateAnalysis, disableRedundantFilePrivateAnalysis) configuration.apply(\.$disableUnusedImportAnalysis, disableUnusedImportAnalysis) configuration.apply(\.$superfluousIgnoreComments, superfluousIgnoreComments) configuration.apply(\.$retainUnusedImportedModules, retainUnusedImportedModules) diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index 6bdf99af10..0c901704f6 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -33,6 +33,10 @@ extension OutputFormatter { "redundantProtocol" case .redundantPublicAccessibility: "redundantPublicAccessibility" + case .redundantInternalAccessibility: + "redundantInternalAccessibility" + case .redundantFilePrivateAccessibility: + "redundantFilePrivateAccessibility" case .superfluousIgnoreCommand: "superfluousIgnoreCommand" } @@ -64,6 +68,12 @@ extension OutputFormatter { case let .redundantPublicAccessibility(modules): let modulesJoined = modules.sorted().joined(separator: ", ") description += "Redundant public accessibility for \(kindDisplayName) '\(name)' (not used outside of \(modulesJoined))" + case let .redundantInternalAccessibility(files): + let filesJoined = files.sorted { $0.path.string < $1.path.string }.map { $0.path.string }.joined(separator: ", ") + description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" + case let .redundantFilePrivateAccessibility(files): + let filesJoined = files.sorted { $0.path.string < $1.path.string }.map { $0.path.string }.joined(separator: ", ") + description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" case .superfluousIgnoreCommand: description += "Superfluous ignore comment for \(kindDisplayName) '\(name)' (declaration is referenced and should not be ignored)" } diff --git a/Sources/PeripheryKit/ScanResult.swift b/Sources/PeripheryKit/ScanResult.swift index 790cbd45d4..3079dccf60 100644 --- a/Sources/PeripheryKit/ScanResult.swift +++ b/Sources/PeripheryKit/ScanResult.swift @@ -7,6 +7,8 @@ public struct ScanResult { case assignOnlyProperty case redundantProtocol(references: Set, inherited: Set) case redundantPublicAccessibility(modules: Set) + case redundantInternalAccessibility(files: Set) + case redundantFilePrivateAccessibility(files: Set) case superfluousIgnoreCommand } diff --git a/Sources/PeripheryKit/ScanResultBuilder.swift b/Sources/PeripheryKit/ScanResultBuilder.swift index c0093d5c77..7a8e0c260d 100644 --- a/Sources/PeripheryKit/ScanResultBuilder.swift +++ b/Sources/PeripheryKit/ScanResultBuilder.swift @@ -10,6 +10,8 @@ public enum ScanResultBuilder { .union(graph.unusedModuleImports) let redundantProtocols = graph.redundantProtocols.filter { !removableDeclarations.contains($0.0) } let redundantPublicAccessibility = graph.redundantPublicAccessibility.filter { !removableDeclarations.contains($0.0) } + let redundantInternalAccessibility = graph.redundantInternalAccessibility.filter { !removableDeclarations.contains($0.0) } + let redundantFilePrivateAccessibility = graph.redundantFilePrivateAccessibility.filter { !removableDeclarations.contains($0.0) } let annotatedRemovableDeclarations: [ScanResult] = removableDeclarations.flatMap { removableDeclaration in var extensionResults = [ScanResult]() @@ -41,6 +43,12 @@ public enum ScanResultBuilder { let annotatedRedundantPublicAccessibility: [ScanResult] = redundantPublicAccessibility.map { .init(declaration: $0.0, annotation: .redundantPublicAccessibility(modules: $0.1)) } + let annotatedRedundantInternalAccessibility: [ScanResult] = redundantInternalAccessibility.map { + .init(declaration: $0.0, annotation: .redundantInternalAccessibility(files: $0.1)) + } + let annotatedRedundantFilePrivateAccessibility: [ScanResult] = redundantFilePrivateAccessibility.map { + .init(declaration: $0.0, annotation: .redundantFilePrivateAccessibility(files: $0.1)) + } let annotatedSuperfluousIgnoreCommands: [ScanResult] = { guard configuration.superfluousIgnoreComments else { return [] } @@ -66,6 +74,8 @@ public enum ScanResultBuilder { annotatedAssignOnlyProperties + annotatedRedundantProtocols + annotatedRedundantPublicAccessibility + + annotatedRedundantInternalAccessibility + + annotatedRedundantFilePrivateAccessibility + annotatedSuperfluousIgnoreCommands return allAnnotatedDeclarations diff --git a/Sources/SourceGraph/Elements/Declaration.swift b/Sources/SourceGraph/Elements/Declaration.swift index 382c58403d..cabd62fd83 100644 --- a/Sources/SourceGraph/Elements/Declaration.swift +++ b/Sources/SourceGraph/Elements/Declaration.swift @@ -231,6 +231,7 @@ public final class Declaration { public var related: Set = [] public var isImplicit: Bool = false public var isObjcAccessible: Bool = false + public var referencedFiles: Set private let hashValueCache: Int @@ -296,6 +297,7 @@ public final class Declaration { self.kind = kind self.usrs = usrs self.location = location + self.referencedFiles = [location.file] hashValueCache = usrs.hashValue } diff --git a/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift b/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift index 450c90428b..a0f55aabe8 100644 --- a/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift +++ b/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift @@ -33,6 +33,19 @@ final class ExtensionReferenceBuilder: SourceGraphMutator { extendedDeclaration.references.formUnion(extensionDeclaration.references) extendedDeclaration.related.formUnion(extensionDeclaration.related) + // Add the extension's file to the extended declaration's referencedFiles set. + extendedDeclaration.referencedFiles.insert(extensionDeclaration.location.file) + // Propagate the extension's file to all member declarations being merged. + for member in extensionDeclaration.declarations { + member.referencedFiles.insert(extensionDeclaration.location.file) + } + // Also update referencedFiles of existing members that are referenced in this extension. + for reference in extensionDeclaration.references { + if let referencedDecl = graph.declaration(withUsr: reference.usr) { + referencedDecl.referencedFiles.insert(extensionDeclaration.location.file) + } + } + extensionDeclaration.declarations.forEach { $0.parent = extendedDeclaration } extensionDeclaration.references.forEach { $0.parent = extendedDeclaration } extensionDeclaration.related.forEach { $0.parent = extendedDeclaration } diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift new file mode 100644 index 0000000000..d7ae24202e --- /dev/null +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -0,0 +1,70 @@ +import Configuration +import Shared + +final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { + private let graph: SourceGraph + private let configuration: Configuration + + required init(graph: SourceGraph, configuration: Configuration, swiftVersion _: SwiftVersion) { + self.graph = graph + self.configuration = configuration + } + + func mutate() throws { + guard !configuration.disableRedundantFilePrivateAnalysis else { return } + + let nonExtensionKinds = graph.rootDeclarations.filter { !$0.kind.isExtensionKind } + let extensionKinds = graph.rootDeclarations.filter(\.kind.isExtensionKind) + + for decl in nonExtensionKinds { + try validate(decl) + } + + for decl in extensionKinds { + try validateExtension(decl) + } + } + + // MARK: - Private + + private func validate(_ decl: Declaration) throws { + if decl.accessibility.isExplicitly(.fileprivate) { + if !graph.isRetained(decl), !isReferencedOutsideFile(decl) { + mark(decl) + markExplicitFilePrivateDescendentDeclarations(from: decl) + } + } else { + markExplicitFilePrivateDescendentDeclarations(from: decl) + } + } + + private func validateExtension(_ decl: Declaration) throws { + if decl.accessibility.isExplicitly(.fileprivate) { + if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), + graph.redundantFilePrivateAccessibility.keys.contains(extendedDecl) { + mark(decl) + } + } + } + + private func mark(_ decl: Declaration) { + guard !graph.isRetained(decl) else { return } + graph.markRedundantFilePrivateAccessibility(decl, file: decl.location.file) + } + + private func markExplicitFilePrivateDescendentDeclarations(from decl: Declaration) { + for descDecl in descendentFilePrivateDeclarations(from: decl) { + mark(descDecl) + } + } + + private func isReferencedOutsideFile(_ decl: Declaration) -> Bool { + let referenceFiles = graph.references(to: decl).map(\.location.file) + return referenceFiles.contains { $0 != decl.location.file } + } + + private func descendentFilePrivateDeclarations(from decl: Declaration) -> Set { + let filePrivateDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) } + return filePrivateDeclarations.flatMapSet { descendentFilePrivateDeclarations(from: $0) }.union(filePrivateDeclarations) + } +} \ No newline at end of file diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift new file mode 100644 index 0000000000..7a9b55811b --- /dev/null +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -0,0 +1,82 @@ +import Configuration +import Shared + +final class RedundantInternalAccessibilityMarker: SourceGraphMutator { + private let graph: SourceGraph + private let configuration: Configuration + + required init(graph: SourceGraph, configuration: Configuration, swiftVersion _: SwiftVersion) { + self.graph = graph + self.configuration = configuration + } + + func mutate() throws { + guard !configuration.disableRedundantInternalAnalysis else { return } + + let nonExtensionKinds = graph.rootDeclarations.filter { !$0.kind.isExtensionKind } + let extensionKinds = graph.rootDeclarations.filter(\.kind.isExtensionKind) + + for decl in nonExtensionKinds { + try validate(decl) + } + + for decl in extensionKinds { + try validateExtension(decl) + } + } + + // MARK: - Private + + private func validate(_ decl: Declaration) throws { + if decl.accessibility.isExplicitly(.internal) { + if !graph.isRetained(decl) { + let isReferencedOutside = isReferencedOutsideFile(decl) + if !isReferencedOutside { + mark(decl) + markExplicitInternalDescendentDeclarations(from: decl) + } + } + } else { + markExplicitInternalDescendentDeclarations(from: decl) + } + } + + private func validateExtension(_ decl: Declaration) throws { + if decl.accessibility.isExplicitly(.internal) { + if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), + graph.redundantInternalAccessibility.keys.contains(extendedDecl) { + mark(decl) + } + } + } + + private func mark(_ decl: Declaration) { + guard !graph.isRetained(decl) else { return } + graph.markRedundantInternalAccessibility(decl, file: decl.location.file) + } + + private func markExplicitInternalDescendentDeclarations(from decl: Declaration) { + for descDecl in descendentInternalDeclarations(from: decl) { + if !graph.isRetained(descDecl) { + let isReferencedOutside = isReferencedOutsideFile(descDecl) + if !isReferencedOutside { + mark(descDecl) + } + } + } + } + + private func isReferencedOutsideFile(_ decl: Declaration) -> Bool { + // Use graph.references(to: decl) to get all references to this declaration + let allReferences = graph.references(to: decl) + let referenceFiles = allReferences.map(\.location.file) + + let result = referenceFiles.contains { $0 != decl.location.file } + return result + } + + private func descendentInternalDeclarations(from decl: Declaration) -> Set { + let internalDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.internal) } + return internalDeclarations.flatMapSet { descendentInternalDeclarations(from: $0) }.union(internalDeclarations) + } +} \ No newline at end of file diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index 63534fb5c5..ffd2407c83 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -9,6 +9,8 @@ public final class SourceGraph { public private(set) var redundantProtocols: [Declaration: (references: Set, inherited: Set)] = [:] public private(set) var rootDeclarations: Set = [] public private(set) var redundantPublicAccessibility: [Declaration: Set] = [:] + public private(set) var redundantInternalAccessibility: [Declaration: Set] = [:] + public private(set) var redundantFilePrivateAccessibility: [Declaration: Set] = [:] public private(set) var rootReferences: Set = [] public private(set) var allReferences: Set = [] public private(set) var retainedDeclarations: Set = [] @@ -88,6 +90,22 @@ public final class SourceGraph { _ = redundantPublicAccessibility.removeValue(forKey: declaration) } + func markRedundantInternalAccessibility(_ declaration: Declaration, file: SourceFile) { + redundantInternalAccessibility[declaration, default: []].insert(file) + } + + func unmarkRedundantInternalAccessibility(_ declaration: Declaration) { + _ = redundantInternalAccessibility.removeValue(forKey: declaration) + } + + func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile) { + redundantFilePrivateAccessibility[declaration, default: []].insert(file) + } + + func unmarkRedundantFilePrivateAccessibility(_ declaration: Declaration) { + _ = redundantFilePrivateAccessibility.removeValue(forKey: declaration) + } + func markIgnored(_ declaration: Declaration) { _ = ignoredDeclarations.insert(declaration) } @@ -193,6 +211,9 @@ public final class SourceGraph { public func add(_ reference: Reference) { _ = allReferences.insert(reference) allReferencesByUsr[reference.usr, default: []].insert(reference) + if let decl = declaration(withUsr: reference.usr) { + decl.referencedFiles.insert(reference.location.file) + } } public func add(_ references: Set) { diff --git a/Sources/SourceGraph/SourceGraphMutatorRunner.swift b/Sources/SourceGraph/SourceGraphMutatorRunner.swift index c9fc129198..3193ee1540 100644 --- a/Sources/SourceGraph/SourceGraphMutatorRunner.swift +++ b/Sources/SourceGraph/SourceGraphMutatorRunner.swift @@ -17,6 +17,10 @@ public final class SourceGraphMutatorRunner { // Must come before ProtocolExtensionReferenceBuilder because it removes references // from the extension to the protocol, thus making them appear to be unknown. ExtensionReferenceBuilder.self, + // Must come after ExtensionReferenceBuilder so that it can detect redundant accessibility + // on properties that are used in extensions. + RedundantInternalAccessibilityMarker.self, + RedundantFilePrivateAccessibilityMarker.self, // Must come before ProtocolConformanceReferenceBuilder because it removes references to // conformed protocols, which CodingKeyEnumReferenceBuilder needs to inspect before removal. // It must also come after ExtensionReferenceBuilder as some types may declare conformance diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift new file mode 100644 index 0000000000..3b72a5366d --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift @@ -0,0 +1,9 @@ +// InternalPropertyExtension.swift +// Extension that uses the internal property from InternalPropertyUsedInExtension +// This should prevent the property from being flagged as redundant + +extension InternalPropertyUsedInExtension { + func useProperty() { + print(propertyUsedInExtension) // This reference should prevent propertyUsedInExtension from being flagged as redundant + } +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift new file mode 100644 index 0000000000..c4969afc80 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift @@ -0,0 +1,4 @@ +// InternalPropertyOwner.swift +internal class InternalPropertyOwner { + internal var value: Int = 42 +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift new file mode 100644 index 0000000000..6c89161f3a --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift @@ -0,0 +1,8 @@ +// InternalPropertyUsedInExtension.swift +// Tests the case where an internal property is used in an extension in a different file +// This should NOT be flagged as redundant + +internal class InternalPropertyUsedInExtension { + internal var propertyUsedInExtension: String = "test" + internal var propertyOnlyUsedInSameFile: String = "test" // This should be flagged as redundant +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift new file mode 100644 index 0000000000..aa70c58783 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift @@ -0,0 +1,6 @@ +// InternalPropertyUserExtension.swift +extension InternalPropertyOwner { + func printValue() { + print(value) + } +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift new file mode 100644 index 0000000000..e63e711243 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift @@ -0,0 +1,28 @@ +// NotRedundantFilePrivateComponents.swift +// Tests for fileprivate classes/members that should NOT be flagged as redundant + +fileprivate class NotRedundantFilePrivateClass { + fileprivate func usedFilePrivateMethod() {} + func publicMethodCallingFilePrivate() { + usedFilePrivateMethod() + } +} + +fileprivate struct NotRedundantFilePrivateStruct { + fileprivate var usedFilePrivateProperty: Int = 0 + func useProperty() -> Int { + return usedFilePrivateProperty + } +} + +fileprivate enum NotRedundantFilePrivateEnum { + case usedCase + func useCase() -> NotRedundantFilePrivateEnum { + return .usedCase + } +} + +// Used by main.swift to ensure these are referenced +class NotRedundantFilePrivateComponents { + public init() {} +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift new file mode 100644 index 0000000000..46a830c946 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift @@ -0,0 +1,22 @@ +// NotRedundantInternalClassComponents.swift +// Tests for internal classes/members that should NOT be flagged as redundant + +class NotRedundantInternalClassComponents { + public init() {} + + internal func usedInternalMethod() {} +} + +internal struct NotRedundantInternalStruct { + internal var usedInternalProperty: Int = 0 + func useProperty() -> Int { + return usedInternalProperty + } +} + +internal enum NotRedundantInternalEnum { + case usedCase + func useCase() -> NotRedundantInternalEnum { + return .usedCase + } +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift new file mode 100644 index 0000000000..edc6add655 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift @@ -0,0 +1,16 @@ +// NotRedundantInternalClassComponents_Support.swift +// Support types/usages for NotRedundantInternalClassComponents + +internal class NotRedundantInternalClass_Support { + internal func helper() {} +} + +// Used by main.swift to ensure these are referenced +class NotRedundantInternalClassComponents_Support { + public init() {} + + func useInternalMethod() { + let cls = NotRedundantInternalClassComponents() + cls.usedInternalMethod() + } +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift new file mode 100644 index 0000000000..70dc1f199d --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift @@ -0,0 +1,19 @@ +// RedundantFilePrivateComponents.swift +// Tests for fileprivate classes/members that should be flagged as redundant + +fileprivate class RedundantFilePrivateClass { + fileprivate func unusedFilePrivateMethod() {} +} + +fileprivate struct RedundantFilePrivateStruct { + fileprivate var unusedFilePrivateProperty: Int = 0 +} + +fileprivate enum RedundantFilePrivateEnum { + case unusedCase +} + +// Used by main.swift to ensure these are referenced +class RedundantFilePrivateComponents { + public init() {} +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift new file mode 100644 index 0000000000..74d1cdbbf6 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift @@ -0,0 +1,19 @@ +// RedundantInternalComponents.swift +// Tests for internal classes/members that should be flagged as redundant + +internal class RedundantInternalClass { + internal func unusedInternalMethod() {} +} + +internal struct RedundantInternalStruct { + internal var unusedInternalProperty: Int = 0 +} + +internal enum RedundantInternalEnum { + case unusedCase +} + +// Used by main.swift to ensure these are referenced +class RedundantInternalClassComponents { + public init() {} +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift new file mode 100644 index 0000000000..5a92acf0ad --- /dev/null +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -0,0 +1,22 @@ +import Configuration +@testable import TestShared +import XCTest + +final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { + override static func setUp() { + super.setUp() + build(projectPath: AccessibilityProjectPath) + } + + func testRedundantFilePrivateClass() { + // This should be flagged as redundant + index() + assertRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) + } + + func testNotRedundantFilePrivateClass() { + // This should NOT be flagged as redundant + index() + assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) + } +} \ No newline at end of file diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift new file mode 100644 index 0000000000..98ec00cfd6 --- /dev/null +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -0,0 +1,51 @@ +import Configuration +@testable import TestShared +import XCTest + +final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { + override static func setUp() { + super.setUp() + build(projectPath: AccessibilityProjectPath) + } + + func testInternalPropertyUsedInExtensionInOtherFile() { + // This should NOT be flagged as redundant + // Tests the case where an internal property is used in an extension in a different file + // Like delayedClearBoatHistory in AppState.swift used in AppState+BoatTracking.swift + index() + + // InternalPropertyUsedInExtension.propertyUsedInExtension should NOT be flagged as redundant + // because it's used in InternalPropertyExtension.swift + assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) + } + + func testInternalPropertyUsedOnlyInSameFile() { + // This should be flagged as redundant + // Tests the case where an internal property is only used within its own file + index() + + // InternalPropertyUsedInExtension.propertyOnlyUsedInSameFile should be flagged as redundant + // because it's only used within InternalPropertyUsedInExtension.swift + assertRedundantInternalAccessibility(.varInstance("propertyOnlyUsedInSameFile")) + } + + func testInternalPropertyUsedInMultipleFiles() { + // This should NOT be flagged as redundant + // Tests the case where an internal property is used across multiple files + index() + + // This test would need additional setup with multiple files + // For now, we'll test that the existing NotRedundantInternalClassComponents work + assertNotRedundantInternalAccessibility(.class("NotRedundantInternalClass")) + } + + func testInternalMethodUsedInExtension() { + // This should NOT be flagged as redundant + // Tests the case where an internal method is used in an extension + index() + + // This test would need additional setup with methods in extensions + // For now, we'll test that the existing NotRedundantInternalClassComponents work + assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) + } +} \ No newline at end of file diff --git a/Tests/Shared/SourceGraphTestCase.swift b/Tests/Shared/SourceGraphTestCase.swift index 1177ed682f..5a10fb344c 100644 --- a/Tests/Shared/SourceGraphTestCase.swift +++ b/Tests/Shared/SourceGraphTestCase.swift @@ -177,6 +177,54 @@ open class SourceGraphTestCase: XCTestCase { scopeStack.removeLast() } + func assertRedundantInternalAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } + + if !Self.results.redundantInternalAccessibilityDeclarations.contains(declaration) { + XCTFail("Expected declaration to have redundant internal accessibility: \(declaration)", file: file, line: line) + } + + scopeStack.append(.declaration(declaration)) + scopedAssertions?() + scopeStack.removeLast() + } + + func assertNotRedundantInternalAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } + + if Self.results.redundantInternalAccessibilityDeclarations.contains(declaration) { + XCTFail("Expected declaration to not have redundant internal accessibility: \(declaration)", file: file, line: line) + } + + scopeStack.append(.declaration(declaration)) + scopedAssertions?() + scopeStack.removeLast() + } + + func assertRedundantFilePrivateAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } + + if !Self.results.redundantFilePrivateAccessibilityDeclarations.contains(declaration) { + XCTFail("Expected declaration to have redundant fileprivate accessibility: \(declaration)", file: file, line: line) + } + + scopeStack.append(.declaration(declaration)) + scopedAssertions?() + scopeStack.removeLast() + } + + func assertNotRedundantFilePrivateAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } + + if Self.results.redundantFilePrivateAccessibilityDeclarations.contains(declaration) { + XCTFail("Expected declaration to not have redundant fileprivate accessibility: \(declaration)", file: file, line: line) + } + + scopeStack.append(.declaration(declaration)) + scopedAssertions?() + scopeStack.removeLast() + } + func assertUsedParameter(_ name: String, file: StaticString = #file, line: UInt = #line) { let declaration = materialize(.varParameter(name), fail: false, file: file, line: line) @@ -351,6 +399,26 @@ private extension [ScanResult] { } } + var redundantInternalAccessibilityDeclarations: Set { + compactMapSet { + if case .redundantInternalAccessibility = $0.annotation { + return $0.declaration + } + + return nil + } + } + + var redundantFilePrivateAccessibilityDeclarations: Set { + compactMapSet { + if case .redundantFilePrivateAccessibility = $0.annotation { + return $0.declaration + } + + return nil + } + } + var superfluousIgnoreCommandDeclarations: Set { compactMapSet { if case .superfluousIgnoreCommand = $0.annotation { From 4fd08d902a9acfdb46b8e191554a50779cca5105 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:12:23 -0800 Subject: [PATCH 02/31] Update Sources/PeripheryKit/Results/OutputFormatter.swift Co-authored-by: Ian Leitch --- Sources/PeripheryKit/Results/OutputFormatter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index 0c901704f6..20bfb15783 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -73,7 +73,7 @@ extension OutputFormatter { description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" case let .redundantFilePrivateAccessibility(files): let filesJoined = files.sorted { $0.path.string < $1.path.string }.map { $0.path.string }.joined(separator: ", ") - description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" + description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (not used outside its enclosing scope in \(filesJoined))" case .superfluousIgnoreCommand: description += "Superfluous ignore comment for \(kindDisplayName) '\(name)' (declaration is referenced and should not be ignored)" } From 40da6ce895ff477cba8230f33f04df0ee28bb122 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:16:19 -0800 Subject: [PATCH 03/31] remove some stuff Ian noticed in my PR --- Sources/SourceGraph/SourceGraph.swift | 8 -------- .../AccessibilityProject/Sources/MainTarget/main.swift | 3 +++ .../RedundantInternalAccessibilityTest.swift | 1 - 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index ffd2407c83..93b694e54a 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -94,18 +94,10 @@ public final class SourceGraph { redundantInternalAccessibility[declaration, default: []].insert(file) } - func unmarkRedundantInternalAccessibility(_ declaration: Declaration) { - _ = redundantInternalAccessibility.removeValue(forKey: declaration) - } - func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile) { redundantFilePrivateAccessibility[declaration, default: []].insert(file) } - func unmarkRedundantFilePrivateAccessibility(_ declaration: Declaration) { - _ = redundantFilePrivateAccessibility.removeValue(forKey: declaration) - } - func markIgnored(_ declaration: Declaration) { _ = ignoredDeclarations.insert(declaration) } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index 48ff0ded23..51f32f290c 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -85,3 +85,6 @@ takeExtensionSameTypeGenericRequirement(.defaultInstance) // Typed throws try? PublicTypeUsedAsPublicFunctionThrowTypeRetainer().retain() + +// Internal accessibility tests +_ = InternalPropertyUsedInExtension() diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 98ec00cfd6..26c34014aa 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -11,7 +11,6 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { func testInternalPropertyUsedInExtensionInOtherFile() { // This should NOT be flagged as redundant // Tests the case where an internal property is used in an extension in a different file - // Like delayedClearBoatHistory in AppState.swift used in AppState+BoatTracking.swift index() // InternalPropertyUsedInExtension.propertyUsedInExtension should NOT be flagged as redundant From 490b99a50a89bc384335840888050793131657f7 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:28:56 -0800 Subject: [PATCH 04/31] =?UTF-8?q?remove=20=E2=80=98referencedFiles?= =?UTF-8?q?=E2=80=99=20which=20I=E2=80=99m=20no=20longer=20needing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/SourceGraph/Elements/Declaration.swift | 2 -- .../Mutators/ExtensionReferenceBuilder.swift | 13 ------------- Sources/SourceGraph/SourceGraph.swift | 3 --- 3 files changed, 18 deletions(-) diff --git a/Sources/SourceGraph/Elements/Declaration.swift b/Sources/SourceGraph/Elements/Declaration.swift index cabd62fd83..382c58403d 100644 --- a/Sources/SourceGraph/Elements/Declaration.swift +++ b/Sources/SourceGraph/Elements/Declaration.swift @@ -231,7 +231,6 @@ public final class Declaration { public var related: Set = [] public var isImplicit: Bool = false public var isObjcAccessible: Bool = false - public var referencedFiles: Set private let hashValueCache: Int @@ -297,7 +296,6 @@ public final class Declaration { self.kind = kind self.usrs = usrs self.location = location - self.referencedFiles = [location.file] hashValueCache = usrs.hashValue } diff --git a/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift b/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift index a0f55aabe8..450c90428b 100644 --- a/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift +++ b/Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift @@ -33,19 +33,6 @@ final class ExtensionReferenceBuilder: SourceGraphMutator { extendedDeclaration.references.formUnion(extensionDeclaration.references) extendedDeclaration.related.formUnion(extensionDeclaration.related) - // Add the extension's file to the extended declaration's referencedFiles set. - extendedDeclaration.referencedFiles.insert(extensionDeclaration.location.file) - // Propagate the extension's file to all member declarations being merged. - for member in extensionDeclaration.declarations { - member.referencedFiles.insert(extensionDeclaration.location.file) - } - // Also update referencedFiles of existing members that are referenced in this extension. - for reference in extensionDeclaration.references { - if let referencedDecl = graph.declaration(withUsr: reference.usr) { - referencedDecl.referencedFiles.insert(extensionDeclaration.location.file) - } - } - extensionDeclaration.declarations.forEach { $0.parent = extendedDeclaration } extensionDeclaration.references.forEach { $0.parent = extendedDeclaration } extensionDeclaration.related.forEach { $0.parent = extendedDeclaration } diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index 93b694e54a..8a0e4d240b 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -203,9 +203,6 @@ public final class SourceGraph { public func add(_ reference: Reference) { _ = allReferences.insert(reference) allReferencesByUsr[reference.usr, default: []].insert(reference) - if let decl = declaration(withUsr: reference.usr) { - decl.referencedFiles.insert(reference.location.file) - } } public func add(_ references: Set) { From 42588922a6f1d5f648d816b77737143ac4de0f8e Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:47:42 -0800 Subject: [PATCH 05/31] Redoing tests - WIP - moving into same module and rearranging --- .../Sources/MainTarget/main.swift | 3 -- .../NotRedundantFilePrivateComponents.swift | 28 ------------------- .../RedundantFilePrivateComponents.swift | 19 ------------- .../TargetA => }/InternalPropertyOwner.swift | 0 ... => InternalPropertyOwner_Extension.swift} | 0 .../InternalPropertyUsedInExtension.swift | 6 +++- ...alPropertyUsedInExtension_Extension.swift} | 4 +-- .../NotRedundantInternalClassComponents.swift | 4 +-- ...ndantInternalClassComponents_Support.swift | 0 ...edundantFilePrivateAccessibilityTest.swift | 17 ++++++++++- .../RedundantInternalAccessibilityTest.swift | 6 +++- .../RedundantInternalComponents.swift | 0 12 files changed, 30 insertions(+), 57 deletions(-) delete mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift delete mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA => }/InternalPropertyOwner.swift (100%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift => InternalPropertyOwner_Extension.swift} (100%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA => }/InternalPropertyUsedInExtension.swift (81%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift => InternalPropertyUsedInExtension_Extension.swift} (90%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA => }/NotRedundantInternalClassComponents.swift (92%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA => }/NotRedundantInternalClassComponents_Support.swift (100%) rename Tests/AccessibilityTests/{AccessibilityProject/Sources/TargetA => }/RedundantInternalComponents.swift (100%) diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index 51f32f290c..48ff0ded23 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -85,6 +85,3 @@ takeExtensionSameTypeGenericRequirement(.defaultInstance) // Typed throws try? PublicTypeUsedAsPublicFunctionThrowTypeRetainer().retain() - -// Internal accessibility tests -_ = InternalPropertyUsedInExtension() diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift deleted file mode 100644 index e63e711243..0000000000 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateComponents.swift +++ /dev/null @@ -1,28 +0,0 @@ -// NotRedundantFilePrivateComponents.swift -// Tests for fileprivate classes/members that should NOT be flagged as redundant - -fileprivate class NotRedundantFilePrivateClass { - fileprivate func usedFilePrivateMethod() {} - func publicMethodCallingFilePrivate() { - usedFilePrivateMethod() - } -} - -fileprivate struct NotRedundantFilePrivateStruct { - fileprivate var usedFilePrivateProperty: Int = 0 - func useProperty() -> Int { - return usedFilePrivateProperty - } -} - -fileprivate enum NotRedundantFilePrivateEnum { - case usedCase - func useCase() -> NotRedundantFilePrivateEnum { - return .usedCase - } -} - -// Used by main.swift to ensure these are referenced -class NotRedundantFilePrivateComponents { - public init() {} -} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift deleted file mode 100644 index 70dc1f199d..0000000000 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateComponents.swift +++ /dev/null @@ -1,19 +0,0 @@ -// RedundantFilePrivateComponents.swift -// Tests for fileprivate classes/members that should be flagged as redundant - -fileprivate class RedundantFilePrivateClass { - fileprivate func unusedFilePrivateMethod() {} -} - -fileprivate struct RedundantFilePrivateStruct { - fileprivate var unusedFilePrivateProperty: Int = 0 -} - -fileprivate enum RedundantFilePrivateEnum { - case unusedCase -} - -// Used by main.swift to ensure these are referenced -class RedundantFilePrivateComponents { - public init() {} -} \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift b/Tests/AccessibilityTests/InternalPropertyOwner.swift similarity index 100% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift rename to Tests/AccessibilityTests/InternalPropertyOwner.swift diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift b/Tests/AccessibilityTests/InternalPropertyOwner_Extension.swift similarity index 100% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUserExtension.swift rename to Tests/AccessibilityTests/InternalPropertyOwner_Extension.swift diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift b/Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift similarity index 81% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift rename to Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift index 6c89161f3a..95bf724c21 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift +++ b/Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift @@ -5,4 +5,8 @@ internal class InternalPropertyUsedInExtension { internal var propertyUsedInExtension: String = "test" internal var propertyOnlyUsedInSameFile: String = "test" // This should be flagged as redundant -} \ No newline at end of file + + func useSameFileProperty() { + print(propertyOnlyUsedInSameFile) + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift b/Tests/AccessibilityTests/InternalPropertyUsedInExtension_Extension.swift similarity index 90% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift rename to Tests/AccessibilityTests/InternalPropertyUsedInExtension_Extension.swift index 3b72a5366d..48f8bba346 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyExtension.swift +++ b/Tests/AccessibilityTests/InternalPropertyUsedInExtension_Extension.swift @@ -3,7 +3,7 @@ // This should prevent the property from being flagged as redundant extension InternalPropertyUsedInExtension { - func useProperty() { + func usePropertyInExtension() { print(propertyUsedInExtension) // This reference should prevent propertyUsedInExtension from being flagged as redundant } -} \ No newline at end of file +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift b/Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift similarity index 92% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift rename to Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift index 46a830c946..4735a2f47e 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift +++ b/Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift @@ -9,7 +9,7 @@ class NotRedundantInternalClassComponents { internal struct NotRedundantInternalStruct { internal var usedInternalProperty: Int = 0 - func useProperty() -> Int { + func useInternalProperty() -> Int { return usedInternalProperty } } @@ -19,4 +19,4 @@ internal enum NotRedundantInternalEnum { func useCase() -> NotRedundantInternalEnum { return .usedCase } -} \ No newline at end of file +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift b/Tests/AccessibilityTests/NotRedundantInternalClassComponents_Support.swift similarity index 100% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift rename to Tests/AccessibilityTests/NotRedundantInternalClassComponents_Support.swift diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift index 5a92acf0ad..d7d9cd253f 100644 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -5,6 +5,7 @@ import XCTest final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { override static func setUp() { super.setUp() + _ = RedundantFilePrivateClass() build(projectPath: AccessibilityProjectPath) } @@ -17,6 +18,20 @@ final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { func testNotRedundantFilePrivateClass() { // This should NOT be flagged as redundant index() + NotRedundantFilePrivateClass.staticMethodCallingFilePrivate() assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) } -} \ No newline at end of file +} + +fileprivate class NotRedundantFilePrivateClass { + fileprivate static func usedFilePrivateMethod() {} + + static func staticMethodCallingFilePrivate() { + usedFilePrivateMethod() + } +} + +fileprivate class RedundantFilePrivateClass { + fileprivate func unusedFilePrivateMethod() {} +} + diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 26c34014aa..606978083c 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -18,6 +18,10 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) } +/* +failed - Expected declaration to have redundant internal accessibility: +Declaration(var.instance, 'propertyOnlyUsedInSameFile', explicit, internal, [internal], [], [], [s:7TargetA31InternalPropertyUsedInExtensionC012propertyOnlydE8SameFileSSvp], InternalPropertyUsedInExtension.swift:7:18) +*/ func testInternalPropertyUsedOnlyInSameFile() { // This should be flagged as redundant // Tests the case where an internal property is only used within its own file @@ -47,4 +51,4 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { // For now, we'll test that the existing NotRedundantInternalClassComponents work assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) } -} \ No newline at end of file +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift b/Tests/AccessibilityTests/RedundantInternalComponents.swift similarity index 100% rename from Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift rename to Tests/AccessibilityTests/RedundantInternalComponents.swift From 0ffef852a8cb6a8393411e32261c410c6308a72b Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:59:27 -0800 Subject: [PATCH 06/31] Redo tests, found bug in source graph --- ...undantFilePrivateAccessibilityMarker.swift | 14 +++++++-- ...RedundantInternalAccessibilityMarker.swift | 14 +++++++-- .../Sources/MainTarget/main.swift | 2 ++ .../TargetA}/InternalPropertyOwner.swift | 0 .../InternalPropertyOwner_Extension.swift | 0 .../InternalPropertyUsedInExtension.swift | 8 +++++ ...nalPropertyUsedInExtension_Extension.swift | 0 .../NotRedundantFilePrivateClass.swift | 23 ++++++++++++++ .../NotRedundantInternalClassComponents.swift | 4 +-- ...ndantInternalClassComponents_Support.swift | 2 +- .../TargetA/RedundantFilePrivateClass.swift | 31 +++++++++++++++++++ .../RedundantInternalComponents.swift | 0 ...edundantFilePrivateAccessibilityTest.swift | 20 ++---------- 13 files changed, 91 insertions(+), 27 deletions(-) rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/InternalPropertyOwner.swift (100%) rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/InternalPropertyOwner_Extension.swift (100%) rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/InternalPropertyUsedInExtension.swift (69%) rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/InternalPropertyUsedInExtension_Extension.swift (100%) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateClass.swift rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/NotRedundantInternalClassComponents.swift (91%) rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/NotRedundantInternalClassComponents_Support.swift (87%) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift rename Tests/AccessibilityTests/{ => AccessibilityProject/Sources/TargetA}/RedundantInternalComponents.swift (100%) diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index d7ae24202e..ad9d82bdac 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -31,11 +31,19 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { if decl.accessibility.isExplicitly(.fileprivate) { if !graph.isRetained(decl), !isReferencedOutsideFile(decl) { mark(decl) - markExplicitFilePrivateDescendentDeclarations(from: decl) } - } else { - markExplicitFilePrivateDescendentDeclarations(from: decl) } + + /* + Always check descendents, even if parent is not redundant. + + A parent declaration may be used outside its file (making it not redundant), + while still having child declarations that are only used within the same file + (making those children redundant). For example, a class used cross-file may have + a fileprivate property only referenced within the same file - that property should + be flagged as redundant even though the parent class is not. + */ + markExplicitFilePrivateDescendentDeclarations(from: decl) } private func validateExtension(_ decl: Declaration) throws { diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 7a9b55811b..62c5ffdbc2 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -33,12 +33,20 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let isReferencedOutside = isReferencedOutsideFile(decl) if !isReferencedOutside { mark(decl) - markExplicitInternalDescendentDeclarations(from: decl) } } - } else { - markExplicitInternalDescendentDeclarations(from: decl) } + + /* + Always check descendents, even if parent is not redundant. + + A parent declaration may be used outside its file (making it not redundant), + while still having child declarations that are only used within the same file + (making those children redundant). For example, a class used cross-file may have + an internal property only referenced within the same file - that property should + be flagged as redundant even though the parent class is not. + */ + markExplicitInternalDescendentDeclarations(from: decl) } private func validateExtension(_ decl: Declaration) throws { diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index 48ff0ded23..f9c5932df5 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -5,6 +5,8 @@ PublicDeclarationInInternalParentRetainer().retain() PublicExtensionOnRedundantPublicKindRetainer().retain() IgnoreCommentCommandRetainer().retain() IgnoreAllCommentCommandRetainer().retain() +RedundantFilePrivateClassRetainer().retain() +InternalPropertyUsedInExtensionRetainer().retain() _ = PublicTypeUsedAsPublicInitializerParameterTypeRetainer() diff --git a/Tests/AccessibilityTests/InternalPropertyOwner.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift similarity index 100% rename from Tests/AccessibilityTests/InternalPropertyOwner.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner.swift diff --git a/Tests/AccessibilityTests/InternalPropertyOwner_Extension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner_Extension.swift similarity index 100% rename from Tests/AccessibilityTests/InternalPropertyOwner_Extension.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyOwner_Extension.swift diff --git a/Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift similarity index 69% rename from Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift index 95bf724c21..25dc53e712 100644 --- a/Tests/AccessibilityTests/InternalPropertyUsedInExtension.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension.swift @@ -9,4 +9,12 @@ internal class InternalPropertyUsedInExtension { func useSameFileProperty() { print(propertyOnlyUsedInSameFile) } +} + +public class InternalPropertyUsedInExtensionRetainer { + public init() {} + public func retain() { + let instance = InternalPropertyUsedInExtension() + instance.useSameFileProperty() + } } diff --git a/Tests/AccessibilityTests/InternalPropertyUsedInExtension_Extension.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension_Extension.swift similarity index 100% rename from Tests/AccessibilityTests/InternalPropertyUsedInExtension_Extension.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalPropertyUsedInExtension_Extension.swift diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateClass.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateClass.swift new file mode 100644 index 0000000000..aabe2452ba --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivateClass.swift @@ -0,0 +1,23 @@ +/* + NotRedundantFilePrivateClass.swift + Tests that a fileprivate class is NOT flagged as redundant when it's used from a different type in the same file +*/ + +fileprivate class NotRedundantFilePrivateClass { + fileprivate static func usedFilePrivateMethod() {} + + static func staticMethodCallingFilePrivate() { + usedFilePrivateMethod() + } +} + +/* + This separate type accesses NotRedundantFilePrivateClass. + Since they're in the same file, fileprivate allows the access. + If NotRedundantFilePrivateClass were private instead, this would fail to compile. +*/ +class NotRedundantFilePrivateClassUser { + func useFilePrivateClass() { + NotRedundantFilePrivateClass.staticMethodCallingFilePrivate() + } +} diff --git a/Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift similarity index 91% rename from Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift index 4735a2f47e..c4f852e72f 100644 --- a/Tests/AccessibilityTests/NotRedundantInternalClassComponents.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift @@ -1,9 +1,9 @@ // NotRedundantInternalClassComponents.swift // Tests for internal classes/members that should NOT be flagged as redundant -class NotRedundantInternalClassComponents { +class NotRedundantInternalClass { public init() {} - + internal func usedInternalMethod() {} } diff --git a/Tests/AccessibilityTests/NotRedundantInternalClassComponents_Support.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift similarity index 87% rename from Tests/AccessibilityTests/NotRedundantInternalClassComponents_Support.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift index edc6add655..055f1e21fd 100644 --- a/Tests/AccessibilityTests/NotRedundantInternalClassComponents_Support.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift @@ -10,7 +10,7 @@ class NotRedundantInternalClassComponents_Support { public init() {} func useInternalMethod() { - let cls = NotRedundantInternalClassComponents() + let cls = NotRedundantInternalClass() cls.usedInternalMethod() } } \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift new file mode 100644 index 0000000000..8aaf989961 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift @@ -0,0 +1,31 @@ +/* + RedundantFilePrivateClass.swift + Tests that a fileprivate class is flagged as redundant when it's only referenced from within its own declaration +*/ + +fileprivate class RedundantFilePrivateClass { + fileprivate func someMethod() { + // Method that does something + } + + /* + This internal method creates a self-reference, ensuring the class is "used" + but only within its own declaration scope, making fileprivate redundant (could be private) + */ + func createInstance() -> RedundantFilePrivateClass { + RedundantFilePrivateClass() + } +} + +/* + This retainer ensures the file is indexed and calls a method on the class. + The fileprivate class is used, but only within the same file and not by other declarations, + making the fileprivate access level redundant (could be private). +*/ +public class RedundantFilePrivateClassRetainer { + public init() {} + public func retain() { + let instance = RedundantFilePrivateClass() + _ = instance.createInstance() + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift similarity index 100% rename from Tests/AccessibilityTests/RedundantInternalComponents.swift rename to Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift index d7d9cd253f..1d1063498a 100644 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -5,33 +5,17 @@ import XCTest final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { override static func setUp() { super.setUp() - _ = RedundantFilePrivateClass() build(projectPath: AccessibilityProjectPath) } - + func testRedundantFilePrivateClass() { - // This should be flagged as redundant index() assertRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) } - + func testNotRedundantFilePrivateClass() { - // This should NOT be flagged as redundant index() - NotRedundantFilePrivateClass.staticMethodCallingFilePrivate() assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) } } - -fileprivate class NotRedundantFilePrivateClass { - fileprivate static func usedFilePrivateMethod() {} - - static func staticMethodCallingFilePrivate() { - usedFilePrivateMethod() - } -} - -fileprivate class RedundantFilePrivateClass { - fileprivate func unusedFilePrivateMethod() {} -} From 1977d4e6698bfb3cd0712c176e753172603205f1 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:29:08 -0800 Subject: [PATCH 07/31] Remove files that got moved --- ...edundantFilePrivateAccessibilityTest.swift | 21 -------- .../RedundantInternalAccessibilityTest.swift | 54 ------------------- 2 files changed, 75 deletions(-) delete mode 100644 Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift delete mode 100644 Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift deleted file mode 100644 index 1d1063498a..0000000000 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Configuration -@testable import TestShared -import XCTest - -final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { - override static func setUp() { - super.setUp() - build(projectPath: AccessibilityProjectPath) - } - - func testRedundantFilePrivateClass() { - index() - assertRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) - } - - func testNotRedundantFilePrivateClass() { - index() - assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) - } -} - diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift deleted file mode 100644 index 606978083c..0000000000 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Configuration -@testable import TestShared -import XCTest - -final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { - override static func setUp() { - super.setUp() - build(projectPath: AccessibilityProjectPath) - } - - func testInternalPropertyUsedInExtensionInOtherFile() { - // This should NOT be flagged as redundant - // Tests the case where an internal property is used in an extension in a different file - index() - - // InternalPropertyUsedInExtension.propertyUsedInExtension should NOT be flagged as redundant - // because it's used in InternalPropertyExtension.swift - assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) - } - -/* -failed - Expected declaration to have redundant internal accessibility: -Declaration(var.instance, 'propertyOnlyUsedInSameFile', explicit, internal, [internal], [], [], [s:7TargetA31InternalPropertyUsedInExtensionC012propertyOnlydE8SameFileSSvp], InternalPropertyUsedInExtension.swift:7:18) -*/ - func testInternalPropertyUsedOnlyInSameFile() { - // This should be flagged as redundant - // Tests the case where an internal property is only used within its own file - index() - - // InternalPropertyUsedInExtension.propertyOnlyUsedInSameFile should be flagged as redundant - // because it's only used within InternalPropertyUsedInExtension.swift - assertRedundantInternalAccessibility(.varInstance("propertyOnlyUsedInSameFile")) - } - - func testInternalPropertyUsedInMultipleFiles() { - // This should NOT be flagged as redundant - // Tests the case where an internal property is used across multiple files - index() - - // This test would need additional setup with multiple files - // For now, we'll test that the existing NotRedundantInternalClassComponents work - assertNotRedundantInternalAccessibility(.class("NotRedundantInternalClass")) - } - - func testInternalMethodUsedInExtension() { - // This should NOT be flagged as redundant - // Tests the case where an internal method is used in an extension - index() - - // This test would need additional setup with methods in extensions - // For now, we'll test that the existing NotRedundantInternalClassComponents work - assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) - } -} From 7dc47a7b10cc8b33212718bb789ec65966b1102a Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:23:53 -0800 Subject: [PATCH 08/31] Revert "Remove files that got moved" This reverts commit 882e58d4d95665bd061bf516350be66cab1225b6. --- ...edundantFilePrivateAccessibilityTest.swift | 21 ++++++++ .../RedundantInternalAccessibilityTest.swift | 54 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift create mode 100644 Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift new file mode 100644 index 0000000000..1d1063498a --- /dev/null +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -0,0 +1,21 @@ +import Configuration +@testable import TestShared +import XCTest + +final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { + override static func setUp() { + super.setUp() + build(projectPath: AccessibilityProjectPath) + } + + func testRedundantFilePrivateClass() { + index() + assertRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) + } + + func testNotRedundantFilePrivateClass() { + index() + assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) + } +} + diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift new file mode 100644 index 0000000000..606978083c --- /dev/null +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -0,0 +1,54 @@ +import Configuration +@testable import TestShared +import XCTest + +final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { + override static func setUp() { + super.setUp() + build(projectPath: AccessibilityProjectPath) + } + + func testInternalPropertyUsedInExtensionInOtherFile() { + // This should NOT be flagged as redundant + // Tests the case where an internal property is used in an extension in a different file + index() + + // InternalPropertyUsedInExtension.propertyUsedInExtension should NOT be flagged as redundant + // because it's used in InternalPropertyExtension.swift + assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) + } + +/* +failed - Expected declaration to have redundant internal accessibility: +Declaration(var.instance, 'propertyOnlyUsedInSameFile', explicit, internal, [internal], [], [], [s:7TargetA31InternalPropertyUsedInExtensionC012propertyOnlydE8SameFileSSvp], InternalPropertyUsedInExtension.swift:7:18) +*/ + func testInternalPropertyUsedOnlyInSameFile() { + // This should be flagged as redundant + // Tests the case where an internal property is only used within its own file + index() + + // InternalPropertyUsedInExtension.propertyOnlyUsedInSameFile should be flagged as redundant + // because it's only used within InternalPropertyUsedInExtension.swift + assertRedundantInternalAccessibility(.varInstance("propertyOnlyUsedInSameFile")) + } + + func testInternalPropertyUsedInMultipleFiles() { + // This should NOT be flagged as redundant + // Tests the case where an internal property is used across multiple files + index() + + // This test would need additional setup with multiple files + // For now, we'll test that the existing NotRedundantInternalClassComponents work + assertNotRedundantInternalAccessibility(.class("NotRedundantInternalClass")) + } + + func testInternalMethodUsedInExtension() { + // This should NOT be flagged as redundant + // Tests the case where an internal method is used in an extension + index() + + // This test would need additional setup with methods in extensions + // For now, we'll test that the existing NotRedundantInternalClassComponents work + assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) + } +} From e9dfd3955cb2b29d08d4dd89872c206db17dcf95 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:29:05 -0800 Subject: [PATCH 09/31] remove old inline failure warning --- .../RedundantInternalAccessibilityTest.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 606978083c..1b806cfe6e 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -18,10 +18,6 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) } -/* -failed - Expected declaration to have redundant internal accessibility: -Declaration(var.instance, 'propertyOnlyUsedInSameFile', explicit, internal, [internal], [], [], [s:7TargetA31InternalPropertyUsedInExtensionC012propertyOnlydE8SameFileSSvp], InternalPropertyUsedInExtension.swift:7:18) -*/ func testInternalPropertyUsedOnlyInSameFile() { // This should be flagged as redundant // Tests the case where an internal property is only used within its own file From 31edb0106d7b0036618eb10d73258df2e538cb27 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:53:13 -0800 Subject: [PATCH 10/31] try to deal with most of the warnings from `mise r scan` --- Sources/Extensions/FilePath+Glob.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Extensions/FilePath+Glob.swift b/Sources/Extensions/FilePath+Glob.swift index 21e2fbd051..57af57301e 100644 --- a/Sources/Extensions/FilePath+Glob.swift +++ b/Sources/Extensions/FilePath+Glob.swift @@ -28,7 +28,7 @@ private final class Glob { private let excludedDirectories: [String] private var isDirectoryCache: [String: Bool] = [:] - fileprivate var paths: Set = [] + internal var paths: Set = [] init( pattern: String, From c99cef67c509bec18fc4da3fcd3ed1f625cc743a Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:07:43 -0800 Subject: [PATCH 11/31] take out specifier completely to make `mise` happy --- Sources/Extensions/FilePath+Glob.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Extensions/FilePath+Glob.swift b/Sources/Extensions/FilePath+Glob.swift index 57af57301e..d946e1a1cc 100644 --- a/Sources/Extensions/FilePath+Glob.swift +++ b/Sources/Extensions/FilePath+Glob.swift @@ -28,7 +28,7 @@ private final class Glob { private let excludedDirectories: [String] private var isDirectoryCache: [String: Bool] = [:] - internal var paths: Set = [] + var paths: Set = [] init( pattern: String, From 343a655b458fd87a92e87b7b1920be8e2c041db1 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:11:35 -0800 Subject: [PATCH 12/31] After running `mise r lint` and `mise r gen-bazel-rules` --- Sources/BUILD.bazel | 2 +- .../Results/OutputFormatter.swift | 4 ++-- ...undantFilePrivateAccessibilityMarker.swift | 19 +++++++-------- ...RedundantInternalAccessibilityMarker.swift | 23 ++++++++++--------- ...edundantFilePrivateAccessibilityTest.swift | 1 - .../RedundantInternalAccessibilityTest.swift | 18 +++++++-------- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index 4ce93f1323..a538fd2bed 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -79,8 +79,8 @@ swift_library( "SourceGraph/Mutators/ProtocolExtensionReferenceBuilder.swift", "SourceGraph/Mutators/PubliclyAccessibleRetainer.swift", "SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift", - "SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift", "SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift", + "SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift", "SourceGraph/Mutators/RedundantProtocolMarker.swift", "SourceGraph/Mutators/ResultBuilderRetainer.swift", "SourceGraph/Mutators/StringInterpolationAppendInterpolationRetainer.swift", diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index 20bfb15783..a5e5b6c5ff 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -69,10 +69,10 @@ extension OutputFormatter { let modulesJoined = modules.sorted().joined(separator: ", ") description += "Redundant public accessibility for \(kindDisplayName) '\(name)' (not used outside of \(modulesJoined))" case let .redundantInternalAccessibility(files): - let filesJoined = files.sorted { $0.path.string < $1.path.string }.map { $0.path.string }.joined(separator: ", ") + let filesJoined = files.sorted { $0.path.string < $1.path.string }.map(\.path.string).joined(separator: ", ") description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" case let .redundantFilePrivateAccessibility(files): - let filesJoined = files.sorted { $0.path.string < $1.path.string }.map { $0.path.string }.joined(separator: ", ") + let filesJoined = files.sorted { $0.path.string < $1.path.string }.map(\.path.string).joined(separator: ", ") description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (not used outside its enclosing scope in \(filesJoined))" case .superfluousIgnoreCommand: description += "Superfluous ignore comment for \(kindDisplayName) '\(name)' (declaration is referenced and should not be ignored)" diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index ad9d82bdac..738adbe724 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -35,21 +35,22 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { } /* - Always check descendents, even if parent is not redundant. + Always check descendents, even if parent is not redundant. - A parent declaration may be used outside its file (making it not redundant), - while still having child declarations that are only used within the same file - (making those children redundant). For example, a class used cross-file may have - a fileprivate property only referenced within the same file - that property should - be flagged as redundant even though the parent class is not. - */ + A parent declaration may be used outside its file (making it not redundant), + while still having child declarations that are only used within the same file + (making those children redundant). For example, a class used cross-file may have + a fileprivate property only referenced within the same file - that property should + be flagged as redundant even though the parent class is not. + */ markExplicitFilePrivateDescendentDeclarations(from: decl) } private func validateExtension(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.fileprivate) { if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), - graph.redundantFilePrivateAccessibility.keys.contains(extendedDecl) { + graph.redundantFilePrivateAccessibility.keys.contains(extendedDecl) + { mark(decl) } } @@ -75,4 +76,4 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { let filePrivateDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) } return filePrivateDeclarations.flatMapSet { descendentFilePrivateDeclarations(from: $0) }.union(filePrivateDeclarations) } -} \ No newline at end of file +} diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 62c5ffdbc2..75a1658163 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -38,21 +38,22 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } /* - Always check descendents, even if parent is not redundant. - - A parent declaration may be used outside its file (making it not redundant), - while still having child declarations that are only used within the same file - (making those children redundant). For example, a class used cross-file may have - an internal property only referenced within the same file - that property should - be flagged as redundant even though the parent class is not. - */ + Always check descendents, even if parent is not redundant. + + A parent declaration may be used outside its file (making it not redundant), + while still having child declarations that are only used within the same file + (making those children redundant). For example, a class used cross-file may have + an internal property only referenced within the same file - that property should + be flagged as redundant even though the parent class is not. + */ markExplicitInternalDescendentDeclarations(from: decl) } private func validateExtension(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.internal) { if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), - graph.redundantInternalAccessibility.keys.contains(extendedDecl) { + graph.redundantInternalAccessibility.keys.contains(extendedDecl) + { mark(decl) } } @@ -78,7 +79,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Use graph.references(to: decl) to get all references to this declaration let allReferences = graph.references(to: decl) let referenceFiles = allReferences.map(\.location.file) - + let result = referenceFiles.contains { $0 != decl.location.file } return result } @@ -87,4 +88,4 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let internalDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.internal) } return internalDeclarations.flatMapSet { descendentInternalDeclarations(from: $0) }.union(internalDeclarations) } -} \ No newline at end of file +} diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift index 1d1063498a..45bf4614c3 100644 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -18,4 +18,3 @@ final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) } } - diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 1b806cfe6e..3f91e497da 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -7,44 +7,44 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { super.setUp() build(projectPath: AccessibilityProjectPath) } - + func testInternalPropertyUsedInExtensionInOtherFile() { // This should NOT be flagged as redundant // Tests the case where an internal property is used in an extension in a different file index() - + // InternalPropertyUsedInExtension.propertyUsedInExtension should NOT be flagged as redundant // because it's used in InternalPropertyExtension.swift assertNotRedundantInternalAccessibility(.varInstance("propertyUsedInExtension")) } - + func testInternalPropertyUsedOnlyInSameFile() { // This should be flagged as redundant // Tests the case where an internal property is only used within its own file index() - + // InternalPropertyUsedInExtension.propertyOnlyUsedInSameFile should be flagged as redundant // because it's only used within InternalPropertyUsedInExtension.swift assertRedundantInternalAccessibility(.varInstance("propertyOnlyUsedInSameFile")) } - + func testInternalPropertyUsedInMultipleFiles() { // This should NOT be flagged as redundant // Tests the case where an internal property is used across multiple files index() - + // This test would need additional setup with multiple files // For now, we'll test that the existing NotRedundantInternalClassComponents work assertNotRedundantInternalAccessibility(.class("NotRedundantInternalClass")) } - + func testInternalMethodUsedInExtension() { // This should NOT be flagged as redundant // Tests the case where an internal method is used in an extension index() - + // This test would need additional setup with methods in extensions // For now, we'll test that the existing NotRedundantInternalClassComponents work assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) } -} +} From 945b77823c17d0200245c6a31360bc2d42d03bce Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:25:37 -0800 Subject: [PATCH 13/31] restore paths to be fileprivate, since we were getting false positive. Fix the scanning logic for that, and update the tests to reflect that. --- Sources/Extensions/FilePath+Glob.swift | 2 +- ...undantFilePrivateAccessibilityMarker.swift | 74 ++++++++++++++++++- ...antFilePrivatePropertyInPrivateClass.swift | 37 ++++++++++ .../TargetA/RedundantFilePrivateClass.swift | 8 +- ...edundantFilePrivateAccessibilityTest.swift | 33 ++++++++- Tests/Shared/SourceGraphTestCase.swift | 12 --- 6 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivatePropertyInPrivateClass.swift diff --git a/Sources/Extensions/FilePath+Glob.swift b/Sources/Extensions/FilePath+Glob.swift index d946e1a1cc..21e2fbd051 100644 --- a/Sources/Extensions/FilePath+Glob.swift +++ b/Sources/Extensions/FilePath+Glob.swift @@ -28,7 +28,7 @@ private final class Glob { private let excludedDirectories: [String] private var isDirectoryCache: [String: Bool] = [:] - var paths: Set = [] + fileprivate var paths: Set = [] init( pattern: String, diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index 738adbe724..981640013b 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -29,7 +29,7 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.fileprivate) { - if !graph.isRetained(decl), !isReferencedOutsideFile(decl) { + if !graph.isRetained(decl), !isReferencedOutsideFile(decl), !isReferencedFromDifferentTypeInSameFile(decl) { mark(decl) } } @@ -63,7 +63,9 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func markExplicitFilePrivateDescendentDeclarations(from decl: Declaration) { for descDecl in descendentFilePrivateDeclarations(from: decl) { - mark(descDecl) + if !graph.isRetained(descDecl), !isReferencedOutsideFile(descDecl), !isReferencedFromDifferentTypeInSameFile(descDecl) { + mark(descDecl) + } } } @@ -76,4 +78,72 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { let filePrivateDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) } return filePrivateDeclarations.flatMapSet { descendentFilePrivateDeclarations(from: $0) }.union(filePrivateDeclarations) } + + /** + Finds the top-level type declaration by walking up the parent chain. + Returns the outermost type that contains the given declaration. + */ + private func topLevelType(of decl: Declaration) -> Declaration? { + let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] + let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) + let ancestors = [decl] + Array(decl.ancestralDeclarations) + return ancestors.last { typeDecl in + guard typeKinds.contains(typeDecl.kind) else { return false } + guard let parent = typeDecl.parent else { return true } + return !typeKinds.contains(parent.kind) + } + } + + /** + Gets the logical type for comparison purposes. + For extensions of types in the SAME FILE, treats the extension as the extended type. + For extensions of types in DIFFERENT FILES (like extending external types), + treats the extension as its own distinct type for the purpose of this file. + */ + private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { + if decl.kind.isExtensionKind { + if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), + extendedDecl.location.file == file + { + return extendedDecl + } + return decl + } + return decl + } + + /** + Checks if a declaration is referenced from a different type in the same file. + Returns true if any same-file reference comes from a different logical type, + indicating that fileprivate access is necessary. + + Even for top-level declarations, private and fileprivate are different: + - private: only accessible within the declaration itself and its extensions in the same file + - fileprivate: accessible from anywhere in the same file + */ + private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { + let file = decl.location.file + let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } + + guard let declTopLevel = topLevelType(of: decl) else { + return false + } + + let declLogicalType = logicalType(of: declTopLevel, inFile: file) + + for ref in sameFileReferences { + guard let refParent = ref.parent, + let refTopLevel = topLevelType(of: refParent) + else { + continue + } + + let refLogicalType = logicalType(of: refTopLevel, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + return false + } } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivatePropertyInPrivateClass.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivatePropertyInPrivateClass.swift new file mode 100644 index 0000000000..3673f0db4f --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantFilePrivatePropertyInPrivateClass.swift @@ -0,0 +1,37 @@ +/* + NotRedundantFilePrivatePropertyInPrivateClass.swift + Tests that a fileprivate property in a private class is NOT flagged as redundant + when accessed from a different type (extension) in the same file. + + This replicates the pattern from FilePath+Glob.swift where: + - A private class has a fileprivate property + - An extension of a different type accesses that property + - The fileprivate modifier is necessary for cross-type same-file access +*/ + +public extension SomePublicType { + static func accessPrivateClass() -> Set { + PrivateClassWithFilePrivateProperty().filePrivatePaths + } +} + +private class PrivateClassWithFilePrivateProperty { + fileprivate var filePrivatePaths: Set = ["path1", "path2"] + + func someMethod() { + _ = filePrivatePaths.count + } +} + +/* + This retainer ensures the extension method is used, making the file indexed. + The key test is that filePrivatePaths is accessed from the SomePublicType extension, + which is a different type than PrivateClassWithFilePrivateProperty. +*/ +public struct SomePublicType { + public init() {} + + public func retain() { + _ = SomePublicType.accessPrivateClass() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift index 8aaf989961..5b4d5e3f38 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantFilePrivateClass.swift @@ -1,6 +1,7 @@ /* RedundantFilePrivateClass.swift - Tests that a fileprivate class is flagged as redundant when it's only referenced from within its own declaration + This file tests a case where fileprivate is actually NOT redundant because the + class is accessed from a different type (RedundantFilePrivateClassRetainer). */ fileprivate class RedundantFilePrivateClass { @@ -18,9 +19,8 @@ fileprivate class RedundantFilePrivateClass { } /* - This retainer ensures the file is indexed and calls a method on the class. - The fileprivate class is used, but only within the same file and not by other declarations, - making the fileprivate access level redundant (could be private). + This retainer accesses RedundantFilePrivateClass from a different type, + which means fileprivate is necessary (NOT redundant). */ public class RedundantFilePrivateClassRetainer { public init() {} diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift index 45bf4614c3..936c7e5d6e 100644 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -8,13 +8,44 @@ final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { build(projectPath: AccessibilityProjectPath) } + /* + Tests that a fileprivate class is NOT flagged as redundant when accessed + from a different type in the same file. + + In Swift, private and fileprivate have distinct meanings even for top-level declarations: + - private: accessible only within the lexical scope and its extensions in the same file + - fileprivate: accessible from anywhere in the same file + + Since RedundantFilePrivateClass is accessed from RedundantFilePrivateClassRetainer + (a different type), fileprivate is the minimum access level required. Changing it to + private would prevent RedundantFilePrivateClassRetainer from accessing it. + */ func testRedundantFilePrivateClass() { index() - assertRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) + assertNotRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) } func testNotRedundantFilePrivateClass() { index() assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) } + + func testNotRedundantFilePrivatePropertyInPrivateClass() { + index() + assertNotRedundantFilePrivateAccessibility(.varInstance("filePrivatePaths")) + } + + /* + NOTE: the opposite of the above, e.g. a function called "assertRedundantFilePrivateAccessibility()", + is intentionally not tested here. + + After fixing the bug where cross-type same-file references were incorrectly + flagged as redundant, there are no meaningful test cases for truly redundant + fileprivate declarations. Here's why: + + - Fileprivate exists specifically for same-file cross-type access + - A fileprivate declaration used only within its own type should be private + - A fileprivate declaration with no references at all is marked as "unused", not + "redundant fileprivate" + */ } diff --git a/Tests/Shared/SourceGraphTestCase.swift b/Tests/Shared/SourceGraphTestCase.swift index 5a10fb344c..445ad253d2 100644 --- a/Tests/Shared/SourceGraphTestCase.swift +++ b/Tests/Shared/SourceGraphTestCase.swift @@ -201,18 +201,6 @@ open class SourceGraphTestCase: XCTestCase { scopeStack.removeLast() } - func assertRedundantFilePrivateAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { - guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } - - if !Self.results.redundantFilePrivateAccessibilityDeclarations.contains(declaration) { - XCTFail("Expected declaration to have redundant fileprivate accessibility: \(declaration)", file: file, line: line) - } - - scopeStack.append(.declaration(declaration)) - scopedAssertions?() - scopeStack.removeLast() - } - func assertNotRedundantFilePrivateAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } From 8e018df35d3bc4f21c9b3133b9ee755095f34743 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:06:54 -0800 Subject: [PATCH 14/31] readme update --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 17d3ad87a8..501158dae5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ - [Enumerations](#enumerations) - [Assign-only Properties](#assign-only-properties) - [Redundant Public Accessibility](#redundant-public-accessibility) + - [Redundant Internal Accessibility](#redundant-internal-accessibility) + - [Redundant Fileprivate Accessibility](#redundant-fileprivate-accessibility) - [Unused Imports](#unused-imports) - [Objective-C](#objective-c) - [Codable](#codable) @@ -292,6 +294,18 @@ Declarations that are marked `public` yet are not referenced from outside their This analysis can be disabled with `--disable-redundant-public-analysis`. +### Redundant Internal Accessibility + +Declarations that are marked `internal` (or are unmarked, since this is Swift's default access level), yet are not referenced outside the file they're defined in are identified as having redundant internal accessibility. In this scenario, the declaration could be marked `private` or `fileprivate`. Reducing the visibility of declarations — encapsulation — helps with code maintainability and can improve compilation performance. + +This analysis can be disabled with `--disable-redundant-internal-analysis`. + +### Redundant Fileprivate Accessibility + +Declarations that are marked `fileprivate` yet are not accessed from other types within the same file are identified as having redundant fileprivate accessibility. If a `fileprivate` declaration is only used within its own type, it should be marked `private` instead. Reducing the visibility of declarations helps with code maintainability and makes access boundaries clearer. + +This analysis can be disabled with `--disable-redundant-fileprivate-analysis`. + ### Unused Imports Periphery can only detect unused imports of targets it has scanned. It cannot detect unused imports of other targets because the Swift source files are unavailable and uses of `@_exported` cannot be observed. `@_exported` is problematic because it changes the public interface of a target such that the declarations exported by the target are no longer necessarily declared by the imported target. For example, the `Foundation` target exports `Dispatch`, among other targets. If any given source file imports `Foundation` and references `DispatchQueue` but no other declarations from `Foundation`, then the `Foundation` import cannot be removed as it would also make the `DispatchQueue` type unavailable. To avoid false positives, therefore, Periphery only detects unused imports of targets it has scanned. From 1e958bef3bc49aebc3b3c36aea2bfe4229984b09 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:18:58 -0800 Subject: [PATCH 15/31] A bit more documentation --- ...RedundantFilePrivateAccessibilityMarker.swift | 16 ++++++++++++++++ .../RedundantInternalAccessibilityMarker.swift | 11 +++++++++++ 2 files changed, 27 insertions(+) diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index 981640013b..cdf40dbabc 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -1,6 +1,22 @@ import Configuration import Shared +/** + Identifies declarations explicitly marked `fileprivate` that don't actually need file-level access. + + Swift's `fileprivate` exists specifically to allow access from other types within the same file. + If a `fileprivate` declaration is only accessed within its own type (not from other types in + the same file), it should be marked `private` instead. + + This mutator is more complex than RedundantInternalAccessibilityMarker because it must: + - Distinguish between access from the same type vs. different types in the same file + - Handle extensions of types (both same-file and cross-file extensions) + - Walk the type hierarchy to find the top-level containing type for comparison + + The key insight: `private` and `fileprivate` differ in that `private` is accessible only within + the declaration and its extensions in the same file, while `fileprivate` is accessible from + anywhere in the same file. + */ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private let graph: SourceGraph private let configuration: Configuration diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 75a1658163..da2d87d357 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -1,6 +1,17 @@ import Configuration import Shared +/** + Identifies declarations explicitly marked `internal` that are not referenced outside + the file they're defined in. + + Since `internal` is Swift's default access level, declarations that are only used within + their defining file should be marked `private` or `fileprivate` instead. This improves + encapsulation and can help with compilation performance. + + This mutator follows the same pattern as RedundantPublicAccessibilityMarker but checks + for file-scoped usage instead of module-scoped usage. + */ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private let graph: SourceGraph private let configuration: Configuration From e782693a690436dacdf6f903feaf4dc33840a3df Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:06:16 -0800 Subject: [PATCH 16/31] Handle implicit internal, fix false positives and false negatives, refactor checking --- Sources/BUILD.bazel | 1 + Sources/Configuration/Configuration.swift | 5 +- Sources/Frontend/Commands/ScanCommand.swift | 4 + .../Results/OutputFormatter.swift | 12 +- Sources/PeripheryKit/ScanResult.swift | 4 +- Sources/PeripheryKit/ScanResultBuilder.swift | 4 +- .../RedundantAccessibilityMarkerShared.swift | 55 ++++ ...antExplicitPublicAccessibilityMarker.swift | 5 +- ...undantFilePrivateAccessibilityMarker.swift | 128 +++++--- ...RedundantInternalAccessibilityMarker.swift | 309 +++++++++++++++--- Sources/SourceGraph/SourceGraph.swift | 23 +- .../Sources/MainTarget/main.swift | 9 + ...ternalSuggestingPrivateVsFileprivate.swift | 46 +++ .../TargetA/NestedTypeAccessibility.swift | 43 +++ .../NotRedundantInternalClassComponents.swift | 5 + ...ndantInternalClassComponents_Support.swift | 13 +- .../PropertyWrapperAccessibility.swift | 49 +++ .../ProtocolRequirementAccessibility.swift | 41 +++ .../TargetA/RedundantInternalComponents.swift | 65 +++- .../TrulyRedundantFilePrivateMembers.swift | 38 +++ ...edundantFilePrivateAccessibilityTest.swift | 55 ++-- .../RedundantInternalAccessibilityTest.swift | 137 +++++++- Tests/Shared/SourceGraphTestCase.swift | 31 +- 23 files changed, 937 insertions(+), 145 deletions(-) create mode 100644 Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalSuggestingPrivateVsFileprivate.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedTypeAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/PropertyWrapperAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolRequirementAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TrulyRedundantFilePrivateMembers.swift diff --git a/Sources/BUILD.bazel b/Sources/BUILD.bazel index a538fd2bed..66a96f3cf9 100644 --- a/Sources/BUILD.bazel +++ b/Sources/BUILD.bazel @@ -78,6 +78,7 @@ swift_library( "SourceGraph/Mutators/ProtocolConformanceReferenceBuilder.swift", "SourceGraph/Mutators/ProtocolExtensionReferenceBuilder.swift", "SourceGraph/Mutators/PubliclyAccessibleRetainer.swift", + "SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift", "SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift", "SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift", "SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift", diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index 82531c73d8..ebb0f21208 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -86,6 +86,9 @@ public final class Configuration { @Setting(key: "disable_redundant_fileprivate_analysis", defaultValue: false) public var disableRedundantFilePrivateAnalysis: Bool + @Setting(key: "show_nested_redundant_accessibility", defaultValue: false) + public var showNestedRedundantAccessibility: Bool + @Setting(key: "disable_unused_import_analysis", defaultValue: false) public var disableUnusedImportAnalysis: Bool @@ -225,7 +228,7 @@ public final class Configuration { $project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat, $retainPublic, $noRetainSPI, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible, $retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis, - $disableRedundantInternalAnalysis, $disableRedundantFilePrivateAnalysis, + $disableRedundantInternalAnalysis, $disableRedundantFilePrivateAnalysis, $showNestedRedundantAccessibility, $disableUnusedImportAnalysis, $superfluousIgnoreComments, $retainUnusedImportedModules, $externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $color, $disableUpdateCheck, $strict, $indexStorePath, diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index cd3d598940..3262f2bf3b 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -69,6 +69,9 @@ struct ScanCommand: ParsableCommand { @Flag(help: "Disable identification of redundant fileprivate accessibility") var disableRedundantFilePrivateAnalysis: Bool = defaultConfiguration.$disableRedundantFilePrivateAnalysis.defaultValue + @Flag(help: "Show redundant internal/fileprivate accessibility warnings for nested declarations even when the containing type is already flagged") + var showNestedRedundantAccessibility: Bool = defaultConfiguration.$showNestedRedundantAccessibility.defaultValue + @Flag(help: "Disable identification of unused imports") var disableUnusedImportAnalysis: Bool = defaultConfiguration.$disableUnusedImportAnalysis.defaultValue @@ -201,6 +204,7 @@ struct ScanCommand: ParsableCommand { configuration.apply(\.$disableRedundantPublicAnalysis, disableRedundantPublicAnalysis) configuration.apply(\.$disableRedundantInternalAnalysis, disableRedundantInternalAnalysis) configuration.apply(\.$disableRedundantFilePrivateAnalysis, disableRedundantFilePrivateAnalysis) + configuration.apply(\.$showNestedRedundantAccessibility, showNestedRedundantAccessibility) configuration.apply(\.$disableUnusedImportAnalysis, disableUnusedImportAnalysis) configuration.apply(\.$superfluousIgnoreComments, superfluousIgnoreComments) configuration.apply(\.$retainUnusedImportedModules, retainUnusedImportedModules) diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index a5e5b6c5ff..a1d9040b7f 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -68,12 +68,12 @@ extension OutputFormatter { case let .redundantPublicAccessibility(modules): let modulesJoined = modules.sorted().joined(separator: ", ") description += "Redundant public accessibility for \(kindDisplayName) '\(name)' (not used outside of \(modulesJoined))" - case let .redundantInternalAccessibility(files): - let filesJoined = files.sorted { $0.path.string < $1.path.string }.map(\.path.string).joined(separator: ", ") - description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of \(filesJoined))" - case let .redundantFilePrivateAccessibility(files): - let filesJoined = files.sorted { $0.path.string < $1.path.string }.map(\.path.string).joined(separator: ", ") - description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (not used outside its enclosing scope in \(filesJoined))" + case let .redundantInternalAccessibility(_, suggestedAccessibility): + let accessibilityText = suggestedAccessibility?.rawValue ?? "private/fileprivate" + description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of file; can be \(accessibilityText)" + case let .redundantFilePrivateAccessibility(_, containingTypeName): + let context = containingTypeName.map { "only used within \($0)" } ?? "not used outside of file" + description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (\(context); can be private)" case .superfluousIgnoreCommand: description += "Superfluous ignore comment for \(kindDisplayName) '\(name)' (declaration is referenced and should not be ignored)" } diff --git a/Sources/PeripheryKit/ScanResult.swift b/Sources/PeripheryKit/ScanResult.swift index 3079dccf60..76d124bc31 100644 --- a/Sources/PeripheryKit/ScanResult.swift +++ b/Sources/PeripheryKit/ScanResult.swift @@ -7,8 +7,8 @@ public struct ScanResult { case assignOnlyProperty case redundantProtocol(references: Set, inherited: Set) case redundantPublicAccessibility(modules: Set) - case redundantInternalAccessibility(files: Set) - case redundantFilePrivateAccessibility(files: Set) + case redundantInternalAccessibility(files: Set, suggestedAccessibility: Accessibility?) + case redundantFilePrivateAccessibility(files: Set, containingTypeName: String?) case superfluousIgnoreCommand } diff --git a/Sources/PeripheryKit/ScanResultBuilder.swift b/Sources/PeripheryKit/ScanResultBuilder.swift index 7a8e0c260d..0f78592ee6 100644 --- a/Sources/PeripheryKit/ScanResultBuilder.swift +++ b/Sources/PeripheryKit/ScanResultBuilder.swift @@ -44,10 +44,10 @@ public enum ScanResultBuilder { .init(declaration: $0.0, annotation: .redundantPublicAccessibility(modules: $0.1)) } let annotatedRedundantInternalAccessibility: [ScanResult] = redundantInternalAccessibility.map { - .init(declaration: $0.0, annotation: .redundantInternalAccessibility(files: $0.1)) + .init(declaration: $0.0, annotation: .redundantInternalAccessibility(files: $0.1.files, suggestedAccessibility: $0.1.suggestedAccessibility)) } let annotatedRedundantFilePrivateAccessibility: [ScanResult] = redundantFilePrivateAccessibility.map { - .init(declaration: $0.0, annotation: .redundantFilePrivateAccessibility(files: $0.1)) + .init(declaration: $0.0, annotation: .redundantFilePrivateAccessibility(files: $0.1.files, containingTypeName: $0.1.containingTypeName)) } let annotatedSuperfluousIgnoreCommands: [ScanResult] = { diff --git a/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift new file mode 100644 index 0000000000..894de53616 --- /dev/null +++ b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift @@ -0,0 +1,55 @@ +// Shared utilities for redundant accessibility analysis mutators. + +extension Declaration { + /// Checks if this declaration is referenced outside its defining file. + /// This is a common check used by multiple accessibility markers to determine + /// if a declaration needs file-level or module-level accessibility. + func isReferencedOutsideFile(graph: SourceGraph) -> Bool { + graph.references(to: self).map(\.location.file).contains { $0 != location.file } + } + + /// Generic recursive descendent declaration finder with filtering. + /// Recursively traverses the declaration tree and returns all descendants matching the predicate. + /// Used by all accessibility markers to find declarations with specific accessibility levels. + func descendentDeclarations(matching predicate: (Declaration) -> Bool) -> Set { + let matchingDeclarations = declarations.filter(predicate) + return matchingDeclarations + .flatMapSet { $0.descendentDeclarations(matching: predicate) } + .union(matchingDeclarations) + } + + /// Checks if any ancestor declaration is marked as redundant in the given accessibility map. + /// Used by accessibility markers to suppress nested warnings when a containing type is already flagged. + /// This avoids redundant warnings since fixing the parent's accessibility fixes the children too. + func isAnyAncestorMarked(in accessibilityMap: [Declaration: Any]) -> Bool { + var current = parent + var visited: Set = [] + + while let currentParent = current { + guard !visited.contains(currentParent) else { + return false + } + + visited.insert(currentParent) + + if accessibilityMap[currentParent] != nil { + return true + } + current = currentParent.parent + } + return false + } + + /// Counts the number of ancestors for this declaration. + /// Used for sorting declarations by depth to ensure parents are marked before children, + /// which is important for nested redundant accessibility suppression logic. + var ancestorCount: Int { + var count = 0 + var current = parent + while current != nil { + count += 1 + current = current?.parent + } + return count + } +} diff --git a/Sources/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift index 20bb55c764..2b011f7d10 100644 --- a/Sources/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantExplicitPublicAccessibilityMarker.swift @@ -198,7 +198,8 @@ final class RedundantExplicitPublicAccessibilityMarker: SourceGraphMutator { } private func descendentPublicDeclarations(from decl: Declaration) -> Set { - let publicDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.public) } - return publicDeclarations.flatMapSet { descendentPublicDeclarations(from: $0) }.union(publicDeclarations) + decl.descendentDeclarations(matching: { + !$0.isImplicit && $0.accessibility.isExplicitly(.public) + }) } } diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index cdf40dbabc..fa118fb461 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -1,22 +1,20 @@ import Configuration import Shared -/** - Identifies declarations explicitly marked `fileprivate` that don't actually need file-level access. - - Swift's `fileprivate` exists specifically to allow access from other types within the same file. - If a `fileprivate` declaration is only accessed within its own type (not from other types in - the same file), it should be marked `private` instead. - - This mutator is more complex than RedundantInternalAccessibilityMarker because it must: - - Distinguish between access from the same type vs. different types in the same file - - Handle extensions of types (both same-file and cross-file extensions) - - Walk the type hierarchy to find the top-level containing type for comparison - - The key insight: `private` and `fileprivate` differ in that `private` is accessible only within - the declaration and its extensions in the same file, while `fileprivate` is accessible from - anywhere in the same file. - */ +/// Identifies declarations explicitly marked `fileprivate` that don't actually need file-level access. +/// +/// Swift's `fileprivate` exists specifically to allow access from other types within the same file. +/// If a `fileprivate` declaration is only accessed within its own type (not from other types in +/// the same file), it should be marked `private` instead. +/// +/// This mutator is more complex than RedundantInternalAccessibilityMarker because it must: +/// - Distinguish between access from the same type vs. different types in the same file +/// - Handle extensions of types (both same-file and cross-file extensions) +/// - Walk the type hierarchy to find the top-level containing type for comparison +/// +/// The key insight: `private` and `fileprivate` differ in that `private` is accessible only within +/// the declaration and its extensions in the same file, while `fileprivate` is accessible from +/// anywhere in the same file. final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private let graph: SourceGraph private let configuration: Configuration @@ -45,20 +43,21 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.fileprivate) { - if !graph.isRetained(decl), !isReferencedOutsideFile(decl), !isReferencedFromDifferentTypeInSameFile(decl) { + if !graph.isRetained(decl), + !decl.isReferencedOutsideFile(graph: graph), + !isReferencedFromDifferentTypeInSameFile(decl) + { mark(decl) } } - /* - Always check descendents, even if parent is not redundant. - - A parent declaration may be used outside its file (making it not redundant), - while still having child declarations that are only used within the same file - (making those children redundant). For example, a class used cross-file may have - a fileprivate property only referenced within the same file - that property should - be flagged as redundant even though the parent class is not. - */ + // Always check descendants, even if parent is not redundant. + // + // A parent declaration may be used outside its file (making it not redundant), + // while still having child declarations that are only used within the same file + // (making those children redundant). For example, a class used cross-file may have + // a fileprivate property only referenced within the same file - that property should + // be flagged as redundant even though the parent class is not. markExplicitFilePrivateDescendentDeclarations(from: decl) } @@ -74,31 +73,44 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func mark(_ decl: Declaration) { guard !graph.isRetained(decl) else { return } - graph.markRedundantFilePrivateAccessibility(decl, file: decl.location.file) + + // Unless explicitly requested, skip marking nested declarations when an ancestor is already marked. + // This avoids redundant warnings since fixing the parent's accessibility fixes the children too. + if !configuration.showNestedRedundantAccessibility, + decl.isAnyAncestorMarked(in: graph.redundantFilePrivateAccessibility) + { + return + } + + let containingTypeName = containingTypeName(for: decl) + graph.markRedundantFilePrivateAccessibility(decl, file: decl.location.file, containingTypeName: containingTypeName) } private func markExplicitFilePrivateDescendentDeclarations(from decl: Declaration) { - for descDecl in descendentFilePrivateDeclarations(from: decl) { - if !graph.isRetained(descDecl), !isReferencedOutsideFile(descDecl), !isReferencedFromDifferentTypeInSameFile(descDecl) { + // Sort descendants by their depth to ensure parents are marked before children. + // This is important for the nested redundant accessibility suppression logic. + let descendants = descendentFilePrivateDeclarations(from: decl).sorted { decl1, decl2 in + decl1.ancestorCount < decl2.ancestorCount + } + + for descDecl in descendants { + if !graph.isRetained(descDecl), + !descDecl.isReferencedOutsideFile(graph: graph), + !isReferencedFromDifferentTypeInSameFile(descDecl) + { mark(descDecl) } } } - private func isReferencedOutsideFile(_ decl: Declaration) -> Bool { - let referenceFiles = graph.references(to: decl).map(\.location.file) - return referenceFiles.contains { $0 != decl.location.file } - } - private func descendentFilePrivateDeclarations(from decl: Declaration) -> Set { - let filePrivateDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) } - return filePrivateDeclarations.flatMapSet { descendentFilePrivateDeclarations(from: $0) }.union(filePrivateDeclarations) + decl.descendentDeclarations(matching: { + !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) + }) } - /** - Finds the top-level type declaration by walking up the parent chain. - Returns the outermost type that contains the given declaration. - */ + /// Finds the top-level type declaration by walking up the parent chain. + /// Returns the outermost type that contains the given declaration. private func topLevelType(of decl: Declaration) -> Declaration? { let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) @@ -106,16 +118,15 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { return ancestors.last { typeDecl in guard typeKinds.contains(typeDecl.kind) else { return false } guard let parent = typeDecl.parent else { return true } + return !typeKinds.contains(parent.kind) } } - /** - Gets the logical type for comparison purposes. - For extensions of types in the SAME FILE, treats the extension as the extended type. - For extensions of types in DIFFERENT FILES (like extending external types), - treats the extension as its own distinct type for the purpose of this file. - */ + /// Gets the logical type for comparison purposes. + /// For extensions of types in the SAME FILE, treats the extension as the extended type. + /// For extensions of types in DIFFERENT FILES (like extending external types), + /// treats the extension as its own distinct type for the purpose of this file. private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { if decl.kind.isExtensionKind { if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), @@ -128,15 +139,24 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { return decl } - /** - Checks if a declaration is referenced from a different type in the same file. - Returns true if any same-file reference comes from a different logical type, - indicating that fileprivate access is necessary. + /// Extracts a display name for the containing type of a declaration. + /// + /// Returns a string like "class Foo" or "struct Bar" that identifies the type + /// containing the declaration. Returns nil for top-level declarations. + private func containingTypeName(for decl: Declaration) -> String? { + guard let topLevel = topLevelType(of: decl) else { return nil } + guard let name = topLevel.name else { return nil } + + return "\(topLevel.kind.displayName) \(name)" + } - Even for top-level declarations, private and fileprivate are different: - - private: only accessible within the declaration itself and its extensions in the same file - - fileprivate: accessible from anywhere in the same file - */ + /// Checks if a declaration is referenced from a different type in the same file. + /// Returns true if any same-file reference comes from a different logical type, + /// indicating that fileprivate access is necessary. + /// + /// Even for top-level declarations, private and fileprivate are different: + /// - private: only accessible within the declaration itself and its extensions in the same file + /// - fileprivate: accessible from anywhere in the same file private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { let file = decl.location.file let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index da2d87d357..23bb9c3731 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -1,17 +1,15 @@ import Configuration import Shared -/** - Identifies declarations explicitly marked `internal` that are not referenced outside - the file they're defined in. - - Since `internal` is Swift's default access level, declarations that are only used within - their defining file should be marked `private` or `fileprivate` instead. This improves - encapsulation and can help with compilation performance. - - This mutator follows the same pattern as RedundantPublicAccessibilityMarker but checks - for file-scoped usage instead of module-scoped usage. - */ +/// Identifies `internal` declarations (implicit, or explicitly marked) that are not referenced outside +/// the file they're defined in. +/// +/// Since `internal` is Swift's default access level, declarations that are only used within +/// their defining file should be marked `private` or `fileprivate` instead. This improves +/// encapsulation and can help with compilation performance. +/// +/// This mutator follows the same pattern as RedundantPublicAccessibilityMarker but checks +/// for file-scoped usage instead of module-scoped usage. final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private let graph: SourceGraph private let configuration: Configuration @@ -39,29 +37,27 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // MARK: - Private private func validate(_ decl: Declaration) throws { - if decl.accessibility.isExplicitly(.internal) { - if !graph.isRetained(decl) { - let isReferencedOutside = isReferencedOutsideFile(decl) + if decl.accessibility.value == .internal { + if !graph.isRetained(decl), !shouldSkipMarking(decl) { + let isReferencedOutside = decl.isReferencedOutsideFile(graph: graph) if !isReferencedOutside { mark(decl) } } } - /* - Always check descendents, even if parent is not redundant. - - A parent declaration may be used outside its file (making it not redundant), - while still having child declarations that are only used within the same file - (making those children redundant). For example, a class used cross-file may have - an internal property only referenced within the same file - that property should - be flagged as redundant even though the parent class is not. - */ - markExplicitInternalDescendentDeclarations(from: decl) + // Always check descendants, even if parent is not redundant. + // + // A parent declaration may be used outside its file (making it not redundant), + // while still having child declarations that are only used within the same file + // (making those children redundant). For example, a class used cross-file may have + // an internal property only referenced within the same file - that property should + // be flagged as redundant even though the parent class is not. + markInternalDescendentDeclarations(from: decl) } private func validateExtension(_ decl: Declaration) throws { - if decl.accessibility.isExplicitly(.internal) { + if decl.accessibility.value == .internal { if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), graph.redundantInternalAccessibility.keys.contains(extendedDecl) { @@ -72,13 +68,52 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private func mark(_ decl: Declaration) { guard !graph.isRetained(decl) else { return } - graph.markRedundantInternalAccessibility(decl, file: decl.location.file) + + // Unless explicitly requested, skip marking nested declarations when an ancestor is already marked. + // This avoids redundant warnings since fixing the parent's accessibility fixes the children too. + if !configuration.showNestedRedundantAccessibility, + decl.isAnyAncestorMarked(in: graph.redundantInternalAccessibility) + { + return + } + + // Determine the suggested accessibility level. + // For top-level declarations, fileprivate is equivalent to private, so we pass nil + // to indicate the ambiguity in the output message. + // If the declaration is referenced from different types in the same file, + // it needs fileprivate. Otherwise, private is sufficient. + let isTopLevel = decl.parent == nil + let suggestedAccessibility: Accessibility? = isTopLevel ? nil : (isReferencedFromDifferentTypeInSameFile(decl) ? .fileprivate : .private) + + // Check if the parent's accessibility already constrains this member. + // If the parent is `private`, the member is already effectively `private`. + // If the parent is `fileprivate` and we would suggest `fileprivate`, it's already constrained. + // Marking these would be misleading since changing them would actually increase visibility. + if let maxAccessibility = effectiveMaximumAccessibility(for: decl), + let suggestedAccessibility + { + let accessibilityOrder: [Accessibility] = [.private, .fileprivate, .internal, .public, .open] + let maxIndex = accessibilityOrder.firstIndex(of: maxAccessibility) ?? 0 + let suggestedIndex = accessibilityOrder.firstIndex(of: suggestedAccessibility) ?? 0 + + if suggestedIndex >= maxIndex { + return + } + } + + graph.markRedundantInternalAccessibility(decl, file: decl.location.file, suggestedAccessibility: suggestedAccessibility) } - private func markExplicitInternalDescendentDeclarations(from decl: Declaration) { - for descDecl in descendentInternalDeclarations(from: decl) { - if !graph.isRetained(descDecl) { - let isReferencedOutside = isReferencedOutsideFile(descDecl) + private func markInternalDescendentDeclarations(from decl: Declaration) { + // Sort descendants by their depth to ensure parents are marked before children. + // This is important for the nested redundant accessibility suppression logic. + let descendants = descendentInternalDeclarations(from: decl).sorted { decl1, decl2 in + decl1.ancestorCount < decl2.ancestorCount + } + + for descDecl in descendants { + if !graph.isRetained(descDecl), !shouldSkipMarking(descDecl) { + let isReferencedOutside = descDecl.isReferencedOutsideFile(graph: graph) if !isReferencedOutside { mark(descDecl) } @@ -86,17 +121,215 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } } - private func isReferencedOutsideFile(_ decl: Declaration) -> Bool { - // Use graph.references(to: decl) to get all references to this declaration - let allReferences = graph.references(to: decl) - let referenceFiles = allReferences.map(\.location.file) + /// Determines if a declaration should be skipped from redundant internal marking. + /// + /// Declarations are skipped if: + /// - They should be skipped from all accessibility analysis (generic type params, implicit decls) + /// - They are protocol requirements (must maintain accessibility for protocol conformance) + /// - They are part of a property wrapper's API (must be accessible to wrapper users) + private func shouldSkipMarking(_ decl: Declaration) -> Bool { + if shouldSkipAccessibilityAnalysis(for: decl) { + return true + } - let result = referenceFiles.contains { $0 != decl.location.file } - return result + if isProtocolRequirement(decl) { + return true + } + + if isPropertyWrapperMember(decl) { + return true + } + + return false } private func descendentInternalDeclarations(from decl: Declaration) -> Set { - let internalDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.internal) } - return internalDeclarations.flatMapSet { descendentInternalDeclarations(from: $0) }.union(internalDeclarations) + decl.descendentDeclarations(matching: { + $0.accessibility.value == .internal + }) + } + + // MARK: - Internal Accessibility Analysis Helpers + + /// Determines if a declaration should be skipped from accessibility analysis entirely. + /// + /// This helper is specific to internal accessibility analysis, checking conditions + /// that make a declaration ineligible for redundant internal marking. + private func shouldSkipAccessibilityAnalysis(for decl: Declaration) -> Bool { + // Generic type parameters must match their container's accessibility. + if decl.kind == .genericTypeParam { return true } + + // Skip implicit (compiler-generated) declarations. + if decl.isImplicit { return true } + + // Deinitializers cannot have explicit access modifiers in Swift. + if decl.kind == .functionDestructor { return true } + + // Override methods must be at least as accessible as what they override. + if decl.isOverride { return true } + + return false + } + + /// Checks if a declaration is a protocol requirement or protocol conformance. + /// + /// Protocol requirements must maintain sufficient accessibility to fulfill the protocol + /// contract, even if only referenced within the same file. This is critical for internal + /// accessibility analysis to avoid marking protocol implementations as redundant. + private func isProtocolRequirement(_ decl: Declaration) -> Bool { + // Case 1: Direct protocol requirement - parent is a protocol. + if let parent = decl.parent, parent.kind == .protocol { + return true + } + + // Case 2: Protocol conformance - this declaration implements a protocol requirement. + // + // When a type conforms to a protocol, Swift's indexer creates "related" references + // from the conforming declaration to the protocol requirement. If this declaration + // has any related references pointing to protocol members with matching names, + // it's implementing a protocol requirement. + let relatedReferences = graph.references(to: decl).filter(\.isRelated) + for ref in relatedReferences { + if let protocolDecl = graph.declaration(withUsr: ref.usr), + protocolDecl.kind.isProtocolMemberKind || protocolDecl.kind == .associatedtype + { + return true + } + } + + // Alternative check: Look for related references FROM this declaration + // to protocol members. The ProtocolConformanceReferenceBuilder inverts + // these relationships, so we might find them either direction. + for ref in decl.related where ref.kind.isProtocolMemberConformingKind { + if let referencedDecl = graph.declaration(withUsr: ref.usr), + let referencedParent = referencedDecl.parent, + referencedParent.kind == .protocol + { + return true + } + } + + return false + } + + /// Checks if a declaration is part of a property wrapper's public API. + /// + /// Property wrappers require certain members to be accessible even for internal + /// accessibility analysis. This prevents marking essential property wrapper components + /// as redundant when they're only referenced within the same file. + private func isPropertyWrapperMember(_ decl: Declaration) -> Bool { + guard let parent = decl.parent else { return false } + + // Check if parent type has @propertyWrapper attribute. + let isPropertyWrapper = parent.attributes.contains { $0.name == "propertyWrapper" } + guard isPropertyWrapper else { return false } + + // Property wrapper initializers are part of the API. + if decl.kind == .functionConstructor { return true } + + // wrappedValue and projectedValue are part of the API. + if decl.kind == .varInstance { + let propertyWrapperSpecialProperties = ["wrappedValue", "projectedValue"] + if propertyWrapperSpecialProperties.contains(decl.name ?? "") { + return true + } + } + + // Typealiases used in method/init signatures must be accessible. + // If this is a typealias and it's referenced by a function/init in the same + // property wrapper type, it's part of the API. + if decl.kind == .typealias { + let siblings = parent.declarations + let hasFunctionReference = siblings.contains { sibling in + sibling.kind.isFunctionKind && sibling.references.contains { ref in + ref.kind == .typealias && decl.usrs.contains(ref.usr) + } + } + if hasFunctionReference { + return true + } + } + + return false + } + + /// Determines the effective maximum accessibility a member can have based on its parent's accessibility. + /// + /// In Swift, a member's effective accessibility is constrained by its parent. This helper + /// ensures internal accessibility analysis respects these constraints when suggesting + /// more restrictive access levels. + private func effectiveMaximumAccessibility(for decl: Declaration) -> Accessibility? { + guard let parent = decl.parent else { return nil } + + let parentAccessibility = parent.accessibility.value + + switch parentAccessibility { + case .private: + return .private + case .fileprivate: + return .fileprivate + case .internal: + return .internal + case .public, .open: + return nil + } + } + + /// Checks if a declaration is referenced from a different type in the same file. + /// + /// For internal accessibility analysis, this determines whether to suggest `fileprivate` + /// versus `private` when a declaration is only used within its file. + private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { + let file = decl.location.file + let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } + + guard let declTopLevel = topLevelType(of: decl) else { + return false + } + + let declLogicalType = logicalType(of: declTopLevel, inFile: file) + + for ref in sameFileReferences { + guard let refParent = ref.parent, + let refTopLevel = topLevelType(of: refParent) + else { + continue + } + + let refLogicalType = logicalType(of: refTopLevel, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + return false + } + + // Finds the top-level type declaration by walking up the parent chain. + private func topLevelType(of decl: Declaration) -> Declaration? { + let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] + let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) + let ancestors = [decl] + Array(decl.ancestralDeclarations) + return ancestors.last { typeDecl in + guard typeKinds.contains(typeDecl.kind) else { return false } + guard let parent = typeDecl.parent else { return true } + + return !typeKinds.contains(parent.kind) + } + } + + // Gets the logical type for comparison purposes when analyzing internal accessibility. + // For extensions of types in the SAME FILE, treats the extension as the extended type. + // For extensions of types in DIFFERENT FILES, treats the extension as its own distinct type. + private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { + if decl.kind.isExtensionKind { + if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), + extendedDecl.location.file == file + { + return extendedDecl + } + return decl + } + return decl } } diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index 8a0e4d240b..01c78f4a15 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -9,8 +9,8 @@ public final class SourceGraph { public private(set) var redundantProtocols: [Declaration: (references: Set, inherited: Set)] = [:] public private(set) var rootDeclarations: Set = [] public private(set) var redundantPublicAccessibility: [Declaration: Set] = [:] - public private(set) var redundantInternalAccessibility: [Declaration: Set] = [:] - public private(set) var redundantFilePrivateAccessibility: [Declaration: Set] = [:] + public private(set) var redundantInternalAccessibility: [Declaration: (files: Set, suggestedAccessibility: Accessibility?)] = [:] + public private(set) var redundantFilePrivateAccessibility: [Declaration: (files: Set, containingTypeName: String?)] = [:] public private(set) var rootReferences: Set = [] public private(set) var allReferences: Set = [] public private(set) var retainedDeclarations: Set = [] @@ -90,12 +90,23 @@ public final class SourceGraph { _ = redundantPublicAccessibility.removeValue(forKey: declaration) } - func markRedundantInternalAccessibility(_ declaration: Declaration, file: SourceFile) { - redundantInternalAccessibility[declaration, default: []].insert(file) + func markRedundantInternalAccessibility(_ declaration: Declaration, file: SourceFile, suggestedAccessibility: Accessibility?) { + if let existing = redundantInternalAccessibility[declaration] { + var files = existing.files + files.insert(file) + redundantInternalAccessibility[declaration] = (files: files, suggestedAccessibility: existing.suggestedAccessibility) + } else { + redundantInternalAccessibility[declaration] = (files: [file], suggestedAccessibility: suggestedAccessibility) + } } - func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile) { - redundantFilePrivateAccessibility[declaration, default: []].insert(file) + func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile, containingTypeName: String?) { + if var existing = redundantFilePrivateAccessibility[declaration] { + existing.files.insert(file) + redundantFilePrivateAccessibility[declaration] = existing + } else { + redundantFilePrivateAccessibility[declaration] = (files: [file], containingTypeName: containingTypeName) + } } func markIgnored(_ declaration: Declaration) { diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index f9c5932df5..d6e56e8f5e 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -87,3 +87,12 @@ takeExtensionSameTypeGenericRequirement(.defaultInstance) // Typed throws try? PublicTypeUsedAsPublicFunctionThrowTypeRetainer().retain() + +// Redundant accessibility tests +TrulyRedundantFilePrivateMembersRetainer().retain() +ProtocolRequirementAccessibilityRetainer().retain() +PropertyWrapperAccessibilityRetainer().retain() +NestedTypeAccessibilityRetainer().retain() +InternalSuggestingPrivateVsFileprivateRetainer().retain() +ImplicitlyInternalRetainer().retain() +NotRedundantInternalClassComponents_Support().useImplicitlyInternalStruct() diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalSuggestingPrivateVsFileprivate.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalSuggestingPrivateVsFileprivate.swift new file mode 100644 index 0000000000..5e01a29d24 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalSuggestingPrivateVsFileprivate.swift @@ -0,0 +1,46 @@ +/* + InternalSuggestingPrivateVsFileprivate.swift + Tests that internal declarations correctly suggest private vs fileprivate + based on whether they're accessed from different types in the same file. +*/ + +// Class with internal property only used within its own type - should suggest private. +public class ClassWithInternalPropertySuggestingPrivate { + // Should be flagged as redundant internal (can be 'private'). + internal var usedOnlyInOwnType: Int = 0 + + public init() {} + + public func useProperty() { + _ = usedOnlyInOwnType + } +} + +// Two classes where one accesses the other's internal property - should suggest fileprivate. +public class ClassA { + // Should be flagged as redundant internal (can be 'fileprivate'). + internal var sharedWithClassB: String = "" + + public init() {} +} + +public class ClassB { + public init() {} + + public func accessClassA(_ a: ClassA) { + _ = a.sharedWithClassB + } +} + +// Used by main.swift to ensure these are referenced. +public class InternalSuggestingPrivateVsFileprivateRetainer { + public init() {} + public func retain() { + let obj1 = ClassWithInternalPropertySuggestingPrivate() + obj1.useProperty() + + let a = ClassA() + let b = ClassB() + b.accessClassA(a) + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedTypeAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedTypeAccessibility.swift new file mode 100644 index 0000000000..a5e1857596 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedTypeAccessibility.swift @@ -0,0 +1,43 @@ +/* + NestedTypeAccessibility.swift + Tests nested type accessibility analysis and redundancy suppression. +*/ + +/* Public class with internal nested types that are only used within the file. */ +public class OuterClassWithNestedTypes { + /* Should be flagged as redundant (not used outside file). */ + internal struct NestedStruct { + /* Should NOT be flagged if parent is already flagged (nested redundancy suppression). */ + internal var nestedProperty: Int = 0 + + /* Should NOT be flagged if parent is already flagged (nested redundancy suppression). */ + internal func nestedMethod() {} + } + + /* Should be flagged as redundant (not used outside file). */ + internal class NestedClass { + /* Should NOT be flagged if parent is already flagged (nested redundancy suppression). */ + internal var anotherProperty: String = "" + } + + internal var nested: NestedStruct = .init() + + public init() {} + + public func useNested() { + _ = nested.nestedProperty + nested.nestedMethod() + + let nc = NestedClass() + _ = nc.anotherProperty + } +} + +/* Used by main.swift to ensure these are referenced. */ +public class NestedTypeAccessibilityRetainer { + public init() {} + public func retain() { + let obj = OuterClassWithNestedTypes() + obj.useNested() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift index c4f852e72f..5a6d606775 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents.swift @@ -19,4 +19,9 @@ internal enum NotRedundantInternalEnum { func useCase() -> NotRedundantInternalEnum { return .usedCase } +} + +// Test case for implicitly internal declaration used from another file - should NOT be flagged. +struct ImplicitlyInternalStructUsedFromAnotherFile { + var implicitlyInternalProperty: Int = 42 } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift index 055f1e21fd..87e6722500 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NotRedundantInternalClassComponents_Support.swift @@ -5,12 +5,17 @@ internal class NotRedundantInternalClass_Support { internal func helper() {} } -// Used by main.swift to ensure these are referenced -class NotRedundantInternalClassComponents_Support { +// Used by main.swift to ensure these are referenced. +public class NotRedundantInternalClassComponents_Support { public init() {} - - func useInternalMethod() { + + public func useInternalMethod() { let cls = NotRedundantInternalClass() cls.usedInternalMethod() } + + public func useImplicitlyInternalStruct() { + let s = ImplicitlyInternalStructUsedFromAnotherFile() + _ = s.implicitlyInternalProperty + } } \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/PropertyWrapperAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/PropertyWrapperAccessibility.swift new file mode 100644 index 0000000000..add9130645 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/PropertyWrapperAccessibility.swift @@ -0,0 +1,49 @@ +/* + PropertyWrapperAccessibility.swift + Tests that property wrapper members are NOT flagged as redundant. +*/ + +/* Property wrapper with internal accessibility. */ +@propertyWrapper +internal struct InternalPropertyWrapper { + private var value: T + + /* Should NOT be flagged - wrappedValue is part of property wrapper API. */ + internal var wrappedValue: T { + get { value } + set { value = newValue } + } + + /* Should NOT be flagged - projectedValue is part of property wrapper API. */ + internal var projectedValue: InternalPropertyWrapper { + self + } + + /* Should NOT be flagged - init is part of property wrapper API. */ + internal init(wrappedValue: T) { + value = wrappedValue + } + + /* This typealias is used in the init signature, so it's part of the API. */ + internal typealias Value = T +} + +/* Class using the property wrapper. */ +internal class ClassUsingPropertyWrapper { + @InternalPropertyWrapper + var wrappedProperty: String = "test" + + func access() { + _ = wrappedProperty + _ = $wrappedProperty + } +} + +/* Used by main.swift to ensure these are referenced. */ +public class PropertyWrapperAccessibilityRetainer { + public init() {} + public func retain() { + let obj = ClassUsingPropertyWrapper() + obj.access() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolRequirementAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolRequirementAccessibility.swift new file mode 100644 index 0000000000..20e214bc40 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ProtocolRequirementAccessibility.swift @@ -0,0 +1,41 @@ +/* + ProtocolRequirementAccessibility.swift + Tests that protocol requirements and conformances are NOT flagged as redundant. +*/ + +/* Internal protocol with internal requirements. */ +internal protocol InternalProtocolWithRequirements { + func requiredMethod() + var requiredProperty: Int { get } +} + +/* Class conforming to the internal protocol. */ +internal class ConformingToInternalProtocol: InternalProtocolWithRequirements { + /* Should NOT be flagged - this implements a protocol requirement. */ + func requiredMethod() {} + + /* Should NOT be flagged - this implements a protocol requirement. */ + var requiredProperty: Int { 42 } +} + +/* Protocol with fileprivate requirement (within same file). */ +fileprivate protocol FilePrivateProtocol { + func filePrivateRequirement() +} + +/* Extension conforming to fileprivate protocol. */ +extension ConformingToInternalProtocol: FilePrivateProtocol { + /* Should NOT be flagged - implements protocol requirement. */ + func filePrivateRequirement() {} +} + +/* Used by main.swift to ensure these are referenced. */ +public class ProtocolRequirementAccessibilityRetainer { + public init() {} + public func retain() { + let obj = ConformingToInternalProtocol() + obj.requiredMethod() + _ = obj.requiredProperty + obj.filePrivateRequirement() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift index 74d1cdbbf6..e45953e9d5 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/RedundantInternalComponents.swift @@ -13,7 +13,70 @@ internal enum RedundantInternalEnum { case unusedCase } -// Used by main.swift to ensure these are referenced +// Test case for implicitly internal declaration that is redundant. +class ImplicitlyInternalClassUsedOnlyInSameFile { + var implicitlyInternalProperty: String = "test" + + func useProperty() { + _ = implicitlyInternalProperty + } +} + +// Retainer to ensure ImplicitlyInternalClassUsedOnlyInSameFile is used in the same file. +public class ImplicitlyInternalRetainer { + public init() {} + + public func retain() { + let obj = ImplicitlyInternalClassUsedOnlyInSameFile() + obj.useProperty() + _ = obj.implicitlyInternalProperty + } +} + +/* + Test case for members of private/fileprivate containers. + Members of a private class are already effectively private, + so marking them as redundant internal would be misleading. + */ +private class PrivateContainerClass { + // This should NOT be flagged - already constrained by parent's private accessibility. + func method() {} + + // This should NOT be flagged - already constrained by parent's private accessibility. + var property: Int = 0 +} + +fileprivate class FileprivateContainerClass { + // This should NOT be flagged - already constrained by parent's fileprivate accessibility. + func method() {} + + // This should NOT be flagged - already constrained by parent's fileprivate accessibility. + var property: Int = 0 +} + +/* + Test case for deinit - should not be flagged. + Deinitializers cannot have explicit access modifiers in Swift. + */ +class ClassWithDeinit { + // This should NOT be flagged - deinit cannot have access modifiers. + deinit {} +} + +/* + Test case for override methods - should not be flagged. + Override methods must be at least as accessible as what they override. + */ +class BaseClass { + func overridableMethod() {} +} + +class DerivedClass: BaseClass { + // This should NOT be flagged - override methods have accessibility constraints. + override func overridableMethod() {} +} + +// Used by main.swift to ensure these are referenced. class RedundantInternalClassComponents { public init() {} } \ No newline at end of file diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TrulyRedundantFilePrivateMembers.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TrulyRedundantFilePrivateMembers.swift new file mode 100644 index 0000000000..7c98a6e82b --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TrulyRedundantFilePrivateMembers.swift @@ -0,0 +1,38 @@ +/* + TrulyRedundantFilePrivateMembers.swift + Tests cases where fileprivate is truly redundant and should be private. +*/ + +/* This class has a fileprivate method that is only used within the class itself. */ +class ClassWithRedundantFilePrivateMethod { + /* Should be flagged as redundant - only used within the same type (can be private). */ + fileprivate func helper() -> Int { + 42 + } + + func publicMethod() -> Int { + helper() + } +} + +/* This struct has a fileprivate property only accessed within its own type. */ +struct StructWithRedundantFilePrivateProperty { + /* Should be flagged as redundant - only used within the same type (can be private). */ + fileprivate var internalState: String = "" + + func access() -> String { + internalState + } +} + +/* Used by main.swift to ensure these are referenced. */ +public class TrulyRedundantFilePrivateMembersRetainer { + public init() {} + public func retain() { + let obj = ClassWithRedundantFilePrivateMethod() + _ = obj.publicMethod() + + let s = StructWithRedundantFilePrivateProperty() + _ = s.access() + } +} diff --git a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift index 936c7e5d6e..2722385859 100644 --- a/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantFilePrivateAccessibilityTest.swift @@ -8,18 +8,16 @@ final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { build(projectPath: AccessibilityProjectPath) } - /* - Tests that a fileprivate class is NOT flagged as redundant when accessed - from a different type in the same file. - - In Swift, private and fileprivate have distinct meanings even for top-level declarations: - - private: accessible only within the lexical scope and its extensions in the same file - - fileprivate: accessible from anywhere in the same file - - Since RedundantFilePrivateClass is accessed from RedundantFilePrivateClassRetainer - (a different type), fileprivate is the minimum access level required. Changing it to - private would prevent RedundantFilePrivateClassRetainer from accessing it. - */ + // Tests that a fileprivate class is NOT flagged as redundant when accessed + // from a different type in the same file. + // + // In Swift, private and fileprivate have distinct meanings even for top-level declarations: + // - private: accessible only within the lexical scope and its extensions in the same file + // - fileprivate: accessible from anywhere in the same file + // + // Since RedundantFilePrivateClass is accessed from RedundantFilePrivateClassRetainer + // (a different type), fileprivate is the minimum access level required. Changing it to + // private would prevent RedundantFilePrivateClassRetainer from accessing it. func testRedundantFilePrivateClass() { index() assertNotRedundantFilePrivateAccessibility(.class("RedundantFilePrivateClass")) @@ -30,22 +28,31 @@ final class RedundantFilePrivateAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantFilePrivateAccessibility(.class("NotRedundantFilePrivateClass")) } + // Tests that a fileprivate property inside a private class is NOT flagged as redundant. + // This is valid because fileprivate expands the property's visibility beyond the private + // container, making it accessible to other types in the same file. The private class + // restricts access to the file, but fileprivate allows the property to be more visible + // than its container within that scope. func testNotRedundantFilePrivatePropertyInPrivateClass() { index() assertNotRedundantFilePrivateAccessibility(.varInstance("filePrivatePaths")) } - /* - NOTE: the opposite of the above, e.g. a function called "assertRedundantFilePrivateAccessibility()", - is intentionally not tested here. - - After fixing the bug where cross-type same-file references were incorrectly - flagged as redundant, there are no meaningful test cases for truly redundant - fileprivate declarations. Here's why: + func testTrulyRedundantFilePrivateMethod() { + // A fileprivate method only used within its own type should be private. + index() + assertRedundantFilePrivateAccessibility( + .functionMethodInstance("helper()", line: 9), + containingTypeName: "class ClassWithRedundantFilePrivateMethod" + ) + } - - Fileprivate exists specifically for same-file cross-type access - - A fileprivate declaration used only within its own type should be private - - A fileprivate declaration with no references at all is marked as "unused", not - "redundant fileprivate" - */ + func testTrulyRedundantFilePrivateProperty() { + // A fileprivate property only used within its own type should be private. + index() + assertRedundantFilePrivateAccessibility( + .varInstance("internalState"), + containingTypeName: "struct StructWithRedundantFilePrivateProperty" + ) + } } diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 3f91e497da..106e8595b8 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -39,12 +39,141 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { } func testInternalMethodUsedInExtension() { - // This should NOT be flagged as redundant - // Tests the case where an internal method is used in an extension + // This should NOT be flagged as redundant. index() - // This test would need additional setup with methods in extensions - // For now, we'll test that the existing NotRedundantInternalClassComponents work assertNotRedundantInternalAccessibility(.functionMethodInstance("usedInternalMethod()")) } + + /// Tests that members of a private class are not flagged as redundant internal. + /// + /// In Swift, members of a private class are already effectively private due to + /// the parent's accessibility constraint. Suggesting to change them to fileprivate + /// would actually increase their visibility, which is incorrect. + func testMembersOfPrivateClassNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionMethodInstance("method()", line: 43)) + assertNotRedundantInternalAccessibility(.varInstance("property", line: 46)) + } + + /// Tests that members of a fileprivate class are not flagged as redundant internal + /// when they would be suggested to be fileprivate. + /// + /// In Swift, members of a fileprivate class are already constrained to fileprivate + /// accessibility. Suggesting to mark them as fileprivate would be redundant. + func testMembersOfFileprivateClassNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionMethodInstance("method()", line: 51)) + assertNotRedundantInternalAccessibility(.varInstance("property", line: 54)) + } + + /// Tests that deinit is not flagged as redundant internal. + /// + /// In Swift, deinitializers cannot have explicit access modifiers. + /// They always match the accessibility of their enclosing type. + func testDeinitNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionDestructor("deinit", line: 63)) + } + + /// Tests that override methods are not flagged as redundant internal. + /// + /// Override methods must be at least as accessible as the method they override, + /// so their accessibility is constrained by the base method. + func testOverrideMethodNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionMethodInstance("overridableMethod()", line: 76)) + } + + /// Tests that protocol requirements are not flagged as redundant internal. + /// + /// Methods and properties that implement protocol requirements must maintain + /// sufficient accessibility to fulfill the protocol contract. + func testProtocolRequirementsNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionMethodInstance("requiredMethod()")) + assertNotRedundantInternalAccessibility(.varInstance("requiredProperty")) + } + + /// Tests that fileprivate protocol conformances are not flagged. + func testFilePrivateProtocolConformanceNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.functionMethodInstance("filePrivateRequirement()")) + } + + /// Tests that property wrapper members are not flagged as redundant internal. + /// + /// Property wrappers require certain members (init, wrappedValue, projectedValue) + /// to be accessible as part of their API contract. + func testPropertyWrapperMembersNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.varInstance("wrappedValue")) + assertNotRedundantInternalAccessibility(.varInstance("projectedValue")) + assertNotRedundantInternalAccessibility(.functionConstructor("init(wrappedValue:)")) + } + + /// Tests internal suggesting private. + /// + /// An internal property only used within its own type should be private. + func testInternalSuggestingPrivate() { + index() + + assertRedundantInternalAccessibility( + .varInstance("usedOnlyInOwnType"), + suggestedAccessibility: .private + ) + } + + /// Tests internal suggesting fileprivate. + /// + /// An internal property used from a different type in the same file should be fileprivate. + func testInternalSuggestingFileprivate() { + index() + + assertRedundantInternalAccessibility( + .varInstance("sharedWithClassB"), + suggestedAccessibility: .fileprivate + ) + } + + /// Tests nested type redundancy with parent already flagged. + /// + /// When nested types are flagged as redundant, their nested members should be + /// suppressed to avoid noise (unless showNestedRedundantAccessibility is enabled). + func testNestedTypeRedundancy() { + index() + + assertRedundantInternalAccessibility(.struct("NestedStruct")) + assertRedundantInternalAccessibility(.class("NestedClass")) + assertRedundantInternalAccessibility(.varInstance("nested")) + } + + /// Tests that implicitly internal declarations (no access modifier) are flagged as redundant + /// when only used within the same file. + /// + /// This ensures the analyzer handles both explicit 'internal' keyword and implicit internal + /// (default accessibility) correctly in positive cases. + func testImplicitlyInternalRedundant() { + index() + + assertRedundantInternalAccessibility(.class("ImplicitlyInternalClassUsedOnlyInSameFile")) + } + + /// Tests that implicitly internal declarations (no access modifier) are NOT flagged as redundant + /// when used from another file. + /// + /// This ensures the analyzer handles both explicit 'internal' keyword and implicit internal + /// (default accessibility) correctly in negative cases. + func testImplicitlyInternalNotRedundant() { + index() + + assertNotRedundantInternalAccessibility(.struct("ImplicitlyInternalStructUsedFromAnotherFile")) + } } diff --git a/Tests/Shared/SourceGraphTestCase.swift b/Tests/Shared/SourceGraphTestCase.swift index 445ad253d2..b713fb1a97 100644 --- a/Tests/Shared/SourceGraphTestCase.swift +++ b/Tests/Shared/SourceGraphTestCase.swift @@ -177,13 +177,22 @@ open class SourceGraphTestCase: XCTestCase { scopeStack.removeLast() } - func assertRedundantInternalAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + func assertRedundantInternalAccessibility(_ description: DeclarationDescription, suggestedAccessibility: Accessibility? = nil, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } if !Self.results.redundantInternalAccessibilityDeclarations.contains(declaration) { XCTFail("Expected declaration to have redundant internal accessibility: \(declaration)", file: file, line: line) } + if let suggestedAccessibility { + if let info = Self.graph.redundantInternalAccessibility[declaration] { + if info.suggestedAccessibility != suggestedAccessibility { + let actualText = info.suggestedAccessibility?.rawValue ?? "nil" + XCTFail("Expected suggested accessibility to be '\(suggestedAccessibility.rawValue)', but got '\(actualText)': \(declaration)", file: file, line: line) + } + } + } + scopeStack.append(.declaration(declaration)) scopedAssertions?() scopeStack.removeLast() @@ -201,6 +210,26 @@ open class SourceGraphTestCase: XCTestCase { scopeStack.removeLast() } + func assertRedundantFilePrivateAccessibility(_ description: DeclarationDescription, containingTypeName: String? = nil, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { + guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } + + if !Self.results.redundantFilePrivateAccessibilityDeclarations.contains(declaration) { + XCTFail("Expected declaration to have redundant fileprivate accessibility: \(declaration)", file: file, line: line) + } + + if let containingTypeName { + if let info = Self.graph.redundantFilePrivateAccessibility[declaration] { + if info.containingTypeName != containingTypeName { + XCTFail("Expected containing type name to be '\(containingTypeName)', but got '\(info.containingTypeName ?? "nil")': \(declaration)", file: file, line: line) + } + } + } + + scopeStack.append(.declaration(declaration)) + scopedAssertions?() + scopeStack.removeLast() + } + func assertNotRedundantFilePrivateAccessibility(_ description: DeclarationDescription, scopedAssertions: (() -> Void)? = nil, file: StaticString = #file, line: UInt = #line) { guard let declaration = materialize(description, in: Self.allIndexedDeclarations, file: file, line: line) else { return } From 5907701cd8abe503ab29bafe9c0b1185ccfcaee7 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:06:09 -0800 Subject: [PATCH 17/31] Fix accessibility warnings throughout the code base so that `mise r scan` results in no warnings --- Sources/Configuration/Configuration.swift | 4 +- Sources/Frontend/Commands/ScanCommand.swift | 102 +++++++++--------- Sources/Frontend/Project.swift | 2 +- Sources/Frontend/main.swift | 4 +- Sources/Indexer/InfoPlistIndexer.swift | 2 +- Sources/Indexer/XCDataModelIndexer.swift | 2 +- Sources/Indexer/XibIndexer.swift | 2 +- Sources/Logger/Logger.swift | 20 ++-- .../ProjectDrivers/GenericProjectDriver.swift | 2 +- Sources/ProjectDrivers/SPMProjectDriver.swift | 2 +- .../ProjectDrivers/XcodeProjectDriver.swift | 2 +- Sources/Shared/JobPool.swift | 2 +- Sources/Shared/SetupGuide.swift | 2 +- Sources/Shared/Shell.swift | 2 +- .../Elements/DeclarationAttribute.swift | 2 +- .../Mutators/EnumCaseReferenceBuilder.swift | 2 +- Sources/SourceGraph/SourceGraph.swift | 2 +- Sources/SyntaxAnalysis/CommentCommand.swift | 2 +- .../DeclarationSyntaxVisitor.swift | 2 +- .../MultiplexingSyntaxVisitor.swift | 4 +- .../SyntaxAnalysis/TypeSyntaxInspector.swift | 2 +- .../UnusedParameterParser.swift | 2 +- Sources/XcodeSupport/XcodeTarget.swift | 2 +- 23 files changed, 85 insertions(+), 85 deletions(-) diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index ebb0f21208..85ca198642 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -224,7 +224,7 @@ public final class Configuration { // MARK: - Private - lazy var settings: [any AbstractSetting] = [ + private lazy var settings: [any AbstractSetting] = [ $project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat, $retainPublic, $noRetainSPI, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible, $retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis, @@ -256,7 +256,7 @@ public final class Configuration { } } -protocol AbstractSetting { +private protocol AbstractSetting { associatedtype Value var key: String { get } diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index 3262f2bf3b..cd805b86bc 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -13,160 +13,160 @@ struct ScanCommand: ParsableCommand { ) @Argument(help: "Arguments following '--' will be passed to the underlying build tool, which is either 'swift build' or 'xcodebuild' depending on your project") - var buildArguments: [String] = defaultConfiguration.$buildArguments.defaultValue + private var buildArguments: [String] = defaultConfiguration.$buildArguments.defaultValue @Flag(help: "Enable guided setup") - var setup: Bool = defaultConfiguration.guidedSetup + private var setup: Bool = defaultConfiguration.guidedSetup @Option(help: "Path to the root directory of your project") - var projectRoot: FilePath = projectRootDefault + private var projectRoot: FilePath = projectRootDefault @Option(help: "Path to configuration file. By default Periphery will look for .periphery.yml in the current directory") - var config: FilePath? + private var config: FilePath? @Option(help: "Path to your project's .xcodeproj or .xcworkspace") - var project: FilePath? + private var project: FilePath? @Option(parsing: .upToNextOption, help: "Schemes to build. All targets built by these schemes will be scanned") - var schemes: [String] = defaultConfiguration.$schemes.defaultValue + private var schemes: [String] = defaultConfiguration.$schemes.defaultValue @Option(help: "Output format") - var format: OutputFormat = defaultConfiguration.$outputFormat.defaultValue + private var format: OutputFormat = defaultConfiguration.$outputFormat.defaultValue @Flag(help: "Exclude test targets from indexing") - var excludeTests: Bool = defaultConfiguration.$excludeTests.defaultValue + private var excludeTests: Bool = defaultConfiguration.$excludeTests.defaultValue @Option(parsing: .upToNextOption, help: "Targets to exclude from indexing") - var excludeTargets: [String] = defaultConfiguration.$excludeTargets.defaultValue + private var excludeTargets: [String] = defaultConfiguration.$excludeTargets.defaultValue @Option(parsing: .upToNextOption, help: "Source file globs to exclude from indexing") - var indexExclude: [String] = defaultConfiguration.$indexExclude.defaultValue + private var indexExclude: [String] = defaultConfiguration.$indexExclude.defaultValue @Option(parsing: .upToNextOption, help: "Source file globs to exclude from the results. Note that this option is purely cosmetic, these files will still be indexed") - var reportExclude: [String] = defaultConfiguration.$reportExclude.defaultValue + private var reportExclude: [String] = defaultConfiguration.$reportExclude.defaultValue @Option(parsing: .upToNextOption, help: "Source file globs to include in the results. This option supersedes '--report-exclude'. Note that this option is purely cosmetic, these files will still be indexed") - var reportInclude: [String] = defaultConfiguration.$reportInclude.defaultValue + private var reportInclude: [String] = defaultConfiguration.$reportInclude.defaultValue @Option(parsing: .upToNextOption, help: "Source file globs for which all containing declarations will be retained") - var retainFiles: [String] = defaultConfiguration.$retainFiles.defaultValue + private var retainFiles: [String] = defaultConfiguration.$retainFiles.defaultValue @Option(parsing: .upToNextOption, help: "Index store paths. Implies '--skip-build'") - var indexStorePath: [FilePath] = defaultConfiguration.$indexStorePath.defaultValue + private var indexStorePath: [FilePath] = defaultConfiguration.$indexStorePath.defaultValue @Flag(help: "Retain all public declarations, recommended for framework/library projects") - var retainPublic: Bool = defaultConfiguration.$retainPublic.defaultValue + private var retainPublic: Bool = defaultConfiguration.$retainPublic.defaultValue @Option(parsing: .upToNextOption, help: "Public SPIs (System Programming Interfaces) to check for unused code even when '--retain-public' is enabled") - var noRetainSPI: [String] = defaultConfiguration.$noRetainSPI.defaultValue + private var noRetainSPI: [String] = defaultConfiguration.$noRetainSPI.defaultValue @Flag(help: "Disable identification of redundant public accessibility") - var disableRedundantPublicAnalysis: Bool = defaultConfiguration.$disableRedundantPublicAnalysis.defaultValue + private var disableRedundantPublicAnalysis: Bool = defaultConfiguration.$disableRedundantPublicAnalysis.defaultValue @Flag(help: "Disable identification of redundant internal accessibility") - var disableRedundantInternalAnalysis: Bool = defaultConfiguration.$disableRedundantInternalAnalysis.defaultValue + private var disableRedundantInternalAnalysis: Bool = defaultConfiguration.$disableRedundantInternalAnalysis.defaultValue @Flag(help: "Disable identification of redundant fileprivate accessibility") - var disableRedundantFilePrivateAnalysis: Bool = defaultConfiguration.$disableRedundantFilePrivateAnalysis.defaultValue + private var disableRedundantFilePrivateAnalysis: Bool = defaultConfiguration.$disableRedundantFilePrivateAnalysis.defaultValue @Flag(help: "Show redundant internal/fileprivate accessibility warnings for nested declarations even when the containing type is already flagged") - var showNestedRedundantAccessibility: Bool = defaultConfiguration.$showNestedRedundantAccessibility.defaultValue + private var showNestedRedundantAccessibility: Bool = defaultConfiguration.$showNestedRedundantAccessibility.defaultValue @Flag(help: "Disable identification of unused imports") - var disableUnusedImportAnalysis: Bool = defaultConfiguration.$disableUnusedImportAnalysis.defaultValue + private var disableUnusedImportAnalysis: Bool = defaultConfiguration.$disableUnusedImportAnalysis.defaultValue @Flag(inversion: .prefixedNo, help: "Report superfluous ignore comments") var superfluousIgnoreComments: Bool = defaultConfiguration.$superfluousIgnoreComments.defaultValue @Option(parsing: .upToNextOption, help: "Names of unused imported modules to retain") - var retainUnusedImportedModules: [String] = defaultConfiguration.$retainUnusedImportedModules.defaultValue + private var retainUnusedImportedModules: [String] = defaultConfiguration.$retainUnusedImportedModules.defaultValue @Flag(help: "Retain properties that are assigned, but never used") - var retainAssignOnlyProperties: Bool = defaultConfiguration.$retainAssignOnlyProperties.defaultValue + private var retainAssignOnlyProperties: Bool = defaultConfiguration.$retainAssignOnlyProperties.defaultValue @Option(parsing: .upToNextOption, help: "Property types to retain if the property is assigned, but never read") - var retainAssignOnlyPropertyTypes: [String] = defaultConfiguration.$retainAssignOnlyPropertyTypes.defaultValue + private var retainAssignOnlyPropertyTypes: [String] = defaultConfiguration.$retainAssignOnlyPropertyTypes.defaultValue @Option(parsing: .upToNextOption, help: "Names of external protocols that inherit Encodable. Properties and CodingKey enums of types conforming to these protocols will be retained") - var externalEncodableProtocols: [String] = defaultConfiguration.$externalEncodableProtocols.defaultValue + private var externalEncodableProtocols: [String] = defaultConfiguration.$externalEncodableProtocols.defaultValue @Option(parsing: .upToNextOption, help: "Names of external protocols that inherit Codable. Properties and CodingKey enums of types conforming to these protocols will be retained") - var externalCodableProtocols: [String] = defaultConfiguration.$externalCodableProtocols.defaultValue + private var externalCodableProtocols: [String] = defaultConfiguration.$externalCodableProtocols.defaultValue @Option(parsing: .upToNextOption, help: "Names of XCTestCase subclasses that reside in external targets") - var externalTestCaseClasses: [String] = defaultConfiguration.$externalTestCaseClasses.defaultValue + private var externalTestCaseClasses: [String] = defaultConfiguration.$externalTestCaseClasses.defaultValue @Flag(help: "Retain declarations that are exposed to Objective-C implicitly by inheriting NSObject classes, or explicitly with the @objc and @objcMembers attributes") - var retainObjcAccessible: Bool = defaultConfiguration.$retainObjcAccessible.defaultValue + private var retainObjcAccessible: Bool = defaultConfiguration.$retainObjcAccessible.defaultValue @Flag(help: "Retain declarations that are exposed to Objective-C explicitly with the @objc and @objcMembers attributes") - var retainObjcAnnotated: Bool = defaultConfiguration.$retainObjcAnnotated.defaultValue + private var retainObjcAnnotated: Bool = defaultConfiguration.$retainObjcAnnotated.defaultValue @Flag(help: "Retain unused protocol function parameters, even if the parameter is unused in all conforming functions") - var retainUnusedProtocolFuncParams: Bool = defaultConfiguration.$retainUnusedProtocolFuncParams.defaultValue + private var retainUnusedProtocolFuncParams: Bool = defaultConfiguration.$retainUnusedProtocolFuncParams.defaultValue @Flag(help: "Retain SwiftUI previews") - var retainSwiftUIPreviews: Bool = defaultConfiguration.$retainSwiftUIPreviews.defaultValue + private var retainSwiftUIPreviews: Bool = defaultConfiguration.$retainSwiftUIPreviews.defaultValue @Flag(help: "Retain properties on Codable types (including Encodable and Decodable)") - var retainCodableProperties: Bool = defaultConfiguration.$retainCodableProperties.defaultValue + private var retainCodableProperties: Bool = defaultConfiguration.$retainCodableProperties.defaultValue @Flag(help: "Retain properties on Encodable types only") - var retainEncodableProperties: Bool = defaultConfiguration.$retainEncodableProperties.defaultValue + private var retainEncodableProperties: Bool = defaultConfiguration.$retainEncodableProperties.defaultValue @Flag(help: "Clean existing build artifacts before building") - var cleanBuild: Bool = defaultConfiguration.$cleanBuild.defaultValue + private var cleanBuild: Bool = defaultConfiguration.$cleanBuild.defaultValue @Flag(help: "Skip the project build step") - var skipBuild: Bool = defaultConfiguration.$skipBuild.defaultValue + private var skipBuild: Bool = defaultConfiguration.$skipBuild.defaultValue @Flag(help: "Skip schemes validation") - var skipSchemesValidation: Bool = defaultConfiguration.$skipSchemesValidation.defaultValue + private var skipSchemesValidation: Bool = defaultConfiguration.$skipSchemesValidation.defaultValue @Flag(help: "Output result paths relative to the current directory") - var relativeResults: Bool = defaultConfiguration.$relativeResults.defaultValue + private var relativeResults: Bool = defaultConfiguration.$relativeResults.defaultValue @Flag(help: "Exit with non-zero status if any unused code is found") - var strict: Bool = defaultConfiguration.$strict.defaultValue + private var strict: Bool = defaultConfiguration.$strict.defaultValue @Flag(help: "Disable checking for updates") - var disableUpdateCheck: Bool = defaultConfiguration.$disableUpdateCheck.defaultValue + private var disableUpdateCheck: Bool = defaultConfiguration.$disableUpdateCheck.defaultValue @Flag(help: "Enable verbose logging") - var verbose: Bool = defaultConfiguration.$verbose.defaultValue + private var verbose: Bool = defaultConfiguration.$verbose.defaultValue @Flag(help: "Only output results") - var quiet: Bool = defaultConfiguration.$quiet.defaultValue + private var quiet: Bool = defaultConfiguration.$quiet.defaultValue @Option(help: "Colored output mode") - var color: ColorOption = defaultConfiguration.$color.defaultValue + private var color: ColorOption = defaultConfiguration.$color.defaultValue @Flag(name: .customLong("no-color"), help: .hidden) - var noColor: Bool = false + private var noColor: Bool = false @Option(help: "JSON package manifest path (obtained using `swift package describe --type json` or manually)") - var jsonPackageManifestPath: FilePath? + private var jsonPackageManifestPath: FilePath? @Option(help: "Baseline file path used to filter results") - var baseline: FilePath? + private var baseline: FilePath? @Option(help: "Baseline file path where results are written. Pass the same path to '--baseline' in subsequent scans to exclude the results recorded in the baseline.") - var writeBaseline: FilePath? + private var writeBaseline: FilePath? @Option(help: "File path where formatted results are written.") - var writeResults: FilePath? + private var writeResults: FilePath? @Option(help: "Project configuration for non-Apple build systems") - var genericProjectConfig: FilePath? + private var genericProjectConfig: FilePath? @Flag(help: "Enable Bazel project mode") - var bazel: Bool = defaultConfiguration.$bazel.defaultValue + private var bazel: Bool = defaultConfiguration.$bazel.defaultValue @Option(help: "Filter pattern applied to the Bazel top-level targets query") - var bazelFilter: String? + private var bazelFilter: String? @Option(help: "Path to a global index store populated by Bazel. If provided, will be used instead of individual module stores.") - var bazelIndexStore: FilePath? + private var bazelIndexStore: FilePath? @Flag(help: "Enable Bazel visibility checking") var bazelCheckVisibility: Bool = defaultConfiguration.$bazelCheckVisibility.defaultValue diff --git a/Sources/Frontend/Project.swift b/Sources/Frontend/Project.swift index 60b694714f..13909fbd4d 100644 --- a/Sources/Frontend/Project.swift +++ b/Sources/Frontend/Project.swift @@ -6,7 +6,7 @@ import Shared import SystemPackage final class Project { - let kind: ProjectKind + private let kind: ProjectKind private let configuration: Configuration private let shell: Shell diff --git a/Sources/Frontend/main.swift b/Sources/Frontend/main.swift index 052f3bf399..9ee79aed5c 100644 --- a/Sources/Frontend/main.swift +++ b/Sources/Frontend/main.swift @@ -3,7 +3,7 @@ import Foundation // When stdout is a pipe, enable line buffering so output is flushed after each // newline rather than block-buffered, ensuring timely output to the consumer. -var info = stat() +private var info = stat() fstat(STDOUT_FILENO, &info) if (info.st_mode & S_IFMT) == S_IFIFO { @@ -11,7 +11,7 @@ if (info.st_mode & S_IFMT) == S_IFIFO { setlinebuf(stderr) } -struct PeripheryCommand: ParsableCommand { +private struct PeripheryCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "periphery", subcommands: [ diff --git a/Sources/Indexer/InfoPlistIndexer.swift b/Sources/Indexer/InfoPlistIndexer.swift index e6f9049c12..cb0f1addb5 100644 --- a/Sources/Indexer/InfoPlistIndexer.swift +++ b/Sources/Indexer/InfoPlistIndexer.swift @@ -5,7 +5,7 @@ import SourceGraph import SystemPackage final class InfoPlistIndexer: Indexer { - enum PlistError: Error { + private enum PlistError: Error { case failedToParse(path: FilePath, underlyingError: Error) } diff --git a/Sources/Indexer/XCDataModelIndexer.swift b/Sources/Indexer/XCDataModelIndexer.swift index 00cc0496c6..3d3a44a952 100644 --- a/Sources/Indexer/XCDataModelIndexer.swift +++ b/Sources/Indexer/XCDataModelIndexer.swift @@ -5,7 +5,7 @@ import SourceGraph import SystemPackage final class XCDataModelIndexer: Indexer { - enum XCDataModelError: Error { + private enum XCDataModelError: Error { case failedToParse(path: FilePath, underlyingError: Error) } diff --git a/Sources/Indexer/XibIndexer.swift b/Sources/Indexer/XibIndexer.swift index 0cf837be0f..8c456c361b 100644 --- a/Sources/Indexer/XibIndexer.swift +++ b/Sources/Indexer/XibIndexer.swift @@ -5,7 +5,7 @@ import SourceGraph import SystemPackage final class XibIndexer: Indexer { - enum XibError: Error { + private enum XibError: Error { case failedToParse(path: FilePath, underlyingError: Error) } diff --git a/Sources/Logger/Logger.swift b/Sources/Logger/Logger.swift index 4ff721a6e3..d6afb5913b 100644 --- a/Sources/Logger/Logger.swift +++ b/Sources/Logger/Logger.swift @@ -28,13 +28,13 @@ public enum LoggerColorMode: Sendable { } public struct Logger: Sendable { - let outputQueue: DispatchQueue - let quiet: Bool - let verbose: Bool - let colorMode: LoggerColorMode + private let outputQueue: DispatchQueue + private let quiet: Bool + private let verbose: Bool + private let colorMode: LoggerColorMode #if canImport(os) - let signposter = OSSignposter() + private let signposter = OSSignposter() #endif public var isColoredOutputEnabled: Bool { @@ -115,7 +115,7 @@ public struct Logger: Sendable { // MARK: - Private - func log(_ line: String, output: UnsafeMutablePointer) { + private func log(_ line: String, output: UnsafeMutablePointer) { _ = outputQueue.sync { fputs(line + "\n", output) } } @@ -132,8 +132,8 @@ public struct Logger: Sendable { } public struct ContextualLogger: Sendable { - let logger: Logger - let context: String + fileprivate let logger: Logger + fileprivate let context: String public func contextualized(with innerContext: String) -> ContextualLogger { logger.contextualized(with: "\(context):\(innerContext)") @@ -154,8 +154,8 @@ public struct ContextualLogger: Sendable { #if canImport(os) public struct SignpostInterval { - let name: StaticString - let state: OSSignpostIntervalState + fileprivate let name: StaticString + fileprivate let state: OSSignpostIntervalState } #else public struct SignpostInterval { diff --git a/Sources/ProjectDrivers/GenericProjectDriver.swift b/Sources/ProjectDrivers/GenericProjectDriver.swift index f791e692bb..30c26497ca 100644 --- a/Sources/ProjectDrivers/GenericProjectDriver.swift +++ b/Sources/ProjectDrivers/GenericProjectDriver.swift @@ -7,7 +7,7 @@ import Shared import SystemPackage public final class GenericProjectDriver { - struct GenericConfig: Decodable { + private struct GenericConfig: Decodable { let indexstores: Set let plists: Set let xibs: Set diff --git a/Sources/ProjectDrivers/SPMProjectDriver.swift b/Sources/ProjectDrivers/SPMProjectDriver.swift index 2d9898b44c..73bc10b402 100644 --- a/Sources/ProjectDrivers/SPMProjectDriver.swift +++ b/Sources/ProjectDrivers/SPMProjectDriver.swift @@ -20,7 +20,7 @@ public final class SPMProjectDriver { self.init(pkg: pkg, configuration: configuration, logger: logger) } - init(pkg: SPM.Package, configuration: Configuration, logger: Logger) { + private init(pkg: SPM.Package, configuration: Configuration, logger: Logger) { self.pkg = pkg self.configuration = configuration self.logger = logger diff --git a/Sources/ProjectDrivers/XcodeProjectDriver.swift b/Sources/ProjectDrivers/XcodeProjectDriver.swift index 943fe363cf..058708c8f0 100644 --- a/Sources/ProjectDrivers/XcodeProjectDriver.swift +++ b/Sources/ProjectDrivers/XcodeProjectDriver.swift @@ -79,7 +79,7 @@ ) } - init( + private init( logger: Logger, configuration: Configuration, xcodebuild: Xcodebuild, diff --git a/Sources/Shared/JobPool.swift b/Sources/Shared/JobPool.swift index ae527d4312..2d2b38eaf5 100644 --- a/Sources/Shared/JobPool.swift +++ b/Sources/Shared/JobPool.swift @@ -2,7 +2,7 @@ import Foundation import Synchronization public struct JobPool { - let jobs: [Job] + private let jobs: [Job] public init(jobs: [Job]) { self.jobs = jobs diff --git a/Sources/Shared/SetupGuide.swift b/Sources/Shared/SetupGuide.swift index bc10dde2a1..35790554d3 100644 --- a/Sources/Shared/SetupGuide.swift +++ b/Sources/Shared/SetupGuide.swift @@ -26,7 +26,7 @@ open class SetupGuideHelpers { self.logger = logger } - func display(options: [String]) { + private func display(options: [String]) { let maxPaddingCount = String(options.count).count for (index, option) in options.enumerated() { diff --git a/Sources/Shared/Shell.swift b/Sources/Shared/Shell.swift index 0f7c1667aa..0d89148ce1 100644 --- a/Sources/Shared/Shell.swift +++ b/Sources/Shared/Shell.swift @@ -2,7 +2,7 @@ import Foundation import Logger import Synchronization -final class ShellProcessStore: Sendable { +private final class ShellProcessStore: Sendable { private let processes = Mutex>([]) func interruptRunning() { diff --git a/Sources/SourceGraph/Elements/DeclarationAttribute.swift b/Sources/SourceGraph/Elements/DeclarationAttribute.swift index d8e1b1b93d..832cb976b4 100644 --- a/Sources/SourceGraph/Elements/DeclarationAttribute.swift +++ b/Sources/SourceGraph/Elements/DeclarationAttribute.swift @@ -1,6 +1,6 @@ public struct DeclarationAttribute: Hashable, CustomStringConvertible { let name: String - let arguments: String? + private let arguments: String? public init(name: String, arguments: String?) { self.name = name diff --git a/Sources/SourceGraph/Mutators/EnumCaseReferenceBuilder.swift b/Sources/SourceGraph/Mutators/EnumCaseReferenceBuilder.swift index d32d303b87..2880a6556a 100644 --- a/Sources/SourceGraph/Mutators/EnumCaseReferenceBuilder.swift +++ b/Sources/SourceGraph/Mutators/EnumCaseReferenceBuilder.swift @@ -38,7 +38,7 @@ final class EnumCaseReferenceBuilder: SourceGraphMutator { // MARK: - Private - func isRawRepresentable(_ enumDeclaration: Declaration) -> Bool { + private func isRawRepresentable(_ enumDeclaration: Declaration) -> Bool { // If the enum has a related struct it's very likely to be raw representable, // and thus is dynamic in nature. diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index 01c78f4a15..aa6c72cb1e 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -330,7 +330,7 @@ public final class SourceGraph { inheritedTypeReferences(of: decl).compactMap { declaration(withUsr: $0.usr) } } - func immediateSubclasses(of decl: Declaration) -> Set { + private func immediateSubclasses(of decl: Declaration) -> Set { references(to: decl) .filter { $0.kind == .related && $0.declarationKind == .class } .flatMap { $0.parent?.usrs ?? [] } diff --git a/Sources/SyntaxAnalysis/CommentCommand.swift b/Sources/SyntaxAnalysis/CommentCommand.swift index 7379a763dd..8943dae7d3 100644 --- a/Sources/SyntaxAnalysis/CommentCommand.swift +++ b/Sources/SyntaxAnalysis/CommentCommand.swift @@ -29,7 +29,7 @@ extension CommentCommand { } ?? [] } - static func parse(_ rawCommand: String) -> Self? { + private static func parse(_ rawCommand: String) -> Self? { if rawCommand == "ignore" { return .ignore } else if rawCommand == "ignore:all" { diff --git a/Sources/SyntaxAnalysis/DeclarationSyntaxVisitor.swift b/Sources/SyntaxAnalysis/DeclarationSyntaxVisitor.swift index cd0432643f..4e127995bb 100644 --- a/Sources/SyntaxAnalysis/DeclarationSyntaxVisitor.swift +++ b/Sources/SyntaxAnalysis/DeclarationSyntaxVisitor.swift @@ -225,7 +225,7 @@ public final class DeclarationSyntaxVisitor: PeripherySyntaxVisitor { } } - func visitVariableTupleBinding(node: VariableDeclSyntax, pattern: TuplePatternSyntax, typeTuple: TupleTypeElementListSyntax?, initializerTuple: LabeledExprListSyntax?) { + private func visitVariableTupleBinding(node: VariableDeclSyntax, pattern: TuplePatternSyntax, typeTuple: TupleTypeElementListSyntax?, initializerTuple: LabeledExprListSyntax?) { let elements = Array(pattern.elements) let types: [TupleTypeElementSyntax?] = typeTuple?.map(\.self) ?? Array(repeating: nil, count: elements.count) let initializers: [LabeledExprSyntax?] = initializerTuple?.map(\.self) ?? Array(repeating: nil, count: elements.count) diff --git a/Sources/SyntaxAnalysis/MultiplexingSyntaxVisitor.swift b/Sources/SyntaxAnalysis/MultiplexingSyntaxVisitor.swift index 9222f86c05..5de68831c8 100644 --- a/Sources/SyntaxAnalysis/MultiplexingSyntaxVisitor.swift +++ b/Sources/SyntaxAnalysis/MultiplexingSyntaxVisitor.swift @@ -95,8 +95,8 @@ public final class MultiplexingSyntaxVisitor: SyntaxVisitor { public let sourceFile: SourceFile public let syntax: SourceFileSyntax public let locationConverter: SourceLocationConverter - let sourceLocationBuilder: SourceLocationBuilder - let swiftVersion: SwiftVersion + private let sourceLocationBuilder: SourceLocationBuilder + private let swiftVersion: SwiftVersion private var visitors: [PeripherySyntaxVisitor] = [] diff --git a/Sources/SyntaxAnalysis/TypeSyntaxInspector.swift b/Sources/SyntaxAnalysis/TypeSyntaxInspector.swift index 9f58d13da3..38bbe4617a 100644 --- a/Sources/SyntaxAnalysis/TypeSyntaxInspector.swift +++ b/Sources/SyntaxAnalysis/TypeSyntaxInspector.swift @@ -28,7 +28,7 @@ struct TypeSyntaxInspector { // MARK: - Private - func types(for typeSyntax: TypeSyntax) -> Set { + private func types(for typeSyntax: TypeSyntax) -> Set { if let identifierType = typeSyntax.as(IdentifierTypeSyntax.self) { // Simple type. var result: Set = identifierType.genericArgumentClause?.arguments.flatMapSet { diff --git a/Sources/SyntaxAnalysis/UnusedParameterParser.swift b/Sources/SyntaxAnalysis/UnusedParameterParser.swift index a585af1173..b6b3734a30 100644 --- a/Sources/SyntaxAnalysis/UnusedParameterParser.swift +++ b/Sources/SyntaxAnalysis/UnusedParameterParser.swift @@ -135,7 +135,7 @@ struct UnusedParameterParser { self.parseProtocols = parseProtocols } - func parse() -> [Function] { + private func parse() -> [Function] { parse(node: syntax, collecting: Function.self) } diff --git a/Sources/XcodeSupport/XcodeTarget.swift b/Sources/XcodeSupport/XcodeTarget.swift index bb184b94fd..89eb06d680 100644 --- a/Sources/XcodeSupport/XcodeTarget.swift +++ b/Sources/XcodeSupport/XcodeTarget.swift @@ -4,7 +4,7 @@ import SystemPackage import XcodeProj public final class XcodeTarget { - let project: XcodeProject + private let project: XcodeProject private let target: PBXTarget private var files: [ProjectFileKind: Set] = [:] From 4c2697619f75333492d71d836df987e7b90f07c8 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:44:27 -0800 Subject: [PATCH 18/31] Fix false positives caught by CI/Bazel building This commit fixes false positive warnings where internal declarations were incorrectly flagged as "not used outside of file" when they were actually being used in test files via @testable import. This only appeared using the --bazel flag on github. Fix a linux-only warning. Also, handle @usableFromInline to avoid false positives. Updated bazel.json and linux-bazel.json to suppress 3 new Bazel-specific false positives. When Bazel builds modules independently, each module's index store doesn't capture cross-module usage, leading to false "unused" warnings. The baseline suppresses these known artifacts while preserving detection of genuine issues. Updated the build script to clean SwiftPM cache before build to prevent occasional CI failure. --- .github/workflows/test.yml | 4 + MODULE.bazel.lock | 372 ++++-------------- .../Results/OutputFormatter.swift | 2 +- ...RedundantInternalAccessibilityMarker.swift | 7 + .../TargetA/InternalTestableImportUsage.swift | 11 + .../InternalTestableImportUsage_Support.swift | 10 + .../UsableFromInlineAccessibility.swift | 46 +++ .../InternalTestableImportTest.swift | 14 + .../RedundantInternalAccessibilityTest.swift | 45 +++ baselines/bazel.json | 24 +- baselines/linux-bazel.json | 5 +- 11 files changed, 239 insertions(+), 301 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage_Support.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/UsableFromInlineAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Tests/TestTarget/InternalTestableImportTest.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ed605ebe7..d14d0c0d6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -122,6 +122,8 @@ jobs: run: ${{ env.swift_build }} - name: Scan run: ${{ env.periphery_scan }} ${{ matrix.baseline && format('--baseline baselines/{0}', matrix.baseline) || '' }} + - name: Clean SwiftPM cache + run: rm -rf ~/Library/Caches/org.swift.swiftpm - name: Test run: ${{ env.swift_test }} linux: @@ -169,5 +171,7 @@ jobs: run: ${{ env.swift_build }} - name: Scan run: ${{ env.periphery_scan }} --baseline baselines/linux.json + - name: Clean SwiftPM cache + run: rm -rf ~/.cache/org.swift.swiftpm - name: Test run: ${{ env.swift_test }} diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 8c0d0801a2..8621f3a50e 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,5 +1,5 @@ { - "lockFileVersion": 26, + "lockFileVersion": 24, "registryFileHashes": { "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", @@ -9,11 +9,7 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", - "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", - "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", - "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", "https://bcr.bazel.build/modules/aexml/4.7.0.1/MODULE.bazel": "4fddb5b50d3ce3e49b88c28dd5389fb0fdc2ad445414316d2581c658cbee16c5", "https://bcr.bazel.build/modules/aexml/4.7.0.1/source.json": "076505d514f668e170c8d47f124c45fdf00c8e06326bfa38d7044432cda17a33", "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", @@ -21,12 +17,9 @@ "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", "https://bcr.bazel.build/modules/apple_support/1.16.0/MODULE.bazel": "e785295d21ccab339c3af131752bfbe50fc33dd8215b357492d05bfad0232400", "https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b", - "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", - "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", - "https://bcr.bazel.build/modules/apple_support/2.2.0/MODULE.bazel": "5e32bc42e413a4544df7afee7fa4c7aff73e670a44e56e8e45ce1954332034ad", - "https://bcr.bazel.build/modules/apple_support/2.4.0/MODULE.bazel": "98c9839ec95a5ab0690a362ebbf33c30955e5df4cdb36267fd0544670d699ddf", - "https://bcr.bazel.build/modules/apple_support/2.4.0/source.json": "c131b0ea2ee0a5abbd6a16e0f24afd11c9d0bb8377651159ba3097a9ae72eac8", + "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442", + "https://bcr.bazel.build/modules/apple_support/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", @@ -35,16 +28,11 @@ "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", - "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", - "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", - "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", - "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", - "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", - "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", @@ -58,105 +46,86 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.1/MODULE.bazel": "2e2e306add04b7c7cd21e73c9293dcbd7528a08a84338e919036f402eb6b1e2e", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.2.1.1/source.json": "4c86fd3a384a09613c2213fb1f71562d6d70471977e6e81173e6625fd6ce53bc", - "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", - "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4", "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", - "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", - "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", - "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", - "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", - "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", - "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/MODULE.bazel": "a1c8bb07b5b91d971727c635f449d05623ac9608f6fe4f5f04254ea12f08e349", - "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/source.json": "93f82a5ae985eb935c539bfee95e04767187818189241ac956f3ccadbdb8fb02", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", "https://bcr.bazel.build/modules/pathkit/1.0.1.1/MODULE.bazel": "b1bbd7970666386ab45969d69f754de801f5437ff160a77a014d62fac2ec93f3", "https://bcr.bazel.build/modules/pathkit/1.0.1.1/source.json": "b84e1bf975b1f961bb49ac7c74ad2101dd316d9ce04e7bc054ba3a61979463b7", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", - "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", - "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", - "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", + "https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", - "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", - "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", - "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/MODULE.bazel": "74e541b0ba877813da786a11707d4e394433c157841d5111a36be0d44b907931", - "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/source.json": "fc174b3d6215aa14197d1bd779f98bb72d9fd666ee5ec0d6bba6ae986baa4535", "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", - "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", - "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022", "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", - "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", - "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4", - "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", + "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4", "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", - "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", - "https://bcr.bazel.build/modules/rules_apple/3.6.0/MODULE.bazel": "7e3979510cbb7d6a20e8523a116cfc1252fe71821236c7340ca5bfd3a74b4479", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", - "https://bcr.bazel.build/modules/rules_apple/4.5.0/MODULE.bazel": "7038309caf203607b095b3aeb075da239a42c37661dfb3b4c570bc1a2acd0a16", - "https://bcr.bazel.build/modules/rules_apple/4.5.2/MODULE.bazel": "af5d1ac7956518e9f915ee63baf8e78fb928de33ac917cf64344651aef24b698", - "https://bcr.bazel.build/modules/rules_apple/4.5.2/source.json": "771fb217a6d88a7649dca0bb2d2b7f6ae7f45c2f94381406f86f96cafdb2253b", + "https://bcr.bazel.build/modules/rules_apple/3.3.0/MODULE.bazel": "7497a6e08c439493b863de42653868f78207bd26d32a0267423260ae2a1d861a", + "https://bcr.bazel.build/modules/rules_apple/4.0.1/MODULE.bazel": "dec3ca18ca4680c66a33341918ef5160ba37b51b282435e2ec01490b2f873475", + "https://bcr.bazel.build/modules/rules_apple/4.0.1/source.json": "8dab1eed33e306e60bd4a066e7ac14f660e0c389659fc80ab300dbfcb3182ad6", "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", - "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", - "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", - "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", - "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", - "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", - "https://bcr.bazel.build/modules/rules_cc/0.2.15/MODULE.bazel": "6a0a4a75a57aa6dc888300d848053a58c6b12a29f89d4304e1c41448514ec6e8", - "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", - "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", - "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/source.json": "d61627377bd7dd1da4652063e368d9366fc9a73920bfa396798ad92172cf645c", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", + "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", + "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", - "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", - "https://bcr.bazel.build/modules/rules_java/9.0.3/MODULE.bazel": "1f98ed015f7e744a745e0df6e898a7c5e83562d6b759dfd475c76456dda5ccea", - "https://bcr.bazel.build/modules/rules_java/9.0.3/source.json": "b038c0c07e12e658135bbc32cc1a2ded6e33785105c9d41958014c592de4593e", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", + "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", - "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", - "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", @@ -168,58 +137,52 @@ "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", - "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", - "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", - "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", - "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", - "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", - "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", - "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", + "https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", - "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", - "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3", "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", "https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9", - "https://bcr.bazel.build/modules/rules_swift/2.0.0/MODULE.bazel": "682a6bcd2828e9a5d8362b5685f246b785ebfefc5f22c2ab4dfa759e375720db", "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", "https://bcr.bazel.build/modules/rules_swift/2.3.0/MODULE.bazel": "6a7c7cc230b67acc7c19361db13cb8d8e5795f7d5c8a7091d6ac41a279f253fc", "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", - "https://bcr.bazel.build/modules/rules_swift/2.9.0/MODULE.bazel": "7f006254505b26275f871fc10b944973e453955b042248f379d9b9f63117c64e", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", - "https://bcr.bazel.build/modules/rules_swift/3.5.0/MODULE.bazel": "e71b6024213df1cb5ed2f327b273f6f4a406611db1e2444f09a37a837eee941d", - "https://bcr.bazel.build/modules/rules_swift/3.5.0/source.json": "265412f803895a90502e315b3318a8eaf4a6d535e0203862679f0b432a792aab", + "https://bcr.bazel.build/modules/rules_swift/2.8.1/MODULE.bazel": "af36114a6dc7378c44f381979fe2c451d99c18d723b973bb8b8cf4c79f81a79a", + "https://bcr.bazel.build/modules/rules_swift/2.8.1/source.json": "131c2d608c97ce307181e7b747367d9aeff795ee10cba0e3b78db8736bc7fc67", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/swift-filename-matcher/2.0.1/MODULE.bazel": "8f383390f67d0df02126268c69c7d9c3e7cfab95911babff9aad76b1fb67d74e", "https://bcr.bazel.build/modules/swift-filename-matcher/2.0.1/source.json": "3ea9aa93b1a4682e70d7584c00730d84b65255ef1193173d717017b09994264f", - "https://bcr.bazel.build/modules/swift-index-store/1.9.2/MODULE.bazel": "ca45bf3fe1d3b490dec20a2fd64e980bf6d15a2b4ef3de3df3d0ab9cd9308ecf", - "https://bcr.bazel.build/modules/swift-index-store/1.9.2/source.json": "9fa69f7da9080d113191e9a42d0bdce921f82cf0d64ba052bba6317e833e04df", - "https://bcr.bazel.build/modules/swift-syntax/603.0.0.bcr.1/MODULE.bazel": "5aa22742debe933634627a190b3ba528534726ac09983b5b656c4ef584002fe2", - "https://bcr.bazel.build/modules/swift-syntax/603.0.0.bcr.1/source.json": "53d9ea06c2048c208af61c9fd045a4d91a367be8e9269213651deddc9a50f0b4", - "https://bcr.bazel.build/modules/swift-system/1.6.4.bcr.1/MODULE.bazel": "7753c075512adf247a6836c2bf98382ac13feff6b5e94cbca507d963f27ac542", - "https://bcr.bazel.build/modules/swift-system/1.6.4.bcr.1/source.json": "dc192de45b46c87a791655a83f421f2f93119b6ef1a531cf81cde97675525cd0", + "https://bcr.bazel.build/modules/swift-indexstore/0.4.0/MODULE.bazel": "5b6e61a45468791af1962ba85397a757b1148aeb47ffb933a86db75c0054c063", + "https://bcr.bazel.build/modules/swift-indexstore/0.4.0/source.json": "ba927bae677bcc0decbfe7ac4d66304d3906a38972a75f4a0ffffee2544ffbd7", + "https://bcr.bazel.build/modules/swift-syntax/602.0.0/MODULE.bazel": "109ca1d7aa91d4f9657add263e42880130f6ac4fc2bad32b68e793123a4a7a72", + "https://bcr.bazel.build/modules/swift-syntax/602.0.0/source.json": "250723837eb36bfcc61640f3a8418701041a70c1db522670ff02a06f2c644736", + "https://bcr.bazel.build/modules/swift-system/1.6.3/MODULE.bazel": "08e0d7ddb7ad3a98fced60811ef48e0b1f3c435bccd93c08873b79a3b98c8c31", + "https://bcr.bazel.build/modules/swift-system/1.6.3/source.json": "7659c0aa4cc37e90f417274df27ad4711b35a87a5bd780061d4eb705875e47e1", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", "https://bcr.bazel.build/modules/swift_argument_parser/1.7.0/MODULE.bazel": "40d4e44950e44973dcf8590bcee637591de196b5dbe3696d07eb342b74d53672", "https://bcr.bazel.build/modules/swift_argument_parser/1.7.0/source.json": "b9b952cba0c748083b9b891e6ac46d347c92d37e8a92ead96d2a54b966bacd87", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "https://bcr.bazel.build/modules/xcodeproj/9.10.1/MODULE.bazel": "4344389d70afbf9dd3f8c7bb677656954e3c4f896a5e133a2b5fca13fef84312", - "https://bcr.bazel.build/modules/xcodeproj/9.10.1/source.json": "9525f3c3bc496d371ee3024d54b70b79b17f69b85df7bb42f1bd38dd3c7fbd69", - "https://bcr.bazel.build/modules/yams/6.2.1/MODULE.bazel": "a5c2e6da3dd3d7d0991e43d1733f93e25fb29a392df9765c5c6f9ffbde1b026b", - "https://bcr.bazel.build/modules/yams/6.2.1/source.json": "7ca7bcdd196777ce9a7889844e6862794607fc89d55d14c56d36b6742df6e0ad", + "https://bcr.bazel.build/modules/xcodeproj/9.7.1/MODULE.bazel": "38fb60ef5130f01b2126bb14fb2bf661aab5f753768047504d35c76723aa4f43", + "https://bcr.bazel.build/modules/xcodeproj/9.7.1/source.json": "3b7bb144dd6e0b940513b67ca3aea33e823397529ff4693e44cc8845f93de805", + "https://bcr.bazel.build/modules/yams/6.2.0/MODULE.bazel": "05521f43141f185044f0d76f7ac4978003b135f04075c695c66ef92d36b36541", + "https://bcr.bazel.build/modules/yams/6.2.0/source.json": "28846a80844c69bfdd663cb55237f6f62a93e20aae1f91e664ac5e2b034432f0", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", @@ -230,23 +193,26 @@ "//bazel:generated.bzl%generated": { "general": { "bzlTransitiveDigest": "nMR2FBcoRPImVocN9DNOnm2NQWyTbJPu7SHJgAXsLFw=", - "usagesDigest": "j2+BEhulziHQg/tfLhGlCnHNgQvsCmmbmOq2hQ05JUk=", - "recordedInputs": [], + "usagesDigest": "S5zTCGsw3nugb0U3s6bPvFYl04dzqTMrLLUzzabunO8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, "generatedRepoSpecs": { "periphery_generated": { "repoRuleId": "@@//bazel:generated.bzl%generated_repo", "attributes": {} } - } + }, + "recordedRepoMappingEntries": [] } }, "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { - "bzlTransitiveDigest": "ABI1D/sbS1ovwaW/kHDoj8nnXjQ0oKU9fzmzEG4iT8o=", + "bzlTransitiveDigest": "rL/34P1aFDq2GqVC2zCFgQ8nTuOC6ziogocpvG50Qz8=", "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", - "recordedInputs": [ - "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools" - ], + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, "generatedRepoSpecs": { "com_github_jetbrains_kotlin_git": { "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", @@ -294,185 +260,23 @@ ] } } - } - } - }, - "@@rules_python+//python/extensions:config.bzl%config": { - "general": { - "bzlTransitiveDigest": "2hLgIvNVTLgxus0ZuXtleBe70intCfo0cHs8qvt6cdM=", - "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=", - "recordedInputs": [ - "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", - "REPO_MAPPING:rules_python+,pypi__build rules_python++config+pypi__build", - "REPO_MAPPING:rules_python+,pypi__click rules_python++config+pypi__click", - "REPO_MAPPING:rules_python+,pypi__colorama rules_python++config+pypi__colorama", - "REPO_MAPPING:rules_python+,pypi__importlib_metadata rules_python++config+pypi__importlib_metadata", - "REPO_MAPPING:rules_python+,pypi__installer rules_python++config+pypi__installer", - "REPO_MAPPING:rules_python+,pypi__more_itertools rules_python++config+pypi__more_itertools", - "REPO_MAPPING:rules_python+,pypi__packaging rules_python++config+pypi__packaging", - "REPO_MAPPING:rules_python+,pypi__pep517 rules_python++config+pypi__pep517", - "REPO_MAPPING:rules_python+,pypi__pip rules_python++config+pypi__pip", - "REPO_MAPPING:rules_python+,pypi__pip_tools rules_python++config+pypi__pip_tools", - "REPO_MAPPING:rules_python+,pypi__pyproject_hooks rules_python++config+pypi__pyproject_hooks", - "REPO_MAPPING:rules_python+,pypi__setuptools rules_python++config+pypi__setuptools", - "REPO_MAPPING:rules_python+,pypi__tomli rules_python++config+pypi__tomli", - "REPO_MAPPING:rules_python+,pypi__wheel rules_python++config+pypi__wheel", - "REPO_MAPPING:rules_python+,pypi__zipp rules_python++config+pypi__zipp" - ], - "generatedRepoSpecs": { - "rules_python_internal": { - "repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo", - "attributes": { - "transition_setting_generators": {}, - "transition_settings": [] - } - }, - "pypi__build": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", - "sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__click": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", - "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__colorama": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", - "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__importlib_metadata": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl", - "sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__installer": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", - "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__more_itertools": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", - "sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__packaging": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", - "sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__pep517": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl", - "sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__pip": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", - "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__pip_tools": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", - "sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__pyproject_hooks": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", - "sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__setuptools": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", - "sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__tomli": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", - "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__wheel": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", - "sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - }, - "pypi__zipp": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl", - "sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e", - "type": "zip", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" - } - } - } + }, + "recordedRepoMappingEntries": [ + [ + "rules_kotlin+", + "bazel_tools", + "bazel_tools" + ] + ] } }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { - "bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=", - "usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=", - "recordedInputs": [ - "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", - "REPO_MAPPING:rules_python+,platforms platforms" - ], + "bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=", + "usagesDigest": "vJ5RHUxAnV24M5swNGiAnkdxMx3Hp/iOLmNANTC5Xc8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, "generatedRepoSpecs": { "uv": { "repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo", @@ -492,26 +296,14 @@ "toolchain_target_settings": {} } } - } - } - }, - "@@swift-index-store+//:repositories.bzl%bzlmod_deps": { - "general": { - "bzlTransitiveDigest": "5SgzvK5bW1AjBEGcIZ+V8t1hdrKQxcL5liC+blXrOeo=", - "usagesDigest": "6gSsMFoZD8MOT1d3qLGp8z92CBXKqQI0zULrhGHXrow=", - "recordedInputs": [ - "REPO_MAPPING:swift-index-store+,bazel_tools bazel_tools" - ], - "generatedRepoSpecs": { - "StaticIndexStore": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", - "attributes": { - "url": "https://github.com/keith/StaticIndexStore/releases/download/5.7/libIndexStore.xcframework.zip", - "sha256": "da69bab932357a817aa0756e400be86d7156040bfbea8eded7a3acc529320731", - "build_file_content": "\nload(\"@build_bazel_rules_apple//apple:apple.bzl\", \"apple_static_xcframework_import\")\n\napple_static_xcframework_import(\n name = \"libIndexStore\",\n visibility = [\"//visibility:public\"],\n xcframework_imports = glob([\"libIndexStore.xcframework/**\"]),\n)\n " - } - } - } + }, + "recordedRepoMappingEntries": [ + [ + "rules_python+", + "platforms", + "platforms" + ] + ] } } }, diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index a1d9040b7f..19312f8d45 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -70,7 +70,7 @@ extension OutputFormatter { description += "Redundant public accessibility for \(kindDisplayName) '\(name)' (not used outside of \(modulesJoined))" case let .redundantInternalAccessibility(_, suggestedAccessibility): let accessibilityText = suggestedAccessibility?.rawValue ?? "private/fileprivate" - description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of file; can be \(accessibilityText)" + description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of file; can be \(accessibilityText))" case let .redundantFilePrivateAccessibility(_, containingTypeName): let context = containingTypeName.map { "only used within \($0)" } ?? "not used outside of file" description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (\(context); can be private)" diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 23bb9c3731..c87b8670fd 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -168,6 +168,13 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Override methods must be at least as accessible as what they override. if decl.isOverride { return true } + // Declarations with @usableFromInline must remain internal (or package). + // This attribute allows internal declarations to be inlined into client code, + // requiring them to maintain internal visibility. + if decl.attributes.contains(where: { $0.name == "usableFromInline" }) { + return true + } + return false } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage.swift new file mode 100644 index 0000000000..a8d44bd3ef --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage.swift @@ -0,0 +1,11 @@ +import Foundation + +// Should NOT be flagged as redundant - used from test via @testable import +internal class InternalUsedOnlyInTest { + internal func testOnlyMethod() {} +} + +// Should NOT be flagged - used from both test AND production code +internal class InternalUsedInBoth { + internal func sharedMethod() {} +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage_Support.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage_Support.swift new file mode 100644 index 0000000000..e6c6cc4e8b --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTestableImportUsage_Support.swift @@ -0,0 +1,10 @@ +import Foundation + +// This file uses InternalUsedInBoth from production code within the same module +// to ensure it's not flagged as redundant internal (since it's used both in production and tests) + +struct InternalTestableImportRetainer { + func retain() { + _ = InternalUsedInBoth().sharedMethod() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/UsableFromInlineAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/UsableFromInlineAccessibility.swift new file mode 100644 index 0000000000..50689f7128 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/UsableFromInlineAccessibility.swift @@ -0,0 +1,46 @@ +// Test file for @usableFromInline attribute handling + +public struct PublicContainer { + // This should NOT be flagged as redundant internal because it has @usableFromInline. + // @usableFromInline requires the declaration to remain internal (or package) so it can + // be inlined into client code. + @usableFromInline + internal init() {} + + // This should NOT be flagged as redundant internal because of @usableFromInline. + @usableFromInline + internal func inlinableHelper() -> Int { + 42 + } + + // This should NOT be flagged as redundant internal because of @usableFromInline. + @usableFromInline + internal var inlinableProperty: String { + "value" + } + + // This should NOT be flagged as redundant internal. + // Even though it's only used in this file, @usableFromInline means it could be + // inlined into client code that imports this module. + @usableFromInline + internal static func inlinableStaticMethod() -> Bool { + true + } + + // Public method that uses the @usableFromInline members + @inlinable + public func publicInlinableMethod() -> String { + "\(inlinableProperty): \(inlinableHelper())" + } +} + +// This SHOULD be flagged as redundant internal - no @usableFromInline attribute +// and only used within the same file in a non-inlinable private function. +internal func regularInternalMethod() -> String { + PublicContainer.inlinableStaticMethod().description +} + +// Use the regular internal method within the same file +private func useRegularMethod() { + _ = regularInternalMethod() +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Tests/TestTarget/InternalTestableImportTest.swift b/Tests/AccessibilityTests/AccessibilityProject/Tests/TestTarget/InternalTestableImportTest.swift new file mode 100644 index 0000000000..09d5901e9c --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Tests/TestTarget/InternalTestableImportTest.swift @@ -0,0 +1,14 @@ +import Foundation +import XCTest +@testable import TargetA + +class InternalTestableImportTest: XCTestCase { + func testInternalAccess() { + // Access internal declarations via @testable import + let obj1 = InternalUsedOnlyInTest() + obj1.testOnlyMethod() + + let obj2 = InternalUsedInBoth() + obj2.sharedMethod() + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 106e8595b8..0bf6f93fb0 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -176,4 +176,49 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.struct("ImplicitlyInternalStructUsedFromAnotherFile")) } + + /// Tests that internal declarations accessed from test files via @testable import + /// are NOT flagged as redundant internal. + /// + /// This verifies that @testable import references count as legitimate cross-file usage, + /// preventing false positives when test files access internal members. Since tests ARE + /// using these internal members from a different file, they require internal accessibility. + func testInternalUsedViaTestableImportNotFlagged() { + index() + + // InternalUsedOnlyInTest should NOT be flagged because it IS used from + // a test file (different file) via @testable import + assertNotRedundantInternalAccessibility(.class("InternalUsedOnlyInTest")) + } + + /// Tests that internal declarations used from production code in the same module + /// are NOT flagged as redundant (baseline behavior verification). + func testInternalUsedInProductionNotFlagged() { + index() + + // InternalUsedInBoth should NOT be flagged because it's used from + // production code (InternalTestableImportUsage_Support.swift) within the same module + assertNotRedundantInternalAccessibility(.class("InternalUsedInBoth")) + } + + /// Tests that declarations with @usableFromInline are NOT flagged as redundant internal. + /// + /// The @usableFromInline attribute allows internal declarations to be inlined into + /// client code, requiring them to maintain internal (or package) visibility. Marking + /// them as fileprivate or private would cause a compiler error because @usableFromInline + /// is incompatible with those access levels. + /// + /// This test verifies the fix for a build error on Linux where @usableFromInline + /// declarations were incorrectly flagged as redundant internal, and changing them + /// to fileprivate caused: "@usableFromInline attribute can only be applied to + /// internal or package declarations". + func testUsableFromInlineNotFlagged() { + index() + + // All @usableFromInline members should NOT be flagged, even if only used in same file + assertNotRedundantInternalAccessibility(.functionConstructor("init()")) + assertNotRedundantInternalAccessibility(.functionMethodInstance("inlinableHelper()")) + assertNotRedundantInternalAccessibility(.varInstance("inlinableProperty")) + assertNotRedundantInternalAccessibility(.functionMethodStatic("inlinableStaticMethod()")) + } } diff --git a/baselines/bazel.json b/baselines/bazel.json index 835493c5d5..eb6740271d 100644 --- a/baselines/bazel.json +++ b/baselines/bazel.json @@ -1,10 +1,16 @@ { - "v1": { - "usrs": [ - "s:11SourceGraph0aB8DebuggerC", - "s:13SystemPackage8FilePathV10ExtensionsE5chdir7closureyyyKXE_tKF", - "s:14SyntaxAnalysis21UnusedParameterParserV5parse4file0F9ProtocolsSayAA8FunctionVG11SourceGraph0J4FileC_SbtKFZ", - "s:14SyntaxAnalysis8FunctionV8fullNameSSvp" - ] - } -} + "v1": { + "usrs": [ + "s:13Configuration15AbstractSettingP5resetyyF", + "s:13Configuration7SettingC5resetyyF", + "s:13ConfigurationAAC13resetMatchersyyF", + "s:13ConfigurationAAC5resetyyF", + "s:13SystemPackage8FilePathV10ExtensionsE5chdir7closureyyyKXE_tKF", + "s:14SyntaxAnalysis21UnusedParameterParserV5parse4file0F9ProtocolsSayAA8FunctionVG11SourceGraph0J4FileC_SbtKFZ", + "s:14SyntaxAnalysis8FunctionV8fullNameSSvp", + "s:14SyntaxAnalysis23UnusedParameterAnalyzerC7analyze8functionShyAA0D0VGAA8FunctionV_tF", + "s:14SyntaxAnalysis9ParameterV5labelAC8PartKindOvp", + "s:14SyntaxAnalysis9ParameterV8location11SourceGraph8LocationCvp" + ] + } +} \ No newline at end of file diff --git a/baselines/linux-bazel.json b/baselines/linux-bazel.json index f55df1d7e7..c5cd8fe94e 100644 --- a/baselines/linux-bazel.json +++ b/baselines/linux-bazel.json @@ -12,7 +12,10 @@ "s:6Shared17SetupGuideHelpersC6select8multipleAA0B9SelectionOSaySSG_tF", "s:SS10ExtensionsE17withEscapedQuotesSSvp", "s:SS10ExtensionsE4djb2Sivp", - "s:SS10ExtensionsE7djb2HexSSvp" + "s:SS10ExtensionsE7djb2HexSSvp", + "s:14SyntaxAnalysis23UnusedParameterAnalyzerC7analyze8functionShyAA0D0VGAA8FunctionV_tF", + "s:14SyntaxAnalysis9ParameterV5labelAC8PartKindOvp", + "s:14SyntaxAnalysis9ParameterV8location11SourceGraph8LocationCvp" ] } } From e6b91419eb7ad4050e7811a3124871bb419504fd Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:41:13 -0800 Subject: [PATCH 19/31] Make new function be private, conforming to new code --- Sources/SourceGraph/SourceGraphDebugger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SourceGraph/SourceGraphDebugger.swift b/Sources/SourceGraph/SourceGraphDebugger.swift index e62f6ddbae..10c78d958e 100644 --- a/Sources/SourceGraph/SourceGraphDebugger.swift +++ b/Sources/SourceGraph/SourceGraphDebugger.swift @@ -14,7 +14,7 @@ final class SourceGraphDebugger { // MARK: - Private - func describe(_ declarations: [Declaration]) { + private func describe(_ declarations: [Declaration]) { for (index, declaration) in declarations.enumerated() { describe(declaration) From d3e5536af7df40f73412c72815850f4a38b4a893 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:46:18 -0800 Subject: [PATCH 20/31] Update to work with upstream changes, fixing external protocol check and adding test cases. --- ...RedundantInternalAccessibilityMarker.swift | 27 ++++++++++++------- ...rnalProtocolConformanceAccessibility.swift | 27 +++++++++++++++++++ .../RedundantInternalAccessibilityTest.swift | 25 +++++++++++++++++ baselines/bazel.json | 4 ++- baselines/linux-bazel.json | 3 ++- 5 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolConformanceAccessibility.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index c87b8670fd..b9fa6da602 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -195,7 +195,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // from the conforming declaration to the protocol requirement. If this declaration // has any related references pointing to protocol members with matching names, // it's implementing a protocol requirement. - let relatedReferences = graph.references(to: decl).filter(\.isRelated) + let relatedReferences = graph.references(to: decl).filter { $0.kind == .related } for ref in relatedReferences { if let protocolDecl = graph.declaration(withUsr: ref.usr), protocolDecl.kind.isProtocolMemberKind || protocolDecl.kind == .associatedtype @@ -204,14 +204,21 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } } - // Alternative check: Look for related references FROM this declaration - // to protocol members. The ProtocolConformanceReferenceBuilder inverts - // these relationships, so we might find them either direction. - for ref in decl.related where ref.kind.isProtocolMemberConformingKind { - if let referencedDecl = graph.declaration(withUsr: ref.usr), - let referencedParent = referencedDecl.parent, - referencedParent.kind == .protocol - { + // Case 3: Check for .related references FROM this declaration to protocol members. + // This covers both internal AND external protocol conformances. + for ref in decl.related where ref.declarationKind.isProtocolMemberConformingKind { + if let referencedDecl = graph.declaration(withUsr: ref.usr) { + // Internal protocol: verify the referenced declaration's parent is a protocol. + if let referencedParent = referencedDecl.parent, + referencedParent.kind == .protocol + { + return true + } + } else if ref.name == decl.name { + // External protocol: the declaration doesn't exist in our graph, + // but the indexer created a .related reference with a protocol member kind + // AND the names match. This means this declaration implements an external + // protocol requirement. return true } } @@ -249,7 +256,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let siblings = parent.declarations let hasFunctionReference = siblings.contains { sibling in sibling.kind.isFunctionKind && sibling.references.contains { ref in - ref.kind == .typealias && decl.usrs.contains(ref.usr) + ref.declarationKind == .typealias && decl.usrs.contains(ref.usr) } } if hasFunctionReference { diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolConformanceAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolConformanceAccessibility.swift new file mode 100644 index 0000000000..77458b0dbc --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolConformanceAccessibility.swift @@ -0,0 +1,27 @@ +/* + ExternalProtocolConformanceAccessibility.swift + Tests that types conforming to external protocols are NOT flagged as redundant internal. +*/ + +import ExternalTarget + +// Internal struct conforming to external protocol - should NOT be flagged. +internal struct InternalStructConformingToExternalProtocol: ExternalProtocol { + func someExternalProtocolMethod() {} +} + +// Implicitly internal class conforming to external protocol - should NOT be flagged. +class ImplicitlyInternalClassConformingToExternalProtocol: ExternalProtocol { + func someExternalProtocolMethod() {} +} + +// Used to ensure these types are referenced. +public class ExternalProtocolConformanceRetainer { + public init() {} + public func retain() { + let s = InternalStructConformingToExternalProtocol() + s.someExternalProtocolMethod() + let c = ImplicitlyInternalClassConformingToExternalProtocol() + c.someExternalProtocolMethod() + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 0bf6f93fb0..0a6393ccbc 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -221,4 +221,29 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.varInstance("inlinableProperty")) assertNotRedundantInternalAccessibility(.functionMethodStatic("inlinableStaticMethod()")) } + + /// Tests that internal types conforming to external protocols are NOT flagged as redundant. + /// + /// When a type conforms to an external protocol (from another module), its protocol + /// requirement implementations must maintain their accessibility to fulfill the + /// protocol contract. This prevents false positives like those seen with + /// CheckUpdateCommand, ScanCommand, etc. implementing ArgumentParser's ParsableCommand. + func testExternalProtocolConformanceNotFlagged() { + index() + + // Internal struct conforming to ExternalProtocol should NOT be flagged + assertNotRedundantInternalAccessibility(.struct("InternalStructConformingToExternalProtocol")) + // The protocol requirement implementation should NOT be flagged (line 10 in fixture) + assertNotRedundantInternalAccessibility(.functionMethodInstance("someExternalProtocolMethod()", line: 10)) + } + + /// Tests that implicitly internal types conforming to external protocols are NOT flagged. + func testImplicitlyInternalExternalProtocolConformanceNotFlagged() { + index() + + // Implicitly internal class conforming to ExternalProtocol should NOT be flagged + assertNotRedundantInternalAccessibility(.class("ImplicitlyInternalClassConformingToExternalProtocol")) + // The protocol requirement implementation should NOT be flagged (line 15 in fixture) + assertNotRedundantInternalAccessibility(.functionMethodInstance("someExternalProtocolMethod()", line: 15)) + } } diff --git a/baselines/bazel.json b/baselines/bazel.json index eb6740271d..f8fd7130ff 100644 --- a/baselines/bazel.json +++ b/baselines/bazel.json @@ -1,6 +1,8 @@ { "v1": { "usrs": [ + "s:11SourceGraph0aB8DebuggerC", + "s:11SourceGraph32InterfaceBuilderPropertyRetainerC19swiftNameToSelectoryS2SFZ", "s:13Configuration15AbstractSettingP5resetyyF", "s:13Configuration7SettingC5resetyyF", "s:13ConfigurationAAC13resetMatchersyyF", @@ -13,4 +15,4 @@ "s:14SyntaxAnalysis9ParameterV8location11SourceGraph8LocationCvp" ] } -} \ No newline at end of file +} diff --git a/baselines/linux-bazel.json b/baselines/linux-bazel.json index c5cd8fe94e..4aadd93bfd 100644 --- a/baselines/linux-bazel.json +++ b/baselines/linux-bazel.json @@ -15,7 +15,8 @@ "s:SS10ExtensionsE7djb2HexSSvp", "s:14SyntaxAnalysis23UnusedParameterAnalyzerC7analyze8functionShyAA0D0VGAA8FunctionV_tF", "s:14SyntaxAnalysis9ParameterV5labelAC8PartKindOvp", - "s:14SyntaxAnalysis9ParameterV8location11SourceGraph8LocationCvp" + "s:14SyntaxAnalysis9ParameterV8location11SourceGraph8LocationCvp", + "s:11SourceGraph32InterfaceBuilderPropertyRetainerC19swiftNameToSelectoryS2SFZ" ] } } From 495ed6e8602db3169a4b20bc85e525bbc90248ad Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:51:30 -0800 Subject: [PATCH 21/31] Make sure that internal types used as return types of functions called from other files are NOT flagged as redundant internal --- ...RedundantInternalAccessibilityMarker.swift | 106 ++++++- .../Sources/MainTarget/main.swift | 3 + .../TargetA/InternalTypeAsReturnType.swift | 25 ++ .../InternalTypeAsReturnType_Consumer.swift | 11 + ...nalTypeTransitivelyExposedInSameFile.swift | 37 +++ .../TargetA/TransitiveAccessExposure.swift | 267 ++++++++++++++++++ .../TransitiveAccessExposure_Consumer.swift | 94 ++++++ .../RedundantInternalAccessibilityTest.swift | 88 ++++++ 8 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType_Consumer.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeTransitivelyExposedInSameFile.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure_Consumer.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index b9fa6da602..449d43e98e 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -40,7 +40,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { if decl.accessibility.value == .internal { if !graph.isRetained(decl), !shouldSkipMarking(decl) { let isReferencedOutside = decl.isReferencedOutsideFile(graph: graph) - if !isReferencedOutside { + if !isReferencedOutside, !isTransitivelyExposedOutsideFile(decl) { mark(decl) } } @@ -82,8 +82,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // to indicate the ambiguity in the output message. // If the declaration is referenced from different types in the same file, // it needs fileprivate. Otherwise, private is sufficient. + // Also check transitive exposure: if the type is used as return/parameter type of a + // function called from a different type in the same file, it needs fileprivate. let isTopLevel = decl.parent == nil - let suggestedAccessibility: Accessibility? = isTopLevel ? nil : (isReferencedFromDifferentTypeInSameFile(decl) ? .fileprivate : .private) + let needsFileprivate = isReferencedFromDifferentTypeInSameFile(decl) || isTransitivelyExposedFromDifferentTypeInSameFile(decl) + let suggestedAccessibility: Accessibility? = isTopLevel ? nil : (needsFileprivate ? .fileprivate : .private) // Check if the parent's accessibility already constrains this member. // If the parent is `private`, the member is already effectively `private`. @@ -114,7 +117,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { for descDecl in descendants { if !graph.isRetained(descDecl), !shouldSkipMarking(descDecl) { let isReferencedOutside = descDecl.isReferencedOutsideFile(graph: graph) - if !isReferencedOutside { + if !isReferencedOutside, !isTransitivelyExposedOutsideFile(descDecl) { mark(descDecl) } } @@ -346,4 +349,101 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } return decl } + + /// Checks if a type is transitively exposed outside its file through an API signature. + /// + /// A type is transitively exposed when it appears in the signature of a function, property, + /// or initializer (as return type, parameter type, etc.) where that API is referenced from + /// a different file. Even if the type itself is never directly referenced outside its file, + /// it must remain `internal` (not `fileprivate` or `private`) if it's part of an API that + /// is used from other files. + /// + /// For example, if `ScanProgress` is the return type of `runFullScanWithStreaming()`, and + /// that function is called from another file, then `ScanProgress` is transitively exposed + /// and should not be marked as redundantly internal. + private func isTransitivelyExposedOutsideFile(_ decl: Declaration) -> Bool { + let refs = graph.references(to: decl) + + for ref in refs { + // Check if this reference is in an API signature role (return type, parameter type, etc.) + guard ref.role.isPubliclyExposable else { continue } + + // Get the parent declaration (the function/property that uses this type in its signature) + guard let parent = ref.parent else { continue } + + // Check if that parent API is referenced from outside this file + if parent.isReferencedOutsideFile(graph: graph) { + return true + } + + // For properties, also check if the containing type is used from outside the file. + // When code accesses `obj.property`, the property's type is exposed even if + // `property` itself isn't directly referenced from outside. + if parent.kind.isVariableKind { + if let containingType = parent.parent, + containingType.isReferencedOutsideFile(graph: graph) + { + return true + } + } + } + + return false + } + + /// Checks if a type is transitively exposed from a different type within the same file. + /// + /// This is similar to `isTransitivelyExposedOutsideFile`, but checks for exposure within + /// the same file from a different type. When a type is used as a return type or parameter + /// type of a function that is called from a different type in the same file, the type + /// needs to be at least `fileprivate` (not `private`). + /// + /// For example: + /// ```swift + /// class ClassA { + /// enum Status { case active } // Only directly referenced in ClassA + /// func getStatus() -> Status { .active } // Called from ClassB + /// } + /// class ClassB { + /// func use() { _ = ClassA().getStatus() } // Uses Status transitively + /// } + /// ``` + /// Here, `Status` should be suggested as `fileprivate`, not `private`. + private func isTransitivelyExposedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { + let file = decl.location.file + let refs = graph.references(to: decl) + + guard let declTopLevel = topLevelType(of: decl) else { + return false + } + + let declLogicalType = logicalType(of: declTopLevel, inFile: file) + + for ref in refs { + // Check if this reference is in an API signature role (return type, parameter type, etc.) + guard ref.role.isPubliclyExposable else { continue } + + // Get the parent declaration (the function/property that uses this type in its signature) + guard let parent = ref.parent else { continue } + + // Check references to that parent from the same file + let parentRefs = graph.references(to: parent).filter { $0.location.file == file } + + for parentRef in parentRefs { + guard let refParent = parentRef.parent, + let refTopLevel = topLevelType(of: refParent) + else { + continue + } + + let refLogicalType = logicalType(of: refTopLevel, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + } + + return false + } } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index d6e56e8f5e..5eac2559f7 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -96,3 +96,6 @@ NestedTypeAccessibilityRetainer().retain() InternalSuggestingPrivateVsFileprivateRetainer().retain() ImplicitlyInternalRetainer().retain() NotRedundantInternalClassComponents_Support().useImplicitlyInternalStruct() +InternalTypeAsReturnTypeRetainer().retain() +InternalTypeTransitivelyExposedInSameFileRetainer().retain() +TransitiveAccessExposureRetainer().retain() diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType.swift new file mode 100644 index 0000000000..608c2f6e75 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType.swift @@ -0,0 +1,25 @@ +// InternalTypeAsReturnType.swift +// Tests for internal types used as return types of functions called from other files. +// These should NOT be flagged as redundant internal. + +// This internal enum is used as the return type of a function that is called from another file. +// Even though InternalReturnTypeEnum is never directly referenced outside this file, +// it is transitively exposed through the function's return type. +internal enum InternalReturnTypeEnum { + case value +} + +internal class InternalReturnTypeContainer { + func getEnum() -> InternalReturnTypeEnum { + return .value + } +} + +public class InternalTypeAsReturnTypeRetainer { + public init() {} + + public func retain() { + let container = InternalReturnTypeContainer() + _ = container.getEnum() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType_Consumer.swift new file mode 100644 index 0000000000..728aebddfd --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeAsReturnType_Consumer.swift @@ -0,0 +1,11 @@ +// InternalTypeAsReturnType_Consumer.swift +// Consumer that calls the function with internal return type from a different file. +// This creates the transitive exposure of InternalReturnTypeEnum. + +class InternalReturnTypeConsumer { + func consume() { + let container = InternalReturnTypeContainer() + // This call uses InternalReturnTypeEnum transitively through the return type + let _ = container.getEnum() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeTransitivelyExposedInSameFile.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeTransitivelyExposedInSameFile.swift new file mode 100644 index 0000000000..0f7c1d6455 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/InternalTypeTransitivelyExposedInSameFile.swift @@ -0,0 +1,37 @@ +// InternalTypeTransitivelyExposedInSameFile.swift +// Tests for internal types that are transitively exposed within the same file +// but from a different type. These should suggest fileprivate, not private. + +class TransitiveExposureClassA { + // This enum is only directly referenced within ClassA, but it's the return type + // of getStatus() which IS called from ClassB in the same file. + // It should be suggested as fileprivate (not private) because ClassB uses it transitively. + internal enum TransitivelyExposedStatus { + case active + case inactive + } + + func getStatus() -> TransitivelyExposedStatus { + .active + } +} + +class TransitiveExposureClassB { + func checkStatus() { + let a = TransitiveExposureClassA() + // This call uses TransitivelyExposedStatus transitively through the return type + let _ = a.getStatus() + } +} + +// Retainer that only uses ClassB (not ClassA.getStatus() directly) +// This ensures getStatus() is only referenced from within this file +public class InternalTypeTransitivelyExposedInSameFileRetainer { + public init() {} + + public func retain() { + // Only call checkStatus(), not getStatus() directly + // So getStatus() is only referenced from checkStatus() in this same file + TransitiveExposureClassB().checkStatus() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure.swift new file mode 100644 index 0000000000..61dad9c51c --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure.swift @@ -0,0 +1,267 @@ +// TransitiveAccessExposure.swift +// Comprehensive test cases for transitive access exposure scenarios. +// Each scenario shows a type that must have sufficient access level because +// it is exposed through another declaration's signature. +// +// For each case: +// - The inner type is marked with the minimum required access level +// - A comment shows what error would occur if we lowered the access level +// +// NOTE: This file focuses on CROSS-FILE exposure scenarios (internal types). +// Same-file scenarios (fileprivate types) are handled in a separate file. + +// MARK: - 1. Function Parameter Type Exposure + +// Scenario 1a: Internal type used as parameter of internal function called from another file +// If ParameterTypeA were fileprivate: +// "Method must be declared fileprivate because its parameter uses a fileprivate type" +internal struct ParameterTypeA { + var value: Int = 0 +} + +class ParameterExposureContainer { + // This function is called from TransitiveAccessExposure_Consumer.swift + func processParameter(_ param: ParameterTypeA) { + _ = param.value + } +} + +// MARK: - 2. Function Return Type Exposure + +// Scenario 2a: Internal type used as return type of function called from another file +// If ReturnTypeA were fileprivate: +// "Function must be declared fileprivate because its result uses a fileprivate type" +internal enum ReturnTypeA { + case success + case failure +} + +class ReturnTypeExposureContainer { + // This function is called from TransitiveAccessExposure_Consumer.swift + func getResult() -> ReturnTypeA { + .success + } +} + +// MARK: - 3. Property Type Exposure + +// Scenario 3a: Internal type used as property type, property accessed from another file +// If PropertyTypeA were fileprivate: +// "Property must be declared fileprivate because its type uses a fileprivate type" +internal struct PropertyTypeA { + var data: String = "" +} + +class PropertyTypeExposureContainer { + // This property is accessed from TransitiveAccessExposure_Consumer.swift + var exposedProperty: PropertyTypeA = PropertyTypeA() +} + +// MARK: - 4. Generic Constraint Exposure + +// Scenario 4a: Internal protocol used as generic constraint, function called from another file +// If GenericConstraintProtocolA were fileprivate: +// "Generic parameter 'T' cannot be declared internal because it uses a fileprivate type in its requirement" +internal protocol GenericConstraintProtocolA { + var identifier: String { get } +} + +internal struct GenericConstraintConformingTypeA: GenericConstraintProtocolA { + var identifier: String = "A" +} + +class GenericConstraintExposureContainer { + // This function is called from TransitiveAccessExposure_Consumer.swift + func processGeneric(_ item: T) -> String { + item.identifier + } +} + +// MARK: - 5. Protocol Requirement Exposure +// Note: Protocol requirements expose types in their signatures + +// Scenario 5a: Internal type used in protocol requirement parameter +// If ProtocolRequirementTypeA were fileprivate: +// "Method in an internal protocol cannot use a fileprivate type" +internal struct ProtocolRequirementTypeA { + var payload: Int = 0 +} + +internal protocol ProtocolWithRequirementA { + func process(input: ProtocolRequirementTypeA) +} + +class ProtocolRequirementExposureContainer: ProtocolWithRequirementA { + func process(input: ProtocolRequirementTypeA) { + _ = input.payload + } +} + +// MARK: - 6. Enum Associated Value Exposure + +// Scenario 6a: Internal type used as enum associated value, enum used from another file +// If EnumAssociatedTypeA were fileprivate: +// "Enum case in an internal enum uses a fileprivate type" +internal struct EnumAssociatedTypeA { + var content: String = "" +} + +internal enum EnumWithAssociatedValueA { + case success(EnumAssociatedTypeA) + case failure(Error) +} + +class EnumAssociatedValueExposureContainer { + // This function is called from TransitiveAccessExposure_Consumer.swift + func getEnumValue() -> EnumWithAssociatedValueA { + .success(EnumAssociatedTypeA(content: "data")) + } +} + +// MARK: - 7. Typealias Exposure + +// Scenario 7a: Internal type aliased by internal typealias, used from another file +// If TypealiasTargetTypeA were fileprivate: +// "Type alias cannot be declared internal because it uses a fileprivate type" +internal struct TypealiasTargetTypeA { + var value: Double = 0.0 +} + +internal typealias AliasedTypeA = TypealiasTargetTypeA + +class TypealiasExposureContainer { + // This property uses the typealias and is accessed from TransitiveAccessExposure_Consumer.swift + var aliasedProperty: AliasedTypeA = AliasedTypeA() +} + +// MARK: - 8. Subscript Exposure + +// Scenario 8a: Internal type used as subscript parameter, subscript accessed from another file +// If SubscriptKeyTypeA were fileprivate: +// "Subscript cannot be declared internal because its parameter uses a fileprivate type" +internal struct SubscriptKeyTypeA: Hashable { + var key: String = "" +} + +class SubscriptExposureContainer { + private var storage: [SubscriptKeyTypeA: Int] = [:] + + // This subscript is accessed from TransitiveAccessExposure_Consumer.swift + subscript(key: SubscriptKeyTypeA) -> Int { + get { storage[key] ?? 0 } + set { storage[key] = newValue } + } +} + +// Scenario 8b: Internal type used as subscript return type +// If SubscriptReturnTypeA were fileprivate: +// "Subscript cannot be declared internal because its element type uses a fileprivate type" +internal struct SubscriptReturnTypeA { + var data: String = "" +} + +class SubscriptReturnTypeExposureContainer { + private var items: [SubscriptReturnTypeA] = [] + + // This subscript is accessed from TransitiveAccessExposure_Consumer.swift + subscript(index: Int) -> SubscriptReturnTypeA { + items.indices.contains(index) ? items[index] : SubscriptReturnTypeA() + } +} + +// MARK: - 9. Default Argument Exposure + +// Scenario 9a: Internal type used in default argument, function called from another file +// If DefaultArgTypeA were fileprivate: +// "Default argument value of internal function uses a fileprivate type" +internal struct DefaultArgTypeA { + var config: String = "default" + static let defaultValue = DefaultArgTypeA() +} + +class DefaultArgumentExposureContainer { + // This function is called from TransitiveAccessExposure_Consumer.swift + func processWithDefault(config: DefaultArgTypeA = DefaultArgTypeA.defaultValue) { + _ = config.config + } +} + +// MARK: - 10. Initializer Parameter Exposure + +// Scenario 10a: Internal type used as initializer parameter, init called from another file +// If InitParamTypeA were fileprivate: +// "Initializer cannot be declared internal because its parameter uses a fileprivate type" +internal struct InitParamTypeA { + var setting: Bool = false +} + +class InitializerExposureContainer { + let config: InitParamTypeA + + // This initializer is called from TransitiveAccessExposure_Consumer.swift + init(config: InitParamTypeA) { + self.config = config + } +} + +// MARK: - 11. Closure Type Exposure + +// Scenario 11a: Internal type used in closure parameter/return, closure accessed from another file +// If ClosureParamTypeA were fileprivate: +// "Property cannot be declared internal because its type uses a fileprivate type" +internal struct ClosureParamTypeA { + var input: Int = 0 +} + +internal struct ClosureReturnTypeA { + var output: Int = 0 +} + +class ClosureTypeExposureContainer { + // This closure property is accessed from TransitiveAccessExposure_Consumer.swift + var transformer: (ClosureParamTypeA) -> ClosureReturnTypeA = { param in + ClosureReturnTypeA(output: param.input * 2) + } +} + +// MARK: - Retainer class to ensure all code is exercised + +public class TransitiveAccessExposureRetainer { + public init() {} + + public func retain() { + // 1. Parameter exposure + _ = ParameterExposureContainer() + + // 2. Return type exposure + _ = ReturnTypeExposureContainer() + + // 3. Property type exposure + _ = PropertyTypeExposureContainer() + + // 4. Generic constraint exposure + _ = GenericConstraintExposureContainer() + + // 5. Protocol requirement exposure + _ = ProtocolRequirementExposureContainer() + + // 6. Enum associated value exposure + _ = EnumAssociatedValueExposureContainer() + + // 7. Typealias exposure + _ = TypealiasExposureContainer() + + // 8. Subscript exposure + _ = SubscriptExposureContainer() + _ = SubscriptReturnTypeExposureContainer() + + // 9. Default argument exposure + _ = DefaultArgumentExposureContainer() + + // 10. Initializer exposure + _ = InitializerExposureContainer(config: InitParamTypeA()) + + // 11. Closure type exposure + _ = ClosureTypeExposureContainer() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure_Consumer.swift new file mode 100644 index 0000000000..ca2c400f69 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/TransitiveAccessExposure_Consumer.swift @@ -0,0 +1,94 @@ +// TransitiveAccessExposure_Consumer.swift +// Consumer file that references declarations from TransitiveAccessExposure.swift +// This creates cross-file transitive exposure of the inner types. + +class TransitiveAccessExposureConsumer { + // 1. Parameter exposure - calls function with ParameterTypeA parameter + func consumeParameterExposure() { + let container = ParameterExposureContainer() + container.processParameter(ParameterTypeA(value: 42)) + } + + // 2. Return type exposure - calls function returning ReturnTypeA + func consumeReturnTypeExposure() { + let container = ReturnTypeExposureContainer() + let result = container.getResult() + switch result { + case .success: break + case .failure: break + } + } + + // 3. Property type exposure - accesses property of PropertyTypeA type + func consumePropertyTypeExposure() { + let container = PropertyTypeExposureContainer() + _ = container.exposedProperty + } + + // 4. Generic constraint exposure - calls generic function constrained by GenericConstraintProtocolA + func consumeGenericConstraintExposure() { + let container = GenericConstraintExposureContainer() + let item = GenericConstraintConformingTypeA() + _ = container.processGeneric(item) + } + + // 5. Protocol requirement exposure - uses conforming type + func consumeProtocolRequirementExposure() { + let container = ProtocolRequirementExposureContainer() + container.process(input: ProtocolRequirementTypeA(payload: 100)) + } + + // 6. Enum associated value exposure - uses enum with associated type + func consumeEnumAssociatedValueExposure() { + let container = EnumAssociatedValueExposureContainer() + let value = container.getEnumValue() + switch value { + case .success(let data): _ = data.content + case .failure: break + } + } + + // 7. Typealias exposure - uses typealias property + func consumeTypealiasExposure() { + let container = TypealiasExposureContainer() + _ = container.aliasedProperty + } + + // 8a. Subscript key exposure - uses subscript with SubscriptKeyTypeA + func consumeSubscriptKeyExposure() { + var container = SubscriptExposureContainer() + let key = SubscriptKeyTypeA(key: "test") + container[key] = 10 + _ = container[key] + } + + // 8b. Subscript return type exposure - uses subscript returning SubscriptReturnTypeA + func consumeSubscriptReturnTypeExposure() { + let container = SubscriptReturnTypeExposureContainer() + let item: SubscriptReturnTypeA = container[0] + _ = item.data + } + + // 9. Default argument exposure - calls function with default argument + func consumeDefaultArgumentExposure() { + let container = DefaultArgumentExposureContainer() + // Calling with default argument + container.processWithDefault() + // Calling with explicit argument + container.processWithDefault(config: DefaultArgTypeA(config: "custom")) + } + + // 10. Initializer exposure - calls initializer with InitParamTypeA + func consumeInitializerExposure() { + let config = InitParamTypeA(setting: true) + _ = InitializerExposureContainer(config: config) + } + + // 11. Closure type exposure - uses closure with exposed types + func consumeClosureTypeExposure() { + let container = ClosureTypeExposureContainer() + let input = ClosureParamTypeA(input: 5) + let output = container.transformer(input) + _ = output.output + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 0a6393ccbc..ed32c419a0 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -246,4 +246,92 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { // The protocol requirement implementation should NOT be flagged (line 15 in fixture) assertNotRedundantInternalAccessibility(.functionMethodInstance("someExternalProtocolMethod()", line: 15)) } + + /// Tests that top-level internal types used only within the same file are flagged + /// as redundant internal. + /// + /// For top-level declarations, private and fileprivate are equivalent, so the + /// suggested accessibility is nil (ambiguous). Nested types are suppressed when + /// their parent is already flagged to reduce noise. + func testInternalTypeTransitivelyExposedInSameFileSuggestsFileprivate() { + index() + + // TransitiveExposureClassA is only used within its file (from ClassB), so it + // should be flagged as redundant internal. Since it's top-level, the suggestion + // is nil (private and fileprivate are equivalent for top-level declarations). + assertRedundantInternalAccessibility(.class("TransitiveExposureClassA")) + + // TransitivelyExposedStatus is suppressed because its parent (ClassA) is already + // flagged. This is by design to reduce noise - fixing the parent is sufficient. + } + + // MARK: - Transitive Access Exposure Tests + + // These tests verify that Periphery does NOT incorrectly flag internal types + // that are transitively exposed through API signatures when those APIs are + // called from other files. See TransitiveAccessExposure.swift for fixtures. + + /// Tests that internal types used in function/method signatures are NOT flagged + /// when the function is called from another file. + /// + /// Covers: parameter types, return types, default argument types, initializer parameters, + /// and closure types in properties. + func testTransitiveExposureThroughFunctionSignatures() { + index() + + // Parameter types: ParameterTypeA used in processParameter() + assertNotRedundantInternalAccessibility(.struct("ParameterTypeA")) + + // Return types: ReturnTypeA returned by getResult(), InternalReturnTypeEnum returned by getEnum() + assertNotRedundantInternalAccessibility(.enum("ReturnTypeA")) + assertNotRedundantInternalAccessibility(.enum("InternalReturnTypeEnum")) + + // Default argument types: DefaultArgTypeA used in processWithDefault() + assertNotRedundantInternalAccessibility(.struct("DefaultArgTypeA")) + + // Initializer parameters: InitParamTypeA used in init(config:) + assertNotRedundantInternalAccessibility(.struct("InitParamTypeA")) + + // Closure types: ClosureParamTypeA/ClosureReturnTypeA in transformer property + assertNotRedundantInternalAccessibility(.struct("ClosureParamTypeA")) + assertNotRedundantInternalAccessibility(.struct("ClosureReturnTypeA")) + } + + /// Tests that internal types used in property and subscript signatures are NOT flagged + /// when accessed from another file. + /// + /// Covers: property types, subscript parameter types, subscript return types. + func testTransitiveExposureThroughPropertyAndSubscriptSignatures() { + index() + + // Property types: PropertyTypeA used in exposedProperty + assertNotRedundantInternalAccessibility(.struct("PropertyTypeA")) + + // Subscript parameter types: SubscriptKeyTypeA used in subscript(key:) + assertNotRedundantInternalAccessibility(.struct("SubscriptKeyTypeA")) + + // Subscript return types: SubscriptReturnTypeA returned by subscript + assertNotRedundantInternalAccessibility(.struct("SubscriptReturnTypeA")) + } + + /// Tests that internal types used in generic constraints, protocol requirements, + /// enum associated values, and typealiases are NOT flagged when exposed from another file. + /// + /// Covers: generic constraint protocols, protocol requirement types, enum associated + /// value types, typealias target types. + func testTransitiveExposureThroughTypeSystemConstructs() { + index() + + // Generic constraints: GenericConstraintProtocolA constrains processGeneric() + assertNotRedundantInternalAccessibility(.protocol("GenericConstraintProtocolA")) + + // Protocol requirement types: ProtocolRequirementTypeA in ProtocolWithRequirementA + assertNotRedundantInternalAccessibility(.struct("ProtocolRequirementTypeA")) + + // Enum associated values: EnumAssociatedTypeA in EnumWithAssociatedValueA + assertNotRedundantInternalAccessibility(.struct("EnumAssociatedTypeA")) + + // Typealias targets: TypealiasTargetTypeA aliased by AliasedTypeA + assertNotRedundantInternalAccessibility(.struct("TypealiasTargetTypeA")) + } } From 1e8a649a828326729ba7bcab114da74af354c304 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:12:08 -0800 Subject: [PATCH 22/31] =?UTF-8?q?don=E2=80=99t=20mark=20enum=20cases=20as?= =?UTF-8?q?=20needing=20to=20be=20private;=20fix=20fileprivate=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: periphery ought to identify enum cases that are unused --- ...RedundantInternalAccessibilityMarker.swift | 175 +++++++++++++++--- .../TargetA/EnumCaseAccessibility.swift | 32 ++++ .../StructMemberwiseInitAccessibility.swift | 53 ++++++ .../RedundantInternalAccessibilityTest.swift | 36 ++++ 4 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/EnumCaseAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StructMemberwiseInitAccessibility.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 449d43e98e..000a361b3f 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -78,15 +78,25 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } // Determine the suggested accessibility level. - // For top-level declarations, fileprivate is equivalent to private, so we pass nil - // to indicate the ambiguity in the output message. // If the declaration is referenced from different types in the same file, // it needs fileprivate. Otherwise, private is sufficient. // Also check transitive exposure: if the type is used as return/parameter type of a // function called from a different type in the same file, it needs fileprivate. + // Additionally, types used in protocol requirement signatures need fileprivate even + // at top level (private would make them inaccessible from the protocol method). let isTopLevel = decl.parent == nil - let needsFileprivate = isReferencedFromDifferentTypeInSameFile(decl) || isTransitivelyExposedFromDifferentTypeInSameFile(decl) - let suggestedAccessibility: Accessibility? = isTopLevel ? nil : (needsFileprivate ? .fileprivate : .private) + let needsFileprivate = isReferencedFromDifferentTypeInSameFile(decl) || + isTransitivelyExposedFromDifferentTypeInSameFile(decl) || + isUsedInProtocolRequirementSignature(decl) + + // For top-level declarations where private and fileprivate would both work, + // we pass nil to indicate the ambiguity. But if fileprivate is specifically needed + // (e.g., the type is used in a protocol requirement signature), we suggest fileprivate. + let suggestedAccessibility: Accessibility? = if isTopLevel, !needsFileprivate { + nil + } else { + needsFileprivate ? .fileprivate : .private + } // Check if the parent's accessibility already constrains this member. // If the parent is `private`, the member is already effectively `private`. @@ -130,6 +140,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// - They should be skipped from all accessibility analysis (generic type params, implicit decls) /// - They are protocol requirements (must maintain accessibility for protocol conformance) /// - They are part of a property wrapper's API (must be accessible to wrapper users) + /// - They are struct stored properties used in an implicit memberwise initializer private func shouldSkipMarking(_ decl: Declaration) -> Bool { if shouldSkipAccessibilityAnalysis(for: decl) { return true @@ -143,6 +154,10 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } + if isStructMemberwiseInitProperty(decl) { + return true + } + return false } @@ -168,6 +183,10 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Deinitializers cannot have explicit access modifiers in Swift. if decl.kind == .functionDestructor { return true } + // Enum cases cannot have explicit access modifiers in Swift. + // They inherit the accessibility of their containing enum. + if decl.kind == .enumelement { return true } + // Override methods must be at least as accessible as what they override. if decl.isOverride { return true } @@ -270,6 +289,46 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return false } + /// Checks if a declaration is a stored property that's part of a struct's implicit memberwise + /// initializer AND that initializer is used from outside the file. + /// + /// Struct stored properties that are parameters to the implicit memberwise initializer + /// must maintain sufficient accessibility for that initializer to work when called from + /// other files. If the memberwise init is only used within the same file, the properties + /// can still be marked as redundantly internal with a suggestion of fileprivate. + private func isStructMemberwiseInitProperty(_ decl: Declaration) -> Bool { + guard decl.kind == .varInstance, + let parent = decl.parent, + parent.kind == .struct + else { return false } + + // Check if the struct has an implicit memberwise initializer that includes this property. + let implicitInits = parent.declarations.filter { $0.kind == .functionConstructor && $0.isImplicit } + + for implicitInit in implicitInits { + guard let initName = implicitInit.name, + let propertyName = decl.name + else { continue } + + // Parse the init parameter names from the init signature (e.g., "init(foo:bar:)") + let parameterNames = initName + .dropFirst("init(".count) + .dropLast(")".count) + .split(separator: ":") + .map(String.init) + + // Only skip if this property is part of the memberwise init AND + // the init is used from outside the file. + if parameterNames.contains(propertyName), + implicitInit.isReferencedOutsideFile(graph: graph) + { + return true + } + } + + return false + } + /// Determines the effective maximum accessibility a member can have based on its parent's accessibility. /// /// In Swift, a member's effective accessibility is constrained by its parent. This helper @@ -296,24 +355,29 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// /// For internal accessibility analysis, this determines whether to suggest `fileprivate` /// versus `private` when a declaration is only used within its file. + /// + /// This uses the **immediate containing type** (not the top-level type) because: + /// - For nested types like `OuterStruct.InnerStruct`, a member of `InnerStruct` that's + /// accessed from code inside `OuterStruct` (but outside `InnerStruct`) needs `fileprivate` + /// - Using top-level type would incorrectly see both as belonging to `OuterStruct` private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { let file = decl.location.file let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } - guard let declTopLevel = topLevelType(of: decl) else { + guard let declContainingType = immediateContainingType(of: decl) else { return false } - let declLogicalType = logicalType(of: declTopLevel, inFile: file) + let declLogicalType = logicalType(of: declContainingType, inFile: file) for ref in sameFileReferences { guard let refParent = ref.parent, - let refTopLevel = topLevelType(of: refParent) + let refContainingType = immediateContainingType(of: refParent) else { continue } - let refLogicalType = logicalType(of: refTopLevel, inFile: file) + let refLogicalType = logicalType(of: refContainingType, inFile: file) if declLogicalType !== refLogicalType { return true @@ -322,22 +386,40 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return false } - // Finds the top-level type declaration by walking up the parent chain. - private func topLevelType(of decl: Declaration) -> Declaration? { + /// Finds the immediate containing type of a declaration. + /// + /// For members (properties, methods, etc.), this returns their containing type. + /// For nested types, this returns the type that contains them (the outer type). + /// For top-level types, this returns the type itself (they are their own container). + private func immediateContainingType(of decl: Declaration) -> Declaration? { let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) - let ancestors = [decl] + Array(decl.ancestralDeclarations) - return ancestors.last { typeDecl in - guard typeKinds.contains(typeDecl.kind) else { return false } - guard let parent = typeDecl.parent else { return true } - return !typeKinds.contains(parent.kind) + // For types, check if they have a parent type (nested type case). + // If so, return the parent type. If not (top-level), return the type itself. + if typeKinds.contains(decl.kind) { + if let parent = decl.parent, typeKinds.contains(parent.kind) { + return parent + } + return decl } + + // Walk up the parent chain to find the first containing type + var current = decl.parent + while let parent = current { + if typeKinds.contains(parent.kind) { + return parent + } + current = parent.parent + } + + return nil } - // Gets the logical type for comparison purposes when analyzing internal accessibility. - // For extensions of types in the SAME FILE, treats the extension as the extended type. - // For extensions of types in DIFFERENT FILES, treats the extension as its own distinct type. + /// Gets the logical type for comparison purposes when analyzing internal accessibility. + /// + /// For extensions of types in the SAME FILE, treats the extension as the extended type. + /// For extensions of types in DIFFERENT FILES, treats the extension as its own distinct type. private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { if decl.kind.isExtensionKind { if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), @@ -376,10 +458,17 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } - // For properties, also check if the containing type is used from outside the file. - // When code accesses `obj.property`, the property's type is exposed even if - // `property` itself isn't directly referenced from outside. - if parent.kind.isVariableKind { + // For properties, also check if they could be accessed from outside the file + // through their containing type. The property's type is exposed when: + // 1. The property is internal (accessible from outside the file) + // 2. The containing type is used from outside the file + // 3. The property is not actually referenced from outside (already checked above) + // If the property is already referenced from outside, we would have returned + // true at line 472. This check catches cases where the property COULD be + // accessed but hasn't been yet. + if parent.kind.isVariableKind, + parent.accessibility.value == .internal || parent.accessibility.isAccessibleCrossModule + { if let containingType = parent.parent, containingType.isReferencedOutsideFile(graph: graph) { @@ -413,11 +502,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let file = decl.location.file let refs = graph.references(to: decl) - guard let declTopLevel = topLevelType(of: decl) else { + guard let declContainingType = immediateContainingType(of: decl) else { return false } - let declLogicalType = logicalType(of: declTopLevel, inFile: file) + let declLogicalType = logicalType(of: declContainingType, inFile: file) for ref in refs { // Check if this reference is in an API signature role (return type, parameter type, etc.) @@ -431,12 +520,12 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { for parentRef in parentRefs { guard let refParent = parentRef.parent, - let refTopLevel = topLevelType(of: refParent) + let refContainingType = immediateContainingType(of: refParent) else { continue } - let refLogicalType = logicalType(of: refTopLevel, inFile: file) + let refLogicalType = logicalType(of: refContainingType, inFile: file) if declLogicalType !== refLogicalType { return true @@ -446,4 +535,38 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return false } + + /// Checks if a type is used in a protocol requirement's signature (return type, parameter type, etc.). + /// + /// When a type is used as the return type or parameter type of a protocol requirement method, + /// the type must be at least `fileprivate` - making it `private` would cause a compiler error + /// because the protocol method's signature would expose an inaccessible type. + /// + /// For example, in NSViewRepresentable: + /// ```swift + /// class FocusableNSView: NSView { ... } // Used as return type of makeNSView + /// + /// struct FocusClaimingView: NSViewRepresentable { + /// func makeNSView(context: Context) -> FocusableNSView { ... } + /// } + /// ``` + /// Here, `FocusableNSView` cannot be `private` because it's exposed through `makeNSView`'s signature. + private func isUsedInProtocolRequirementSignature(_ decl: Declaration) -> Bool { + let refs = graph.references(to: decl) + + for ref in refs { + // Check if this reference is in an API signature role (return type, parameter type, etc.) + guard ref.role.isPubliclyExposable else { continue } + + // Get the parent declaration (the function/property that uses this type in its signature) + guard let parent = ref.parent else { continue } + + // Check if that parent is a protocol requirement + if isProtocolRequirement(parent) { + return true + } + } + + return false + } } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/EnumCaseAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/EnumCaseAccessibility.swift new file mode 100644 index 0000000000..588d71a105 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/EnumCaseAccessibility.swift @@ -0,0 +1,32 @@ +/* + EnumCaseAccessibility.swift + Tests that enum cases are NOT flagged as redundant internal. + + Enum cases cannot have explicit access modifiers in Swift - they inherit + the accessibility of their containing enum. Suggesting to make them private + or fileprivate would cause a syntax error. +*/ + +// Internal enum with cases only used in this file - should NOT flag the cases. +internal enum InternalEnumWithCasesUsedOnlyInSameFile { + case usedCase + case anotherUsedCase +} + +// Public enum with internal cases - cases should NOT be flagged. +public enum PublicEnumWithInternalCases { + case internalCase + case anotherInternalCase +} + +// Usage within the file to exercise the enum cases. +public class EnumCaseAccessibilityRetainer { + public init() {} + + public func retain() { + _ = InternalEnumWithCasesUsedOnlyInSameFile.usedCase + _ = InternalEnumWithCasesUsedOnlyInSameFile.anotherUsedCase + _ = PublicEnumWithInternalCases.internalCase + _ = PublicEnumWithInternalCases.anotherInternalCase + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StructMemberwiseInitAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StructMemberwiseInitAccessibility.swift new file mode 100644 index 0000000000..4d2ed9fe74 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StructMemberwiseInitAccessibility.swift @@ -0,0 +1,53 @@ +/* + StructMemberwiseInitAccessibility.swift + Tests that struct stored properties used in implicit memberwise initializers + are NOT flagged as redundant internal. + + When a struct relies on its implicit memberwise initializer, the stored + properties that are parameters to that initializer are part of the struct's + public API and must maintain their accessibility. +*/ + +// Struct with internal stored properties used in memberwise init. +// These properties should NOT be flagged as redundant internal. +public struct StructWithMemberwiseInit { + internal var memberwiseProperty1: String + internal var memberwiseProperty2: Int + internal let memberwiseConstant: Bool + + // No explicit init - relies on implicit memberwise init +} + +// Struct with mixed properties - some in memberwise init, some not. +public struct StructWithMixedProperties { + internal var memberwiseProperty: String + + // Computed property - not part of memberwise init + internal var computedProperty: String { memberwiseProperty.uppercased() } + + // Property with default value - still part of memberwise init + internal var propertyWithDefault: Int = 42 + + // No explicit init - relies on implicit memberwise init +} + +// Usage within the file to exercise the structs. +public class StructMemberwiseInitAccessibilityRetainer { + public init() {} + + public func retain() { + // Using memberwise initializer + let s1 = StructWithMemberwiseInit( + memberwiseProperty1: "hello", + memberwiseProperty2: 42, + memberwiseConstant: true + ) + _ = s1 + + let s2 = StructWithMixedProperties( + memberwiseProperty: "world", + propertyWithDefault: 100 + ) + _ = s2.computedProperty + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index ed32c419a0..a7c163d85f 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -334,4 +334,40 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { // Typealias targets: TypealiasTargetTypeA aliased by AliasedTypeA assertNotRedundantInternalAccessibility(.struct("TypealiasTargetTypeA")) } + + // MARK: - Enum Case Tests + + /// Tests that enum cases are NOT flagged as redundant internal. + /// + /// Enum cases cannot have explicit access modifiers in Swift - they always + /// inherit the accessibility of their containing enum. Suggesting to make + /// them private or fileprivate would cause a syntax error. + func testEnumCasesNotFlagged() { + index() + + // Enum cases should never be flagged + assertNotRedundantInternalAccessibility(.enumelement("usedCase")) + assertNotRedundantInternalAccessibility(.enumelement("anotherUsedCase")) + assertNotRedundantInternalAccessibility(.enumelement("internalCase")) + assertNotRedundantInternalAccessibility(.enumelement("anotherInternalCase")) + } + + // MARK: - Struct Memberwise Initializer Tests + + /// Tests that struct stored properties used in implicit memberwise initializers + /// are NOT flagged as redundant internal. + /// + /// Stored properties that are parameters to a struct's memberwise initializer + /// are part of the struct's public API. Even if they're only directly accessed + /// within the same file, they must remain accessible for the initializer. + func testStructMemberwiseInitPropertiesNotFlagged() { + index() + + // Properties used in memberwise init should not be flagged + assertNotRedundantInternalAccessibility(.varInstance("memberwiseProperty1")) + assertNotRedundantInternalAccessibility(.varInstance("memberwiseProperty2")) + assertNotRedundantInternalAccessibility(.varInstance("memberwiseConstant")) + assertNotRedundantInternalAccessibility(.varInstance("memberwiseProperty")) + assertNotRedundantInternalAccessibility(.varInstance("propertyWithDefault")) + } } From cb07b03f0c08203b65fc31c47f098b4628062f0a Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:29:49 -0800 Subject: [PATCH 23/31] Fix checking against an external protocol --- ...RedundantInternalAccessibilityMarker.swift | 58 +++++++++++++++++++ .../ExternalProtocolSignatureType.swift | 38 ++++++++++++ .../RedundantInternalAccessibilityTest.swift | 17 ++++++ 3 files changed, 113 insertions(+) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolSignatureType.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 000a361b3f..bf1414ce07 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -158,6 +158,10 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } + if isUsedInExternalProtocolRequirementSignature(decl) { + return true + } + return false } @@ -569,4 +573,58 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return false } + + /// Checks if a type is used in the signature of a method that conforms to an external protocol. + /// + /// Types used in external protocol requirement signatures must remain `internal` because: + /// 1. The method implementing the protocol requirement can't be more restrictive than the protocol + /// 2. The types used in its signature must be at least as accessible as the method + /// + /// For example, with NSViewRepresentable: + /// ```swift + /// class FocusableNSView: NSView { ... } // Must stay internal! + /// + /// struct FocusClaimingView: NSViewRepresentable { + /// func makeNSView(context: Context) -> FocusableNSView { ... } // Protocol requirement + /// func updateNSView(_ nsView: FocusableNSView, context: Context) { ... } // Protocol requirement + /// } + /// ``` + /// Here, `FocusableNSView` cannot be made `fileprivate` or `private` because the protocol + /// methods that use it must remain at the protocol's required accessibility level. + private func isUsedInExternalProtocolRequirementSignature(_ decl: Declaration) -> Bool { + let refs = graph.references(to: decl) + + for ref in refs { + // Check if this reference is in an API signature role (return type, parameter type, etc.) + guard ref.role.isPubliclyExposable else { continue } + + // Get the parent declaration (the function/property that uses this type in its signature) + guard let parent = ref.parent else { continue } + + // Check if that parent conforms to an external protocol requirement + if isExternalProtocolRequirement(parent) { + return true + } + } + + return false + } + + /// Checks if a declaration implements an external protocol requirement. + /// + /// External protocols are those defined outside our codebase (e.g., NSViewRepresentable, + /// Codable, etc.). When a declaration implements such a protocol, its accessibility is + /// constrained by the protocol. + private func isExternalProtocolRequirement(_ decl: Declaration) -> Bool { + // Check for .related references FROM this declaration to protocol members + // where the protocol is external (not in our graph). + for ref in decl.related where ref.declarationKind.isProtocolMemberConformingKind { + // If we can't find the declaration in our graph, it's external + if graph.declaration(withUsr: ref.usr) == nil, ref.name == decl.name { + return true + } + } + + return false + } } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolSignatureType.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolSignatureType.swift new file mode 100644 index 0000000000..f6214d34d1 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/ExternalProtocolSignatureType.swift @@ -0,0 +1,38 @@ +/* + ExternalProtocolSignatureType.swift + Tests that types used in external protocol requirement signatures are NOT + flagged as redundant internal. + + When a type is used as the return type or parameter type of a method that + implements an external protocol requirement (like NSViewRepresentable.makeNSView), + the type must remain internal because the protocol method can't be more + restrictive than the protocol requires. + + This mimics patterns like: + - NSViewRepresentable/UIViewRepresentable returning custom NSView/UIView subclasses + - Codable types with custom coding containers +*/ + +import Foundation + +// This type is used as the return type of an external protocol requirement. +// It should NOT be flagged as redundant internal because Equatable.== must +// remain internal, and its parameter types must be at least as accessible. +internal struct TypeUsedInExternalProtocolSignature: Equatable { + internal var value: Int + + // The == function is an external protocol requirement (from Equatable). + // Since this struct conforms to Equatable, the == method's parameter type + // (this struct) must remain internal. +} + +// Usage to retain the type +public class ExternalProtocolSignatureTypeRetainer { + public init() {} + + public func retain() { + let a = TypeUsedInExternalProtocolSignature(value: 1) + let b = TypeUsedInExternalProtocolSignature(value: 2) + _ = a == b + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index a7c163d85f..a8cf790f1e 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -370,4 +370,21 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.varInstance("memberwiseProperty")) assertNotRedundantInternalAccessibility(.varInstance("propertyWithDefault")) } + + // MARK: - External Protocol Requirement Signature Tests + + /// Tests that types used in external protocol requirement signatures are NOT flagged. + /// + /// When a type is used as a return type or parameter type of a method that implements + /// an external protocol (like Equatable, NSViewRepresentable, etc.), the type must + /// remain internal because the protocol method's accessibility is constrained by the + /// protocol, and its signature types must be at least as accessible. + func testTypeUsedInExternalProtocolSignatureNotFlagged() { + index() + + // TypeUsedInExternalProtocolSignature conforms to Equatable, which means + // it's used as the parameter type for the == operator (an external protocol + // requirement). It should NOT be flagged as redundant internal. + assertNotRedundantInternalAccessibility(.struct("TypeUsedInExternalProtocolSignature")) + } } From 7804bfea65dd5c4d32238d8d212fc4bca0476b52 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:57:12 -0800 Subject: [PATCH 24/31] stored property type transitive exposure --- ...RedundantInternalAccessibilityMarker.swift | 74 ++++++++++++++++--- .../TargetA/StoredPropertyTypeExposure.swift | 71 ++++++++++++++++++ .../StoredPropertyTypeExposure_Consumer.swift | 24 ++++++ .../RedundantInternalAccessibilityTest.swift | 41 ++++++++++ 4 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure_Consumer.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index bf1414ce07..284cb0f454 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -447,7 +447,20 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// For example, if `ScanProgress` is the return type of `runFullScanWithStreaming()`, and /// that function is called from another file, then `ScanProgress` is transitively exposed /// and should not be marked as redundantly internal. + /// + /// This check is recursive: if TypeA is used as a property type in Container, and Container + /// is used as a property type in OuterContainer, and OuterContainer is referenced from + /// outside the file, then TypeA is transitively exposed through the chain. private func isTransitivelyExposedOutsideFile(_ decl: Declaration) -> Bool { + var visited: Set = [] + return isTransitivelyExposedOutsideFileRecursive(decl, visited: &visited) + } + + private func isTransitivelyExposedOutsideFileRecursive(_ decl: Declaration, visited: inout Set) -> Bool { + let id = ObjectIdentifier(decl) + guard !visited.contains(id) else { return false } + visited.insert(id) + let refs = graph.references(to: decl) for ref in refs { @@ -465,18 +478,20 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // For properties, also check if they could be accessed from outside the file // through their containing type. The property's type is exposed when: // 1. The property is internal (accessible from outside the file) - // 2. The containing type is used from outside the file + // 2. The containing type is used from outside the file OR transitively exposed // 3. The property is not actually referenced from outside (already checked above) - // If the property is already referenced from outside, we would have returned - // true at line 472. This check catches cases where the property COULD be - // accessed but hasn't been yet. if parent.kind.isVariableKind, parent.accessibility.value == .internal || parent.accessibility.isAccessibleCrossModule { - if let containingType = parent.parent, - containingType.isReferencedOutsideFile(graph: graph) - { - return true + if let containingType = parent.parent { + // Direct reference from outside the file + if containingType.isReferencedOutsideFile(graph: graph) { + return true + } + // Recursive: the containing type itself is transitively exposed + if isTransitivelyExposedOutsideFileRecursive(containingType, visited: &visited) { + return true + } } } } @@ -502,15 +517,39 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// } /// ``` /// Here, `Status` should be suggested as `fileprivate`, not `private`. + /// + /// This check is recursive: if TypeA is used as a property type in Container, and Container + /// is used as a property type in another type that is accessed from a different type in + /// the same file, then TypeA needs fileprivate. private func isTransitivelyExposedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { let file = decl.location.file - let refs = graph.references(to: decl) guard let declContainingType = immediateContainingType(of: decl) else { return false } let declLogicalType = logicalType(of: declContainingType, inFile: file) + var visited: Set = [] + + return isTransitivelyExposedFromDifferentTypeInSameFileRecursive( + decl, + declLogicalType: declLogicalType, + file: file, + visited: &visited + ) + } + + private func isTransitivelyExposedFromDifferentTypeInSameFileRecursive( + _ decl: Declaration, + declLogicalType: Declaration?, + file: SourceFile, + visited: inout Set + ) -> Bool { + let id = ObjectIdentifier(decl) + guard !visited.contains(id) else { return false } + visited.insert(id) + + let refs = graph.references(to: decl) for ref in refs { // Check if this reference is in an API signature role (return type, parameter type, etc.) @@ -535,6 +574,23 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } } + + // For properties, also check if the containing type is transitively exposed + // from a different type in the same file + if parent.kind.isVariableKind, + parent.accessibility.value == .internal || parent.accessibility.isAccessibleCrossModule + { + if let containingType = parent.parent { + if isTransitivelyExposedFromDifferentTypeInSameFileRecursive( + containingType, + declLogicalType: declLogicalType, + file: file, + visited: &visited + ) { + return true + } + } + } } return false diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure.swift new file mode 100644 index 0000000000..4b8add0e70 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure.swift @@ -0,0 +1,71 @@ +// StoredPropertyTypeExposure.swift +// Test cases for types used as stored property types that are transitively exposed. +// +// When a type T is used as a property type in a struct/class C, and C is instantiated +// from another file, T is transitively exposed and should NOT be flagged as redundant internal. + +// MARK: - Simple Property Type Exposure + +// StoredPropertyRole is used as the property type in StoredPropertyContainer. +// StoredPropertyContainer is instantiated from StoredPropertyTypeExposure_Consumer.swift. +// Therefore StoredPropertyRole is transitively exposed and should NOT be flagged. +internal enum StoredPropertyRole { + case primary + case secondary +} + +internal struct StoredPropertyContainer { + let role: StoredPropertyRole +} + +// MARK: - Nested Type as Property Type + +// NestedPhase is a nested enum used as a property type in its containing class. +// ClassWithNestedType is instantiated from StoredPropertyTypeExposure_Consumer.swift. +// Therefore NestedPhase is transitively exposed and should NOT be flagged. +internal class ClassWithNestedType { + internal enum NestedPhase { + case idle + case running + case completed + } + + var phase: NestedPhase = .idle + + func advance() { + switch phase { + case .idle: phase = .running + case .running: phase = .completed + case .completed: break + } + } +} + +// MARK: - Chained Property Type Exposure + +// InnerType is used in MiddleContainer, which is used in OuterContainer. +// OuterContainer is instantiated from StoredPropertyTypeExposure_Consumer.swift. +// Therefore both InnerType and MiddleContainer are transitively exposed. +internal struct InnerType { + var value: Int = 0 +} + +internal struct MiddleContainer { + var inner: InnerType +} + +internal struct OuterContainer { + var middle: MiddleContainer +} + +// MARK: - Retainer class to ensure all code is exercised + +public class StoredPropertyTypeExposureRetainer { + public init() {} + + public func use() { + _ = StoredPropertyContainer(role: .primary) + _ = ClassWithNestedType() + _ = OuterContainer(middle: MiddleContainer(inner: InnerType(value: 42))) + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure_Consumer.swift new file mode 100644 index 0000000000..9b18f49206 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/StoredPropertyTypeExposure_Consumer.swift @@ -0,0 +1,24 @@ +// StoredPropertyTypeExposure_Consumer.swift +// Consumer file that references types from StoredPropertyTypeExposure.swift, +// creating cross-file transitive exposure of the property types. + +class StoredPropertyTypeExposureConsumer { + // Uses StoredPropertyContainer, which transitively exposes StoredPropertyRole + func consumeSimplePropertyType() { + let container = StoredPropertyContainer(role: .primary) + _ = container.role + } + + // Uses ClassWithNestedType, which transitively exposes NestedPhase + func consumeNestedType() { + let obj = ClassWithNestedType() + obj.advance() + _ = obj.phase + } + + // Uses OuterContainer, which transitively exposes MiddleContainer and InnerType + func consumeChainedPropertyTypes() { + let outer = OuterContainer(middle: MiddleContainer(inner: InnerType(value: 100))) + _ = outer.middle.inner.value + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index a8cf790f1e..10f967bd9f 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -387,4 +387,45 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { // requirement). It should NOT be flagged as redundant internal. assertNotRedundantInternalAccessibility(.struct("TypeUsedInExternalProtocolSignature")) } + + // MARK: - Stored Property Type Transitive Exposure Tests + + /// Tests that types used as stored property types are NOT flagged when the containing + /// type is instantiated from another file. + /// + /// When a type T is used as a property type in struct/class C, and C is used from + /// outside the file, T is transitively exposed and must remain internal. + func testStoredPropertyTypeNotFlagged() { + index() + + // StoredPropertyRole is used as the type of StoredPropertyContainer.role + // StoredPropertyContainer is instantiated from StoredPropertyTypeExposure_Consumer.swift + assertNotRedundantInternalAccessibility(.enum("StoredPropertyRole")) + assertNotRedundantInternalAccessibility(.struct("StoredPropertyContainer")) + } + + /// Tests that nested types used as property types are NOT flagged when the containing + /// type is instantiated from another file. + func testNestedTypeAsPropertyTypeNotFlagged() { + index() + + // NestedPhase is used as the type of ClassWithNestedType.phase + // ClassWithNestedType is instantiated from StoredPropertyTypeExposure_Consumer.swift + assertNotRedundantInternalAccessibility(.enum("NestedPhase")) + assertNotRedundantInternalAccessibility(.class("ClassWithNestedType")) + } + + /// Tests that types in a chain of property types are NOT flagged when the outermost + /// type is instantiated from another file. + /// + /// This tests the recursive transitive exposure check: InnerType -> MiddleContainer -> OuterContainer + func testChainedPropertyTypeExposureNotFlagged() { + index() + + // InnerType is used in MiddleContainer, which is used in OuterContainer + // OuterContainer is instantiated from StoredPropertyTypeExposure_Consumer.swift + assertNotRedundantInternalAccessibility(.struct("InnerType")) + assertNotRedundantInternalAccessibility(.struct("MiddleContainer")) + assertNotRedundantInternalAccessibility(.struct("OuterContainer")) + } } From a3069658dd4894d74a78397377a6540d7e607893 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:18:17 -0800 Subject: [PATCH 25/31] Fix more problems with same file --- ...RedundantInternalAccessibilityMarker.swift | 118 ++++++++++++++++-- .../TargetA/SameFileMemberwiseInit.swift | 20 +++ .../SameFileMemberwiseInit_Consumer.swift | 6 + .../TargetA/SameFileTypeConstraint.swift | 59 +++++++++ .../SameFileTypeConstraint_Consumer.swift | 15 +++ .../RedundantInternalAccessibilityTest.swift | 60 +++++++++ 6 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit_Consumer.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint_Consumer.swift diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 284cb0f454..9078aa62db 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -162,6 +162,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } + // Check if type is constrained by same-file type usage + if isConstrainedBySameFileTypeUsage(decl) { + return true + } + return false } @@ -294,19 +299,14 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } /// Checks if a declaration is a stored property that's part of a struct's implicit memberwise - /// initializer AND that initializer is used from outside the file. - /// - /// Struct stored properties that are parameters to the implicit memberwise initializer - /// must maintain sufficient accessibility for that initializer to work when called from - /// other files. If the memberwise init is only used within the same file, the properties - /// can still be marked as redundantly internal with a suggestion of fileprivate. + /// initializer AND that initializer is used (either from outside the file OR from within + /// the same file when the struct must remain internal). private func isStructMemberwiseInitProperty(_ decl: Declaration) -> Bool { guard decl.kind == .varInstance, let parent = decl.parent, parent.kind == .struct else { return false } - // Check if the struct has an implicit memberwise initializer that includes this property. let implicitInits = parent.declarations.filter { $0.kind == .functionConstructor && $0.isImplicit } for implicitInit in implicitInits { @@ -314,18 +314,110 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let propertyName = decl.name else { continue } - // Parse the init parameter names from the init signature (e.g., "init(foo:bar:)") let parameterNames = initName .dropFirst("init(".count) .dropLast(")".count) .split(separator: ":") .map(String.init) - // Only skip if this property is part of the memberwise init AND - // the init is used from outside the file. - if parameterNames.contains(propertyName), - implicitInit.isReferencedOutsideFile(graph: graph) + guard parameterNames.contains(propertyName) else { continue } + + // Case 1: Init referenced outside file -> properties must stay internal + if implicitInit.isReferencedOutsideFile(graph: graph) { + return true + } + + // Case 2: Init referenced in same file AND struct must remain internal + // (because it's used outside file or transitively exposed) + let hasAnyReference = !graph.references(to: implicitInit).isEmpty + if hasAnyReference { + if parent.isReferencedOutsideFile(graph: graph) { + return true + } + if isTransitivelyExposedOutsideFile(parent) { + return true + } + } + } + + return false + } + + /// Checks if a type is constrained by being used in the signature of another + /// internal declaration (in the same file) that must remain internal. + /// + /// In Swift, types used in a declaration's signature must be at least as accessible + /// as that declaration. If TypeA is used as a property type, return type, parameter + /// type, or generic constraint in TypeB/MemberB, and TypeB must remain internal + /// (because it's referenced outside the file or transitively exposed), then TypeA + /// cannot be made fileprivate/private. + /// + /// This complements isTransitivelyExposedOutsideFile() which handles cross-file + /// scenarios. This function handles same-file scenarios where the constraint chain + /// exists entirely within one file. + private func isConstrainedBySameFileTypeUsage(_ decl: Declaration) -> Bool { + let typeKinds: Set = [.enum, .struct, .class, .protocol] + guard typeKinds.contains(decl.kind) else { return false } + + var visited: Set = [] + return isConstrainedBySameFileTypeUsageRecursive(decl, visited: &visited) + } + + private func isConstrainedBySameFileTypeUsageRecursive( + _ decl: Declaration, + visited: inout Set + ) -> Bool { + let id = ObjectIdentifier(decl) + guard !visited.contains(id) else { return false } + + visited.insert(id) + + let typeKinds: Set = [.enum, .struct, .class, .protocol] + let file = decl.location.file + let refs = graph.references(to: decl) + + for ref in refs { + // Check if this reference is in ANY publicly exposable role + // (property type, return type, parameter type, generic constraint, etc.) + guard ref.role.isPubliclyExposable else { continue } + + // Must be in the same file (cross-file is handled by isTransitivelyExposedOutsideFile) + guard ref.location.file == file else { continue } + + // Get the declaration that uses this type in its signature + guard let usingDecl = ref.parent else { continue } + + // Find the containing type of that declaration + let containingType: Declaration? + if typeKinds.contains(usingDecl.kind) || usingDecl.kind.isExtensionKind { + // The using declaration IS a type (e.g., conformedType, inheritedType) + containingType = usingDecl + } else if let parent = usingDecl.parent, + typeKinds.contains(parent.kind) || parent.kind.isExtensionKind { + // The using declaration is a member of a type + containingType = parent + } else { + continue + } + + guard let containingType else { continue } + + // Check if the containing type must remain internal + guard containingType.accessibility.value == .internal else { continue } + + // If the containing type is referenced outside the file, our type is constrained + if containingType.isReferencedOutsideFile(graph: graph) { + return true + } + + // If the containing type is transitively exposed outside the file + if isTransitivelyExposedOutsideFile(containingType) { + return true + } + + // Recursive: if the containing type is itself constrained + if isConstrainedBySameFileTypeUsageRecursive(containingType, visited: &visited) { return true } } @@ -459,6 +551,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private func isTransitivelyExposedOutsideFileRecursive(_ decl: Declaration, visited: inout Set) -> Bool { let id = ObjectIdentifier(decl) guard !visited.contains(id) else { return false } + visited.insert(id) let refs = graph.references(to: decl) @@ -547,6 +640,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { ) -> Bool { let id = ObjectIdentifier(decl) guard !visited.contains(id) else { return false } + visited.insert(id) let refs = graph.references(to: decl) diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit.swift new file mode 100644 index 0000000000..bd2107cf2d --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit.swift @@ -0,0 +1,20 @@ +// Tests that struct memberwise init properties are NOT flagged when the init +// is used within the same file AND the struct is part of a type hierarchy +// that must remain internal. + +internal struct SameFileMemberwiseStruct { + let field1: String + let field2: Int +} + +internal struct SameFileOuterStruct { + let inner: SameFileMemberwiseStruct +} + +public class SameFileMemberwiseInitRetainer { + public init() {} + public func use() { + let inner = SameFileMemberwiseStruct(field1: "test", field2: 42) + _ = SameFileOuterStruct(inner: inner) + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit_Consumer.swift new file mode 100644 index 0000000000..44b018e85b --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileMemberwiseInit_Consumer.swift @@ -0,0 +1,6 @@ +class SameFileMemberwiseInitConsumer { + func consume() { + let inner = SameFileMemberwiseStruct(field1: "test", field2: 42) + _ = SameFileOuterStruct(inner: inner) + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint.swift new file mode 100644 index 0000000000..edd864004c --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint.swift @@ -0,0 +1,59 @@ +// Tests that types used in signatures of other internal types are NOT flagged +// when the containing type must remain internal. + +// MARK: - Property Type Constraint + +internal enum SameFileConstrainedEnum { + case one + case two +} + +internal struct SameFileConstrainingStruct { + let enumValue: SameFileConstrainedEnum +} + +// MARK: - Return Type Constraint + +internal struct SameFileReturnType { + var value: Int = 0 +} + +internal class SameFileClassWithReturnType { + func getReturnType() -> SameFileReturnType { + SameFileReturnType(value: 42) + } +} + +// MARK: - Parameter Type Constraint + +internal struct SameFileParamType { + var data: String = "" +} + +internal class SameFileClassWithParamType { + func process(_ param: SameFileParamType) { + _ = param.data + } +} + +// MARK: - Generic Constraint + +internal protocol SameFileConstraintProtocol { + var id: String { get } +} + +internal class SameFileClassWithGenericConstraint { + func process(_ item: T) -> String { + item.id + } +} + +public class SameFileTypeConstraintRetainer { + public init() {} + public func use() { + _ = SameFileConstrainingStruct(enumValue: .one) + _ = SameFileClassWithReturnType() + _ = SameFileClassWithParamType() + _ = SameFileClassWithGenericConstraint() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint_Consumer.swift new file mode 100644 index 0000000000..b09551f67e --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/SameFileTypeConstraint_Consumer.swift @@ -0,0 +1,15 @@ +class SameFileTypeConstraintConsumer { + func consumePropertyType() { + _ = SameFileConstrainingStruct(enumValue: .one) + } + + func consumeReturnType() { + let obj = SameFileClassWithReturnType() + _ = obj.getReturnType() + } + + func consumeParamType() { + let obj = SameFileClassWithParamType() + obj.process(SameFileParamType(data: "test")) + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 10f967bd9f..5cf6d6712b 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -428,4 +428,64 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.struct("MiddleContainer")) assertNotRedundantInternalAccessibility(.struct("OuterContainer")) } + + // MARK: - Same-File Type Constraint Tests + + /// Tests that types used as property types in internal types are NOT flagged + /// when the containing type must remain internal. + func testSameFilePropertyTypeConstraintNotFlagged() { + index() + + // SameFileConstrainedEnum is used as property type in SameFileConstrainingStruct + // SameFileConstrainingStruct is used from another file + assertNotRedundantInternalAccessibility(.enum("SameFileConstrainedEnum")) + assertNotRedundantInternalAccessibility(.struct("SameFileConstrainingStruct")) + } + + /// Tests that types used as return types are NOT flagged when the containing + /// type must remain internal. + func testSameFileReturnTypeConstraintNotFlagged() { + index() + + // SameFileReturnType is returned by SameFileClassWithReturnType.getReturnType() + // SameFileClassWithReturnType is used from another file + assertNotRedundantInternalAccessibility(.struct("SameFileReturnType")) + assertNotRedundantInternalAccessibility(.class("SameFileClassWithReturnType")) + } + + /// Tests that types used as parameter types are NOT flagged when the containing + /// type must remain internal. + func testSameFileParamTypeConstraintNotFlagged() { + index() + + // SameFileParamType is a parameter to SameFileClassWithParamType.process() + // SameFileClassWithParamType is used from another file + assertNotRedundantInternalAccessibility(.struct("SameFileParamType")) + assertNotRedundantInternalAccessibility(.class("SameFileClassWithParamType")) + } + + /// Tests that protocols used as generic constraints are NOT flagged when the + /// containing type must remain internal. + func testSameFileGenericConstraintNotFlagged() { + index() + + // SameFileConstraintProtocol is used as generic constraint in SameFileClassWithGenericConstraint + // SameFileClassWithGenericConstraint is used from another file + assertNotRedundantInternalAccessibility(.protocol("SameFileConstraintProtocol")) + assertNotRedundantInternalAccessibility(.class("SameFileClassWithGenericConstraint")) + } + + // MARK: - Same-File Memberwise Init Tests + + /// Tests that struct memberwise init properties are NOT flagged when the struct + /// is instantiated in the same file AND the struct must remain internal due to + /// being used as a property type in another struct that's used from outside. + func testSameFileMemberwiseInitPropertiesNotFlagged() { + index() + + // SameFileMemberwiseStruct is used as property type in SameFileOuterStruct + // SameFileOuterStruct is used from another file + assertNotRedundantInternalAccessibility(.struct("SameFileMemberwiseStruct")) + assertNotRedundantInternalAccessibility(.struct("SameFileOuterStruct")) + } } From 12b8cc92a782382a218d150ea2ffbc076eb7f492 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:13:15 -0800 Subject: [PATCH 26/31] fix warning we found for newly introduced upstream property --- Sources/Frontend/Commands/ScanCommand.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index cd805b86bc..baf2e31c33 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -76,7 +76,7 @@ struct ScanCommand: ParsableCommand { private var disableUnusedImportAnalysis: Bool = defaultConfiguration.$disableUnusedImportAnalysis.defaultValue @Flag(inversion: .prefixedNo, help: "Report superfluous ignore comments") - var superfluousIgnoreComments: Bool = defaultConfiguration.$superfluousIgnoreComments.defaultValue + private var superfluousIgnoreComments: Bool = defaultConfiguration.$superfluousIgnoreComments.defaultValue @Option(parsing: .upToNextOption, help: "Names of unused imported modules to retain") private var retainUnusedImportedModules: [String] = defaultConfiguration.$retainUnusedImportedModules.defaultValue From ee5700185e92d588a13fdc29fc34c56e49561855 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:40:31 -0800 Subject: [PATCH 27/31] =?UTF-8?q?Don=E2=80=99t=20mark=20#Preview=20blocks?= =?UTF-8?q?=20as=20fileprivate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...RedundantInternalAccessibilityMarker.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 9078aa62db..eafc3c8b43 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -141,6 +141,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// - They are protocol requirements (must maintain accessibility for protocol conformance) /// - They are part of a property wrapper's API (must be accessible to wrapper users) /// - They are struct stored properties used in an implicit memberwise initializer + /// - They are referenced by a `#Preview` macro expansion (when retainSwiftUIPreviews is enabled) private func shouldSkipMarking(_ decl: Declaration) -> Bool { if shouldSkipAccessibilityAnalysis(for: decl) { return true @@ -167,6 +168,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } + // Skip declarations referenced by #Preview macro expansions + if isReferencedByPreviewMacro(decl) { + return true + } + return false } @@ -425,6 +431,26 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return false } + /// Checks if a declaration is referenced by a `#Preview` macro expansion. + /// + /// When `retainSwiftUIPreviews` is enabled, `#Preview` macros are retained. However, the + /// implicit declarations generated by these macros cannot have access modifiers. Types + /// referenced only by `#Preview` must remain `internal` — making them `fileprivate` or + /// `private` would break the preview because the macro expansion needs to access them. + private func isReferencedByPreviewMacro(_ decl: Declaration) -> Bool { + guard configuration.retainSwiftUIPreviews else { return false } + let previewRegistryUsr = "s:21DeveloperToolsSupport15PreviewRegistryP" + for ref in graph.references(to: decl) { + guard let parent = ref.parent, parent.isImplicit else { continue } + // Check if this implicit parent references PreviewRegistry, + // which identifies it as a #Preview macro expansion. + if parent.references.contains(where: { $0.usr == previewRegistryUsr }) { + return true + } + } + return false + } + /// Determines the effective maximum accessibility a member can have based on its parent's accessibility. /// /// In Swift, a member's effective accessibility is constrained by its parent. This helper From 50d2448cb0617260cad3ad45f459df40aba7145e Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:07:33 -0800 Subject: [PATCH 28/31] For when retainSwiftUIPreviews, fix the detection of #Preview macro --- .../Mutators/RedundantInternalAccessibilityMarker.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index eafc3c8b43..838aad79c5 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -439,12 +439,15 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// `private` would break the preview because the macro expansion needs to access them. private func isReferencedByPreviewMacro(_ decl: Declaration) -> Bool { guard configuration.retainSwiftUIPreviews else { return false } + let previewRegistryUsr = "s:21DeveloperToolsSupport15PreviewRegistryP" + for ref in graph.references(to: decl) { guard let parent = ref.parent, parent.isImplicit else { continue } - // Check if this implicit parent references PreviewRegistry, + + // Check if this implicit parent has a related reference to PreviewRegistry, // which identifies it as a #Preview macro expansion. - if parent.references.contains(where: { $0.usr == previewRegistryUsr }) { + if parent.related.contains(where: { $0.usr == previewRegistryUsr }) { return true } } From 54f0725d3ccc4dbbcfc6b171482210c5c22112a4 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:02:34 -0800 Subject: [PATCH 29/31] solve some obscure false positive corner cases --- ...undantFilePrivateAccessibilityMarker.swift | 8 ++-- ...RedundantInternalAccessibilityMarker.swift | 45 +++++++++++++++---- .../Sources/MainTarget/main.swift | 2 + ...emberwiseInitCalledFromDifferentType.swift | 27 +++++++++++ .../MethodCalledFromFreeFunction.swift | 29 ++++++++++++ .../RedundantInternalAccessibilityTest.swift | 34 ++++++++++++++ 6 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MemberwiseInitCalledFromDifferentType.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MethodCalledFromFreeFunction.swift diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index fa118fb461..aac807fa02 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -168,10 +168,10 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { let declLogicalType = logicalType(of: declTopLevel, inFile: file) for ref in sameFileReferences { - guard let refParent = ref.parent, - let refTopLevel = topLevelType(of: refParent) - else { - continue + guard let refParent = ref.parent else { continue } + guard let refTopLevel = topLevelType(of: refParent) else { + // Reference from a free function or top-level code — no containing type. + return true } let refLogicalType = logicalType(of: refTopLevel, inFile: file) diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 838aad79c5..942441bc1b 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -335,7 +335,8 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Case 2: Init referenced in same file AND struct must remain internal // (because it's used outside file or transitively exposed) - let hasAnyReference = !graph.references(to: implicitInit).isEmpty + let initReferences = graph.references(to: implicitInit) + let hasAnyReference = !initReferences.isEmpty if hasAnyReference { if parent.isReferencedOutsideFile(graph: graph) { return true @@ -344,6 +345,31 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } } + + // Case 3: Init referenced from a different type (or free function) in the same file. + // The properties need at least fileprivate access, so skip marking them as private. + let file = parent.location.file + let sameFileInitRefs = initReferences.filter { $0.location.file == file } + for ref in sameFileInitRefs { + guard let refParent = ref.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + // Called from a free function or top-level code + return true + } + + let parentLogicalType = logicalType(of: parent, inFile: file) + let refLogicalType = logicalType(of: refContainingType, inFile: file) + if parentLogicalType !== refLogicalType { + return true + } + } + + // Case 4: Init is referenced AND the parent struct is referenced by a #Preview macro. + // Preview macro expansions generate implicit types that cannot have access modifiers, + // so the properties must remain accessible. + if hasAnyReference, isReferencedByPreviewMacro(parent) { + return true + } } return false @@ -496,10 +522,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let declLogicalType = logicalType(of: declContainingType, inFile: file) for ref in sameFileReferences { - guard let refParent = ref.parent, - let refContainingType = immediateContainingType(of: refParent) - else { - continue + guard let refParent = ref.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + // Reference from a free function or top-level code — no containing type. + // This is effectively a different type, so fileprivate is needed. + return true } let refLogicalType = logicalType(of: refContainingType, inFile: file) @@ -685,10 +712,10 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { let parentRefs = graph.references(to: parent).filter { $0.location.file == file } for parentRef in parentRefs { - guard let refParent = parentRef.parent, - let refContainingType = immediateContainingType(of: refParent) - else { - continue + guard let refParent = parentRef.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + // Reference from a free function or top-level code — no containing type. + return true } let refLogicalType = logicalType(of: refContainingType, inFile: file) diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift index 5eac2559f7..8e430ee871 100644 --- a/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/MainTarget/main.swift @@ -99,3 +99,5 @@ NotRedundantInternalClassComponents_Support().useImplicitlyInternalStruct() InternalTypeAsReturnTypeRetainer().retain() InternalTypeTransitivelyExposedInSameFileRetainer().retain() TransitiveAccessExposureRetainer().retain() +MethodCalledFromFreeFunctionRetainer().retain() +MemberwiseInitCalledFromDifferentTypeRetainer().retain() diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MemberwiseInitCalledFromDifferentType.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MemberwiseInitCalledFromDifferentType.swift new file mode 100644 index 0000000000..bf3d6af889 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MemberwiseInitCalledFromDifferentType.swift @@ -0,0 +1,27 @@ +/* + MemberwiseInitCalledFromDifferentType.swift + Tests that struct properties are NOT flagged as redundant internal when the + struct's implicit memberwise initializer is called from a different type + in the same file. + + When a ViewModifier or another type creates a struct using its memberwise init, + the properties need at least fileprivate access and should not be flagged. +*/ + +struct MemberwiseInitStruct { + var crossTypeProperty1: String + var crossTypeProperty2: Int +} + +class MemberwiseInitCaller { + func create() -> MemberwiseInitStruct { + MemberwiseInitStruct(crossTypeProperty1: "hello", crossTypeProperty2: 42) + } +} + +public class MemberwiseInitCalledFromDifferentTypeRetainer { + public init() {} + public func retain() { + _ = MemberwiseInitCaller().create() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MethodCalledFromFreeFunction.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MethodCalledFromFreeFunction.swift new file mode 100644 index 0000000000..0244c1fb44 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/MethodCalledFromFreeFunction.swift @@ -0,0 +1,29 @@ +/* + MethodCalledFromFreeFunction.swift + Tests that type members are NOT flagged as private when they are called + from a free function in the same file. + + A free function has no containing type, so accessing a type's member from + a free function requires at least fileprivate access, not private. +*/ + +public class ClassWithMethodCalledFromFreeFunction { + public init() {} + + func methodCalledFromFreeFunction() -> String { "result" } + var propertyUsedFromFreeFunction: Int = 0 +} + +func freeFunctionCallingMethod() { + let obj = ClassWithMethodCalledFromFreeFunction() + _ = obj.methodCalledFromFreeFunction() + _ = obj.propertyUsedFromFreeFunction +} + +// Retainer to ensure the free function is used +public class MethodCalledFromFreeFunctionRetainer { + public init() {} + public func retain() { + freeFunctionCallingMethod() + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index 5cf6d6712b..ef16222c15 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -475,6 +475,40 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.class("SameFileClassWithGenericConstraint")) } + // MARK: - Memberwise Init Called From Different Type Tests + + /// Tests that struct properties are NOT flagged as redundant internal when + /// the struct's memberwise initializer is called from a different type in + /// the same file (e.g., a ViewModifier creating a struct). + func testMemberwiseInitCalledFromDifferentTypeNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.varInstance("crossTypeProperty1")) + assertNotRedundantInternalAccessibility(.varInstance("crossTypeProperty2")) + } + + // MARK: - Method Called From Free Function Tests + + /// Tests that type members are NOT flagged as private when called from + /// a free function in the same file. + /// + /// Free functions have no containing type, so members accessed from them + /// need at least fileprivate, not private. + func testMethodCalledFromFreeFunctionNotFlaggedAsPrivate() { + index() + + // Members called from a free function should suggest fileprivate, not private, + // because a free function has no containing type. + assertRedundantInternalAccessibility( + .functionMethodInstance("methodCalledFromFreeFunction()"), + suggestedAccessibility: .fileprivate + ) + assertRedundantInternalAccessibility( + .varInstance("propertyUsedFromFreeFunction"), + suggestedAccessibility: .fileprivate + ) + } + // MARK: - Same-File Memberwise Init Tests /// Tests that struct memberwise init properties are NOT flagged when the struct From a176ff93aadb5dfae0ba3926b41da674c4556a19 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:03:41 -0800 Subject: [PATCH 30/31] correctly mark children of symbol --- .../RedundantAccessibilityMarkerShared.swift | 24 +++++++++++ ...undantFilePrivateAccessibilityMarker.swift | 27 +++++++++++- ...RedundantInternalAccessibilityMarker.swift | 27 +++++++++++- ...estedEnumChildReferenceAccessibility.swift | 41 +++++++++++++++++++ ...ChildReferenceAccessibility_Consumer.swift | 12 ++++++ .../RedundantInternalAccessibilityTest.swift | 15 +++++++ 6 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility.swift create mode 100644 Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility_Consumer.swift diff --git a/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift index 894de53616..2df4a67d18 100644 --- a/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift +++ b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift @@ -40,6 +40,30 @@ extension Declaration { return false } + /// Checks if this declaration or any of its immediate child declarations are + /// referenced outside the defining file. + /// + /// For type declarations (enum, struct, class, protocol), Swift's indexer may create + /// references to child declarations (e.g., enum cases via type inference like `.small`) + /// without creating a reference to the parent type itself. This method catches those + /// indirect cross-file usages that `isReferencedOutsideFile` would miss. + func isReferencedOutsideFileIncludingChildren(graph: SourceGraph) -> Bool { + if isReferencedOutsideFile(graph: graph) { + return true + } + + let typeKinds: Set = [.enum, .struct, .class, .protocol] + guard typeKinds.contains(kind) else { return false } + + for child in declarations { + if child.isReferencedOutsideFile(graph: graph) { + return true + } + } + + return false + } + /// Counts the number of ancestors for this declaration. /// Used for sorting declarations by depth to ensure parents are marked before children, /// which is important for nested redundant accessibility suppression logic. diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index aac807fa02..042d782540 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -44,7 +44,7 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.fileprivate) { if !graph.isRetained(decl), - !decl.isReferencedOutsideFile(graph: graph), + !decl.isReferencedOutsideFileIncludingChildren(graph: graph), !isReferencedFromDifferentTypeInSameFile(decl) { mark(decl) @@ -95,7 +95,7 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { for descDecl in descendants { if !graph.isRetained(descDecl), - !descDecl.isReferencedOutsideFile(graph: graph), + !descDecl.isReferencedOutsideFileIncludingChildren(graph: graph), !isReferencedFromDifferentTypeInSameFile(descDecl) { mark(descDecl) @@ -180,6 +180,29 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { return true } } + + // For type declarations, also check if any child declaration is referenced + // from a different type in the same file. This catches cases where enum cases + // are used via type inference (e.g., `.small`) from outside the parent type. + let typeKinds: Set = [.enum, .struct, .class, .protocol] + if typeKinds.contains(decl.kind) { + for child in decl.declarations { + let childSameFileRefs = graph.references(to: child).filter { $0.location.file == file } + for ref in childSameFileRefs { + guard let refParent = ref.parent else { continue } + guard let refTopLevel = topLevelType(of: refParent) else { + return true + } + + let refLogicalType = logicalType(of: refTopLevel, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + } + } + return false } } diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 942441bc1b..738c31d06e 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -39,7 +39,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { if decl.accessibility.value == .internal { if !graph.isRetained(decl), !shouldSkipMarking(decl) { - let isReferencedOutside = decl.isReferencedOutsideFile(graph: graph) + let isReferencedOutside = decl.isReferencedOutsideFileIncludingChildren(graph: graph) if !isReferencedOutside, !isTransitivelyExposedOutsideFile(decl) { mark(decl) } @@ -126,7 +126,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { for descDecl in descendants { if !graph.isRetained(descDecl), !shouldSkipMarking(descDecl) { - let isReferencedOutside = descDecl.isReferencedOutsideFile(graph: graph) + let isReferencedOutside = descDecl.isReferencedOutsideFileIncludingChildren(graph: graph) if !isReferencedOutside, !isTransitivelyExposedOutsideFile(descDecl) { mark(descDecl) } @@ -535,6 +535,29 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } } + + // For type declarations, also check if any child declaration is referenced + // from a different type in the same file. This catches cases where enum cases + // are used via type inference (e.g., `.small`) from outside the parent type. + let typeKinds: Set = [.enum, .struct, .class, .protocol] + if typeKinds.contains(decl.kind) { + for child in decl.declarations { + let childSameFileRefs = graph.references(to: child).filter { $0.location.file == file } + for ref in childSameFileRefs { + guard let refParent = ref.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + return true + } + + let refLogicalType = logicalType(of: refContainingType, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + } + } + return false } diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility.swift new file mode 100644 index 0000000000..c25b2309d4 --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility.swift @@ -0,0 +1,41 @@ +// NestedEnumChildReferenceAccessibility.swift +// Test cases for nested enums whose cases are referenced from outside the parent type +// via type inference (e.g., `.small` instead of `TransportButtonSize.small`). +// +// When enum cases are used via type inference, the Swift indexer creates references +// to the enum cases but NOT to the parent enum type. Periphery must recognize these +// indirect references to avoid falsely suggesting `private` for the nested enum. + +struct TransportButtonHost { + enum TransportButtonSize { + case small + case medium + case large + } + + var size: TransportButtonSize + + func description() -> String { + switch size { + case .small: "S" + case .medium: "M" + case .large: "L" + } + } +} + +// Same-file consumer that uses enum cases from outside the parent struct. +// This exercises the isReferencedFromDifferentTypeInSameFile child-reference path. +class SameFileTransportConsumer { + func use() { + _ = TransportButtonHost(size: .medium) + } +} + +public class NestedEnumChildReferenceAccessibilityRetainer { + public init() {} + public func retain() { + _ = TransportButtonHost(size: .small) + _ = SameFileTransportConsumer() + } +} diff --git a/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility_Consumer.swift b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility_Consumer.swift new file mode 100644 index 0000000000..c1c082b13e --- /dev/null +++ b/Tests/AccessibilityTests/AccessibilityProject/Sources/TargetA/NestedEnumChildReferenceAccessibility_Consumer.swift @@ -0,0 +1,12 @@ +// NestedEnumChildReferenceAccessibility_Consumer.swift +// Cross-file consumer that uses nested enum cases via type inference. +// This creates cross-file references to the enum cases without directly +// referencing the parent enum type. + +class NestedEnumChildReferenceConsumer { + func consume() { + // Uses .large via type inference — the indexer references the enum case + // but not TransportButtonSize itself. + _ = TransportButtonHost(size: .large) + } +} diff --git a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift index ef16222c15..cdece48d67 100644 --- a/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift +++ b/Tests/AccessibilityTests/RedundantInternalAccessibilityTest.swift @@ -475,6 +475,21 @@ final class RedundantInternalAccessibilityTest: SPMSourceGraphTestCase { assertNotRedundantInternalAccessibility(.class("SameFileClassWithGenericConstraint")) } + // MARK: - Nested Enum Child Reference Tests + + /// Tests that a nested enum is NOT flagged as redundant internal when its cases + /// are referenced from outside the file via type inference. + /// + /// When enum cases are used via type inference (e.g., `.small` instead of + /// `TransportButtonSize.small`), the Swift indexer creates references to the + /// enum cases but NOT to the parent enum type. Periphery must recognize these + /// indirect references to avoid falsely suggesting `private`. + func testNestedEnumCaseUsedFromOutsideFileNotFlagged() { + index() + + assertNotRedundantInternalAccessibility(.enum("TransportButtonSize")) + } + // MARK: - Memberwise Init Called From Different Type Tests /// Tests that struct properties are NOT flagged as redundant internal when From 4a441f79d3dbdda327449a9ebeb688faebfe4ef1 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:55:40 -0800 Subject: [PATCH 31/31] Refactor --- .../Results/OutputFormatter.swift | 4 +- Sources/PeripheryKit/ScanResult.swift | 4 +- Sources/PeripheryKit/ScanResultBuilder.swift | 4 +- .../SourceGraph/Elements/Accessibility.swift | 20 +- .../SourceGraph/Elements/Declaration.swift | 4 + .../RedundantAccessibilityMarkerShared.swift | 150 +++++++++- ...undantFilePrivateAccessibilityMarker.swift | 110 +------- ...RedundantInternalAccessibilityMarker.swift | 266 ++++-------------- Sources/SourceGraph/SourceGraph.swift | 23 +- Tests/Shared/SourceGraphTestCase.swift | 12 +- 10 files changed, 245 insertions(+), 352 deletions(-) diff --git a/Sources/PeripheryKit/Results/OutputFormatter.swift b/Sources/PeripheryKit/Results/OutputFormatter.swift index 19312f8d45..306bc0aac8 100644 --- a/Sources/PeripheryKit/Results/OutputFormatter.swift +++ b/Sources/PeripheryKit/Results/OutputFormatter.swift @@ -68,10 +68,10 @@ extension OutputFormatter { case let .redundantPublicAccessibility(modules): let modulesJoined = modules.sorted().joined(separator: ", ") description += "Redundant public accessibility for \(kindDisplayName) '\(name)' (not used outside of \(modulesJoined))" - case let .redundantInternalAccessibility(_, suggestedAccessibility): + case let .redundantInternalAccessibility(suggestedAccessibility): let accessibilityText = suggestedAccessibility?.rawValue ?? "private/fileprivate" description += "Redundant internal accessibility for \(kindDisplayName) '\(name)' (not used outside of file; can be \(accessibilityText))" - case let .redundantFilePrivateAccessibility(_, containingTypeName): + case let .redundantFilePrivateAccessibility(containingTypeName): let context = containingTypeName.map { "only used within \($0)" } ?? "not used outside of file" description += "Redundant fileprivate accessibility for \(kindDisplayName) '\(name)' (\(context); can be private)" case .superfluousIgnoreCommand: diff --git a/Sources/PeripheryKit/ScanResult.swift b/Sources/PeripheryKit/ScanResult.swift index 76d124bc31..0708272805 100644 --- a/Sources/PeripheryKit/ScanResult.swift +++ b/Sources/PeripheryKit/ScanResult.swift @@ -7,8 +7,8 @@ public struct ScanResult { case assignOnlyProperty case redundantProtocol(references: Set, inherited: Set) case redundantPublicAccessibility(modules: Set) - case redundantInternalAccessibility(files: Set, suggestedAccessibility: Accessibility?) - case redundantFilePrivateAccessibility(files: Set, containingTypeName: String?) + case redundantInternalAccessibility(suggestedAccessibility: Accessibility?) + case redundantFilePrivateAccessibility(containingTypeName: String?) case superfluousIgnoreCommand } diff --git a/Sources/PeripheryKit/ScanResultBuilder.swift b/Sources/PeripheryKit/ScanResultBuilder.swift index 0f78592ee6..28adf35aa7 100644 --- a/Sources/PeripheryKit/ScanResultBuilder.swift +++ b/Sources/PeripheryKit/ScanResultBuilder.swift @@ -44,10 +44,10 @@ public enum ScanResultBuilder { .init(declaration: $0.0, annotation: .redundantPublicAccessibility(modules: $0.1)) } let annotatedRedundantInternalAccessibility: [ScanResult] = redundantInternalAccessibility.map { - .init(declaration: $0.0, annotation: .redundantInternalAccessibility(files: $0.1.files, suggestedAccessibility: $0.1.suggestedAccessibility)) + .init(declaration: $0.key, annotation: .redundantInternalAccessibility(suggestedAccessibility: $0.value)) } let annotatedRedundantFilePrivateAccessibility: [ScanResult] = redundantFilePrivateAccessibility.map { - .init(declaration: $0.0, annotation: .redundantFilePrivateAccessibility(files: $0.1.files, containingTypeName: $0.1.containingTypeName)) + .init(declaration: $0.key, annotation: .redundantFilePrivateAccessibility(containingTypeName: $0.value)) } let annotatedSuperfluousIgnoreCommands: [ScanResult] = { diff --git a/Sources/SourceGraph/Elements/Accessibility.swift b/Sources/SourceGraph/Elements/Accessibility.swift index bc6fa41a55..efc13b4f30 100644 --- a/Sources/SourceGraph/Elements/Accessibility.swift +++ b/Sources/SourceGraph/Elements/Accessibility.swift @@ -1,9 +1,23 @@ import Foundation -public enum Accessibility: String { - case `public` - case `internal` +public enum Accessibility: String, Comparable { case `private` case `fileprivate` + case `internal` + case `public` case open + + private var sortOrder: Int { + switch self { + case .private: 0 + case .fileprivate: 1 + case .internal: 2 + case .public: 3 + case .open: 4 + } + } + + public static func < (lhs: Accessibility, rhs: Accessibility) -> Bool { + lhs.sortOrder < rhs.sortOrder + } } diff --git a/Sources/SourceGraph/Elements/Declaration.swift b/Sources/SourceGraph/Elements/Declaration.swift index 382c58403d..bcfca976be 100644 --- a/Sources/SourceGraph/Elements/Declaration.swift +++ b/Sources/SourceGraph/Elements/Declaration.swift @@ -98,6 +98,10 @@ public final class Declaration { Set(Kind.allCases.filter(\.isExtensionKind)) } + static let concreteTypeKinds: Set = [.class, .struct, .enum, .protocol] + + static let allTypeKinds: Set = concreteTypeKinds.union(extensionKinds) + public var extendedKind: Kind? { switch self { case .extensionClass: diff --git a/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift index 2df4a67d18..6c6b847c2b 100644 --- a/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift +++ b/Sources/SourceGraph/Mutators/RedundantAccessibilityMarkerShared.swift @@ -1,5 +1,16 @@ // Shared utilities for redundant accessibility analysis mutators. +/// Tracks visited declarations to prevent infinite recursion in graph traversals. +struct RecursionGuard { + private var visited: Set = [] + + /// Returns true if the declaration has not been visited before, and marks it as visited. + /// Returns false if already visited (caller should bail out). + mutating func firstVisit(_ decl: Declaration) -> Bool { + visited.insert(ObjectIdentifier(decl)).inserted + } +} + extension Declaration { /// Checks if this declaration is referenced outside its defining file. /// This is a common check used by multiple accessibility markers to determine @@ -21,7 +32,7 @@ extension Declaration { /// Checks if any ancestor declaration is marked as redundant in the given accessibility map. /// Used by accessibility markers to suppress nested warnings when a containing type is already flagged. /// This avoids redundant warnings since fixing the parent's accessibility fixes the children too. - func isAnyAncestorMarked(in accessibilityMap: [Declaration: Any]) -> Bool { + func isAnyAncestorMarked(in markedDeclarations: Dictionary.Keys) -> Bool { var current = parent var visited: Set = [] @@ -32,7 +43,7 @@ extension Declaration { visited.insert(currentParent) - if accessibilityMap[currentParent] != nil { + if markedDeclarations.contains(currentParent) { return true } current = currentParent.parent @@ -52,8 +63,7 @@ extension Declaration { return true } - let typeKinds: Set = [.enum, .struct, .class, .protocol] - guard typeKinds.contains(kind) else { return false } + guard Declaration.Kind.concreteTypeKinds.contains(kind) else { return false } for child in declarations { if child.isReferencedOutsideFile(graph: graph) { @@ -76,4 +86,136 @@ extension Declaration { } return count } + + /// Determines if a declaration should be skipped from all accessibility analysis. + /// + /// These are declarations where changing the access level is either impossible + /// (compiler-generated, destructors, enum cases) or constrained by other rules + /// (generic type params, overrides, @usableFromInline). + var shouldSkipAccessibilityAnalysis: Bool { + // Generic type parameters must match their container's accessibility. + if kind == .genericTypeParam { return true } + + // Skip implicit (compiler-generated) declarations. + if isImplicit { return true } + + // Deinitializers cannot have explicit access modifiers in Swift. + if kind == .functionDestructor { return true } + + // Enum cases cannot have explicit access modifiers in Swift. + if kind == .enumelement { return true } + + // Override methods must be at least as accessible as what they override. + if isOverride { return true } + + // Declarations with @usableFromInline must remain internal (or package). + if attributes.contains(where: { $0.name == "usableFromInline" }) { + return true + } + + return false + } +} + +extension SourceGraph { + /// Gets the logical type for comparison purposes when analyzing accessibility. + /// + /// For extensions of types in the SAME FILE, treats the extension as the extended type. + /// For extensions of types in DIFFERENT FILES, treats the extension as its own distinct type. + func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { + if decl.kind.isExtensionKind { + if let extendedDecl = try? extendedDeclaration(forExtension: decl), + extendedDecl.location.file == file + { + return extendedDecl + } + return decl + } + return decl + } + + /// Finds the immediate containing type of a declaration. + /// + /// For members (properties, methods, etc.), this returns their containing type. + /// For nested types, this returns the type that contains them (the outer type). + /// For top-level types, this returns the type itself (they are their own container). + func immediateContainingType(of decl: Declaration) -> Declaration? { + // For types, check if they have a parent type (nested type case). + // If so, return the parent type. If not (top-level), return the type itself. + if Declaration.Kind.allTypeKinds.contains(decl.kind) { + if let parent = decl.parent, Declaration.Kind.allTypeKinds.contains(parent.kind) { + return parent + } + return decl + } + + // Walk up the parent chain to find the first containing type + var current = decl.parent + while let parent = current { + if Declaration.Kind.allTypeKinds.contains(parent.kind) { + return parent + } + current = parent.parent + } + + return nil + } + + /// Checks if a declaration is referenced from a different type in the same file. + /// + /// Uses the immediate containing type (not top-level type) for comparison because: + /// - For nested types like `Outer.Inner`, a member of `Inner` accessed from `Outer` + /// (but outside `Inner`) needs `fileprivate` + /// - Using top-level type would incorrectly see both as belonging to `Outer` + /// + /// Also checks child declarations (e.g., enum cases used via type inference like `.small`) + /// since the indexer may reference children without referencing the parent type. + func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { + let file = decl.location.file + let sameFileReferences = references(to: decl).filter { $0.location.file == file } + + guard let declContainingType = immediateContainingType(of: decl) else { + return false + } + + let declLogicalType = logicalType(of: declContainingType, inFile: file) + + for ref in sameFileReferences { + guard let refParent = ref.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + // Reference from a free function or top-level code — no containing type. + // This is effectively a different type, so fileprivate is needed. + return true + } + + let refLogicalType = logicalType(of: refContainingType, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + + // For type declarations, also check if any child declaration is referenced + // from a different type in the same file. This catches cases where enum cases + // are used via type inference (e.g., `.small`) from outside the parent type. + if Declaration.Kind.concreteTypeKinds.contains(decl.kind) { + for child in decl.declarations { + let childSameFileRefs = references(to: child).filter { $0.location.file == file } + for ref in childSameFileRefs { + guard let refParent = ref.parent else { continue } + guard let refContainingType = immediateContainingType(of: refParent) else { + return true + } + + let refLogicalType = logicalType(of: refContainingType, inFile: file) + + if declLogicalType !== refLogicalType { + return true + } + } + } + } + + return false + } } diff --git a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift index 042d782540..530aa67e22 100644 --- a/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantFilePrivateAccessibilityMarker.swift @@ -10,7 +10,6 @@ import Shared /// This mutator is more complex than RedundantInternalAccessibilityMarker because it must: /// - Distinguish between access from the same type vs. different types in the same file /// - Handle extensions of types (both same-file and cross-file extensions) -/// - Walk the type hierarchy to find the top-level containing type for comparison /// /// The key insight: `private` and `fileprivate` differ in that `private` is accessible only within /// the declaration and its extensions in the same file, while `fileprivate` is accessible from @@ -43,9 +42,10 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { private func validate(_ decl: Declaration) throws { if decl.accessibility.isExplicitly(.fileprivate) { - if !graph.isRetained(decl), + if !decl.shouldSkipAccessibilityAnalysis, + !graph.isRetained(decl), !decl.isReferencedOutsideFileIncludingChildren(graph: graph), - !isReferencedFromDifferentTypeInSameFile(decl) + !graph.isReferencedFromDifferentTypeInSameFile(decl) { mark(decl) } @@ -77,13 +77,13 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { // Unless explicitly requested, skip marking nested declarations when an ancestor is already marked. // This avoids redundant warnings since fixing the parent's accessibility fixes the children too. if !configuration.showNestedRedundantAccessibility, - decl.isAnyAncestorMarked(in: graph.redundantFilePrivateAccessibility) + decl.isAnyAncestorMarked(in: graph.redundantFilePrivateAccessibility.keys) { return } let containingTypeName = containingTypeName(for: decl) - graph.markRedundantFilePrivateAccessibility(decl, file: decl.location.file, containingTypeName: containingTypeName) + graph.markRedundantFilePrivateAccessibility(decl, containingTypeName: containingTypeName) } private func markExplicitFilePrivateDescendentDeclarations(from decl: Declaration) { @@ -94,9 +94,10 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { } for descDecl in descendants { - if !graph.isRetained(descDecl), + if !descDecl.shouldSkipAccessibilityAnalysis, + !graph.isRetained(descDecl), !descDecl.isReferencedOutsideFileIncludingChildren(graph: graph), - !isReferencedFromDifferentTypeInSameFile(descDecl) + !graph.isReferencedFromDifferentTypeInSameFile(descDecl) { mark(descDecl) } @@ -109,100 +110,15 @@ final class RedundantFilePrivateAccessibilityMarker: SourceGraphMutator { }) } - /// Finds the top-level type declaration by walking up the parent chain. - /// Returns the outermost type that contains the given declaration. - private func topLevelType(of decl: Declaration) -> Declaration? { - let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] - let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) - let ancestors = [decl] + Array(decl.ancestralDeclarations) - return ancestors.last { typeDecl in - guard typeKinds.contains(typeDecl.kind) else { return false } - guard let parent = typeDecl.parent else { return true } - - return !typeKinds.contains(parent.kind) - } - } - - /// Gets the logical type for comparison purposes. - /// For extensions of types in the SAME FILE, treats the extension as the extended type. - /// For extensions of types in DIFFERENT FILES (like extending external types), - /// treats the extension as its own distinct type for the purpose of this file. - private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { - if decl.kind.isExtensionKind { - if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), - extendedDecl.location.file == file - { - return extendedDecl - } - return decl - } - return decl - } - - /// Extracts a display name for the containing type of a declaration. + /// Extracts a display name for the immediate containing type of a declaration. /// /// Returns a string like "class Foo" or "struct Bar" that identifies the type /// containing the declaration. Returns nil for top-level declarations. private func containingTypeName(for decl: Declaration) -> String? { - guard let topLevel = topLevelType(of: decl) else { return nil } - guard let name = topLevel.name else { return nil } - - return "\(topLevel.kind.displayName) \(name)" - } - - /// Checks if a declaration is referenced from a different type in the same file. - /// Returns true if any same-file reference comes from a different logical type, - /// indicating that fileprivate access is necessary. - /// - /// Even for top-level declarations, private and fileprivate are different: - /// - private: only accessible within the declaration itself and its extensions in the same file - /// - fileprivate: accessible from anywhere in the same file - private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { - let file = decl.location.file - let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } - - guard let declTopLevel = topLevelType(of: decl) else { - return false - } - - let declLogicalType = logicalType(of: declTopLevel, inFile: file) - - for ref in sameFileReferences { - guard let refParent = ref.parent else { continue } - guard let refTopLevel = topLevelType(of: refParent) else { - // Reference from a free function or top-level code — no containing type. - return true - } - - let refLogicalType = logicalType(of: refTopLevel, inFile: file) - - if declLogicalType !== refLogicalType { - return true - } - } - - // For type declarations, also check if any child declaration is referenced - // from a different type in the same file. This catches cases where enum cases - // are used via type inference (e.g., `.small`) from outside the parent type. - let typeKinds: Set = [.enum, .struct, .class, .protocol] - if typeKinds.contains(decl.kind) { - for child in decl.declarations { - let childSameFileRefs = graph.references(to: child).filter { $0.location.file == file } - for ref in childSameFileRefs { - guard let refParent = ref.parent else { continue } - guard let refTopLevel = topLevelType(of: refParent) else { - return true - } - - let refLogicalType = logicalType(of: refTopLevel, inFile: file) - - if declLogicalType !== refLogicalType { - return true - } - } - } - } + guard let containingType = graph.immediateContainingType(of: decl) else { return nil } + guard containingType !== decl else { return nil } + guard let name = containingType.name else { return nil } - return false + return "\(containingType.kind.displayName) \(name)" } } diff --git a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift index 738c31d06e..538b231f59 100644 --- a/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift +++ b/Sources/SourceGraph/Mutators/RedundantInternalAccessibilityMarker.swift @@ -72,7 +72,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Unless explicitly requested, skip marking nested declarations when an ancestor is already marked. // This avoids redundant warnings since fixing the parent's accessibility fixes the children too. if !configuration.showNestedRedundantAccessibility, - decl.isAnyAncestorMarked(in: graph.redundantInternalAccessibility) + decl.isAnyAncestorMarked(in: graph.redundantInternalAccessibility.keys) { return } @@ -85,7 +85,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Additionally, types used in protocol requirement signatures need fileprivate even // at top level (private would make them inaccessible from the protocol method). let isTopLevel = decl.parent == nil - let needsFileprivate = isReferencedFromDifferentTypeInSameFile(decl) || + let needsFileprivate = graph.isReferencedFromDifferentTypeInSameFile(decl) || isTransitivelyExposedFromDifferentTypeInSameFile(decl) || isUsedInProtocolRequirementSignature(decl) @@ -103,18 +103,13 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // If the parent is `fileprivate` and we would suggest `fileprivate`, it's already constrained. // Marking these would be misleading since changing them would actually increase visibility. if let maxAccessibility = effectiveMaximumAccessibility(for: decl), - let suggestedAccessibility + let suggestedAccessibility, + suggestedAccessibility >= maxAccessibility { - let accessibilityOrder: [Accessibility] = [.private, .fileprivate, .internal, .public, .open] - let maxIndex = accessibilityOrder.firstIndex(of: maxAccessibility) ?? 0 - let suggestedIndex = accessibilityOrder.firstIndex(of: suggestedAccessibility) ?? 0 - - if suggestedIndex >= maxIndex { - return - } + return } - graph.markRedundantInternalAccessibility(decl, file: decl.location.file, suggestedAccessibility: suggestedAccessibility) + graph.markRedundantInternalAccessibility(decl, suggestedAccessibility: suggestedAccessibility) } private func markInternalDescendentDeclarations(from decl: Declaration) { @@ -143,7 +138,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// - They are struct stored properties used in an implicit memberwise initializer /// - They are referenced by a `#Preview` macro expansion (when retainSwiftUIPreviews is enabled) private func shouldSkipMarking(_ decl: Declaration) -> Bool { - if shouldSkipAccessibilityAnalysis(for: decl) { + if decl.shouldSkipAccessibilityAnalysis { return true } @@ -184,37 +179,6 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // MARK: - Internal Accessibility Analysis Helpers - /// Determines if a declaration should be skipped from accessibility analysis entirely. - /// - /// This helper is specific to internal accessibility analysis, checking conditions - /// that make a declaration ineligible for redundant internal marking. - private func shouldSkipAccessibilityAnalysis(for decl: Declaration) -> Bool { - // Generic type parameters must match their container's accessibility. - if decl.kind == .genericTypeParam { return true } - - // Skip implicit (compiler-generated) declarations. - if decl.isImplicit { return true } - - // Deinitializers cannot have explicit access modifiers in Swift. - if decl.kind == .functionDestructor { return true } - - // Enum cases cannot have explicit access modifiers in Swift. - // They inherit the accessibility of their containing enum. - if decl.kind == .enumelement { return true } - - // Override methods must be at least as accessible as what they override. - if decl.isOverride { return true } - - // Declarations with @usableFromInline must remain internal (or package). - // This attribute allows internal declarations to be inlined into client code, - // requiring them to maintain internal visibility. - if decl.attributes.contains(where: { $0.name == "usableFromInline" }) { - return true - } - - return false - } - /// Checks if a declaration is a protocol requirement or protocol conformance. /// /// Protocol requirements must maintain sufficient accessibility to fulfill the protocol @@ -328,48 +292,38 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { guard parameterNames.contains(propertyName) else { continue } - // Case 1: Init referenced outside file -> properties must stay internal + // Init referenced outside file -> properties must stay internal. if implicitInit.isReferencedOutsideFile(graph: graph) { return true } - // Case 2: Init referenced in same file AND struct must remain internal - // (because it's used outside file or transitively exposed) let initReferences = graph.references(to: implicitInit) - let hasAnyReference = !initReferences.isEmpty - if hasAnyReference { - if parent.isReferencedOutsideFile(graph: graph) { - return true - } - if isTransitivelyExposedOutsideFile(parent) { - return true - } + guard !initReferences.isEmpty else { continue } + + // The struct must remain internal if it's referenced outside the file, + // transitively exposed, or used by a #Preview macro expansion (whose + // implicit types cannot have access modifiers). + if parent.isReferencedOutsideFile(graph: graph) + || isTransitivelyExposedOutsideFile(parent) + || isReferencedByPreviewMacro(parent) + { + return true } - // Case 3: Init referenced from a different type (or free function) in the same file. - // The properties need at least fileprivate access, so skip marking them as private. + // Init referenced from a different type (or free function) in the same file. + // The properties need at least fileprivate access, so skip marking them. let file = parent.location.file - let sameFileInitRefs = initReferences.filter { $0.location.file == file } - for ref in sameFileInitRefs { + let parentLogicalType = graph.logicalType(of: parent, inFile: file) + for ref in initReferences where ref.location.file == file { guard let refParent = ref.parent else { continue } - guard let refContainingType = immediateContainingType(of: refParent) else { - // Called from a free function or top-level code + guard let refContainingType = graph.immediateContainingType(of: refParent) else { return true } - let parentLogicalType = logicalType(of: parent, inFile: file) - let refLogicalType = logicalType(of: refContainingType, inFile: file) - if parentLogicalType !== refLogicalType { + if graph.logicalType(of: refContainingType, inFile: file) !== parentLogicalType { return true } } - - // Case 4: Init is referenced AND the parent struct is referenced by a #Preview macro. - // Preview macro expansions generate implicit types that cannot have access modifiers, - // so the properties must remain accessible. - if hasAnyReference, isReferencedByPreviewMacro(parent) { - return true - } } return false @@ -388,23 +342,18 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// scenarios. This function handles same-file scenarios where the constraint chain /// exists entirely within one file. private func isConstrainedBySameFileTypeUsage(_ decl: Declaration) -> Bool { - let typeKinds: Set = [.enum, .struct, .class, .protocol] - guard typeKinds.contains(decl.kind) else { return false } + guard Declaration.Kind.concreteTypeKinds.contains(decl.kind) else { return false } - var visited: Set = [] - return isConstrainedBySameFileTypeUsageRecursive(decl, visited: &visited) + var guard_ = RecursionGuard() + return isConstrainedBySameFileTypeUsageRecursive(decl, guard: &guard_) } private func isConstrainedBySameFileTypeUsageRecursive( _ decl: Declaration, - visited: inout Set + guard guard_: inout RecursionGuard ) -> Bool { - let id = ObjectIdentifier(decl) - guard !visited.contains(id) else { return false } + guard guard_.firstVisit(decl) else { return false } - visited.insert(id) - - let typeKinds: Set = [.enum, .struct, .class, .protocol] let file = decl.location.file let refs = graph.references(to: decl) @@ -421,11 +370,11 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { // Find the containing type of that declaration let containingType: Declaration? - if typeKinds.contains(usingDecl.kind) || usingDecl.kind.isExtensionKind { + if Declaration.Kind.allTypeKinds.contains(usingDecl.kind) { // The using declaration IS a type (e.g., conformedType, inheritedType) containingType = usingDecl } else if let parent = usingDecl.parent, - typeKinds.contains(parent.kind) || parent.kind.isExtensionKind + Declaration.Kind.allTypeKinds.contains(parent.kind) { // The using declaration is a member of a type containingType = parent @@ -449,7 +398,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { } // Recursive: if the containing type is itself constrained - if isConstrainedBySameFileTypeUsageRecursive(containingType, visited: &visited) { + if isConstrainedBySameFileTypeUsageRecursive(containingType, guard: &guard_) { return true } } @@ -489,122 +438,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { guard let parent = decl.parent else { return nil } let parentAccessibility = parent.accessibility.value - - switch parentAccessibility { - case .private: - return .private - case .fileprivate: - return .fileprivate - case .internal: - return .internal - case .public, .open: - return nil - } - } - - /// Checks if a declaration is referenced from a different type in the same file. - /// - /// For internal accessibility analysis, this determines whether to suggest `fileprivate` - /// versus `private` when a declaration is only used within its file. - /// - /// This uses the **immediate containing type** (not the top-level type) because: - /// - For nested types like `OuterStruct.InnerStruct`, a member of `InnerStruct` that's - /// accessed from code inside `OuterStruct` (but outside `InnerStruct`) needs `fileprivate` - /// - Using top-level type would incorrectly see both as belonging to `OuterStruct` - private func isReferencedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { - let file = decl.location.file - let sameFileReferences = graph.references(to: decl).filter { $0.location.file == file } - - guard let declContainingType = immediateContainingType(of: decl) else { - return false - } - - let declLogicalType = logicalType(of: declContainingType, inFile: file) - - for ref in sameFileReferences { - guard let refParent = ref.parent else { continue } - guard let refContainingType = immediateContainingType(of: refParent) else { - // Reference from a free function or top-level code — no containing type. - // This is effectively a different type, so fileprivate is needed. - return true - } - - let refLogicalType = logicalType(of: refContainingType, inFile: file) - - if declLogicalType !== refLogicalType { - return true - } - } - - // For type declarations, also check if any child declaration is referenced - // from a different type in the same file. This catches cases where enum cases - // are used via type inference (e.g., `.small`) from outside the parent type. - let typeKinds: Set = [.enum, .struct, .class, .protocol] - if typeKinds.contains(decl.kind) { - for child in decl.declarations { - let childSameFileRefs = graph.references(to: child).filter { $0.location.file == file } - for ref in childSameFileRefs { - guard let refParent = ref.parent else { continue } - guard let refContainingType = immediateContainingType(of: refParent) else { - return true - } - - let refLogicalType = logicalType(of: refContainingType, inFile: file) - - if declLogicalType !== refLogicalType { - return true - } - } - } - } - - return false - } - - /// Finds the immediate containing type of a declaration. - /// - /// For members (properties, methods, etc.), this returns their containing type. - /// For nested types, this returns the type that contains them (the outer type). - /// For top-level types, this returns the type itself (they are their own container). - private func immediateContainingType(of decl: Declaration) -> Declaration? { - let baseTypeKinds: Set = [.class, .struct, .enum, .protocol] - let typeKinds = baseTypeKinds.union(Declaration.Kind.extensionKinds) - - // For types, check if they have a parent type (nested type case). - // If so, return the parent type. If not (top-level), return the type itself. - if typeKinds.contains(decl.kind) { - if let parent = decl.parent, typeKinds.contains(parent.kind) { - return parent - } - return decl - } - - // Walk up the parent chain to find the first containing type - var current = decl.parent - while let parent = current { - if typeKinds.contains(parent.kind) { - return parent - } - current = parent.parent - } - - return nil - } - - /// Gets the logical type for comparison purposes when analyzing internal accessibility. - /// - /// For extensions of types in the SAME FILE, treats the extension as the extended type. - /// For extensions of types in DIFFERENT FILES, treats the extension as its own distinct type. - private func logicalType(of decl: Declaration, inFile file: SourceFile) -> Declaration? { - if decl.kind.isExtensionKind { - if let extendedDecl = try? graph.extendedDeclaration(forExtension: decl), - extendedDecl.location.file == file - { - return extendedDecl - } - return decl - } - return decl + return parentAccessibility <= .internal ? parentAccessibility : nil } /// Checks if a type is transitively exposed outside its file through an API signature. @@ -623,15 +457,12 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { /// is used as a property type in OuterContainer, and OuterContainer is referenced from /// outside the file, then TypeA is transitively exposed through the chain. private func isTransitivelyExposedOutsideFile(_ decl: Declaration) -> Bool { - var visited: Set = [] - return isTransitivelyExposedOutsideFileRecursive(decl, visited: &visited) + var guard_ = RecursionGuard() + return isTransitivelyExposedOutsideFileRecursive(decl, guard: &guard_) } - private func isTransitivelyExposedOutsideFileRecursive(_ decl: Declaration, visited: inout Set) -> Bool { - let id = ObjectIdentifier(decl) - guard !visited.contains(id) else { return false } - - visited.insert(id) + private func isTransitivelyExposedOutsideFileRecursive(_ decl: Declaration, guard guard_: inout RecursionGuard) -> Bool { + guard guard_.firstVisit(decl) else { return false } let refs = graph.references(to: decl) @@ -661,7 +492,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { return true } // Recursive: the containing type itself is transitively exposed - if isTransitivelyExposedOutsideFileRecursive(containingType, visited: &visited) { + if isTransitivelyExposedOutsideFileRecursive(containingType, guard: &guard_) { return true } } @@ -696,18 +527,18 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { private func isTransitivelyExposedFromDifferentTypeInSameFile(_ decl: Declaration) -> Bool { let file = decl.location.file - guard let declContainingType = immediateContainingType(of: decl) else { + guard let declContainingType = graph.immediateContainingType(of: decl) else { return false } - let declLogicalType = logicalType(of: declContainingType, inFile: file) - var visited: Set = [] + let declLogicalType = graph.logicalType(of: declContainingType, inFile: file) + var guard_ = RecursionGuard() return isTransitivelyExposedFromDifferentTypeInSameFileRecursive( decl, declLogicalType: declLogicalType, file: file, - visited: &visited + guard: &guard_ ) } @@ -715,12 +546,9 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { _ decl: Declaration, declLogicalType: Declaration?, file: SourceFile, - visited: inout Set + guard guard_: inout RecursionGuard ) -> Bool { - let id = ObjectIdentifier(decl) - guard !visited.contains(id) else { return false } - - visited.insert(id) + guard guard_.firstVisit(decl) else { return false } let refs = graph.references(to: decl) @@ -736,12 +564,12 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { for parentRef in parentRefs { guard let refParent = parentRef.parent else { continue } - guard let refContainingType = immediateContainingType(of: refParent) else { + guard let refContainingType = graph.immediateContainingType(of: refParent) else { // Reference from a free function or top-level code — no containing type. return true } - let refLogicalType = logicalType(of: refContainingType, inFile: file) + let refLogicalType = graph.logicalType(of: refContainingType, inFile: file) if declLogicalType !== refLogicalType { return true @@ -758,7 +586,7 @@ final class RedundantInternalAccessibilityMarker: SourceGraphMutator { containingType, declLogicalType: declLogicalType, file: file, - visited: &visited + guard: &guard_ ) { return true } diff --git a/Sources/SourceGraph/SourceGraph.swift b/Sources/SourceGraph/SourceGraph.swift index aa6c72cb1e..3cd1ed7987 100644 --- a/Sources/SourceGraph/SourceGraph.swift +++ b/Sources/SourceGraph/SourceGraph.swift @@ -9,8 +9,8 @@ public final class SourceGraph { public private(set) var redundantProtocols: [Declaration: (references: Set, inherited: Set)] = [:] public private(set) var rootDeclarations: Set = [] public private(set) var redundantPublicAccessibility: [Declaration: Set] = [:] - public private(set) var redundantInternalAccessibility: [Declaration: (files: Set, suggestedAccessibility: Accessibility?)] = [:] - public private(set) var redundantFilePrivateAccessibility: [Declaration: (files: Set, containingTypeName: String?)] = [:] + public private(set) var redundantInternalAccessibility: [Declaration: Accessibility?] = [:] + public private(set) var redundantFilePrivateAccessibility: [Declaration: String?] = [:] public private(set) var rootReferences: Set = [] public private(set) var allReferences: Set = [] public private(set) var retainedDeclarations: Set = [] @@ -90,23 +90,12 @@ public final class SourceGraph { _ = redundantPublicAccessibility.removeValue(forKey: declaration) } - func markRedundantInternalAccessibility(_ declaration: Declaration, file: SourceFile, suggestedAccessibility: Accessibility?) { - if let existing = redundantInternalAccessibility[declaration] { - var files = existing.files - files.insert(file) - redundantInternalAccessibility[declaration] = (files: files, suggestedAccessibility: existing.suggestedAccessibility) - } else { - redundantInternalAccessibility[declaration] = (files: [file], suggestedAccessibility: suggestedAccessibility) - } + func markRedundantInternalAccessibility(_ declaration: Declaration, suggestedAccessibility: Accessibility?) { + redundantInternalAccessibility[declaration] = suggestedAccessibility } - func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile, containingTypeName: String?) { - if var existing = redundantFilePrivateAccessibility[declaration] { - existing.files.insert(file) - redundantFilePrivateAccessibility[declaration] = existing - } else { - redundantFilePrivateAccessibility[declaration] = (files: [file], containingTypeName: containingTypeName) - } + func markRedundantFilePrivateAccessibility(_ declaration: Declaration, containingTypeName: String?) { + redundantFilePrivateAccessibility[declaration] = containingTypeName } func markIgnored(_ declaration: Declaration) { diff --git a/Tests/Shared/SourceGraphTestCase.swift b/Tests/Shared/SourceGraphTestCase.swift index b713fb1a97..11c748e368 100644 --- a/Tests/Shared/SourceGraphTestCase.swift +++ b/Tests/Shared/SourceGraphTestCase.swift @@ -185,9 +185,9 @@ open class SourceGraphTestCase: XCTestCase { } if let suggestedAccessibility { - if let info = Self.graph.redundantInternalAccessibility[declaration] { - if info.suggestedAccessibility != suggestedAccessibility { - let actualText = info.suggestedAccessibility?.rawValue ?? "nil" + if let actual = Self.graph.redundantInternalAccessibility[declaration] { + if actual != suggestedAccessibility { + let actualText = actual?.rawValue ?? "nil" XCTFail("Expected suggested accessibility to be '\(suggestedAccessibility.rawValue)', but got '\(actualText)': \(declaration)", file: file, line: line) } } @@ -218,9 +218,9 @@ open class SourceGraphTestCase: XCTestCase { } if let containingTypeName { - if let info = Self.graph.redundantFilePrivateAccessibility[declaration] { - if info.containingTypeName != containingTypeName { - XCTFail("Expected containing type name to be '\(containingTypeName)', but got '\(info.containingTypeName ?? "nil")': \(declaration)", file: file, line: line) + if let actual = Self.graph.redundantFilePrivateAccessibility[declaration] { + if actual != containingTypeName { + XCTFail("Expected containing type name to be '\(containingTypeName)', but got '\(actual ?? "nil")': \(declaration)", file: file, line: line) } } }