オフィシャル感のあるSwiftのformatter/linterであるところの、swift-formatを試した。
SwiftのGoogleフォークで生まれたものがベースになっているようだ。開発が進んでいるmaster
ブランチと、Swift 5.1に対応するswift-5.1-branch
ブランチがあり、後者を試している。
インストール
HomebrewでインストールしたかったのでFormulaを用意した。
$ brew install https://gist.githubusercontent.com/cockscomb/183acd19d2f5e127045dc43c6c472535/raw/63c935672b4e8c9d6f2056785283a6d6b7d31b77/swift-format.rb
ビルドに2分くらいかかった。
(もしかするとMintというのを使うといいのかもしれないが、試していない。)
ヘルプを見てみる。
$ swift-format --help OVERVIEW: Format or lint Swift source code. When no files are specified, it expects the source from standard input. USAGE: swift-format [options] [filename or path ...] OPTIONS: --assume-filename When using standard input, the filename of the source to include in diagnostics. --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
dump-configuration
swift-formatの設定は、JSONで表現される。デフォルトの設定は以下のようにdumpできる。
$ swift-format --mode dump-configuration > .swift-format
{ "blankLineBetweenMembers" : { "ignoreSingleLineProperties" : true }, "indentation" : { "spaces" : 2 }, "indentConditionalCompilationBlocks" : true, "lineBreakBeforeControlFlowKeywords" : false, "lineBreakBeforeEachArgument" : false, "lineLength" : 100, "maximumBlankLines" : 1, "respectsExistingLineBreaks" : true, "rules" : { "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : 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, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : true, "NoLeadingUnderscores" : true, "NoParensAroundConditions" : true, "NoVoidReturnOnFunctionSignature" : true, "OneCasePerLine" : true, "OneVariableDeclarationPerLine" : true, "OnlyOneTrailingClosureArgument" : true, "OrderedImports" : true, "ReturnVoidInsteadOfEmptyTuple" : true, "UseEnumForNamespacing" : true, "UseLetInEveryBoundCaseVariable" : true, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : true, "UseSynthesizedInitializer" : true, "UseTripleSlashForDocumentationComments" : true, "ValidateDocumentationComments" : true }, "tabWidth" : 8, "version" : 1 }
Documentation/Configuration.mdというドキュメントがある。
デフォルトでインデントがスペース2つだった。開発の最初期からスペース2つだったようで、要するにGoogleのSwift Style Guideに倣っているためである。SwiftUIやFunction buildersのようなDSL的なユースケースが増えてきたときに、インデントが深くなりやすいから、スペース2つの方がいいというトレンドになるかもしれない。しかしひとまず、一般的なスペース4つに変えた。
lint
$ swift-format --mode lint --configuration .swift-format --recursive .
けっこういろいろ出てくる。
--configuration .swift-format
は、ファイル名が.swift-format
の場合には省略できる。
SwiftLintを真似て、XcodeのBuild PhasesにRun Script Phaseを追加。
if which swift-format >/dev/null; then swift-format --mode lint --recursive . || true else echo "warning: swift-format not installed" fi
(lintが通らないと終了コードが1になり、後続のphaseに進まないので、|| true
しておくといい。)
こうすると、出力がXcodeの求める形式と合っているので、エディタにwarningが表示される。
OnlyOneTrailingClosureArgument
という、引数にクロージャが2つ以上あるときtrailing closureを許さないルールに引っかかる。可読性が落ちるので妥当なルールとも思うが、SwiftUIのTutorialをみると、Button
で使っているパターンである。action
のクロージャをメソッドとして切り出すのが正攻法だろうが、無効にしてもいいだろう。
NeverUseImplicitlyUnwrappedOptionals
というのでも引っかかる。var str: String!
のようなのは、あまり行儀がいいとはいえないものの、Xcodeのテンプレートでも使われるパターンである。部分的にswift-formatのルールを無効にできるといいのだが。
部分的なlintの無効化
masterにはDocumentation/IgnoringSource.mdというのがあった。以下のようなコメントを書くことで、コメントが書かれた次の行からASTで1ノード分、swift-formatが無視してくれるというものらしい。
// swift-format-ignore
// swift-format-ignore: [comma delimited list of rule names]
使いたいけど、swift-5.1-branch
には入っておらず、Swift 5.1ではまだ使えなさそうだった。
format
swift-formatなので、もちろんformatできる。
$ swift-format --mode format --recursive --in-place .
Makefileも用意しておく。
.PHONY: format lint format: swift-format --mode format --recursive --in-place . lint: swift-format --mode lint --recursive .
おもしろいところでは、ネームスペース代わりのinit
を潰したstruct
をenum
に書き換えてくれた。
フォーマットの感じは悪くなく、(プロジェクトが小さいためか)パフォーマンスも特に気にならない。手元でちょっと使うのには適しているだろう。
CIで動かそうとするともうちょっと準備が必要で、競合するSwiftLintの方がノウハウが蓄積されていて便利かもしれない。
そのうち安定版がリリースされて、Swiftのツールチェーンにバンドルされたり、Xcodeとのインテグレーションが整備されたりすると、さらに使い勝手がよくなりそう。
追記
2019/12/19 18:50
--configuration [json file]
が、ファイル名が.swift-format
の場合に省略できる旨を追加し、全体的に省略するようにした。
参考