Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

AndroidチームのIDE警告ゼロへの取り組み

 こんにちは。エンジニアのshogo4405です。普段は、ミラティブで開発しながら、余暇にOSSのHaishinKit*1*2をつくっています。本稿では、Androidチームで取り組んだAndroid Studioから出てくる警告対応ゼロの仕組み化について紹介します。

はじめに

 筆者は、警告は一つ一つ適切なアクションを行うべき対象と考えています。警告一つ一つに意味を持っています。リリース後にクラッシュレポートを見て「あー警告」で指摘されてたという失敗経験もあります。チーム開発においては、IDEの警告ゼロが当然になるように働かきかけています。

 Androidチームでは、2021年第3四半期のOKRの一つとして警告ゼロ対応を掲げました。着手当初は278 warnings, 306 weak warnings, 75 infosの警告等がありました。

ゼロ警告へのアプローチ

 まず初めにチームで実施するためにあたって先にどのように警告対応をとるか方針を立てました。以下は、ミラティブにおける警告ゼロのアプローチになります。

  1. 件数が少なく今後の発生が低いと推測できる警告については手動対応する
  2. 件数がおおく今後の発生が高いと推測できる警告については自動対応する
    • スクリプトを作成して次回以降も利用できるようにするということ

手動対応

 先ずは、手動対応のアプローチについて紹介します。Android Studioには、IDEのクリック操作で警告を修正する機能があります。この機能を利用して修正を行っていきました。Android Studioメニューの[Analyze][Inspect Code...]でプロジェクト内のコードを解析して一覧表示してくれます。

f:id:shogo4405:20211021171816p:plain

  • JavaからKotlinにマイグレーションしたときに発生する警告の対応
  • 単純に置換で済ませることができる警告は、正規表現とIDEの力を借りて置換を実施
    • privateの付け忘れ。variables?.method() のようにつける必要のない?の警告の対応など
    • protectedprivateにできる警告の対応
  • Koin起因のNo need cast警告の対応
  • 利用しているがIDEで利用を検知できない警告については@Suppressのアノテーションをつけることで対応

自動対応

 次に、自動対応のアプローチについて紹介します。ミラティブの場合では利用していないシンボル系の警告が多数を占めていました。特に開発の過程で利用しなくなったクラスは気付きにくく自動的に削除できるようにしたくバッチ処理を検討しました。

CLIベースによる処理

 自動化するにあたって[Inspect Code...]の結果がxmlやjsonなどで欲しくなりました。調査したところ、CLIベースでも得ることができるようで、コマンドラインからコードインスペクションを実行する | IntelliJ IDEA こちらの記事を参考にし、Android Studioでも同様のことを行いました。

実行

$ '/Applications/Android Studio.app/Contents/bin/inspect.sh' ~/MyProject ~/MyProject/inspect.xml ~/MyProject/InspectionResults

inspect.xml

<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
  </profile>
</component>

InspectionResultsのディレクトリの中身

 実行してしばらく待つと次のような結果を得られます。

shogo4405@XXXX inspect % ls -all
total 86912
drwxr-xr-x  40 shogo4405  staff      1280 10  3 13:12 .
drwxr-xr-x   7 shogo4405  staff       224 10  3 23:16 ..
-rw-r--r--   1 shogo4405  staff   1167642 10  3 07:41 .descriptions.xml
-rw-r--r--   1 shogo4405  staff      1157 10  3 07:41 AndroidLintAllowBackup.xml
-rw-r--r--   1 shogo4405  staff       725 10  3 07:41 AndroidLintAuthLeak.xml
-rw-r--r--   1 shogo4405  staff       740 10  3 07:41 AndroidLintGradleDependency.xml
-rw-r--r--   1 shogo4405  staff      6618 10  3 07:41 AndroidLintHardcodedText.xml
-rw-r--r--   1 shogo4405  staff       838 10  3 07:41 AndroidLintInefficientWeight.xml
-rw-r--r--   1 shogo4405  staff       750 10  3 07:41 AndroidLintIntentFilterExportedReceiver.xml
-rw-r--r--   1 shogo4405  staff      1624 10  3 07:41 AndroidLintObsoleteLayoutParam.xml
-rw-r--r--@  1 shogo4405  staff       930 10  3 07:41 AndroidLintOverdraw.xml
-rw-r--r--   1 shogo4405  staff      3876 10  3 07:41 AndroidLintPxUsage.xml
-rw-r--r--   1 shogo4405  staff      4401 10  3 07:41 AndroidLintRtlHardcoded.xml
-rw-r--r--@  1 shogo4405  staff      4482 10  3 07:41 AndroidLintUnusedResources.xml
-rw-r--r--   1 shogo4405 staff      1485 10  3 07:41 CanBeParameter.xml
-rw-r--r--   1 shogo4405  staff   2297884 10  3 07:41 CheckTagEmptyBody.xml
-rw-r--r--   1 shogo4405  staff       782 10  3 07:41 DifferentStdlibGradleVersion.xml
-rw-r--r--   1 shogo4405  staff      1729 10  3 07:41 GrDeprecatedAPIUsage.xml
-rw-r--r--   1 shogo4405  staff       821 10  3 07:41 HasPlatformType.xml
... 省略

xmlファイルの中身

 .descriptions.xmlファイルを除き、次のファイルフォーマットでした。<problems>を親とする<problem>が見えます。

