forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathForWhereRule.swift
165 lines (149 loc) · 5.53 KB
/
ForWhereRule.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
161
162
163
164
165
import SourceKittenFramework
public struct ForWhereRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "for_where",
name: "For Where",
description: "`where` clauses are preferred over a single `if` inside a `for`.",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
for user in users where user.id == 1 { }
"""),
// if let
Example("""
for user in users {
if let id = user.id { }
}
"""),
// if var
Example("""
for user in users {
if var id = user.id { }
}
"""),
// if with else
Example("""
for user in users {
if user.id == 1 { } else { }
}
"""),
// if with else if
Example("""
for user in users {
if user.id == 1 {
} else if user.id == 2 { }
}
"""),
// if is not the only expression inside for
Example("""
for user in users {
if user.id == 1 { }
print(user)
}
"""),
// if a variable is used
Example("""
for user in users {
let id = user.id
if id == 1 { }
}
"""),
// if something is after if
Example("""
for user in users {
if user.id == 1 { }
return true
}
"""),
// condition with multiple clauses
Example("""
for user in users {
if user.id == 1 && user.age > 18 { }
}
"""),
// if case
Example("""
for (index, value) in array.enumerated() {
if case .valueB(_) = value {
return index
}
}
""")
],
triggeringExamples: [
Example("""
for user in users {
↓if user.id == 1 { return true }
}
""")
]
)
private static let commentKinds = SyntaxKind.commentAndStringKinds
public func validate(file: SwiftLintFile, kind: StatementKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .forEach,
let subDictionary = forBody(dictionary: dictionary),
subDictionary.substructure.count == 1,
let bodyDictionary = subDictionary.substructure.first,
bodyDictionary.statementKind == .if,
isOnlyOneIf(dictionary: bodyDictionary),
isOnlyIfInsideFor(forDictionary: subDictionary, ifDictionary: bodyDictionary, file: file),
!isComplexCondition(dictionary: bodyDictionary, file: file),
let offset = bodyDictionary .offset else {
return []
}
return [
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func forBody(dictionary: SourceKittenDictionary) -> SourceKittenDictionary? {
return dictionary.substructure.first(where: { subDict -> Bool in
subDict.statementKind == .brace
})
}
private func isOnlyOneIf(dictionary: SourceKittenDictionary) -> Bool {
let substructure = dictionary.substructure
guard substructure.count == 1 else {
return false
}
return dictionary.substructure.first?.statementKind == .brace
}
private func isOnlyIfInsideFor(forDictionary: SourceKittenDictionary,
ifDictionary: SourceKittenDictionary,
file: SwiftLintFile) -> Bool {
guard let offset = forDictionary.offset,
let length = forDictionary.length,
let ifOffset = ifDictionary.offset,
let ifLength = ifDictionary.length else {
return false
}
let beforeIfRange = ByteRange(location: offset, length: ifOffset - offset)
let ifFinalPosition = ifOffset + ifLength
let afterIfRange = ByteRange(location: ifFinalPosition, length: offset + length - ifFinalPosition)
let allKinds = file.syntaxMap.kinds(inByteRange: beforeIfRange) +
file.syntaxMap.kinds(inByteRange: afterIfRange)
let doesntContainComments = !allKinds.contains { kind in
!ForWhereRule.commentKinds.contains(kind)
}
return doesntContainComments
}
private func isComplexCondition(dictionary: SourceKittenDictionary, file: SwiftLintFile) -> Bool {
let kind = "source.lang.swift.structure.elem.condition_expr"
return dictionary.elements.contains { element in
guard element.kind == kind,
let range = element.byteRange.flatMap(file.stringView.byteRangeToNSRange)
else {
return false
}
let containsKeyword = file.match(pattern: "\\blet|var|case\\b", with: [.keyword], range: range).isNotEmpty
if containsKeyword {
return true
}
return file.match(pattern: "\\|\\||&&", with: [], range: range).isNotEmpty
}
}
}