LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログです。

SwiftFormatで機械的にコーディングルールを統一する

モバイルアプリエンジニアの山下です。
W杯開幕からほぼ全ての試合を観ているため、在宅勤務を活かして出社時間ギリギリまで寝る生活をしています。

今回は、LCLが運営している高速バス比較のiOS版の開発時に可読性を維持する支えとなっている『SwiftFormat』について紹介したいと思います。

SwiftFormatとは

github.com

Swiftで書かれたコードのフォーマットを整えるライブラリです。
SwiftLintをご存知の方は多いと思いますが、あちらは静的解析を目的としており、こちらは自動整形するのが目的となります。
補足ですが、SwiftLintもAuto correct機能を使うことで自動整形が可能です。

少し例を挙げると、以下のスタイルを統一したい場合に有効です。

  • 可能な限りselfを追加/削除したい
  • 括弧や式の前後には必ず空白を挿入したい
  • 複数の書き方があるものを一方の書き方に統一したい
  • Import文をアルファベット順にしたい ..等々

この他にも多くのルールが存在します。

導入のメリット

以下のメリットがあると考えています。

  • コードを機械的に統一できる
  • コードを漏れなく統一できる
  • 間違いを早期に発見できる
  • チーム開発の場合
    • コードレビューで本質的ではない細かい指摘が発生するのを防げる
    • メンバーにコーディングスタイルを覚えてもらうコストを減らせる

typoやエラーであれば、IDE等の既存の環境で検出可能かもしれませんがコーディングスタイルとなると難しいです。

コードレビューを依頼してから指摘されるようでは、最低でも一往復の時間が無駄になってしまいます。
また、人によっては本質的でない指摘を心理的負担に感じる場合もあるかもしれません。
そこで、機械的に早期に問題を発見する仕組みを導入することにより、時間的にも心理的にも負担のないチーム開発環境をつくることができます。

個人開発の場合はコーディングスタイルを明確に設定することはあまり無いと思いますので、導入することで書き方にブレなくコーディングができます。

使い方

どの環境でも動作させたいため、CocoaPodsでインストールしました。
個人開発で自分のPCでしか動作させないという方は、brewやmintなど様々なインストール方法が用意されていますので好みでよいと思います。

Podfile

pod 'SwiftFormat/CLI'

Podsフォルダにあるswiftformatを第一引数に対象のパスを指定して実行します。
以下のコマンドでは全てのファイルに対してフォーマットが行われます。

$ Pods/SwiftFormat/CommandLineTool/swiftformat .

ディレクトリやファイルを指定する場合は、以下のように実行します。

$ Pods/SwiftFormat/CommandLineTool/swiftformat AppDelegate.swift

実行するといくつかの箇所がフォーマットされたと思います。
しかし、このままでは意図しないフォーマットが行われているかもしれません。 そこで、以下のようにルールを個別に追加していきます。

$ Pods/SwiftFormat/CommandLineTool/swiftformat . --self insert 

--selfのルールに対してinsertオプションを追加したことにより、selfが付くべき箇所には自動的に挿入されるようになります。
このようにデフォルトのスタイルとは違う運用をしたい場合は、ルールとオプションを設定することにより、柔軟にコーディングスタイルを定義することが可能です。

これでフォーマットができるようになりましたが、このままでは毎回手動でコマンドを叩かなくていけません。
そこでRun Scriptを追加して以下のように記述します。

"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" . --self insert

これでコンパイル時に毎回フォーマットされるようになりました。

ルール一覧

