こんにちは。エンジニアのshogo4405です。普段は、ミラティブで開発しながら、余暇にOSSのHaishinKit*1*2をつくっています。本稿では、Androidチームで取り組んだAndroid Studioから出てくる警告対応ゼロの仕組み化について紹介します。
はじめに
筆者は、警告は一つ一つ適切なアクションを行うべき対象と考えています。警告一つ一つに意味を持っています。リリース後にクラッシュレポートを見て「あー警告」で指摘されてたという失敗経験もあります。チーム開発においては、IDEの警告ゼロが当然になるように働かきかけています。
Androidチームでは、2021年第3四半期のOKRの一つとして警告ゼロ対応を掲げました。着手当初は278 warnings, 306 weak warnings, 75 infos
の警告等がありました。
ゼロ警告へのアプローチ
まず初めにチームで実施するためにあたって先にどのように警告対応をとるか方針を立てました。以下は、ミラティブにおける警告ゼロのアプローチになります。
- 件数が少なく今後の発生が低いと推測できる警告については手動対応する
- 件数がおおく今後の発生が高いと推測できる警告については自動対応する
- スクリプトを作成して次回以降も利用できるようにするということ
手動対応
先ずは、手動対応のアプローチについて紹介します。Android Studioには、IDEのクリック操作で警告を修正する機能があります。この機能を利用して修正を行っていきました。Android Studioメニューの[Analyze]
→[Inspect Code...]
でプロジェクト内のコードを解析して一覧表示してくれます。
- JavaからKotlinにマイグレーションしたときに発生する警告の対応
- 単純に置換で済ませることができる警告は、正規表現とIDEの力を借りて置換を実施
private
の付け忘れ。variables?
.method() のようにつける必要のない?
の警告の対応などprotected
をprivate
にできる警告の対応
- Koin起因のNo need cast警告の対応
- 利用しているがIDEで利用を検知できない警告については
@Suppress
のアノテーションをつけることで対応- Fluxアーキテクチャを採用にあたり
EventBus
を利用しています。例えば、fun on(event: LiveActionEvent.FetchLiveSucceeded)のようなコードでevent
の部分がメソッド内で非参照だと警告がでてきます。 - Suppress - Kotlin Programming Language
- Fluxアーキテクチャを採用にあたり
自動対応
次に、自動対応のアプローチについて紹介します。ミラティブの場合では利用していないシンボル系の警告が多数を占めていました。特に開発の過程で利用しなくなったクラスは気付きにくく自動的に削除できるようにしたくバッチ処理を検討しました。
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><default></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><html>On SDK version 23 and up, your app data will be automatically backed up and restored on app install. Consider adding the attribute <code>android:fullBackupContent</code> to specify an <code>@xml</code> resource which configures which files to backup, or just set <code>android:fullBackupOnly=true</code>. More info: <a href="https://developer.android.com/guide/topics/data/autobackup">https://developer.android.com/guide/topics/data/autobackup</a></html></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
Danger
このDangerプラグインはOSSで公開してあります。利用方法はこちらをご覧ください。尚inspect.shの実行に時間がかかるためミラティブのプロジェクトでどのように導入していくかは検討段階です。
終わりに
2021年の9月上旬に見事、Android Studioから出てきた278 warnings
が0
になりました。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上で検索しましたがが見つけられず自作をしました。