cockscomblog?

cockscomb on hatena blog

apple/swift-formatを試す

オフィシャル感のある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を真似て、XcodeBuild PhasesRun 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を潰したstructenumに書き換えてくれた。


フォーマットの感じは悪くなく、(プロジェクトが小さいためか)パフォーマンスも特に気にならない。手元でちょっと使うのには適しているだろう。

CIで動かそうとするともうちょっと準備が必要で、競合するSwiftLintの方がノウハウが蓄積されていて便利かもしれない。

そのうち安定版がリリースされて、Swiftのツールチェーンにバンドルされたり、Xcodeとのインテグレーションが整備されたりすると、さらに使い勝手がよくなりそう。

追記

2019/12/19 18:50

--configuration [json file]が、ファイル名が.swift-formatの場合に省略できる旨を追加し、全体的に省略するようにした。

参考