オフィシャル感のあるSwiftのformatter/linterであるところの、swift-formatを試した。
SwiftのGoogleフォークで生まれたものがベースになっているようだ。開発が進んでいるmasterブランチと、Swift 5.1に対応するswift-5.1-branchブランチがあり、後者を試している。
インストール
HomebrewでインストールしたかったのでFormulaを用意した。
| class SwiftFormat < Formula | |
| desc "Formatting technology for Swift source code" | |
| homepage "https://github.com/apple/swift-format" | |
| url "https://github.com/apple/swift-format.git", :branch => "swift-5.1-branch" | |
| head "https://github.com/apple/swift-format.git" | |
| depends_on :xcode => ["11.0", :build] | |
| def install | |
| system "swift", "build", "--configuration", "release", | |
| "--disable-sandbox", | |
| "--build-path", "#{buildpath}/swift-format" | |
| system "install", "-d", "#{prefix}/bin" | |
| system "install", "#{buildpath}/swift-format/release/swift-format", "#{prefix}/bin" | |
| end | |
| test do | |
| system "#{bin}/swift-format", "--version" | |
| end | |
| end |
$ 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の場合に省略できる旨を追加し、全体的に省略するようにした。
参考