Mirrativ Tech Blog

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

【iOS】SwiftLint のバグに遭遇したのでコントリビュートしてみたら学びを得た

こんにちは、クライアントエンジニアのちぎら(@_naru_jpn)です。

ミラティブの iOS アプリでは SwiftLint を活用して開発を行なっています。先日アプリの開発中に、SwiftLint 0.44.0 で、あるルールについての autocorrect の結果が想定外のものになりアプリのビルドが通らなくなるという事象に遭遇しました。1 今回はそのバグを修正したという話、修正する中で SwiftLint の中身には参考になる箇所が沢山あった話、を書こうと思います。

背景

今回遭遇した事象は redundant_optional_initialization ルールに関するもので、このルールは「Optional な stored property は = nil による初期化は省略できるよ」という内容です。

var hoge: Int? = nil
//            ~
// Redundant Optional Initialization Violation: Initializing an optional variable with nil is redundant.

redundant_optional_initialization についての autocorrect 実行時の結果は以下のようになります。

// autocorrect 前
var hoge: Int? = nil

// autocorrect 後
var hoge: Int?

SwiftLint 0.44.0 の autocorrect で発生した問題は、didSet などの Property observers の記述がある場合に、 = nil のみではなく、その後に続くかっこ( { )も削除してしまうというものでした。2

// autocorrect 前
var hoge: Int? = nil {
  didSet { /* ... */ }
}

// autocorrect 後(必要な { まで削除されてしまってビルドに失敗する)
var hoge: Int?
  didSet { /* ... */ }
}

redundant_optional_initialization ルールを disable にするなどの一時的な対応を入れれば開発を続けることはできるのですが、きっと他の人も困っているしせっかくなので修正してみることにしました。

修正内容

問題の箇所

SwiftLint の redundant_optional_initialization ルールについての記述は RedundantOptionalInitializationRule.swift に書かれています。ここで定義されている struct RedundantOptionalInitializationRuleprotocol SubstitutionCorrectableASTRule に準拠していて、関数

func violationRanges(in file: SwiftLintFile, 
                     kind: SwiftDeclarationKind, 
                     dictionary: SourceKittenDictionary) -> [NSRange]

で置換対象の文字列の範囲を返却しています。3

修正前の実装は正規表現 \s*=\s*nil\b\s*\{? にマッチする範囲を置換対象としていて、これを空文字列と置換していました。従って、 = nil { のように nil の後に { がある場合、 { も含めて空文字列と置換をし、前述したような結果になっていました。

修正方針

上記のように { を含む範囲を返却してしまうのが問題なので、置換する範囲として { を含まないものを返せばよいだろうと考えました。マッチに使用する正規表現を (\s*=\s*nil\b)\s*\{? と変更し、{ を含まないグループを抽出することで置換する範囲の指定を修正します。

let pattern = "(\\s*=\\s*nil\\b)\\s*\\{?"

// ...
if matched.hasSuffix("{"), match.numberOfRanges > 1 {
  // マッチした文字列が末尾に `{` を持つ場合はグループの範囲を返す
  return [match.range(at: 1)]
} else {
  // 末尾に `{` がない場合は全体を置換対象とする
  return [match.range]
}

実際の Pull Request にはテストなど(後述します)関連する修正が含まれていますが、上述した修正で問題はほぼ解消します。問題が発生した箇所と修正が必要な範囲がきれいに対応していました。

SwiftLint の実装で参考になったポイント

上記で修正内容の概要は説明をしたのですが、今後同じような修正をされる機会がある方のためにも、修正する中でなるほどと勉強になった点をいくつかご紹介します。

Package.swift を見ればプロジェクトの構成がわかる

SwiftLint は依存関係が Package.swift にまとめられています。 canImport(CommonCrypto) で import できるか確認をして、できなければ CryptoSwift を使用しているところなども参考になりますね。

ArgumentParser でコマンドライン引数をハンドリングしている

ArgumentParser は Apple が管理している OSS ですが、SwiftLint はこれを利用してコマンドライン引数のハンドリングをしています。 SwiftLint.swift に定義されている struct SwiftLint は、プロトコル ParsableCommand に準拠していて、 CommandConfiguration に使用できるコマンドが列挙されています。4

どういったコマンドがどのような条件で実行されるのかは、 subcommands に登録されているコマンドの定義を確認すれば分かるようになっていますし、run() の内容を眺めれば興味のあるコマンドがどのように実行されるかも分かります。

Lint の警告と置換についてのテストの表現がわかりやすい

実際に提出した修正にもテストを追記したのですが、このテストがめちゃくちゃわかりやすく表現されています。僕が追記したテストの為の記述は以下のようなものです。

// ...
,
Example("""
var myVar: Int?↓ = nil {
  didSet { }
}
"""):
  Example("""
  var myVar: Int? {
    didSet { }
  }
  """),
// ...

上記の部分的なコードは , 置換前+警告の場所: 置換後, という構造になっています。警告の出るべき場所は で指定されていて 5autocorrect による置換後の文字列が期待するものと一致するかどうかをテストで確認することができます。ルールの定義の中に分かりやすい形式で期待値の記載があるという構造は SwiftLint 初学者の僕にとっては非常にありがたく、もしテスト対象の記載が離れた場所にあったら、テストをどこに書けばよいか探す為にしばらくさまよっていたと思います。

まとめ

SwiftLint のバグに遭遇したついでに修正をして、ついでに記事にまとめてみました。広く使用されているライブラリで問題に遭遇した場合、大抵は自分以外にも困っている人がいるはずです。今回はたまたま問題を修正できてそれはもちろんいいことなのですが、もし修正まで至らなかったとしても、関連する処理を issue のコメントに追記するだけでも修正を試みている人にとってはとても価値があると思います。今後も同じような機会に遭遇した際には、なにかしらに貢献していきたいですね。

We are hiring!

ミラティブでは一緒にアプリを作ってくれる iOS エンジニアを募集しています!
少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、気軽にご連絡ください。 www.mirrativ.co.jp

speakerdeck.com


  1. 2021/10/19 のリリースバージョン 0.45.0 には修正が反映されています。

  2. 他の方が作成された Issue #3718 がありますが、こちらにも事象が分かりやすくまとめられています。

  3. 置換する文字列は、関数 func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? で指定しています。

  4. autocorrect を実行する際のオプションは途中で --fix に変更された関係で少しややこしくなっていますが、オプションの定義などは Lint.swift から読み取ることができます。

  5. はコード中では主に violationMarker という名前で表現されているようで、violationMarker をキーワードにしてコードを検索すると関連する処理を見つけることができます。