Permalink
Browse files

Add trailing_closure rule

Fixes realm#54
  • Loading branch information...
marcelofabri committed Jan 15, 2017
1 parent a8f70d5 commit f1fc995369bc9d369c304939beb8f45d965c6671
@@ -74,6 +74,11 @@
[Samuel Susla](https://github.com/sammy-SC)
[#1387](https://github.com/realm/SwiftLint/issues/1165)
* Add `trailing_closure` opt-in rule that validates that trailing
closure syntax should be used whenever possible.
[Marcelo Fabri](https://github.com/marcelofabri)
[#54](https://github.com/realm/SwiftLint/issues/54)
##### Bug Fixes
* Fix false positive on `redundant_discardable_let` rule when using
@@ -85,6 +85,7 @@ public let masterRuleList = RuleList(rules: [
SwitchCaseOnNewlineRule.self,
SyntacticSugarRule.self,
TodoRule.self,
TrailingClosureRule.self,
TrailingCommaRule.self,
TrailingNewlineRule.self,
TrailingSemicolonRule.self,
@@ -0,0 +1,100 @@
//
// TrailingClosureRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 01/15/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct TrailingClosureRule: OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "trailing_closure",
name: "Trailing Closure",
description: "Trailing closure syntax should be used whenever possible",
nonTriggeringExamples: [
"foo.map { $0 + 1 }\n",
"foo.bar()\n",
"foo.reduce(0) { $0 + 1 }\n",
"if let foo = bar.map({ $0 + 1 }) { }\n"
],
triggeringExamples: [
"↓foo.map({ $0 + 1 })\n",
"↓foo.reduce(0, combine: { $0 + 1 })\n"
]
)
public func validate(file: File) -> [StyleViolation] {
return violationOffsets(for: file.structure.dictionary, file: file).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0))
}
}
private func violationOffsets(for dictionary: [String: SourceKitRepresentable], file: File) -> [Int] {
var results = [Int]()
if (dictionary["key.kind"] as? String).flatMap(SwiftExpressionKind.init) == .call,
shouldBeTrailingClosure(dictionary: dictionary, file: file),
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }) {
results = [offset]
}
if let kind = (dictionary["key.kind"] as? String).flatMap(StatementKind.init), kind != .brace {
// trailing closures are not allowed in `if`, `guard`, etc
results += dictionary.substructure.flatMap { subDict -> [Int] in
guard (subDict["key.kind"] as? String).flatMap(StatementKind.init) == .brace else {
return []
}
return violationOffsets(for: subDict, file: file)
}
} else {
results += dictionary.substructure.flatMap { subDict in
violationOffsets(for: subDict, file: file)
}
}
return results
}
private func shouldBeTrailingClosure(dictionary: [String: SourceKitRepresentable], file: File) -> Bool {
let arguments = dictionary.enclosedArguments
// check if last parameter should be trailing closure
if arguments.count > 1,
let lastArgument = dictionary.enclosedArguments.last,
(lastArgument["key.name"] as? String) != nil,
let offset = (lastArgument["key.bodyoffset"] as? Int64).flatMap({ Int($0) }),
let length = (lastArgument["key.bodylength"] as? Int64).flatMap({ Int($0) }),
let range = file.contents.bridge().byteRangeToNSRange(start: offset, length: length),
let match = regex("\\s*\\{").firstMatch(in: file.contents, options: [], range: range)?.range,
match.location == range.location {
return true
}
// check if there's only one unnamed parameter that is a closure
if arguments.isEmpty,
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }),
let totalLength = (dictionary["key.length"] as? Int64).flatMap({ Int($0) }),
let nameOffset = (dictionary["key.nameoffset"] as? Int64).flatMap({ Int($0) }),
let nameLength = (dictionary["key.namelength"] as? Int64).flatMap({ Int($0) }),
case let start = nameOffset + nameLength,
case let length = totalLength + offset - start,
let range = file.contents.bridge().byteRangeToNSRange(start: start, length: length),
let match = regex("\\s*\\(\\s*\\{").firstMatch(in: file.contents, options: [], range: range)?.range,
match.location == range.location {
return true
}
return false
}
}
@@ -177,6 +177,7 @@
D4DABFD31E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD21E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift */; };
D4DABFD71E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD61E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift */; };
D4DABFD91E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */; };
D4DABFD51E2B350F009617B6 /* TrailingClosureRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD41E2B350F009617B6 /* TrailingClosureRule.swift */; };
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 */; };
@@ -490,6 +491,7 @@
D4DABFD21E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardedNotificationCenterObserverRule.swift; sourceTree = "<group>"; };
D4DABFD61E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCenterDetachmentRule.swift; sourceTree = "<group>"; };
D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCenterDetachmentRuleExamples.swift; sourceTree = "<group>"; };
D4DABFD41E2B350F009617B6 /* TrailingClosureRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingClosureRule.swift; sourceTree = "<group>"; };
D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NimbleOperatorRule.swift; sourceTree = "<group>"; };
D4DB92241E628898005DE9C1 /* TodoRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoRuleTests.swift; sourceTree = "<group>"; };
D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorUsageWhitespaceRule.swift; sourceTree = "<group>"; };
@@ -975,6 +977,7 @@
D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */,
D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */,
E88DEA811B0990A700A66CB0 /* TodoRule.swift */,
D4DABFD41E2B350F009617B6 /* TrailingClosureRule.swift */,
D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */,
E88DEA871B09924C00A66CB0 /* TrailingNewlineRule.swift */,
E87E4A041BFB927C00FCFE46 /* TrailingSemicolonRule.swift */,
@@ -1354,6 +1357,7 @@
006204DC1E1E492F00FFFBE1 /* VerticalWhitespaceConfiguration.swift in Sources */,
E88198441BEA93D200333A11 /* ColonRule.swift in Sources */,
E809EDA11B8A71DF00399043 /* Configuration.swift in Sources */,
D4DABFD51E2B350F009617B6 /* TrailingClosureRule.swift in Sources */,
D4B022981E102EE8007E5297 /* ObjectLiteralRule.swift in Sources */,
2E336D1B1DF08BFB00CCFE77 /* EmojiReporter.swift in Sources */,
E8EA41171C2D1DBE004F9930 /* CheckstyleReporter.swift in Sources */,
@@ -284,6 +284,10 @@ class RulesTests: XCTestCase {
verifyRule(SyntacticSugarRule.description)
}
func testTrailingClosure() {
verifyRule(TrailingClosureRule.description)
}
func testTrailingNewline() {
verifyRule(TrailingNewlineRule.description, commentDoesntViolate: false,
stringDoesntViolate: false)

0 comments on commit f1fc995

Please sign in to comment.