Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Peripheryを用いたiOSアプリ開発における未使用コードの検出とCIの構築方法

こんにちは、iOSエンジニアの三浦知明です。今年の1月にマネーフォワードに入社し、マネーフォワード Pay for Business(以下、P4B)のiOSアプリの開発を担当しています。新しい技術のキャッチアップや英語環境の適応など大変なこともありますが、周りの方に支えられ楽しく開発を進めることができています。 オンボーディングを終え、コードリーディングを進めていく上で、実際には使われていないコードがあるように見受けられました。これを安全に削除し、かつ継続的に検知していくために、Peripheryの導入を提案しました。

背景

私たちのチームが開発しているP4Bは、おかげさまで多くの方にご利用いただき、リリースから3年以上経過いたしました。しかし、長期運用に伴い、私たちはレガシーコードの蓄積という課題に直面していました。アプリの成長とともに、機能追加や改修を重ねる中で、どうしてもコードは肥大化していきます。特に、過去のプロジェクトメンバーが書いたコードや、仕様変更によって使われなくなったコードが、いつの間にか残ってしまうことがあります。 本記事ではPeripheryというツールを用いてSwiftプロジェクトにおける未使用のコードを検知する方法と、それをCI/CDツールを用いて定期実行する方法を説明します。

Peripheryとは

一言でまとめると「未使用コードの静的解析ツール」です。Peripheryは、単に使われていないコードを検出するだけでなく、未使用の関数引数や不必要なpublic指定に対しても警告を出してくれます。

例) 未使用の関数引数

Parameter 'age' is unused

func greet(name: String, age: Int) {
  print("Hello, \(name)!") 
}

例) 不必要なpublic指定

Struct 'Hoge' is declared public, but not used outside of XX

// モジュールの外で呼ばれることはない
public Struct Hoge {

}

3種類の導入パターン

Peripheryの導入には主に3つの方法があります。それぞれの方法にはメリットとデメリットがあり、プロジェクトのニーズに応じて選択することが重要です。

導入方法 メリット デメリット
Xcodeに統合しAggregateターゲットを用いて、未使用コードの警告をXcode上で表示する - 開発者がAggregateターゲットでビルドした際に未使用コードを検知できる
- 問題箇所をXcode上で確認し、即座に修正できる
- Xcode上で警告を確認できるため、他のツールとの連携が不要
- Aggregateターゲットの設定が必要
- 大規模なプロジェクトでは、解析に時間がかかる場合がある
CI/CDツールを用いて、定期的にPeripheryを実行し、不要なコードを検知する - 定期的な実行により、未使用コードの蓄積を防ぐことができる
- CI/CDツールとの連携により、自動化が可能
- チーム全体で未使用コードの情報を共有できる
- PR作成時に実行するよりはコストを抑えることができる
- 不要なコードの発見が遅れる可能性がある
PR(Pull Request)作成時にCI/CDツールを用いてPeripheryを実行し、不要なコードを検知する - コードレビュー時に未使用コードを検知できるため、コードの品質向上に繋がる
- CI/CDツールとの連携により、自動化が可能
- マージ前に問題を修正できるため、手戻りを減らせる
- CI/CDの時間が増加し、PRの承認に時間がかかる場合がある
- PRの度にCIを実行することによりコストがかかる

私たちのプロジェクトでの選択

私たちのプロジェクトでは、上記の方法の中から「Xcodeに統合しAggregateターゲットを用いて未使用コードの警告をXcode上で表示する方法」と「CI/CDツールを用いて定期的にPeripheryを実行し、不要なコードを検知する方法」を採用しました。
これにより、開発者が日常的に未使用コードを確認できる環境を整えつつ、定期的なチェックでコードの品質を維持しています。
「PR作成時にCI/CDツールを用いてPeripheryを実行し、不要なコード検知する方法」を選択しなかった理由としては、CI/CDの時間が増加しPRの承認に時間がかかることから、私たちのプロジェクトでは採用しませんでした。

Xcodeに統合する方式では、以下のようにエディタ上に警告が表示されるようになります。

OurSelection-1

定期的に実行するCIワークフローとしては、Peripheryを実行するだけでなく、以下のような形でSlackに結果を投稿する実装も組み込みました。

OurSelection-2

採用しなかったアイデアも含め、ここからは上記の3つの実現手段の実装を紹介します。

Peripheryのセットアップ

Peripheryのインストール

HomeBrew、Mint、Bazelでインストールできます。私たちのプロジェクトでは、すでにいくつからのライブラリーをMintfileを用いてライブラリの管理を行っているため、PeripheryもMintを用いてインストールをしました。以下に示す例は全てMintを使用していることを前提としています。

mint install peripheryapp/periphery

periphery.ymlの作成

