Finding unused code with Periphery

The Muse codebase is over 5 years old with over 350,000 lines of Swift, and I’m sure is filled with more than a few archeological code-fossils. Like any startup (frankly, like literally every code project), it’s difficult to prune old unused code while keeping up velocity of new features. Code cleanliness is always a tradeoff with velocity, and often comes in second place. That’s why I love tooling that can help automate this otherwise brutally slow manual task.

I was recently told about periphery, a command line tool to find unused code in Swift projects. What’s a day off work for but for having fun playing with new tools? First up, install:

$ brew install periphery

God bless brew, amirite.

Next up, the README suggests we run its setup for a handheld first run:

$ periphery scan --setup
Welcome to Periphery!
This guided setup will help you select the appropriate configuration for your project.

* Inspecting project...

Select build targets to analyze:
? Delimit choices with a single space, e.g: 1 2 3, or 'all' to select all options
1 AppKitBridge
2 Muse
3 Muse Integration Tests
4 Muse Tests
5 MuseShare
6 SparklePlugin

...

Super easy to get setup, what a pleasure! I selected the targets I needed, then the schemes. Next it asks about Objective-C code, and I selected Yes to assume obj-c code is in use. Muse has only a little, but some interactions with UIKit or AppKit still reach into Objective-C.

Assume Objective-C accessible declarations are in use?
? Declarations exposed to the Objective-C runtime explicitly with @objc, or implicitly by inheriting NSObject will be assumed to be in use. Choose 'No' if your project is pure Swift.
(Y)es/(N)o > y

Next, it asked about assuming all public declarations are in use – which would be very useful when building a framework or library, for for an app like Muse I answered No.

Assume all 'public' declarations are in use?
? You should choose 'Yes' here if your public interfaces are not used by any selected build target, as may be the case for a framework/library project.
(Y)es/(N)o > n

Last, I saved the configuration to .periphery.yml and let the first scan run. Luckily, it found the codebase squeaky clean! 😉

I use Sourcery to auto-generate some connector files between the custom Muse sync codebase and Muse-the-application codebase. There’s also a fair bit of old CoreData code that’s still in the codebase so that very old Muse libraries can be migrated to the current sync world. For each of these, I don’t need them included in periphery‘s output.

To remove them, I used the --report-exclude command line option to remove a few paths that I don’t need in the report. I’m also not concerned with redundant public, so I included
--disable-redundant-public-analysis
too. Last, I’m not worried about unused function parameters, so i used --retain-unused-protocol-func-params. I ran with --verbose which shows the .yml configuration changes I needed to make to always run with these excluded.

Last, I setup an Aggregate target in Xcode so that I can run periphery and see unused code in the Issue navigator. I updated the Build Settings to use iOS, added a User Defined setting for SUPPORTS_MACCATALYST=YES, and added a Run Script phase with the following:

export PATH="$PATH:/opt/homebrew/bin"

if which periphery >/dev/null; then
    periphery scan --config "${SRCROOT}/.periphery.yml"
else
    echo "warning: periphery not installed, install from https://github.com/peripheryapp/periphery"
fi

Now, when building the Periphery aggregate target within Xcode, I can see all of the unused code inline right inside Xcode.

Just a little bit of clean-up work to do! 😅

Leave a Reply

Your email address will not be published. Required fields are marked *