コネヒト開発者ブログ

コネヒト開発者ブログ

swift-formatで自動コード整形

こんにちは!エンジニアの柳村です。

前回SwiftFormatの導入について紹介しましたが、今回はswift-formatについて紹介します。

ここではappleのほうのswift-formatの使い方を紹介していきます。ただこちらはSwift 5.1以上に対応しているので、Swift 5.0向けにgoogleのforkのほうのswift-formatの使い方も紹介しますがこちらはあまりオススメできません。

インストール

各プロジェクト個別にswift-formatをインストールすることを前提にします。もしグローバルにインストールしたい場合はMintもしくはNSHipsterのforkをhomebrewでインストールしてください。

プロジェクトのルートディレクトリにPackage.swiftを配置します。

※ 事前にxcode-selectなどでXcode11を指定してください

// swift-tools-version:5.1
import PackageDescription

let package = Package(
    name: "Tools",
    dependencies: [
        .package(url: "https://github.com/apple/swift-format", .branch("master"))
    ]
)

設定

以下のコマンドを実行してデフォルトのフォーマットの設定を.swift-formatに書き出します。

$ swift run -c release swift-format --mode dump-configuration > .swift-format

swift-format実行時にこの.swfit-formatに書いた設定に基づいてフォーマットされます。 デフォルトはこのようになっています。

{
  "blankLineBetweenMembers" : {
    "ignoreSingleLineProperties" : true
  },
  "indentation" : {
    "spaces" : 2
  },
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : true,
  "lineLength" : 100,
  "maximumBlankLines" : 1,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : true,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "AvoidInitializersForLiterals" : true,
    "BeginDocumentationCommentWithOneLineSummary" : true,
    "BlankLineBetweenMembers" : true,
    "CaseIndentLevelEqualsSwitch" : true,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "MultiLineTrailingCommas" : true,
    "NeverForceUnwrap" : true,
    "NeverUseForceTry" : true,
    "NeverUseImplicitlyUnwrappedOptionals" : true,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : true,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyAssociatedValues" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : true,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEnumForNamespacing" : true,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseOnlyUTF8" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSpecialEscapeSequences" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "ValidateDocumentationComments" : true
  },
  "tabWidth" : 8,
  "version" : 1
}

設定項目についてはdocumentに説明が書いてありますがruleについては詳細にかかれていないのでルール名から察するしかありません。

実行

以下のコマンドでコードをフォーマットできます。

$ swift run -c relesase swift-format -r ソースコードのフォルダのパス -i
  • -r で指定したフォルダ内を再帰的にswiftのファイルを探します。個別のファイルを指定したい場合は-rは不要です。
  • -i を指定するとswiftのファイルをフォーマットします。(これを指定しないとコンソールにフォーマット結果が出力されるだけです)

その他のオプションについては以下をご参照ください。

OVERVIEW: Format or lint Swift source code.

USAGE: swift-format [options] <filename or path> ...

OPTIONS:
  --configuration         The path to a JSON file containing the configuration of the linter/formatter.
  --in-place, -i          Overwrite the current file when formatting ('format' mode only).
  --mode, -m              The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.
  --recursive, -r         Recursively run on '.swift' files in any provided directories.
  --version, -v           Prints the version and exists
  --help                  Display available options

POSITIONAL ARGUMENTS:
  filenames or paths      One or more input filenames

実行してみるとわかりますが、プロジェクト全体にかけると結構遅いです。(ママリだと1分かかりました)

また、exclude directoryとかの指定ができないので3rd Partyのソースコードなどがあった場合に特定のディレクトリのみを除外するといったことができません。

これではちょっと頻繁に実行するのはちょっと厳しいです。

そこで以下のように差分のあるswiftのファイルのみをswift-fomatに食わせるようなスクリプトをprojectのbuild phasesやgitのprecommit hookに設定するといい感じになると思います。

$ git diff --name-only | grep .*\.swift$ | xargs swift run -c release swift-format -i

まとめ

Swift 5.1以降に限定されますがswift-formatでフォーマットできました。

swift-formatは2019年8月現在まだreleaseは一つもされてないのでまだこれからといった感じはしますが、ママリのアプリではXcode11およびSwift 5.1に対応するタイミングくらいに導入しようかと考えています。

------✂------✂------✂------✂------✂------

オマケ:Swift 5.0でswift-formatを使いたい場合

Swift 5.1でやる場合との違いはインストールです。

インストール

googleのswiftのforkの特定のコミットを利用します。

$ git clone git@github.com:google/swift.git swift-format
$ cd swift-format
$ git checkout 74995aa1473b213977a14c0cb477ce89875ee27b
$ git submodule update --init
$ swift build -c release
$ cd ..

あとはSwift 5.1の手順と同じです。

実行結果

ただappleのと比べると実行結果には違いがあります。(5.0で動かすために古いコミットを指定しているのが原因なのですが)

例えばRxSwiftのインデントがこんな感じになってしまいます。。 たぶん不具合でrespectsExistingLineBreaks の指定が効いていないのではないかなと思います。

-        cameraButton.rx.tap
-            .flatMapLatest { [weak self] _ in
-                return UIImagePickerController.rx.createWithParent(self) { picker in
-                    picker.sourceType = .camera
-                    picker.allowsEditing = false
-                }
-                .flatMap { $0.rx.didFinishPickingMediaWithInfo }
-                .take(1)
-            }
-            .map { info in
-                return info[.originalImage] as? UIImage
-            }
-            .bind(to: imageView.rx.image)
-            .disposed(by: disposeBag)
+        cameraButton.rx.tap.flatMapLatest { [weak self] _ in
+            return UIImagePickerController.rx.createWithParent(self) { picker in
+                picker.sourceType = .camera
+                picker.allowsEditing = false
+            }.flatMap { $0.rx.didFinishPickingMediaWithInfo }.take(1)
+        }.map { info in
+            return info[.originalImage] as? UIImage
+        }.bind(to: imageView.rx.image).disposed(by: disposeBag)

あと@unknown default@unknowndefault になってしまうバグがあってコンパイルに通らなくなってしまったりするのでお気をつけください。