プロジェクトのルートに設定ファイルであるperiphery.ymlを作成します。

project: {プロジェクト名}.xcodeproj
schemes:
  - {スキーム名}
index_exclude:
  - {除外したいフォルダのPath}
retain_swift_ui_previews: true
build_arguments:
  - -destination
  - 'generic/platform=iOS Simulator'

不要なコードを検知する

scanコマンドを使用して不要なコードを検知します。ここまでの手順で自分のローカル環境で不要なコードを検知することができます。

mint run periphery scan --config periphery.yml   

Xcodeに統合し、Aggregateターゲットを用いて未使用コードの警告をXcode上で表示する

公式ドキュメントで詳細されている通りの手順で進めていきます。

Aggregate Targetの作成

プロジェクトを選択し、左下にあるプラスボタンからAggregateを選択します。Product NameはPeripheryにします。 Integrate-1

Aggregate Targetの作成

TARGETSのPeripheryを選択し、Build Phasesを選択します。
左上にあるプラスボタンからNew Run Script Phaseを選択します。

Integrate-2

Integrate-3

periphery scan を実行するスクリプトを書きます。

 export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:${PATH+:$PATH}";
 mint run periphery scan --config periphery.yml

実行する

Aggregate Targetを選択し、Runをクリックします。しばらくすると解析結果が表示されます。

Integrate-4

CI/CDツールを用いて、定期的にPeripheryを実行し、不要なコードを検知した結果をSlackに通知する

ここではGitHub Actionsを用いて定期的にCIワークフローを実行する方法を解説します。

作成したワークフロー

まずは今回作成したワークフローを紹介します。Peripheryを実行し、その結果をSlackに送信する、という処理を毎週行うものです。

name: Run Periphery

on:
  workflow_dispatch:
  schedule:
    - cron: '0 9 * * 0'

jobs:
  periphery:
    runs-on: macos-15

    env:
      MINT_PATH: .mint/lib
      MINT_LINK_PATH: .mint/bin
      
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Select Xcode Version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app
        
      - name: Install mint
        run: brew install mint

      - name: Cache Mint Packages
        uses: actions/cache@v4
        with:
          path: .mint
          key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
          restore-keys: |
            ${{ runner.os }}-mint-

      - name: Run Periphery
        id: periphery
        run: |
          mint run periphery scan --config periphery.yml > periphery_result.txt
          cat periphery_result.txt | grep -v "^\* " | grep -v "🌱" | sed '/^$/d' > temp.txt && mv temp.txt periphery_result.txt || true
          
          if [ ! -s periphery_result.txt ]; then
            echo "No unused codes found 🎉" > periphery_result.txt
          fi
          echo "result<<EOF" >> $GITHUB_OUTPUT
          cat periphery_result.txt >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Send Slack Notification
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_URL }}
          payload: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*🔍 Periphery Scan Result*\n```${{ steps.periphery.outputs.result }}```"
                  }
                }
              ]
            }

それぞれについて解説していきます。

スケジュール設定

定期実行をするためにスケジュール設定をしています。0 9 \* \* \* 0 は協定世界時の毎週日曜9:00で、日本時間では毎週日曜日18:00を示します。

on:
  workflow_dispatch:
  schedule:
    - cron: '0 9 * * 0'

ワークスペースにクローン後、Peripheryのインストール

リポジトリのコードをワークスペースにダウンロードし、使用するXcodeのバージョンを指定しています。その後Mintをインストールします。

      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Select Xcode Version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app
        
      - name: Install mint
        run: brew install mint

キャッシュを設定

GitHub Actions内のリポジトリでmintをキャッシュする方法があるのでそのまま実装します。

    env:
      MINT_PATH: .mint/lib
      MINT_LINK_PATH: .mint/bin

      - name: Cache Mint Packages
        uses: actions/cache@v4
        with:
          path: .mint
          key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
          restore-keys: |
            ${{ runner.os }}-mint-

Peripheryの実行

Peripheryのscanを実行し実行結果をperiphery_result.txtに書き出しています。 しかしながら、MintでPeripheryをインストールする際に出力される🌱 Cloning periphery 3.0.1のようなログやPeripheryのscanを実行時に出力される* Inspecting project...のようなログまで書き出してしまいます。
そのためcat periphery_result.txt | grep -v "^\* " | grep -v "🌱" | sed '/^$/d' > temp.txt && mv temp.txt periphery_result.txt || trueで"🌱"、アスタリスク、空白を削除し不要なログを除去しています。 最後にPeripheryの実行結果をファイルから読み込み、 $GITHUB_OUTPUT にresultという名前で保存しています。

       - name: Run Periphery
        id: periphery
        run: |
          mint run periphery scan --config periphery.yml > periphery_result.txt
          cat periphery_result.txt | grep -v "^\* " | grep -v "🌱" | sed '/^$/d' > temp.txt && mv temp.txt periphery_result.txt || true
          
          if [ ! -s periphery_result.txt ]; then
            echo "No unused codes found 🎉" > periphery_result.txt
          fi
          echo "result<<EOF" >> $GITHUB_OUTPUT
          cat periphery_result.txt >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

