SwiftLintのオレオレカスタムルール一覧

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

- [Swiftの静的解析ツール「SwiftLint」のセットアップ方法](https://qiita.com/uhooi/items/bf888b53b4b8210108aa)
- [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のオレオレカスタムルール一覧　__←イマココ__
- [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)

## はじめに

本記事は [SwiftWednesday Advent Calendar 2023](https://qiita.com/advent-calendar/2023/swift-wednesday) の5日目の記事です。
昨日は @ojun_9 さんで [@Observable Macro を構造体へ適用させる難しさについて](https://zenn.dev/dena/articles/6e70c30d148523) でした。

SwiftLintで私が自作しているカスタムルールを紹介します。

## 環境

- OS：macOS Sonoma 14.0（23A344）
- Swift：5.9

## オレオレカスタムルール一覧

私が自作しているカスタムルールの一覧です。

### enum_lower_case

enumのcase名の先頭が大文字より小文字を好むルールです。

```yaml:.swiftlint.yml
  enum_lower_case:
    name: 'Enum Lower Case'
    regex: '\bcase\s+([A-Z][a-zA-Z0-9]*)\b'
    capture_group: 1
    message: 'The enum case should be lower camel case'
    severity: warning
```

```swift
// Non Triggering Examples
enum Foo {
  case bar
}

// Triggering Examples
enum Foo {
  case ↓Bar
}
```

### final_class

classに `final` を必ず付けるルールです。

```yaml:.swiftlint.yml
  final_class:
    name: 'Final Class'
    regex: '\b(?<!final)\s+(class)\b'
    capture_group: 1
    message: 'Always add `final` to classes that do not inherit'
    severity: warning
```

```swift
// Non Triggering Examples
final class Foo {}

// Triggering Examples
↓class Foo {}
```

### date_now

`Date()` より [`.now`](https://developer.apple.com/documentation/foundation/date/3766590-now) を好むルールです。

`.now` は `Date()` と同等ですが、 `.now` のほうがわかりやすいので好みです。

```yaml:.swiftlint.yml
  # iOS 15.0+
  date_now:
    name: 'Date Now'
    regex: '[=:(]\s*(Date\(\))'
    capture_group: 1
    message: 'Prefer using `.now` over `Date()`'
    severity: warning
```

```swift
// Non Triggering Examples
let weekday = Calendar.current.component(.weekday, from: .now)

// Triggering Examples
let weekday = Calendar.current.component(.weekday, from: ↓Date())
```

### enumerated

[`enumerated()`](https://developer.apple.com/documentation/swift/array/enumerated()) より [swift-algorithms](https://github.com/apple/swift-algorithms) の [`indexed()`](https://github.com/apple/swift-algorithms/blob/c3b3acbca3aa70adbd30756fe137b14e25140c0d/Sources/Algorithms/Indexed.swift#L114) を好むルールです。

```yaml:.swiftlint.yml
  enumerated:
    name: 'Enumerated'
    regex: '.+\.(enumerated\([^)]*\))'
    capture_group: 1
    message: 'Prefer using `indexed()` over `enumerated()`'
    severity: warning
```

```swift
// Non Triggering Examples
import Algorithms
ForEach(monsters.indexed(), id: \.element.id) { index, monster in
  // Do something
}

// Triggering Examples
ForEach(Array(monsters.↓enumerated()), id: \.element.id) { index, monster in
  // Do something
}
```

### print

[`print(_:separator:terminator:)`](https://developer.apple.com/documentation/swift/print(_:separator:terminator:)) より `Logger.debug()` を好むルールです。

 `Logger.debug()` は自作の関数なので、必要に応じてメッセージを変更してください。

```yaml:.swiftlint.yml
  print:
    name: 'Print'
    regex: '\bprint\([^)]*\)'
    capture_group: 0
    message: 'Prefer using `Logger.debug()` over `print()`'
    severity: warning
```

```swift
// Non Triggering Examples
Logger.debug("Foo")

// Triggering Examples
↓print("Foo")
```

### aspect_ratio_fill

`.aspectRatio(contentMode: .fill)` より [`.scaledToFill()`](https://developer.apple.com/documentation/swiftui/view/scaledtofill()) を好むルールです。

`.scaledToFill()` は `.aspectRatio(contentMode: .fill)` と同等ですが、短いほうが好みです。

```yaml:.swiftlint.yml
  # iOS 13.0+
  aspect_ratio_fill:
    name: 'Aspect Ratio Fill'
    regex: '.+\.(aspectRatio\(contentMode:\s*\.fill\s*\))'
    capture_group: 1
    message: 'Prefer using `scaledToFill()` over `aspectRatio(contentMode: .fill)`'
    severity: warning
```

```swift
// Non Triggering Examples
Image(systemName: "swift")
  .resizable()
  .scaledToFill()

// Triggering Examples
Image(systemName: "swift")
  .resizable()
  .↓aspectRatio(contentMode: .fill)
```

### aspect_ratio_fit

`.aspectRatio(contentMode: .fit)` より [`.scaledToFit()`](https://developer.apple.com/documentation/swiftui/view/scaledtofit()) を好むルールです。

`aspect_ratio_fill` と同じ理由でルール化しています。

```yaml:.swiftlint.yml
  # iOS 13.0+
  aspect_ratio_fit:
    name: 'Aspect Ratio Fit'
    regex: '.+\.(aspectRatio\(contentMode:\s*\.fit\s*\))'
    capture_group: 1
    message: 'Prefer using `scaledToFit()` over `aspectRatio(contentMode: .fit)`'
    severity: warning
```

```swift
// Non Triggering Examples
Image(systemName: "swift")
  .resizable()
  .scaledToFit()

// Triggering Examples
Image(systemName: "swift")
  .resizable()
  .↓aspectRatio(contentMode: .fit)
```

### corner_radius

[`cornerRadius(_:antialiased:)`](https://developer.apple.com/documentation/swiftui/view/cornerradius(_:antialiased:)) は非推奨になったため、 [`clipShape(_:style:)`](https://developer.apple.com/documentation/swiftui/view/clipshape(_:style:)) または [`fill(style:)`](https://developer.apple.com/documentation/swiftui/shape/fill(style:)) を好むルールです。

`clipShape()` 内は [`RoundedRectangle(roundedRect:cornerRadius:style:)`](https://developer.apple.com/documentation/swiftui/path/init(roundedrect:cornerradius:style:)) に置き換えることが多いです。

```yaml:.swiftlint.yml
  # iOS 13.0+
  corner_radius:
    name: 'Corner Radius'
    regex: '.+\.(cornerRadius\([^)]*\))'
    capture_group: 1
    message: 'Prefer using `clipShape(RoundedRectangle(cornerRadius:))` or `fill()` over `cornerRadius()` because it was deprecated'
    severity: warning
```

```swift
// Non Triggering Examples
Text("Foo")
  .clipShape(RoundedRectangle(cornerRadius: 8))

// Triggering Examples
Text("Foo")
  .↓cornerRadius(8)
```

### foreground_color

[`foregroundColor(_:)`](https://developer.apple.com/documentation/SwiftUI/View/foregroundColor(_:)) は非推奨になったため、 [`foregroundStyle(_:)`](https://developer.apple.com/documentation/swiftui/view/foregroundstyle(_:)) を好むルールです。

```yaml:.swiftlint.yml
  # iOS 15.0+
  foreground_color:
    name: 'Foreground Color'
    regex: '.+\.(foregroundColor\([^)]*\))'
    capture_group: 1
    message: 'Prefer using `foregroundStyle()` over `foregroundColor()` because it was deprecated'
    severity: warning
```

```swift
// Non Triggering Examples
Text("Foo")
  .foregroundStyle(.blue)

// Triggering Examples
Text("Foo")
  .↓foregroundColor(.blue)
```

### navigation_bar_leading

[`.navigationBarLeading`](https://developer.apple.com/documentation/swiftui/toolbaritemplacement/navigationbarleading) は非推奨になったため、 [`.topBarLeading`](https://developer.apple.com/documentation/swiftui/toolbaritemplacement/topbarleading) を好むルールです。

```yaml:.swiftlint.yml
  # iOS 14.0+
  navigation_bar_leading:
    name: 'Navigation Bar Leading'
    regex: '\bToolbarItem\(placement:\s*(.navigationBarLeading)\s*\)'
    capture_group: 1
    message: 'Prefer using `.topBarLeading` over `.navigationBarLeading` because it was deprecated'
    severity: warning
```

```swift
// Non Triggering Examples
Text("Foo")
  .toolbar {
    ToolbarItem(placement: .topBarLeading) {
      Button {
        print("Bar")
      } label: {
        Text("Bar")
      }
    }
  }

// Triggering Examples
Text("Foo")
  .toolbar {
    ToolbarItem(placement: ↓.navigationBarLeading) {
      Button {
        print("Bar")
      } label: {
        Text("Bar")
      }
    }
  }
```

### navigation_bar_trailing

[`.navigationBarTrailing`](https://developer.apple.com/documentation/swiftui/toolbaritemplacement/navigationbartrailing) は非推奨になったため、 [`.topBarTrailing`](https://developer.apple.com/documentation/swiftui/toolbaritemplacement/topbartrailing) を好むルールです。

```yaml:.swiftlint.yml
  # iOS 14.0+
  navigation_bar_trailing:
    name: 'Navigation Bar Trailing'
    regex: '\bToolbarItem\(placement:\s*(.navigationBarTrailing)\s*\)'
    capture_group: 1
    message: 'Prefer using `.topBarTrailing` over `.navigationBarTrailing` because it was deprecated'
    severity: warning
```

```swift
// Non Triggering Examples
Text("Foo")
  .toolbar {
    ToolbarItem(placement: .topBarTrailing) {
      Button {
        print("Bar")
      } label: {
        Text("Bar")
      }
    }
  }

// Triggering Examples
Text("Foo")
  .toolbar {
    ToolbarItem(placement: ↓.navigationBarTrailing) {
      Button {
        print("Bar")
      } label: {
        Text("Bar")
      }
    }
  }
```

### import_lottie

[Lottie](https://github.com/airbnb/lottie-ios) のコンポーネントを直接使うより、自作の `UICore.LottieAnimationView` を好むルールです。

私はできる限りサードパーティ製のライブラリをカプセル化したいため、このようなルールを自作しています。

```yaml:.swiftlint.yml
  import_lottie:
    name: 'Import Lottie'
    regex: '\bimport\s+(Lottie)\b'
    capture_group: 1
    message: 'Prefer using `UICore.LottieAnimationView` over Lottie components'
    severity: warning
```

```swift
// Non Triggering Examples
import UICore

// Triggering Examples
import ↓Lottie
```

## おわりに

SwiftLintのカスタムルールは正規表現で書くので、慣れていないと難しいです。
私も試行錯誤しながら書いたので、誤っていたり、よりいい書き方があればコメントしてくださると嬉しいです :relaxed:

以上 [SwiftWednesday Advent Calendar 2023](https://qiita.com/advent-calendar/2023/swift-wednesday) の5日目の記事でした。
明日も @ojun_9 さんです。