diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80150d62c3a..790efed459e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -87,6 +87,11 @@
[Marcelo Fabri](https://github.com/marcelofabri)
[#1294](https://github.com/realm/SwiftLint/issues/1294)
+* Add `block_based_kvo` rule that enforces the usage of the new block based
+ KVO API added on Swift 3.2 and later.
+ [Marcelo Fabri](https://github.com/marcelofabri)
+ [#1714](https://github.com/realm/SwiftLint/issues/1714)
+
##### Bug Fixes
* Fix false positive on `redundant_discardable_let` rule when using
diff --git a/Rules.md b/Rules.md
index 5469a670d24..171d5d5694f 100644
--- a/Rules.md
+++ b/Rules.md
@@ -2,6 +2,7 @@
# Rules
* [Attributes](#attributes)
+* [Block Based KVO](#block-based-kvo)
* [Class Delegate Protocol](#class-delegate-protocol)
* [Closing Brace Spacing](#closing-brace-spacing)
* [Closure End Indentation](#closure-end-indentation)
@@ -439,6 +440,47 @@ func foo(completionHandler: @escaping () -> Void)
+## Block Based KVO
+
+Identifier | Enabled by default | Supports autocorrection | Kind
+--- | --- | --- | ---
+`block_based_kvo` | Enabled | No | idiomatic
+
+Prefer the new block based KVO API with keypaths when using Swift 3.2 or later.
+
+### Examples
+
+
+Non Triggering Examples
+
+```swift
+let observer = foo.observe(\.value, options: [.new]) { (foo, change) in
+ print(change.newValue)
+}
+```
+
+
+
+Triggering Examples
+
+```swift
+class Foo: NSObject {
+ override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
+ change: [NSKeyValueChangeKey : Any]?,
+ context: UnsafeMutableRawPointer?) {}
+```
+
+```swift
+class Foo: NSObject {
+ override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
+ change: Dictionary?,
+ context: UnsafeMutableRawPointer?) {}
+```
+
+
+
+
+
## Class Delegate Protocol
Identifier | Enabled by default | Supports autocorrection | Kind
@@ -9011,7 +9053,7 @@ Identifier | Enabled by default | Supports autocorrection | Kind
--- | --- | --- | ---
`trailing_closure` | Disabled | No | style
-Trailing closure syntax should be used whenever possible
+Trailing closure syntax should be used whenever possible.
### Examples
diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift
index 248aa09cee8..6afedf44d92 100644
--- a/Source/SwiftLintFramework/Models/MasterRuleList.swift
+++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift
@@ -10,6 +10,7 @@
public let masterRuleList = RuleList(rules: [
AttributesRule.self,
+ BlockBasedKVORule.self,
ClassDelegateProtocolRule.self,
ClosingBraceRule.self,
ClosureEndIndentationRule.self,
diff --git a/Source/SwiftLintFramework/Rules/BlockBasedKVORule.swift b/Source/SwiftLintFramework/Rules/BlockBasedKVORule.swift
new file mode 100644
index 00000000000..0c7db3316e4
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/BlockBasedKVORule.swift
@@ -0,0 +1,78 @@
+//
+// BlockBasedKVORule.swift
+// SwiftLint
+//
+// Created by Marcelo Fabri on 07/27/17.
+// Copyright © 2017 Realm. All rights reserved.
+//
+
+import Foundation
+import SourceKittenFramework
+
+public struct BlockBasedKVORule: ASTRule, ConfigurationProviderRule {
+ public var configuration = SeverityConfiguration(.warning)
+
+ public init() {}
+
+ public static let description = RuleDescription(
+ identifier: "block_based_kvo",
+ name: "Block Based KVO",
+ description: "Prefer the new block based KVO API with keypaths when using Swift 3.2 or later.",
+ kind: .idiomatic,
+ nonTriggeringExamples: [
+ "let observer = foo.observe(\\.value, options: [.new]) { (foo, change) in\n" +
+ " print(change.newValue)\n" +
+ "}"
+ ],
+ triggeringExamples: [
+ "class Foo: NSObject {\n" +
+ " override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,\n" +
+ " change: [NSKeyValueChangeKey : Any]?,\n" +
+ " context: UnsafeMutableRawPointer?) {}",
+ "class Foo: NSObject {\n" +
+ " override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,\n" +
+ " change: Dictionary?,\n" +
+ " context: UnsafeMutableRawPointer?) {}"
+ ]
+ )
+
+ public func validate(file: File, kind: SwiftDeclarationKind,
+ dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
+ guard SwiftVersion.current == .four, kind == .functionMethodInstance,
+ dictionary.enclosedSwiftAttributes.contains("source.decl.attribute.override"),
+ dictionary.name == "observeValue(forKeyPath:of:change:context:)",
+ hasExpectedParamTypes(types: dictionary.enclosedVarParameters.parameterTypes),
+ let offset = dictionary.offset else {
+ return []
+ }
+
+ return [
+ StyleViolation(ruleDescription: type(of: self).description,
+ location: Location(file: file, byteOffset: offset))
+ ]
+ }
+
+ private func hasExpectedParamTypes(types: [String]) -> Bool {
+ guard types.count == 4,
+ types[0] == "String?",
+ types[1] == "Any?",
+ types[2] == "[NSKeyValueChangeKey:Any]?" || types[2] == "Dictionary?",
+ types[3] == "UnsafeMutableRawPointer?" else {
+ return false
+ }
+
+ return true
+ }
+}
+
+private extension Array where Element == [String: SourceKitRepresentable] {
+ var parameterTypes: [String] {
+ return flatMap { element in
+ guard element.kind.flatMap(SwiftDeclarationKind.init) == .varParameter else {
+ return nil
+ }
+
+ return element.typeName?.replacingOccurrences(of: " ", with: "")
+ }
+ }
+}
diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj
index ed816479c22..346d6ae3d91 100644
--- a/SwiftLint.xcodeproj/project.pbxproj
+++ b/SwiftLint.xcodeproj/project.pbxproj
@@ -181,6 +181,7 @@
D4DAE8BC1DE14E8F00B0AE7A /* NimbleOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */; };
D4DB92251E628898005DE9C1 /* TodoRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */; };
D4FBADD01E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */; };
+ D4FD4C851F2A260A00DD8AA8 /* BlockBasedKVORule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */; };
D4FD58B21E12A0200019503C /* LinterCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FD58B11E12A0200019503C /* LinterCache.swift */; };
D93DA3D11E699E6300809827 /* NestingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93DA3CF1E699E4E00809827 /* NestingConfiguration.swift */; };
DAD3BE4A1D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */; };
@@ -495,6 +496,7 @@
D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleOperatorRule.swift; sourceTree = ""; };
D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoRuleTests.swift; sourceTree = ""; };
D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorUsageWhitespaceRule.swift; sourceTree = ""; };
+ D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockBasedKVORule.swift; sourceTree = ""; };
D4FD58B11E12A0200019503C /* LinterCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinterCache.swift; sourceTree = ""; };
D93DA3CF1E699E4E00809827 /* NestingConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NestingConfiguration.swift; sourceTree = ""; };
DAD3BE491D6ECD9500660239 /* PrivateOutletRuleConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOutletRuleConfiguration.swift; sourceTree = ""; };
@@ -898,6 +900,7 @@
D47A510F1DB2DD4800A4CC21 /* AttributesRule.swift */,
D48AE2CB1DFB58C5001C6A4A /* AttributesRulesExamples.swift */,
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */,
+ D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */,
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */,
D43B046A1E075905004016AF /* ClosureEndIndentationRule.swift */,
D47079A81DFDBED000027086 /* ClosureParameterPositionRule.swift */,
@@ -1408,6 +1411,7 @@
C328A2F71E6759AE00A9E4D7 /* ExplicitTypeInterfaceRule.swift in Sources */,
93E0C3CE1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift in Sources */,
D43DB1081DC573DA00281215 /* ImplicitGetterRule.swift in Sources */,
+ D4FD4C851F2A260A00DD8AA8 /* BlockBasedKVORule.swift in Sources */,
7C0C2E7A1D2866CB0076435A /* ExplicitInitRule.swift in Sources */,
E88DEA771B098D0C00A66CB0 /* Rule.swift in Sources */,
D47079AB1DFDCF7A00027086 /* SwiftExpressionKind.swift in Sources */,
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index 6f2a779d98c..6233406b955 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -312,6 +312,7 @@ extension RuleTests {
extension RulesTests {
static var allTests: [(String, (RulesTests) -> () throws -> Void)] = [
+ ("testBlockBasedKVO", testBlockBasedKVO),
("testClassDelegateProtocol", testClassDelegateProtocol),
("testClosingBrace", testClosingBrace),
("testClosureEndIndentation", testClosureEndIndentation),
diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift
index 4c0611394b9..f57d76ae48a 100644
--- a/Tests/SwiftLintFrameworkTests/RulesTests.swift
+++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift
@@ -12,6 +12,12 @@ import XCTest
// swiftlint:disable file_length
class RulesTests: XCTestCase {
+ func testBlockBasedKVO() {
+ #if swift(>=3.2)
+ verifyRule(BlockBasedKVORule.description)
+ #endif
+ }
+
func testClassDelegateProtocol() {
verifyRule(ClassDelegateProtocolRule.description)
}