Slackに通知

Run Peripheryステップで整形したログをSlackに通知します。 SLACK_URLにはSlackチャンネルのWebhook URLが設定されており、GitHubのシークレットに保存しています。 前のステップの実行結果を${{ steps.periphery.outputs.result }}で取り出し、Slackで表示されるメッセージとして設定しています。

     - name: Send Slack Notification
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_URL }}
          payload: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*🔍 Periphery Scan Result*\n```${{ steps.periphery.outputs.result }}```"
                  }
                }
              ]
            }

PR作成時にCI/CDツールを用いてPeripheryを実行し、不要なコードを検知する

最後に、PR作成時にPeripheryを実行する方法を紹介します。本セクションのコードは個人のリポジトリで公開しているので参考にしてみてください。

前準備

danger-periphery

Dangerという自動コードレビューツールとPeripheryを組み合わせたツールです。これにより、PR作成時に未使用コードの検出を自動化できます。

Danger

Dangerは、コードレビュープロセスを自動化するためのツールです。

Gemfileに必要なライブラリーをまとめます。

source "https://rubygems.org"

gem "danger"
gem "faraday-retry"
gem "danger-periphery"

periphery.ymlの作成

先ほどのperiphery.ymlと大きく違う箇所はRun Dangerでdangerを実行している点です。 MintとGemfile経由で必要なライブラリをインストール後に実行しています。

name: Periphery (Detect Unused Code in PR)

on:
  pull_request:
    branches:
      - main
      - develop
      - master

jobs:
  periphery:
    runs-on: macos-15
    permissions:
      pull-requests: write
      statuses: write
      contents: read
    timeout-minutes: 10

    env:
      MINT_PATH: .mint/lib
      MINT_LINK_PATH: .mint/bin
      
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Select Xcode Version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app
        
      - name: Install mint
        run: |
          brew install mint

      - name: Cache Mint Packages
        uses: actions/cache@v4
        with:
          path: .mint
          key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
          restore-keys: |
            ${{ runner.os }}-mint-

      - name: Install depenencies
        run: |
          gem install bundler
          bundle install

      - name: Set Periphery Binary Path
        run: |
          echo "PERIPHERY_PATH=$(mint which periphery | tail -n 1 | tr -d '\n')" >> $GITHUB_ENV

      - name: Run Danger
        env:
          echo "Periphery Binary Path: $PERIPHERY_PATH"
          DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: bundle exec danger --verbose

Dangerfileの作成

periphery.ymlで取得したPERIPHERY_PATHをperiphery.binary_pathに設定します。 その後periphery.scanを実行し、検知したワーニングを取得しています。

periphery.binary_path = ENV['PERIPHERY_PATH']
# すべてのファイルに対して警告を表示
periphery.scan_all_files = true

# 警告をエラーとして扱う
periphery.warning_as_error = true

# PRの変更差分に対して未使用コードを検出
periphery.scan(
  project: "Peripheryperiodically.xcodeproj",
  schemes: ["Peripheryperiodically"],
  clean_build: true,
  build_args: "-sdk iphonesimulator"
) do |violation|
  violation.message = "Pay attention please! #{violation.message}"
end

PRを出すとCIワークフローが実行され、不要なコードの検出ができました。 PR-1

まとめ

この記事では、P4BのiOSアプリにPeripheryを導入し、不要なコードの検知とそれを定期的に実行するための取り組みを紹介しました。

Peripheryは、未使用の関数引数や不必要なpublic指定など、きめ細かい未使用コードの検出が可能な強力なツールです。導入にあたっては、Xcodeへの統合、CI/CDツールとの連携など、プロジェクトやチームの状況に合わせた方法を選択できます。

私たちのチームでは、Xcodeへの統合とCI/CDツールを用いた定期実行を組み合わせることで、開発中の品質向上と長期的なコードベースの品質維持を目指しました。結果として、コードの保守性、可読性を向上させ、それを維持するために必要な有意義な取り組みになったと感じています。この記事がPeripheryの導入を検討されている方や、レガシーコードに悩まれている方の参考になれば幸いです。

さいごに

マネーフォワード 福岡開発拠点ではエンジニアを募集しています! よろしくお願いします!

hrmos.co

福岡開発拠点のサイトもあるので、ぜひご覧ください!

fukuoka.moneyforward.com