Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2342c8f
Initial commit adding redundant internal and redundant fileprivate ac…
danwood Jul 25, 2025
4fd08d9
Update Sources/PeripheryKit/Results/OutputFormatter.swift
danwood Dec 15, 2025
40da6ce
remove some stuff Ian noticed in my PR
danwood Dec 15, 2025
490b99a
remove ‘referencedFiles’ which I’m no longer needing
danwood Dec 15, 2025
4258892
Redoing tests - WIP - moving into same module and rearranging
danwood Dec 15, 2025
0ffef85
Redo tests, found bug in source graph
danwood Dec 15, 2025
1977d4e
Remove files that got moved
danwood Dec 15, 2025
7dc47a7
Revert "Remove files that got moved"
danwood Dec 30, 2025
e9dfd39
remove old inline failure warning
danwood Dec 30, 2025
31edb01
try to deal with most of the warnings from `mise r scan`
danwood Dec 31, 2025
c99cef6
take out specifier completely to make `mise` happy
danwood Jan 1, 2026
343a655
After running `mise r lint` and `mise r gen-bazel-rules`
danwood Jan 1, 2026
945b778
restore paths to be fileprivate, since we were getting false positive.
danwood Jan 1, 2026
8e018df
readme update
danwood Jan 2, 2026
1e958be
A bit more documentation
danwood Jan 2, 2026
e782693
Handle implicit internal, fix false positives and false negatives, re…
danwood Jan 7, 2026
5907701
Fix accessibility warnings throughout the code base so that `mise r s…
danwood Jan 7, 2026
4c26976
Fix false positives caught by CI/Bazel building
danwood Jan 7, 2026
e6b9141
Make new function be private, conforming to new code
danwood Jan 12, 2026
d3e5536
Update to work with upstream changes, fixing external protocol check …
danwood Jan 20, 2026
495ed6e
Make sure that internal types used as return types of functions calle…
danwood Jan 24, 2026
1e8a649
don’t mark enum cases as needing to be private; fix fileprivate detec…
danwood Jan 25, 2026
cb07b03
Fix checking against an external protocol
danwood Jan 25, 2026
7804bfe
stored property type transitive exposure
danwood Jan 25, 2026
a306965
Fix more problems with same file
danwood Jan 26, 2026
12b8cc9
fix warning we found for newly introduced upstream property
danwood Jan 26, 2026
ee57001
Don’t mark #Preview blocks as fileprivate
danwood Jan 29, 2026
50d2448
For when retainSwiftUIPreviews, fix the detection of #Preview macro
danwood Jan 29, 2026
54f0725
solve some obscure false positive corner cases
danwood Jan 29, 2026
a176ff9
correctly mark children of symbol
danwood Jan 31, 2026
4a441f7
Refactor
danwood Feb 1, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
372 changes: 82 additions & 290 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions Sources/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ 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",
"SourceGraph/Mutators/RedundantProtocolMarker.swift",
"SourceGraph/Mutators/ResultBuilderRetainer.swift",
"SourceGraph/Mutators/StringInterpolationAppendInterpolationRetainer.swift",
Expand Down
14 changes: 12 additions & 2 deletions Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ 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: "show_nested_redundant_accessibility", defaultValue: false)
public var showNestedRedundantAccessibility: Bool

@Setting(key: "disable_unused_import_analysis", defaultValue: false)
public var disableUnusedImportAnalysis: Bool

Expand Down Expand Up @@ -215,10 +224,11 @@ 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,
$disableRedundantInternalAnalysis, $disableRedundantFilePrivateAnalysis, $showNestedRedundantAccessibility,
$disableUnusedImportAnalysis, $superfluousIgnoreComments, $retainUnusedImportedModules,
$externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $color,
$disableUpdateCheck, $strict, $indexStorePath,
Expand Down Expand Up @@ -246,7 +256,7 @@ public final class Configuration {
}
}

protocol AbstractSetting {
private protocol AbstractSetting {
associatedtype Value

var key: String { get }
Expand Down
110 changes: 61 additions & 49 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,151 +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")
private var disableRedundantInternalAnalysis: Bool = defaultConfiguration.$disableRedundantInternalAnalysis.defaultValue

@Flag(help: "Disable identification of redundant fileprivate accessibility")
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")
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
private 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
Expand Down Expand Up @@ -193,6 +202,9 @@ 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(\.$showNestedRedundantAccessibility, showNestedRedundantAccessibility)
configuration.apply(\.$disableUnusedImportAnalysis, disableUnusedImportAnalysis)
configuration.apply(\.$superfluousIgnoreComments, superfluousIgnoreComments)
configuration.apply(\.$retainUnusedImportedModules, retainUnusedImportedModules)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Frontend/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading