LoginSignup
20
7

SwiftLintで追加・変更・廃止されたルールまとめ(Swift 4.2→5.1.2版)

Last updated at Posted at 2019-12-08

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

はじめに

SwiftLintの全ルール一覧(Swift 4.2版) - Qiita から2019/12/08現在の最新版までの間に、追加・変更・廃止されたルールを解説します。

前回の記事を読んでいなくても読み進められますが、併せて読むことをオススメします。

比較方法

以下の2ファイルを比較し、差分を翻訳しました。
https://github.com/realm/SwiftLint/blob/0.30.1/Rules.md
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md

ルール
追加 21
変更 5
廃止 1

※ソースでなくドキュメントを比較して調査したため、実際の数と異なる可能性があります。

環境

  • Swift:5.1.2
  • Xcode:11.2.1 (11B500)
  • SwiftLint:0.38.0

追加されたルール

Contains Over Filter Count

  • デフォルト:無効

filter(where:).count0 と比較するより contains を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#contains-over-filter-count

// bad
let result = myList.filter { $0 % 2 == 0 }.count > 0
let result = myList.filter { $0 % 2 == 0 }.count == 0

// good
let result = myList.contains { $0 % 2 == 0 }
let result = myList.filter { $0 % 2 == 0 }.count > 1 // 0以外と比較するのはOK

Contains Over Filter Is Empty

  • デフォルト:無効

filter(where:).isEmpty より contains を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#contains-over-filter-is-empty

// bad
let result = myList.filter { $0 % 2 == 0 } .isEmpty

// good
let result = myList.contains { $0 % 2 == 0 }

Contains over range(of:) comparison to nil

  • デフォルト:無効

range(of:) != nilrange(of:) == nil より contains を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#contains-over-rangeof-comparison-to-nil

// bad
myString.range(of: "Test") != nil
myString.range(of: "Test") == nil

// good
myString.contains("Test")
let range = myString.range(of: "Test")
resourceString.range(of: rule.regex, options: .regularExpression) != nil

Deployment Target

  • デフォルト:有効

可用性のチェックや属性は、デプロイメントターゲットで満たす古いバージョンを使うべきではありません。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#deployment-target

// bad
@available(iOS 6.0, *)
class A {}

if #available(iOS 6.0, *) {}

// good
@available(iOS 12.0, *)
class A {}

if #available(iOS 10.0, *) {}

Duplicate Enum Cases

  • デフォルト:有効

列挙型には同名のケースを複数含められません。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#duplicate-enum-cases

// bad
enum PictureImport {
    case add(image: UIImage)
    case add(data: Data)
}

// good
enum PictureImport {
    case addImage(image: UIImage)
    case addData(data: Data)
}

Empty Collection Literal

  • デフォルト:無効

コレクションを空の配列や辞書リテラルと比較するより、 isEmpty をチェックすべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#empty-collection-literal

// bad
myArray == []
myArray != []
myDict == [:]
myDict != [:]

// good
myArray.isEmpty
!myArray.isEmpty
myDict.isEmpty
!myDict.isEmpty

ExpiringTodo

  • デフォルト:無効

TODOとFIXMEは有効期限前に解決すべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#expiringtodo

// bad
// TODO: [12/7/2019]
// FIXME: [12/7/2019]

// good
// TODO: [12/31/9999]
// FIXME: [12/31/9999]

File Types Order

  • デフォルト:無効

ファイル内の型の表示順を指定します。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#file-types-order

// bad
// `protocol` (サポートする型)は `class` (主となる型)の前に書くべき
class TestViewController: UIViewController {}
protocol TestViewControllerDelegate {
    func didPressTrackedButton()
}

// `extension` は `class` の後に書くべき
extension TestViewController: UITableViewDataSource { // (省略) }
class TestViewController: UIViewController {}

// good
// Supporting Types
protocol TestViewControllerDelegate: AnyObject {
    func didPressTrackedButton()
}

// Main Type
class TestViewController: UIViewController {
    // Type Aliases
    typealias CompletionHandler = ((TestEnum) -> Void)

    // Subtypes
    class TestClass {
        // 10 lines
    }

    struct TestStruct {
        // 3 lines
    }

    enum TestEnum {
        // 5 lines
    }

    // Stored Type Properties
    static let cellIdentifier: String = "AmazingCell"

    // Stored Instance Properties
    var shouldLayoutView1: Bool!
    weak var delegate: TestViewControllerDelegate?
    private var hasLayoutedView1: Bool = false
    private var hasLayoutedView2: Bool = false