<problems>
<problem>
  <file>file://$PROJECT_DIR$/app/src/main/AndroidManifest.xml</file>
  <line>12</line>
  <module>HaishinKit.kt.app</module>
  <package>&lt;default&gt;</package>
  <entry_point TYPE="file" FQNAME="file://$PROJECT_DIR$/app/src/main/AndroidManifest.xml" />
  <problem_class id="AndroidLintAllowBackup" severity="WARNING" attribute_key="WARNING_ATTRIBUTES">AllowBackup/FullBackupContent Problems</problem_class>
  <hints />
  <description>&lt;html&gt;On SDK version 23 and up, your app data will be automatically backed up and restored on app install. Consider adding the attribute &lt;code&gt;android:fullBackupContent&lt;/code&gt; to specify an &lt;code&gt;@xml&lt;/code&gt; resource which configures which files to backup, or just set &lt;code&gt;android:fullBackupOnly=true&lt;/code&gt;. More info: &lt;a href=&quot;https://developer.android.com/guide/topics/data/autobackup&quot;&gt;https://developer.android.com/guide/topics/data/autobackup&lt;/a&gt;&lt;/html&gt;</description>
  <highlighted_element>android:allowBackup</highlighted_element>
  <language>XML</language>
  <offset>8</offset>
  <length>19</length>
</problem></problems>

利用していないシンボルのFastlaneカスタムActionでの削除処理

 ミラティブのコードで一番指摘が多かったのはUnusedSymbol系の警告です。FastlaneのカスタムActionを作成しinspect.shの実行結果を元にプログラムベースで削除できるようにしました。実際のコードは、本稿の最後に掲載しました。

$ bundle exec fastlane action inspect_fix_unused_symbol

Dangerプラグイン

 ミラティブのiOS、Androidチーム共にDangerを利用しており、機械的なコードレビューはDangerに任せています。inspect.shで得られたxmlを見ながら、これDangerで指摘できると便利だよなと思い。Dangerプラグインを作成しました*3。Android Studioから出る警告は控え目なため。よく見過ごします。Dangerで指摘できれば少なくとも見過ごすということは無くなります。

Android Studio

f:id:shogo4405:20211021182207p:plain

Danger

f:id:shogo4405:20211021182218p:plain

 このDangerプラグインはOSSで公開してあります。利用方法はこちらをご覧ください。尚inspect.shの実行に時間がかかるためミラティブのプロジェクトでどのように導入していくかは検討段階です。

github.com

終わりに

 2021年の9月上旬に見事、Android Studioから出てきた278 warnings0になりました。OKR達成で🏆です。2021年10月中も、定期的に利用していないシンボル削除ツールを動かすことにより警告をゼロを保っています。

We are hiring!

 ミラティブでは一緒にアプリを作ってくれるAndroidエンジニアを募集しています! 警告ゼロの環境で一緒に開発をしませんか。 www.mirrativ.co.jp speakerdeck.com


付録

作成したコードを置いておきます。

UnusedSymbol系を対応するFastlaneのカスタムAction

require 'rexml/document'

module Fastlane
  module Actions
    class InspectFixUnusedSymbolAction < Action
      def self.run(params)
        return [] unless File.exists?('fastlane/inspect/UnusedSymbol.xml')
        results = {}
        results[:class] = self.strip_class(params)
        results
      end

      def self.description
        '[Inspect] 利用していないクラスを削除します。'
      end

      def self.available_options
        [
        ]
      end

      def self.is_supported?(platform)
        platform == :android
      end

      def self.strip_class(params)
        results = []
        targets = {}
        xml =
          REXML::Document.new(File.read('fastlane/inspect/UnusedSymbol.xml'))
        xml
          .get_elements('problems/problem')
          .each do |problem|
            if problem
                 .get_elements('description')
                 .first
                 .text
                 .start_with?('Class') ||
                 problem
                   .get_elements('description')
                   .first
                   .text
                   .start_with?('Object')
              file =
                problem
                  .get_elements('file')
                  .first
                  .text
                  .gsub('file://$PROJECT_DIR$/', '')
              className = problem.get_elements('highlighted_element').first.text
              if file.end_with?("#{className}.kt")
                begin
                  File.delete(file)
                  results.push(file)
                rescue StandardError
                end
              else
                lines = File.read(file).split("\n")
                line = problem.get_elements('line').first.text.to_i - 1

                brace = 0
                has_brace = false
                data = [line]
                for num in (line)..(lines.count - 1)
                  lines[num].each_char do |c|
                    case c
                    when '{'
                      brace += 1
                    when '}'
                      brace -= 1
                      has_brace = true
                    end
                  end
                  if has_brace && brace == 0
                    data.push(num)
                    break
                  end
                end
                data.push(line)

                targets[file.to_s] = [] if targets[file.to_s].nil?
                targets[file.to_s].push(data)
              end
            end
          end

        self.strip_file(targets)
      end

      private

      def self.strip_file(targets)
        results = []

        targets.each do |file, value|
          text = File.read(file.to_s)
          lines = text.split("\n")
          value.reverse.each do |v|
            len = v[1] - v[0] + 1
            lines.slice!(v[0], len)

            # アノテーション
            (-(v[0] - 1)..0).each do |i|
              if lines[i * -1].include?('@')
                lines.delete_at(i * -1)
              else
                break
              end
            end

            result = lines.join("\n") + "\n"
            if text != result
              results.push(file)
              File.write(file.to_s, result)
            end
          end
        end
        results
      end
    end
  end
end

 

*1:https://github.com/shogo4405/HaishinKit.kt

*2:本稿のスクリーンショットや動作結果は、HaishinKitを利用しています

*3:GitHub上で検索しましたがが見つけられず自作をしました。