見出し画像

Now in REALITY Tech #36 Peripheryを用いて REALITY iOSの未使用コードを自動検知した話

今週の「Now in REALITY Tech」は、最近追加したPeripheryでのiOSプロジェクトの静的解析とCI導入についてiOSチームからお送りします。

Peripheryとは

Peripheryは静的解析によりSwiftプロジェクトから未使用のコードを検出するツールです。

Homebrew、Swift Package Manager、Cocoapodsのpackage managerに対応しており簡易に導入可能となっています。

検出可能なコード

The goal of Periphery is to report instances of unused declarations. A declaration is a class, struct, protocol, function, property, constructor, enum, typealias, associatedtype, etc. As you'd expect, Periphery is able to identify simple unreferenced declarations, e.g a class that is no longer used anywhere in your codebase.

https://github.com/peripheryapp/periphery#analysis

上記の通り宣言されたclassやstruct、プロパティなどが検出可能で、他にも未使用のメソッド引数や不必要なアクセスレベルなどの高度な解析も可能となっています。

使い方

Peripheryで利用可能なコマンドは下記の通りで、解析には scan を使用します。
(※ Swift Package ManagerでPeripheryをinstallしています

❯ swift run periphery --help
USAGE: periphery <subcommand>

OPTIONS:
  -h, --help              Show help information.

SUBCOMMANDS:
  scan                    Scan for unused code
  check-update            Check for available update
  version                 Display this version of Periphery

scan コマンドで利用可能なオプションは下記の通りで、対象のTargetや解析対象から除外したいファイルの指定などが可能です。

❯ swift run periphery scan --help
OVERVIEW: Scan for unused code

USAGE: periphery scan [<options>] [<build-arguments> ...]

ARGUMENTS:
  <build-arguments>       Arguments following '--' will be passed to the underlying build tool, which is either 'swift build' or 'xcodebuild'
                          depending on your project

OPTIONS:
  --setup                 Enable guided setup
  --config <config>       Path to configuration file. By default Periphery will look for .periphery.yml in the current directory
  --workspace <workspace> Path to your project's .xcworkspace. Xcode projects only
  --project <project>     Path to your project's .xcodeproj - supply this option if your project doesn't have an .xcworkspace. Xcode projects
                          only
  --schemes <schemes>     Comma-separated list of schemes that must be built in order to produce the targets passed to the --targets option.
                          Xcode projects only (default: [])
  --targets <targets>     Comma-separated list of target names to scan. Required for Xcode projects. Optional for Swift Package Manager
                          projects, default behavior is to scan all targets defined in Package.swift (default: [])
  --format <format>       Output format (allowed: xcode, csv, json, checkstyle) (default: xcode)
  --index-exclude <index-exclude>
                          Path glob of source files which should be excluded from indexing. Declarations and references within these files
                          will not be considered during analysis. Multiple globs may be delimited by a pipe (default: [])
  --report-exclude <report-exclude>
                          Path glob of source files which should be excluded from the results. Note that this option is purely cosmetic, these
                          files will still be indexed. Multiple globs may be delimited by a pipe (default: [])
  --index-store-path <index-store-path>
                          Path to index store to use. Implies '--skip-build'
  --retain-public         Retain all public declarations - you'll likely want to enable this if you're scanning a framework/library project
  --disable-redundant-public-analysis
                          Disable identification of redundant public accessibility
  --retain-assign-only-properties
                          Retain properties that are assigned, but never used
  --retain-assign-only-property-types <retain-assign-only-property-types>
                          Comma-separated list of property types to retain if the property is assigned, but never read (default: [])
  --external-encodable-protocols <external-encodable-protocols>
                          Comma-separated list of external protocols that inherit Encodable. Properties of types conforming to these protocols
                          will be retained (default: [])
  --retain-objc-accessible
                          Retain declarations that are exposed to Objective-C implicitly by inheriting NSObject classes, or explicitly with
                          the @objc and @objcMembers attributes
  --retain-unused-protocol-func-params
                          Retain unused protocol function parameters, even if the parameter is unused in all conforming functions
  --clean-build           Clean existing build artifacts before building
  --skip-build            Skip the project build step
  --strict                Exit with non-zero status if any unused code is found
  --disable-update-check  Disable checking for updates
  --verbose               Enable verbose logging
  --quiet                 Only output results
  -h, --help              Show help information.

例えば、下記のコマンドを実行することで R.generated.swift ファイルを解析から除外し、アクセス修飾子が public である必要がないファイルを許容する解析となります。

❯ swift run periphery scan --project プロジェクト名 --targets ターゲット名 --schemes スキーム名 --index-exclude "src/R.generated.swift"  --disable-redundant-public-analysis

導入例

ローカル環境+Xcodeで解析

上記はターミナルアプリからCLIで実行する例を紹介しましたが、ターミナルの出力内容だと解析結果がわかりにくいため、Xcode上でwarningとして解析結果を可視化し、対象のコードにジャンプ出来るようにします。

導入方法はPeripheryのリポジトリのREADMEで詳細に記載されているのでここでは割愛します。

GitHub Pull Request で解析

ローカル環境+Xcodeで解析する方法では、プロジェクトの全コードを対象としていましたが、大きなプロジェクトなどにおいては小さく導入したいケースがあるでしょう。
そのようなケースではdanger-peripheryを使用することで、GitHubのPull Request(以下、PR)で発生した差分のみを対象に未使用コードの検知およびPRへのコメントが可能です。

下記は実際にdangerをbitriseで実行し、検出した内容です。

実際に使用した際のコメント

REALITY iOSでのPeripheryの運用

上記で紹介したGitHub PRへの導入は、REALITY iOSでは採用しませんでした。理由としては、Peripheryの解析時間が長いためCIのボトルネックとなるためです。
そのため、我々はBitriseのscheduling buildを使用して1日に1回深夜に実行し、実行結果をslackに通知するようにしました。

具体的には、リリース済みの最新バージョンのコードと現在開発中(develop branch)との差分があるファイルから、peripheryでscanした結果をslack通知するようにしています。

この方法により、未使用コードの自動検知をしつつ日々の開発に影響を与えないようにしています。

// Makefile
periphery:
	xcrun --sdk macosx swift run periphery scan --workspace "ワークスペース名" --schemes "スキーム名" --targets "ターゲット名" --index-exclude "**/R.generated.swift|**/*.{xib,storyboard}" --quiet --disable-redundant-public-analysis > scanned.txt // 解析
	VERSION=${VERSION} sh script/periphery/filter.sh // 対象versionとの差分を検出
	VERSION=${VERSION} sh script/periphery/slack_notify.sh // slack通知
 
 
// filter.sh
diff_files=$(git diff v${VERSION} --diff-filter=d --name-only -- "*.swift" -- ':(exclude)vLive-viewer-iosTests/*')
touch periphery_report.txt
for filename in ${diff_files[@]}; do
    grep $filename scanned.txt >> periphery_report.txt
done

exit 0
slackに通知している内容

まとめ

今回はPeripheryを用いた未使用コードの自動検知の導入を紹介しました。
開発しているプロジェクトの規模によって、導入方法を検討する必要がありますが、コードが不必要に増えて運用保守コストが上がる前の対策としては導入しがいがありそうです。
また、danger-peripheryを導入するとGitHub PRへのレビューコスト削減にも繋がるので検討してみてはいかがでしょうか?

REALITY では一緒に開発してくれるメンバーを大募集しています! まずは気軽にカジュアル面談も受け付けていますので、お気軽にお申し込みください!