    // Computed Instance Properties
    private var hasAnyLayoutedView: Bool {
         return hasLayoutedView1 || hasLayoutedView2
    }

    // IBOutlets
    @IBOutlet private var view1: UIView!
    @IBOutlet private var view2: UIView!

    // Initializers
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // Type Methods
    static func makeViewController() -> TestViewController {
        return TestViewController()
    }

    // Life-Cycle Methods
    override func viewDidLoad() {
        super.viewDidLoad()

        view1.setNeedsLayout()
        view1.layoutIfNeeded()
        hasLayoutedView1 = true
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        view2.setNeedsLayout()
        view2.layoutIfNeeded()
        hasLayoutedView2 = true
    }

    // IBActions
    @IBAction func goNextButtonPressed() {
        goToNextVc()
        delegate?.didPressTrackedButton()
    }

    @objc
    func goToRandomVcButtonPressed() {
        goToRandomVc()
    }

    // MARK: Other Methods
    func goToNextVc() { /* TODO */ }

    func goToInfoVc() { /* TODO */ }

    func goToRandomVc() {
        let viewCtrl = getRandomVc()
        present(viewCtrl, animated: true)
    }

    private func getRandomVc() -> UIViewController { return UIViewController() }

    // Subscripts
    subscript(_ someIndexThatIsNotEvenUsed: Int) -> String {
        get {
            return "This is just a test"
        }

        set {
            print("Just a test \(newValue)")
        }
    }
}

// Extensions
extension TestViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

FlatMap over map and reduce

  • デフォルト:無効

reduce([], +) が後に続く map よりも flatMap を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#flatmap-over-map-and-reduce

// bad
let foo = bar.map { $0.array }.reduce([], +)

// good
let foo = bar.flatMap { $0.array }

Legacy Multiple

  • デフォルト:無効

% より isMultiple(of:) を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#legacy-multiple

// bad
cell.contentView.backgroundColor = indexPath.row % 2 == 0 ? .gray : .white

// good
cell.contentView.backgroundColor = indexPath.row.isMultiple(of: 2) ? .gray : .white

No Space in Method Call

  • デフォルト:有効

メソッド名と括弧の間にスペースを入れてはいけません。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#no-space-in-method-call

// bad
foo ()

// good
foo()

NSLocalizedString Require Bundle

  • デフォルト:無効

NSLocalizedString の呼び出しは、文字列ファイルを含むバンドルを指定すべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#nslocalizedstring-require-bundle

// bad
NSLocalizedString("someKey", comment: "test")

// good
NSLocalizedString("someKey", bundle: .main, comment: "test")
NSLocalizedString("someKey", bundle: Bundle(for: A.self), comment: "test")
arbitraryFunctionCall("something")

NSObject Prefer isEqual

  • デフォルト:有効

NSObject の子クラスは == の代わりに isEqual を実装すべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#nsobject-prefer-isequal

// bad
class AClass: NSObject {
    static func ==(lhs: AClass, rhs: AClass) -> Bool {
        return true
    }
}

// good
class AClass: NSObject {}
class AClass: NSObject {
    override func isEqual(_ object: Any?) -> Bool {
        return true
    }
}

Raw Value For Camel Cased Codable Enum

  • デフォルト:無効

Codable プロトコルに準拠している String の列挙型において、キャメルケースのケースはRaw valueを持つべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#raw-value-for-camel-cased-codable-enum

// bad
enum Status: String, Codable {
    case ok
    case notAcceptable
    case maybeAcceptable = "maybe_acceptable"
}
enum Status: String, Decodable {
   case ok
   case notAcceptable
   case maybeAcceptable = "maybe_acceptable"
}
enum Status: String, Encodable {
   case ok
   case notAcceptable
   case maybeAcceptable = "maybe_acceptable"
}

// good
enum Status: String, Codable {
    case ok
    case notAcceptable = "not_acceptable"
    case maybeAcceptable = "maybe_acceptable"
}

Reduce Boolean

  • デフォルト:有効

reduce(true)reduce(false) より、 .allSatisfy().contains() を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#reduce-boolean

// bad
let allNines = nums.reduce(true) { $0.0 && $0.1 == 9 }
let anyNines = nums.reduce(false) { $0.0 || $0.1 == 9 }

// good
let allNines = nums.allSatisfy { $0 == 9 }
let anyNines = nums.contains { $0 == 9 }

Reduce Into

  • デフォルト:無効

コピーオンライト型の場合、 reduce(_:_:) より reduce(into:_:) を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#reduce-into

// bad
let bar = values.reduce("abc") { $0 + "\($1)" }

// good
let foo = values.reduce(into: "abc") { $0 += "\($1)" }

Required Deinit

  • デフォルト:無効

クラスは明示的に deinit メソッドを持つべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#required-deinit

// bad
class Apple { }

// good
class Apple {
    deinit { }
}

Type Contents Order

  • デフォルト:無効

型内のサブタイプ、プロパティ、メソッドなどの表示順を指定します。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#type-contents-order

// bad
// `typealias` は `class` の前に書くべき
class TestViewController: UIViewController {
    class TestClass { }
    typealias CompletionHandler = ((TestEnum) -> Void)
}

// good
protocol TestViewControllerDelegate: AnyObject {
    func didPressTrackedButton()
}

class TestViewController: UIViewController {
    // Type Aliases
    typealias CompletionHandler = ((TestEnum) -> Void)

    // Subtypes
    class TestClass {
        // 10 lines
    }

    struct TestStruct {
        // 3 lines
    }

    enum TestEnum {
        // 5 lines
    }

    // Type Properties
    static let cellIdentifier: String = "AmazingCell"

    // Instance Properties
    var shouldLayoutView1: Bool!
    weak var delegate: TestViewControllerDelegate?
    private var hasLayoutedView1: Bool = false
    private var hasLayoutedView2: Bool = false

    private var hasAnyLayoutedView: Bool {
         return hasLayoutedView1 || hasLayoutedView2
    }

    // IBOutlets
    @IBOutlet private var view1: UIView!
    @IBOutlet private var view2: UIView!

    // Initializers
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // Type Methods
    static func makeViewController() -> TestViewController {
        return TestViewController()
    }

    // View Life-Cycle Methods
    override func viewDidLoad() {
        super.viewDidLoad()

        view1.setNeedsLayout()
        view1.layoutIfNeeded()
        hasLayoutedView1 = true
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        view2.setNeedsLayout()
        view2.layoutIfNeeded()
        hasLayoutedView2 = true
    }

    // IBActions
    @IBAction func goNextButtonPressed() {
        goToNextVc()
        delegate?.didPressTrackedButton()
    }

    // Other Methods
    func goToNextVc() { /* TODO */ }

    func goToInfoVc() { /* TODO */ }

    func goToRandomVc() {
        let viewCtrl = getRandomVc()
        present(viewCtrl, animated: true)
    }

    private func getRandomVc() -> UIViewController { return UIViewController() }

    // Subscripts
    subscript(_ someIndexThatIsNotEvenUsed: Int) -> String {
        get {
            return "This is just a test"
        }

        set {
            print("Just a test \(newValue)")
        }
    }
}

Unowned Variable Capture

  • デフォルト:無効

潜在的なクラッシュを避けるため、弱参照としてキャプチャすべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#unowned-variable-capture

// bad
foo { [unowned self] in _ }
foo { [unowned bar] in _ }

// good
foo { [weak self] in _ }
foo { [weak bar] in _ }

Unused Capture List

  • デフォルト:有効

キャプチャリスト内の未使用の参照は削除すべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#unused-capture-list

// bad
[1, 2].map { [weak self] num in
    print(num)
}

// good
[1, 2].map { num in
    print(num)
}

[1, 2].map { [weak self] num in
    self?.handle(num)
}

Unused Declaration

  • デフォルト:無効

Unused Private Declarationが廃止され、代わりに追加されました。

private 以外でも、リントされた全ファイル内で1回は参照されるべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#unused-declaration

// bad
let a = 0

// good
let a = 0
_ = a

変更されたルール

Contains over first not nil

first(where:) != nilfirstIndex(where:) != nil より contains を使うべきです。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#contains-over-first-not-nil

firstIndex(where:) != nil が追加されました。
https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#contains-over-first-not-nil

// bad
myList.firstIndex { $0 % 2 == 0 } != nil
myList.firstIndex(where: { $0 % 2 == 0 }) != nil

// good
let firstIndex = myList.firstIndex { $0 % 2 == 0 }
let firstIndex = myList.firstIndex(where: { $0 % 2 == 0 })

Control Statement

オートコレクトされるようになりました。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#control-statement

Redundant @objc Attribute

オートコレクトされるようになりました。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#redundant-objc-attribute

Syntactic Sugar

オートコレクトされるようになりました。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#syntactic-sugar

Toggle Bool

オートコレクトされるようになりました。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#toggle-bool

廃止されたルール

Unused Private Declaration

Unused Declaration に統合されました。
https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#unused-private-declaration

おわりに

これでSwift 5.1.2でもSwiftLintを適切に使うことができます!

20
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
7