Swiftの静的解析ツール「SwiftLint」のセットアップ方法

SwiftLintの記事はシリーズになっています。
記事を順番に読み進めると、SwiftLintを使いこなせるようになります。

- Swiftの静的解析ツール「SwiftLint」のセットアップ方法　__←イマココ__
- [SwiftLintの全ルール一覧(Swift 4.2版)](https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed)
- [SwiftLintで追加・変更・廃止されたルールまとめ(Swift 4.2→5.1.2版)](https://qiita.com/uhooi/items/8e9767c2e746f4171ded)
- [SwiftLintで追加・廃止されたルールまとめ(Swift 5.1.2→5.3.1版)](https://qiita.com/uhooi/items/10b9e49d3bd6f1344cad)
- [SwiftLintのオレオレカスタムルール一覧](https://qiita.com/uhooi/items/9deb56416aaa81709f69)
- [SwiftLintのAnalyzeを使って高度な解析をする方法](https://qiita.com/uhooi/items/655655f56be8bd94b06d)
- [SwiftLintの設定ファイルをサーバーに配置して共有する方法](https://qiita.com/uhooi/items/b5b26caeeefd8dbe1afd)
- [SwiftLintの「Currently running SwiftLint x.y.z but configuration specified version a.b.c.」エラーの解決法](https://qiita.com/uhooi/items/b1f2f0df2822f04c2161)

## 「SwiftLint」とは？

Swift用の静的解析ツールです。
1行あたりの文字数やコロンの両側のスペースの有無など、非常に細かくチェックしてくれるのが特徴です。

## 環境

- OS：macOS Mojave 10.14
- Xcode：10.1 (10B61)
- Swift：4.2.1
- SwiftLint：0.30.1

## セットアップ

### インストール

#### Mint

2020/01/31現在、私は[Mint](https://github.com/yonaskolb/Mint)からインストールしています。
理由は、その他の方法よりバージョン管理が簡単なためです。

```diff:Mintfile
+ realm/SwiftLint@0.38.0
```

```shell-session
$ mint bootstrap
```

#### Homebrew

Homebrewからもインストールできます。

```bash
$ brew install swiftlint
```

#### ソースコードのビルド

ソースコードをビルドしてインストールすることもできます。

```bash
$ git clone https://github.com/realm/SwiftLint.git
$ cd SwiftLint/
$ git submodule update --init --recursive; make install
```

### ビルド時に静的解析を行うようにする

Xcodeで対象のプロジェクトファイルを開く>TARGETS
→対象のターゲットを選択>[Build Phases]タブ
→左上にある[+]をクリック>New Run Script Phase
Run Scriptに以下のスクリプトを記述すると、ビルド時に自動で修正してから静的解析されるようになります。

```bash
if which mint >/dev/null; then
  mint run swiftlint swiftlint autocorrect --format
  mint run swiftlint swiftlint
else
  echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
```

（2024/01/04追記）
0.43.0で非推奨になった `autocorrect` は、0.49.0で完全に削除されました。
代わりに `--fix` を使います。

```diff_shell
if which mint >/dev/null; then
- mint run swiftlint swiftlint autocorrect --format
+ mint run swiftlint swiftlint --fix
  mint run swiftlint swiftlint
else
  echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
```

Mintを使っている
<img width="1249" alt="スクリーンショット 2020-04-07 10.54.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/74c1b680-d25f-80df-3f10-7870055390d4.png">

Mintを使っていない
<img width="834" alt="スクリーンショット 2019-02-11 22.41.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/138245/aa24f2f3-fed9-20f7-58f6-d14c2c8ffcae.png">

Mintを使っていない場合、 `mint run swiftlint` を外し、if文の条件を変更する必要があります。

### ルールの詳細設定

プロジェクトのルートディレクトリに `.swiftlint.yml` を作成し、その中にルールの詳細を記述します。
設定の書き方は[公式ページ](https://github.com/realm/SwiftLint#configuration)に詳しく載っています。

参考になるかわかりませんが、私の設定ファイルも公開します。(随時更新)

```yaml:.swiftlint.yml
# デフォルト有効で無効にするルール
disabled_rules:
  #- block_based_kvo
  #- class_delegate_protocol
  #- closing_brace
  #- closure_parameter_position
  #- colon
  #- comma
  #- compiler_protocol_init
  #- control_statement
  #- custom_rules
  #- cyclomatic_complexity
  #- deployment_target
  #- discarded_notification_center_observer
  #- discouraged_direct_init
  #- duplicate_imports
  #- dynamic_inline
  #- empty_enum_arguments
  #- empty_parameters
  #- empty_parentheses_with_trailing_closure
  #- file_length
  #- for_where
  #- force_cast
  #- force_try
  #- function_body_length
  #- function_parameter_count
  #- generic_type_name
  #- identifier_name
  #- implicit_getter
  #- inert_defer
  #- is_disjoint
  #- large_tuple
  #- leading_whitespace
  #- legacy_cggeometry_functions
  #- legacy_constant
  #- legacy_constructor
  #- legacy_hashing
  #- legacy_nsgeometry_functions
  #- line_length
  #- mark
  #- multiple_closures_with_trailing_closure
  #- nesting
  #- no_fallthrough_only
  #- notification_center_detachment
  #- opening_brace
  #- operator_whitespace
  #- private_over_fileprivate
  #- private_unit_test
  #- protocol_property_accessors_order
  #- redundant_discardable_let
  #- redundant_objc_attribute
  #- redundant_optional_initialization
  #- redundant_set_access_control
  #- redundant_string_enum_value
  #- redundant_void_return
  #- return_arrow_whitespace
  #- shorthand_operator
  #- statement_position
  #- superfluous_disable_command
  #- switch_case_alignment
  #- syntactic_sugar
  #- todo
  #- trailing_comma
  #- trailing_newline
  #- trailing_semicolon
  #- trailing_whitespace
  #- type_body_length
  #- type_name
  #- unneeded_break_in_switch
  #- unused_closure_parameter
  #- unused_control_flow_label
  #- unused_enumerated
  #- unused_optional_binding
  #- unused_setter_value
  #- valid_ibinspectable
  #- vertical_parameter_alignment
  #- vertical_whitespace
  #- void_return
  #- weak_computed_property
  #- weak_delegate
  #- xctfail_message

# デフォルト無効で有効にするルール
opt_in_rules:
  - anyobject_protocol
  - array_init
  - attributes
  - closure_body_length
  - closure_end_indentation
  - closure_spacing
  - collection_alignment
  - conditional_returns_on_newline
  - contains_over_first_not_nil
  - convenience_type
  #- discouraged_object_literal # オブジェクトリテラルを使いたいため
  - discouraged_optional_boolean
  - discouraged_optional_collection
  - empty_count
  - empty_string
  - empty_xctest_method
  #- explicit_acl # できる限りACLを省略したいため
  #- explicit_enum_raw_value # ローバリューを省略することもあるため
  - explicit_init
  #- explicit_self # 関数は `self.` を付けずに呼び出したいため
  #- explicit_top_level_acl # できる限りACLを省略したいため
  #- explicit_type_interface # できる限り型推論したいため
  #- extension_access_modifier # このルールの意味を理解していないため
  - fallthrough
  - fatal_error_message
  #- file_header # このルールの意味を理解していないため
  - file_name
  - first_where
  - force_unwrapping
  - function_default_parameter_at_end
  - identical_operands
  - implicit_return
  #- implicitly_unwrapped_optional # VIPERで変数を `!` で定義したいため
  - joined_default_parameter
  - last_where
  - legacy_random
  #- let_var_whitespace # 空白行を設けたくないこともあるため
  - literal_expression_end_indentation
  - lower_acl_than_parent
  - missing_docs
  - modifier_order
  #- multiline_arguments # 引数は同じ行に2つ入れたいこともあるため
  #- multiline_arguments_brackets # 括弧で行を増やしたくないため
  #- multiline_function_chains # 関数の呼び出しは同じ行に2つ入れたいこともあるため
  #- multiline_literal_brackets # 括弧で行を増やしたくないため
  #- multiline_parameters # 引数は同じ行に2つ入れたいこともあるため
  #- multiline_parameters_brackets # 括弧で行を増やしたくないため
  - nimble_operator
  #- no_extension_access_modifier # エクステンションにACLを設定したいことがあるため
  - no_grouping_extension # グループ化するためにエクステンションを使わないため
  - nslocalizedstring_key
  #- number_separator # 数字を `_` で区切りたくないため
  #- object_literal # リテラルで生成したくないこともあるため
  - operator_usage_whitespace
  - overridden_super_call
  - override_in_extension
  - pattern_matching_keywords
  #- prefixed_toplevel_constant # 定数のプリフィックスに `k` を付けたくないため
  - private_action
  - private_outlet
  #- prohibited_interface_builder # ストーリーボードを使ってビューを生成したいため
  - prohibited_super_call
  - quick_discouraged_call
  - quick_discouraged_focused_test
  - quick_discouraged_pending_test
  - redundant_nil_coalescing
  - redundant_type_annotation
  #- required_deinit # できる限りデイニシャライザを省略したいため
  - required_enum_case
  - single_test_class
  - sorted_first_last
  #- sorted_imports # インポート文をアルファベット順以外に並び替えたいこともあるため
  - static_operator
  - strict_fileprivate
  #- strong_iboutlet # `@IBOutlet` を `weak` で定義したいこともあるため
  - switch_case_on_newline
  - toggle_bool
  - trailing_closure
  - unavailable_function
  - unneeded_parentheses_in_closure_argument
  - untyped_error_in_catch
  - unused_import
  - unused_private_declaration
  - vertical_parameter_alignment_on_call
  #- vertical_whitespace_between_cases # Switch文のケース間に空白行を設けたくないこともあるため
  #- vertical_whitespace_closing_braces # 中括弧を閉じる前に空白行を設けたいことがあるため
  #- vertical_whitespace_opening_braces # 中括弧を開く前に空白行を設けたいことがあるため
  - xct_specific_matcher
  - yoda_condition

# 対象のファイル・フォルダ
# デフォルトからフォルダ名を変更していない場合、プロジェクト名と同名のフォルダを指定すればいい
included:
  - {プロジェクト名}

# 対象外のファイル・フォルダ
excluded:
  - Pods
  - Carthage
  - SourcePackages
  - Generated

line_length:
  warning: 300
  error: 500

identifier_name:
  min_length:
    warning: 1 # `r` `g` `b` などを使いたいため
```

R.swiftやMockoloなどで自動生成したファイルをまとめて1つのフォルダ(例：「Generated」フォルダ)に格納すると、フォルダを指定するだけで静的解析の対象外にできるのでオススメです。

## 静的解析

ここまで設定したら、ビルドするたびに静的解析が行われます。
めちゃくちゃたくさんの警告とエラーが出ると思うので、コードを修正するか、ルールを緩和するかしましょう！

## 注意

`included:` で指定したフォルダが存在しない場合、以下のビルドエラーが発生します。

```
Command /bin/sh failed with exit code 1
```

## 特定の箇所のみルールを無効化

`// swiftlint:disable:{next|this|previous} {ルール名}` コメントを使うことで、特定の箇所のみ特定のルールを無効化することができます。
使い過ぎるとルールを設定した意味がなくなるので、デバッグやテストでしか使わないコードのみに適用するのがいいと思います。

```swift
// swiftlint:disable:next force_cast
let noWarning = NSNumber() as! Int
let hasWarning = NSNumber() as! Int
let noWarning2 = NSNumber() as! Int // swiftlint:disable:this force_cast
let noWarning3 = NSNumber() as! Int
// swiftlint:disable:previous force_cast
```

## おまけ

### 全ルール一覧のまとめ

SwiftLintの全ルールを簡単に翻訳した記事を投稿しました。
[SwiftLintの全ルール一覧(Swift 4.2版)](https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed)

どのルールを採用するかの参考になれば幸いです。

### SwiftLintを既存PJに導入する方法

[Otemachi.swift #3](https://nikkei.connpass.com/event/123343/)という勉強会に「既存PJにSwiftLintを導入したら大変だった話:sweat_smile:」というタイトルで登壇しました。
そのときに使ったスライドをSpeaker Deckに上げているので、こちらもよかったら参考にしてください。
https://speakerdeck.com/uhooi/setup-swiftlint

スライドだけだとわかりづらいので、私のツイートで補ってください。。
https://twitter.com/the_uhooi/status/1117787831710040064
https://twitter.com/the_uhooi/status/1115979648851034112

## 参考リンク

- [GitHub - realm/SwiftLint: A tool to enforce Swift style and conventions.](https://github.com/realm/SwiftLint)
- [SwiftLint/Rules.md at master · realm/SwiftLint](https://github.com/realm/SwiftLint/blob/master/Rules.md)
- [Release 0.49.0: Asynchronous Defuzzer · realm/SwiftLint](https://github.com/realm/SwiftLint/releases/tag/0.49.0)
- [SwiftLint導入 - Qiita](https://qiita.com/syamaoka/items/2edfe315940d91846bf0)
- [SwiftLintのRules全まとめ - Qiita](https://qiita.com/kagemiku/items/80e6d905dc0059c342b3)
- [swiftlint 最新版(0.18.1) の設定ファイル(.swiftlint.yml)のサンプル - Qiita](https://qiita.com/rd0501/items/ceabe756c419ec082030)
- [SwiftLintの運用ノウハウ - Qiita](https://qiita.com/shtnkgm/items/6dd756aa14926736c6f5)