Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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",
Expand Down
7 changes: 7 additions & 0 deletions Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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

Expand Down Expand Up @@ -204,6 +210,7 @@ public final class Configuration {
$project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat,
$retainPublic, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible,
$retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis,
$disableRedundantInternalAnalysis, $disableRedundantFilePrivateAnalysis,
$disableUnusedImportAnalysis, $retainUnusedImportedModules, $externalEncodableProtocols, $externalCodableProtocols,
$externalTestCaseClasses, $verbose, $quiet, $disableUpdateCheck, $strict, $indexStorePath, $skipBuild,
$skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults, $jsonPackageManifestPath,
Expand Down
8 changes: 8 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ struct ScanCommand: FrontendCommand {
@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

Expand Down Expand Up @@ -174,6 +180,8 @@ struct ScanCommand: FrontendCommand {
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(\.$retainUnusedImportedModules, retainUnusedImportedModules)
configuration.apply(\.$externalEncodableProtocols, externalEncodableProtocols)
Expand Down
10 changes: 10 additions & 0 deletions Sources/PeripheryKit/Results/OutputFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ extension OutputFormatter {
"redundantProtocol"
case .redundantPublicAccessibility:
"redundantPublicAccessibility"
case .redundantInternalAccessibility:
"redundantInternalAccessibility"
case .redundantFilePrivateAccessibility:
"redundantFilePrivateAccessibility"
}
}

Expand Down Expand Up @@ -65,6 +69,12 @@ extension OutputFormatter {
case let .redundantPublicAccessibility(modules):
let modulesJoined = modules.sorted().joined(separator: ", ")
description += " is declared public, but 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 += " is declared internal, but 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 += " is declared fileprivate, but not used outside of \(filesJoined)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message isn't quite right, how about:

Suggested change
description += " is declared fileprivate, but not used outside of \(filesJoined)"
description += " is declared fileprivate, but not used outside its enclosing scope in \(filesJoined)"

}
} else {
description += "unused"
Expand Down
2 changes: 2 additions & 0 deletions Sources/PeripheryKit/ScanResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public struct ScanResult {
case assignOnlyProperty
case redundantProtocol(references: Set<Reference>, inherited: Set<String>)
case redundantPublicAccessibility(modules: Set<String>)
case redundantInternalAccessibility(files: Set<SourceFile>)
case redundantFilePrivateAccessibility(files: Set<SourceFile>)
}

let declaration: Declaration
Expand Down
12 changes: 11 additions & 1 deletion Sources/PeripheryKit/ScanResultBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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]()
Expand Down Expand Up @@ -40,10 +42,18 @@ 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 allAnnotatedDeclarations = annotatedRemovableDeclarations +
annotatedAssignOnlyProperties +
annotatedRedundantProtocols +
annotatedRedundantPublicAccessibility
annotatedRedundantPublicAccessibility +
annotatedRedundantInternalAccessibility +
annotatedRedundantFilePrivateAccessibility

return allAnnotatedDeclarations
.filter {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SourceGraph/Elements/Declaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public final class Declaration {
public var related: Set<Reference> = []
public var isImplicit: Bool = false
public var isObjcAccessible: Bool = false
public var referencedFiles: Set<SourceFile>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this property? I see that we insert items into it, but it doesn't appear to be used anywhere.


private let hashValueCache: Int

Expand Down Expand Up @@ -290,6 +291,7 @@ public final class Declaration {
self.kind = kind
self.usrs = usrs
self.location = location
self.referencedFiles = [location.file]
hashValueCache = usrs.hashValue
}

Expand Down
13 changes: 13 additions & 0 deletions Sources/SourceGraph/Mutators/ExtensionReferenceBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Declaration> {
let filePrivateDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.fileprivate) }
return filePrivateDeclarations.flatMapSet { descendentFilePrivateDeclarations(from: $0) }.union(filePrivateDeclarations)
}
}
Original file line number Diff line number Diff line change
@@ -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<Declaration> {
let internalDeclarations = decl.declarations.filter { !$0.isImplicit && $0.accessibility.isExplicitly(.internal) }
return internalDeclarations.flatMapSet { descendentInternalDeclarations(from: $0) }.union(internalDeclarations)
}
}
21 changes: 21 additions & 0 deletions Sources/SourceGraph/SourceGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public final class SourceGraph {
public private(set) var redundantProtocols: [Declaration: (references: Set<Reference>, inherited: Set<Reference>)] = [:]
public private(set) var rootDeclarations: Set<Declaration> = []
public private(set) var redundantPublicAccessibility: [Declaration: Set<String>] = [:]
public private(set) var redundantInternalAccessibility: [Declaration: Set<SourceFile>] = [:]
public private(set) var redundantFilePrivateAccessibility: [Declaration: Set<SourceFile>] = [:]
public private(set) var rootReferences: Set<Reference> = []
public private(set) var allReferences: Set<Reference> = []
public private(set) var retainedDeclarations: Set<Declaration> = []
Expand Down Expand Up @@ -85,6 +87,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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess all PRs in this repo should be self-checked by periphery 😁


func markRedundantFilePrivateAccessibility(_ declaration: Declaration, file: SourceFile) {
redundantFilePrivateAccessibility[declaration, default: []].insert(file)
}

func unmarkRedundantFilePrivateAccessibility(_ declaration: Declaration) {
_ = redundantFilePrivateAccessibility.removeValue(forKey: declaration)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused?


func markIgnored(_ declaration: Declaration) {
_ = ignoredDeclarations.insert(declaration)
}
Expand Down Expand Up @@ -143,6 +161,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<Reference>) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/SourceGraph/SourceGraphMutatorRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ inlinableFunction()
// Associated types
_ = PublicInheritedAssociatedTypeClass().items
_ = PublicInheritedAssociatedTypeDefaultTypeClass().items

// Internal accessibility tests
// _ = InternalPropertyUsedInExtension() // Commented out for now
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would we need to uncomment this?

Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// InternalPropertyOwner.swift
internal class InternalPropertyOwner {
internal var value: Int = 42
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading