forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ForWhereRule.swift
146 lines (127 loc) · 5.32 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
//
// ForWhereRule.swift
// SwiftLint
//
// Created by Marcelo Fabri on 01/29/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct ForWhereRule: ASTRule, ConfigurationProviderRule {
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: [
"for user in users where user.id == 1 { }\n",
// if let
"for user in users {\n" +
" if let id = user.id { }\n" +
"}\n",
// if with else
"for user in users {\n" +
" if user.id == 1 { } else { }\n" +
"}\n",
// if with else if
"for user in users {\n" +
" if user.id == 1 {\n" +
"} else if user.id == 2 { }\n" +
"}\n",
// if is not the only expression inside for
"for user in users {\n" +
" if user.id == 1 { }\n" +
" print(user)\n" +
"}\n",
// if a variable is used
"for user in users {\n" +
" let id = user.id\n" +
" if id == 1 { }\n" +
"}\n",
// if something is after if
"for user in users {\n" +
" if user.id == 1 { }\n" +
" return true\n" +
"}\n",
// condition with multiple clauses
"for user in users {\n" +
" if user.id == 1 && user.age > 18 { }\n" +
"}\n"
],
triggeringExamples: [
"for user in users {\n" +
" ↓if user.id == 1 { return true }\n" +
"}\n"
]
)
private static let commentKinds = Set(SyntaxKind.commentAndStringKinds())
public func validate(file: File, kind: StatementKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard kind == .forEach,
let subDictionary = forBody(dictionary: dictionary),
subDictionary.substructure.count == 1,
let bodyDictionary = subDictionary.substructure.first,
bodyDictionary.kind.flatMap(StatementKind.init) == .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: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func forBody(dictionary: [String: SourceKitRepresentable]) -> [String : SourceKitRepresentable]? {
return dictionary.substructure.first(where: { subDict -> Bool in
subDict.kind.flatMap(StatementKind.init) == .brace
})
}
private func isOnlyOneIf(dictionary: [String: SourceKitRepresentable]) -> Bool {
let substructure = dictionary.substructure
guard substructure.count == 1 else {
return false
}
return dictionary.substructure.first?.kind.flatMap(StatementKind.init) == .brace
}
private func isOnlyIfInsideFor(forDictionary: [String: SourceKitRepresentable],
ifDictionary: [String: SourceKitRepresentable],
file: File) -> Bool {
guard let offset = forDictionary.offset,
let length = forDictionary.length,
let ifOffset = ifDictionary.offset,
let ifLength = ifDictionary.length else {
return false
}
let beforeIfRange = NSRange(location: offset, length: ifOffset - offset)
let ifFinalPosition = ifOffset + ifLength
let afterIfRange = NSRange(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: [String: SourceKitRepresentable], file: File) -> Bool {
let kind = "source.lang.swift.structure.elem.condition_expr"
let contents = file.contents.bridge()
return !dictionary.elements.filter { element in
guard element.kind == kind,
let offset = element.offset,
let length = element.length,
let range = contents.byteRangeToNSRange(start: offset, length: length) else {
return false
}
let containsLet = !file.match(pattern: "\\blet\\b", with: [.keyword], range: range).isEmpty
if containsLet {
return true
}
return !file.match(pattern: "\\|\\||&&", with: [], range: range).isEmpty
}.isEmpty
}
}