Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
626ad97
Initial commit adding redundant internal and redundant fileprivate ac…
danwood Jul 25, 2025
4da84e5
Merge branch 'peripheryapp:master' into master
danwood Oct 10, 2025
e7ccc8b
Merge branch 'peripheryapp:master' into master
danwood Dec 11, 2025
97cb91b
Merge branch 'peripheryapp:master' into master
danwood Dec 15, 2025
bd369a8
Update Sources/PeripheryKit/Results/OutputFormatter.swift
danwood Dec 15, 2025
3e13ecb
remove some stuff Ian noticed in my PR
danwood Dec 15, 2025
262b3f9
remove ‘referencedFiles’ which I’m no longer needing
danwood Dec 15, 2025
431ac49
Redoing tests - WIP - moving into same module and rearranging
danwood Dec 15, 2025
17e0ee8
Redo tests, found bug in source graph
danwood Dec 15, 2025
882e58d
Remove files that got moved
danwood Dec 15, 2025
425e438
Update OutputFormatter to match format found in new changes upstream
danwood Dec 30, 2025
f8650f8
Merge remote-tracking branch 'upstream/master'
danwood Dec 30, 2025
0c71d32
Revert "Remove files that got moved"
danwood Dec 30, 2025
5ad3a85
remove old inline failure warning
danwood Dec 30, 2025
0543aa1
try to deal with most of the warnings from `mise r scan`
danwood Dec 31, 2025
bde0395
take out specifier completely to make `mise` happy
danwood Jan 1, 2026
62859d1
After running `mise r lint` and `mise r gen-bazel-rules`
danwood Jan 1, 2026
99b7fe9
restore paths to be fileprivate, since we were getting false positive.
danwood Jan 1, 2026
06ca954
readme update
danwood Jan 2, 2026
bbc7990
A bit more documentation
danwood Jan 2, 2026
6776d51
Merge remote-tracking branch 'upstream/master'
danwood Jan 6, 2026
d4483b0
Handle implicit internal, fix false positives and false negatives, re…
danwood Jan 7, 2026
35357b8
Fix accessibility warnings throughout the code base so that `mise r s…
danwood Jan 7, 2026
9868cdd
Fix false positives caught by CI/Bazel building
danwood Jan 7, 2026
862c352
Merge with upstream changes and resolve commits
danwood Jan 12, 2026
0a1f5d6
Make new function be private, conforming to new code
danwood Jan 12, 2026
cc548d1
Merge branch 'peripheryapp:master' into master
danwood Jan 19, 2026
c9338c6
Update to work with upstream changes, fixing external protocol check …
danwood Jan 20, 2026
7c26750
Make sure that internal types used as return types of functions calle…
danwood Jan 24, 2026
ecaee87
don’t mark enum cases as needing to be private; fix fileprivate detec…
danwood Jan 25, 2026
cc11547
Fix checking against an external protocol
danwood Jan 25, 2026
c368910
stored property type transitive exposure
danwood Jan 25, 2026
b13d41a
Fix more problems with same file
danwood Jan 26, 2026
8a22f74
Merge branch 'master' into master
danwood Jan 26, 2026
c5f28ee
fix warning we found for newly introduced upstream property
danwood Jan 26, 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 @@ -125,6 +125,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 @@ -172,5 +174,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 }}
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 @@ -77,7 +77,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 @@ -212,10 +221,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 @@ -243,7 +253,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?

private static let defaultConfiguration = Configuration()

Expand Down Expand Up @@ -190,6 +199,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