ルール名 説明 オプション 初期値
--allman オールマン・スタイルを適応 "true", "false" false
--binarygrouping 2進数をグループ化 指定の値, "none", "ignore" 4,8
--commas 配列の最後にコンマを付与 "always", "inline" always
--comments コメント本文のインデント "indent", "ignore" indent
--decimalgrouping 小数点をグループ化 指定の値, "none", "ignore". 3,6
--elseposition else / catchの配置 "same-line", "next-line" same-line
--empty voidとタプルの表示 "void", "tuple" void
--experimental experimental rules. "enabled", "disabled" disabled
--exponentcase case of 'e' in numbers. "lowercase", "uppercase" uppercase
--header ヘッダーコメント 任意のテキスト, "strip", "ignore", -
--hexgrouping 16進数をグループ化 指定の値, "none", "ignore". 4,8
--hexliteralcase 16進数リテラルの大文字、小文字 "uppercase", "lowercase" uppercase
--ifdef #ifのインデント "indent", "noindent", "outdent" indent
--indent インデントの数 指定の値, "tab" tab
--indentcase Swtich文内のインデント "true", "false" false
--linebreaks 改行文字 "cr", "crlf", "lf" lf
--octalgrouping 8進数をグループ化 指定の値, "none", "ignore" 4,8
--operatorfunc 関数同士の空行 "spaced", "nospace" spaced
--patternlet let/var の表記パターン "hoist, "inline" hoist
--ranges Rangeの前後のスペース "spaced", "nospace" spaced
--semicolons セミコロンの表示 "never", "inline" inline
--self selfの表示 "remove" , "insert" remove
--stripunusedargs 未使用の値をの表示 "closure-only", "unnamed-only", "always" always
--trimwhitespace 空白の削除 "always", "nonblank-lines" always
--wraparguments 関数の引数の改行 "beforefirst", "afterfirst", "disabled" beforefirst
--wrapelements 配列や辞書の改行 "beforefirst", "afterfirst", "disabled" beforefirst

オプション一覧

上記のルールに紐づくオプションは他にも多くあります。

オプション名 説明 有効なルール
blankLinesAroundMark // MARKコメントの前後に空白行を追加 --insertlines
blankLinesAtEndOfScope 中括弧や閉じ括弧などの末尾の空白行を削除 --removelines
blankLinesAtStartOfScope 中括弧や閉じ括弧などの先頭の空白行を削除 --removelines
blankLinesBetweenScopes class, struct, enum, extension, protocol, functionの前に空白行を追加 --insertlines
braces 字下げスタイルを「K&R」(デフォルト)または「Allman style」に変更 --allman
consecutiveBlankLines 複数の連続する空白行を1つの空白行に減らす -
consecutiveSpaces 複数の連続するスペースを1つのスペースに減らす -
duplicateImports 重複するimport文を削除 -
elseOnSameLine else, catch, whileを閉じ括弧と同じ行に表示 --elseposition
fileHeader ファイル生成時に自動挿入される上部のコメントブロックを削除 --header strip:全て削除
--header "Copyright Text {year}":指定したテキストに置換
hoistPatternLet 値バインディングパターンのlet, varを式の先頭に移動 -
indent indentを調整 --indent
indentcase: Switch文内のcaseのインデント
--comments: コメントのインデント
--ifdef: if..defのインデント
linebreakAtEndOfFile ファイルの最後の行を空行か確認 -
linebreaks すべての改行コードを正規化 --linebreaks
numberFormatting 大文字、小文字のリテラルを揃える --hexliteralcase, --exponentcase, --hexgrouping, --binarygrouping, --decimalgrouping, --octalgrouping
ranges 範囲演算子のスペースを制御 --ranges
redundantBackticks バッククォートを使用している識別子の不要なエスケープを削除 -
redundantGet 読み取り専用プロパティの不要なget句を削除 -
redundantInit バインディング内の使用されてない変数から冗長なletまたはvarを削除 -
redundantLet オプションのvarsの不要なnil初期化を削除 -
redundantNilInit 式と条件分岐から不要な括弧を削除 -
redundantParens 使用されてない変数の冗長パターンマッチング引数を削除 -
redundantPattern ケース名と一致する列挙型の値を削除 -
redundantRawValues 単一行のクロージャから不要なreturnを削除 -
redundantReturn 関数から不要なVoid戻り型を削除 -
redundantSelf クラスおよびインスタンスのメンバー参照からselfを削除 --self
redundantVoidReturnType 型をインスタンス化するときに不要なinitを削除 -
semicolons 行末のセミコロンを削除 --semicolons
sortedImports import文をソート -
spaceAroundBraces 波括弧の前後にスペースを追加 -
spaceAroundBrackets 括弧の前後にスペースを追加 -
spaceAroundComments / * ... * /コメントと//コメントの前後にスペースを追加 --comments
spaceAroundGenerics ジェネリクスの前後のスペースを削除 -
spaceAroundOperators 演算子の前後のスペースを削除、または追加 --operatorfunc
spaceAroundParens 括弧の前後のスペースを削除 -
spaceInsideBraces 波括弧の中にスペースを追加 -
spaceInsideBrackets 角括弧の中のスペースを削除 -
spaceInsideComments / * ... * /コメントと//コメントの中にスペースを追加 --comments
spaceInsideGenerics ジェネリクスの中のスペースを削除 -
spaceInsideParens 括弧の中のスペースを削除 -
specifiers アクセス指定子、その他のプロパティ/関数/クラス/などの順序を正規化 -
strongOutlets Appleの推奨に従って、@IBOutletプロパティから弱い指定子を削除 -
todos TODO:MARK:およびFIXME:コメントにコロンが含まれていることを確認 -
trailingClosures (無効) 可能であれば、関数呼び出しの最後のクロージャ引数を末尾のクロージャ構文に変換 -
trailingCommas 行末のカンマを削除 --commas
trailingSpace 行末の空白を削除 --trimwhitespace
unusedArguments 関数内で未使用の引数を_に変換 --stripunusedargs
void 空の引数リストと戻り値を表す空のタプルの使用を標準化 --empty
wrapArguments 関数の引数と配列要素を整形 --wraparguments, --wrapelements

