こんにちは、クライアントエンジニアのちぎら(@_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 RedundantOptionalInitializationRule
は protocol 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 { } } """), // ...
上記の部分的なコードは , 置換前+警告の場所: 置換後,
という構造になっています。警告の出るべき場所は ↓
で指定されていて 5 、autocorrect
による置換後の文字列が期待するものと一致するかどうかをテストで確認することができます。ルールの定義の中に分かりやすい形式で期待値の記載があるという構造は SwiftLint 初学者の僕にとっては非常にありがたく、もしテスト対象の記載が離れた場所にあったら、テストをどこに書けばよいか探す為にしばらくさまよっていたと思います。
まとめ
SwiftLint のバグに遭遇したついでに修正をして、ついでに記事にまとめてみました。広く使用されているライブラリで問題に遭遇した場合、大抵は自分以外にも困っている人がいるはずです。今回はたまたま問題を修正できてそれはもちろんいいことなのですが、もし修正まで至らなかったとしても、関連する処理を issue のコメントに追記するだけでも修正を試みている人にとってはとても価値があると思います。今後も同じような機会に遭遇した際には、なにかしらに貢献していきたいですね。
We are hiring!
ミラティブでは一緒にアプリを作ってくれる iOS エンジニアを募集しています!
少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、気軽にご連絡ください。
www.mirrativ.co.jp
-
置換する文字列は、関数
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)?
で指定しています。↩ -
autocorrect
を実行する際のオプションは途中で--fix
に変更された関係で少しややこしくなっていますが、オプションの定義などは Lint.swift から読み取ることができます。↩ -
↓
はコード中では主にviolationMarker
という名前で表現されているようで、violationMarker
をキーワードにしてコードを検索すると関連する処理を見つけることができます。↩