forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TrailingClosureRule.swift
130 lines (110 loc) · 4.9 KB
/
TrailingClosureRule.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//
// 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.",
kind: .style,
nonTriggeringExamples: [
"foo.map { $0 + 1 }\n",
"foo.bar()\n",
"foo.reduce(0) { $0 + 1 }\n",
"if let foo = bar.map({ $0 + 1 }) { }\n",
"foo.something(param1: { $0 }, param2: { $0 + 1 })\n",
"offsets.sorted { $0.offset < $1.offset }\n"
],
triggeringExamples: [
"↓foo.map({ $0 + 1 })\n",
"↓foo.reduce(0, combine: { $0 + 1 })\n",
"↓offsets.sorted(by: { $0.offset < $1.offset })\n",
"↓foo.something(0, { $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.kind.flatMap(SwiftExpressionKind.init) == .call,
shouldBeTrailingClosure(dictionary: dictionary, file: file),
let offset = dictionary.offset {
results = [offset]
}
if let kind = dictionary.kind.flatMap(StatementKind.init), kind != .brace {
// trailing closures are not allowed in `if`, `guard`, etc
results += dictionary.substructure.flatMap { subDict -> [Int] in
guard subDict.kind.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 {
func isTrailingClosure() -> Bool {
return isAlreadyTrailingClosure(dictionary: dictionary, file: file)
}
let arguments = dictionary.enclosedArguments
// check if last parameter should be trailing closure
if !arguments.isEmpty,
case let closureArguments = filterClosureArguments(arguments, file: file),
closureArguments.count == 1,
closureArguments.last?.bridge() == arguments.last?.bridge() {
return !isTrailingClosure()
}
// check if there's only one unnamed parameter that is a closure
if arguments.isEmpty,
let offset = dictionary.offset,
let totalLength = dictionary.length,
let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
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 !isTrailingClosure()
}
return false
}
private func filterClosureArguments(_ arguments: [[String: SourceKitRepresentable]],
file: File) -> [[String: SourceKitRepresentable]] {
return arguments.filter { argument in
guard let offset = argument.bodyOffset,
let length = argument.bodyLength,
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 else {
return false
}
return true
}
}
private func isAlreadyTrailingClosure(dictionary: [String: SourceKitRepresentable], file: File) -> Bool {
guard let offset = dictionary.offset,
let length = dictionary.length,
let text = file.contents.bridge().substringWithByteRange(start: offset, length: length) else {
return false
}
return !text.hasSuffix(")")
}
}