From a3b7cb9b420a77e86f4eb0e4fd7953d511e33eae Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Thu, 27 Jul 2017 16:12:23 +0200 Subject: [PATCH] Add block_based_kvo rule Fixes #1714. --- CHANGELOG.md | 5 ++ Rules.md | 44 ++++++++++- .../Models/MasterRuleList.swift | 1 + .../Rules/BlockBasedKVORule.swift | 78 +++++++++++++++++++ SwiftLint.xcodeproj/project.pbxproj | 4 + Tests/LinuxMain.swift | 1 + .../SwiftLintFrameworkTests/RulesTests.swift | 6 ++ 7 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 Source/SwiftLintFramework/Rules/BlockBasedKVORule.swift 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) }