forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ClosureEndIndentationRule.swift
160 lines (140 loc) · 6.58 KB
/
ClosureEndIndentationRule.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//
// ClosureEndIndentationRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 12/18/16.
// Copyright © 2016 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct ClosureEndIndentationRule: ASTRule, OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "closure_end_indentation",
name: "Closure End Indentation",
description: "Closure end should have the same indentation as the line that started it.",
kind: .style,
nonTriggeringExamples: [
"SignalProducer(values: [1, 2, 3])\n" +
" .startWithNext { number in\n" +
" print(number)\n" +
" }\n",
"[1, 2].map { $0 + 1 }\n",
"return match(pattern: pattern, with: [.comment]).flatMap { range in\n" +
" return Command(string: contents, range: range)\n" +
"}.flatMap { command in\n" +
" return command.expand()\n" +
"}\n",
"foo(foo: bar,\n" +
" options: baz) { _ in }\n",
"someReallyLongProperty.chainingWithAnotherProperty\n" +
" .foo { _ in }",
"foo(abc, 123)\n" +
"{ _ in }\n"
],
triggeringExamples: [
"SignalProducer(values: [1, 2, 3])\n" +
" .startWithNext { number in\n" +
" print(number)\n" +
"↓}\n",
"return match(pattern: pattern, with: [.comment]).flatMap { range in\n" +
" return Command(string: contents, range: range)\n" +
" ↓}.flatMap { command in\n" +
" return command.expand()\n" +
"↓}\n"
]
)
private static let notWhitespace = regex("[^\\s]")
public func validate(file: File, kind: SwiftExpressionKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard kind == .call else {
return []
}
let contents = file.contents.bridge()
guard let offset = dictionary.offset,
let length = dictionary.length,
let bodyLength = dictionary.bodyLength,
let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
bodyLength > 0,
case let endOffset = offset + length - 1,
contents.substringWithByteRange(start: endOffset, length: 1) == "}",
let startOffset = startOffset(forDictionary: dictionary, file: file),
let (startLine, _) = contents.lineAndCharacter(forByteOffset: startOffset),
let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset),
case let nameEndPosition = nameOffset + nameLength,
let (bodyOffsetLine, _) = contents.lineAndCharacter(forByteOffset: nameEndPosition),
startLine != endLine, bodyOffsetLine != endLine,
!containsSingleLineClosure(dictionary: dictionary, endPosition: endOffset, file: file) else {
return []
}
let range = file.lines[startLine - 1].range
let regex = ClosureEndIndentationRule.notWhitespace
let actual = endPosition - 1
guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range,
case let expected = match.location - range.location,
expected != actual else {
return []
}
let reason = "Closure end should have the same indentation as the line that started it. " +
"Expected \(expected), got \(actual)."
return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: endOffset),
reason: reason)
]
}
private func startOffset(forDictionary dictionary: [String: SourceKitRepresentable], file: File) -> Int? {
guard let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength else {
return nil
}
let newLineRegex = regex("\n(\\s*\\}?\\.)")
let contents = file.contents.bridge()
guard let range = contents.byteRangeToNSRange(start: nameOffset, length: nameLength),
let match = newLineRegex.matches(in: file.contents, options: [],
range: range).last?.range(at: 1),
let methodByteRange = contents.NSRangeToByteRange(start: match.location,
length: match.length) else {
return nameOffset
}
return methodByteRange.location
}
private func containsSingleLineClosure(dictionary: [String: SourceKitRepresentable],
endPosition: Int,
file: File) -> Bool {
let contents = file.contents.bridge()
guard let closure = trailingClosure(dictionary: dictionary, file: file),
let start = closure.bodyOffset,
let (startLine, _) = contents.lineAndCharacter(forByteOffset: start),
let (endLine, _) = contents.lineAndCharacter(forByteOffset: endPosition) else {
return false
}
return startLine == endLine
}
private func trailingClosure(dictionary: [String: SourceKitRepresentable],
file: File) -> [String: SourceKitRepresentable]? {
let arguments = dictionary.enclosedArguments
let closureArguments = filterClosureArguments(arguments, file: file)
if closureArguments.count == 1,
closureArguments.last?.bridge() == arguments.last?.bridge() {
return closureArguments.last
}
return nil
}
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
}
}
}