高速バス比較で使用中のオプション

高速バス比較では、基本的にデフォルトのルールを尊重しています。

$ Pods/SwiftFormat/CommandLineTool/swiftformat . --exclude Carthage,Pods --stripunusedargs closure-only --disable strongOutlets,trailingCommas,numberFormatting
ルール 説明
--exclude Carthage,Pods CarthageとPodsのソースは対象から除外
--stripunusedargs closure-only クロージャでのみ使用していない引数を_ に置換
--disable strongOutlets,trailingCommas,numberFormatting ルールを無効化

既存コードで直された箇所

コマンド実行時に修正が多発した項目を紹介します。

アクセス修飾子の入れ替え

-    required public init?(coder aDecoder: NSCoder) {
+    public required init?(coder aDecoder: NSCoder) {

selfの削除

-        self.navigationController?.setNavigationBarHidden(true, animated: false)
+        navigationController?.setNavigationBarHidden(true, animated: false)

Importアイテムの並び替え

-import UIKit
-import RxSwift
import RxCocoa
+import RxSwift
+import UIKit

値バインディングパターンのlet, varを式の先頭に移動

-        case .push(let action):
+        case let .push(action):
            return action

その他

  • 計算式の前後の空白を挿入/削除
  • class/structの一行目の空行を削除

無効化を検討したルール

既存のコーディングスタイルや実装方法から採用の見送りを検討したルールを紹介します。

selfの削除

理由

  • Extensionの場合はselfがあった方が可読性が良い
  • initilizeを定義した際に代入元の変数名がプロパティ名と同じでない限り左辺のselfがすべて消える
 init(json: JSON) {
-        self.maxAmount = json["max_amount"].int
-        self.lastAmount = json["last_amount"].int
+        maxAmount = json["max_amount"].int
+        lastAmount = json["last_amount"].int
  • 以下の書き方の場合、selfがないと自身を参照しようとしてしまう
    • Attempting to access 'realm' within its own getter
    static var realm: Realm {
        do {
            return try Realm()
        } catch {
            Log.fatal("Could not access database: \(error)")
        }
-        return self.realm
+       return realm
    }

結論

書き方を揃えた方が良いので採用しました。
コーディングに支障が出る場合は変更予定です。

数字のフォーマット

理由

Realmマイグレーションのバージョンを日付で設定しているため、このルールを適応すると問題がある。

-       let config = Realm.Configuration(schemaVersion: 2018053109, migrationBlock: { migration, oldSchemaVersion in
-            if oldSchemaVersion < 2018051913 {
+       let config = Realm.Configuration(schemaVersion: 2_018_053_109, migrationBlock: { migration, oldSchemaVersion in
+            if oldSchemaVersion < 2_018_051_913 {

結論

この運用は今後も続けるため不採用にしました。

最後に

SwiftFormatの導入により、コーディングスタイルのばらつきがなくなりました。
実行時に機械的にフォーマットしてくれるため、コーディング中に丁寧に書かなくてもいいという使い方もできました。

コーディングスタイルを見直した際に、ルールを変えたくなったらいつでもそのスタイルへフォーマットすることが可能なので積極的に採用しています。

ただし、なぜそのコーディングスタイルを採用するのか議論することは大切だと思います。意図をチームメンバーと共有し、同意を得た上で採用することをおすすめします。

LCLの自動化の環境も段々と充実してきました。今後も自動化の範囲を広めて開発速度を上げていきたいと思います。

techblog.lclco.com

techblog.lclco.com

techblog.lclco.com

フロントエンドではどうようなことをPrettierで行っているようです。

techblog.lclco.com