Permalink
Browse files

Add trailing_closure rule

Fixes realm#54
  • Loading branch information...
marcelofabri committed Jan 15, 2017
1 parent ff132bd commit 3cc98f24699d352c9d2dee417e2cfee9f41dfe92
View
@@ -9,6 +9,11 @@
* Improve `unused_optional_binding` rule on tuples check.
[Rafael Machado](https://github.com/rakaramos/)
* Add `trailing_closure` 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 positives on `shorthand_operator` rule.
@@ -129,6 +129,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: Rule, 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
}
}
@@ -132,11 +132,12 @@
D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */; };
D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */; };
D4C4A3521DEFBBB700E0E04C /* FileHeaderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */; };
D4DA1DF81E175E8A0037413D /* LinterCache+CommandLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF71E175E8A0037413D /* LinterCache+CommandLine.swift */; };
D4D5A5FF1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */; };
D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */; };
D4DA1DF81E175E8A0037413D /* LinterCache+CommandLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF71E175E8A0037413D /* LinterCache+CommandLine.swift */; };
D4DA1DFA1E18D6200037413D /* LargeTupleRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */; };
D4DA1DFE1E1A10DB0037413D /* NumberSeparatorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DA1DFD1E1A10DB0037413D /* NumberSeparatorConfiguration.swift */; };
D4DABFD51E2B350F009617B6 /* TrailingClosureRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DABFD41E2B350F009617B6 /* TrailingClosureRule.swift */; };
D4DAE8BC1DE14E8F00B0AE7A /* NimbleOperatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */; };
D4FBADD01E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */; };
D4FD58B21E12A0200019503C /* LinterCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FD58B11E12A0200019503C /* LinterCache.swift */; };
@@ -398,11 +399,12 @@
D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesConfiguration.swift; sourceTree = "<group>"; };
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRule.swift; sourceTree = "<group>"; };
D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderConfiguration.swift; sourceTree = "<group>"; };
D4DA1DF71E175E8A0037413D /* LinterCache+CommandLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LinterCache+CommandLine.swift"; sourceTree = "<group>"; };
D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShorthandOperatorRule.swift; sourceTree = "<group>"; };
D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompilerProtocolInitRule.swift; sourceTree = "<group>"; };
D4DA1DF71E175E8A0037413D /* LinterCache+CommandLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LinterCache+CommandLine.swift"; sourceTree = "<group>"; };
D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeTupleRule.swift; sourceTree = "<group>"; };
D4DA1DFD1E1A10DB0037413D /* NumberSeparatorConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorConfiguration.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>"; };
D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperatorUsageWhitespaceRule.swift; sourceTree = "<group>"; };
D4FD58B11E12A0200019503C /* LinterCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinterCache.swift; sourceTree = "<group>"; };
@@ -714,7 +716,6 @@
D4998DE81DF194F20006E05D /* FileHeaderRuleTests.swift */,
3B63D46C1E1F05160057BE35 /* LineLengthConfigurationTests.swift */,
3B63D46E1E1F09DF0057BE35 /* LineLengthRuleTests.swift */,
C9802F2E1E0C8AEE008AB27F /* TrailingCommaRuleTests.swift */,
E832F10C1B17E725003F265F /* IntegrationTests.swift */,
D4C27BFF1E12DFF500DF713E /* LinterCacheTests.swift */,
E86396C61BADAFE6002C9E88 /* ReporterTests.swift */,
@@ -840,6 +841,7 @@
D47A510D1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift */,
D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */,
E88DEA811B0990A700A66CB0 /* TodoRule.swift */,
D4DABFD41E2B350F009617B6 /* TrailingClosureRule.swift */,
D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */,
E88DEA871B09924C00A66CB0 /* TrailingNewlineRule.swift */,
E87E4A041BFB927C00FCFE46 /* TrailingSemicolonRule.swift */,
@@ -1189,6 +1191,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 */,
@@ -246,6 +246,10 @@ class RulesTests: XCTestCase {
verifyRule(TodoRule.description, commentDoesntViolate: false)
}
func testTrailingClosure() {
verifyRule(TrailingClosureRule.description)
}
func testTrailingNewline() {
verifyRule(TrailingNewlineRule.description, commentDoesntViolate: false,
stringDoesntViolate: false)
@@ -396,6 +400,7 @@ extension RulesTests {
("testSwitchCaseOnNewline", testSwitchCaseOnNewline),
("testSyntacticSugar", testSyntacticSugar),
("testTodo", testTodo),
("testTrailingClosure", testTrailingClosure),
("testTrailingNewline", testTrailingNewline),
("testTrailingSemicolon", testTrailingSemicolon),
("testTrailingWhitespace", testTrailingWhitespace),

0 comments on commit 3cc98f2

Please sign in to comment.