forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathForceUnwrappingRule.swift
160 lines (141 loc) · 6.85 KB
/
ForceUnwrappingRule.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
//
// ForceUnwrappingRule.swift
// SwiftLint
//
// Created by Benjamin Otto on 14/01/16.
// Copyright © 2015 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct ForceUnwrappingRule: OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "force_unwrapping",
name: "Force Unwrapping",
description: "Force unwrapping should be avoided.",
kind: .idiomatic,
nonTriggeringExamples: [
"if let url = NSURL(string: query)",
"navigationController?.pushViewController(viewController, animated: true)",
"let s as! Test",
"try! canThrowErrors()",
"let object: Any!",
"@IBOutlet var constraints: [NSLayoutConstraint]!",
"setEditing(!editing, animated: true)",
"navigationController.setNavigationBarHidden(!navigationController." +
"navigationBarHidden, animated: true)",
"if addedToPlaylist && (!self.selectedFilters.isEmpty || " +
"self.searchBar?.text?.isEmpty == false) {}",
"print(\"\\(xVar)!\")",
"var test = (!bar)",
"var a: [Int]!",
"private var myProperty: (Void -> Void)!"
],
triggeringExamples: [
"let url = NSURL(string: query)↓!",
"navigationController↓!.pushViewController(viewController, animated: true)",
"let unwrapped = optional↓!",
"return cell↓!",
"let url = NSURL(string: \"http://www.google.com\")↓!",
"let dict = [\"Boooo\": \"👻\"]func bla() -> String { return dict[\"Boooo\"]↓! }",
"let dict = [\"Boooo\": \"👻\"]func bla() -> String { return dict[\"Boooo\"]↓!.contains(\"B\") }",
"let a = dict[\"abc\"]↓!.contains(\"B\")",
"dict[\"abc\"]↓!.bar(\"B\")",
"if dict[\"a\"]↓!!!! {",
"var foo: [Bool]! = dict[\"abc\"]↓!"
]
)
public func validate(file: File) -> [StyleViolation] {
return violationRanges(in: file).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
// capture previous of "!"
// http://userguide.icu-project.org/strings/regexp
private static let pattern = "([^\\s\\p{Ps}])(!+)"
private static let regularExpression = regex(pattern, options: [.dotMatchesLineSeparators])
private static let excludingSyntaxKindsForFirstCapture = SyntaxKind.commentKeywordStringAndTypeidentifierKinds()
private static let excludingSyntaxKindsForSecondCapture = SyntaxKind.commentAndStringKinds()
private func violationRanges(in file: File) -> [NSRange] {
let contents = file.contents
let nsstring = contents.bridge()
let range = NSRange(location: 0, length: nsstring.length)
let syntaxMap = file.syntaxMap
return ForceUnwrappingRule.regularExpression
.matches(in: contents, options: [], range: range)
.flatMap { match -> NSRange? in
return violationRange(match: match, nsstring: nsstring, syntaxMap: syntaxMap, file: file)
}
}
private func violationRange(match: NSTextCheckingResult, nsstring: NSString, syntaxMap: SyntaxMap,
file: File) -> NSRange? {
if match.numberOfRanges < 3 { return nil }
let firstRange = match.range(at: 1)
let secondRange = match.range(at: 2)
guard let matchByteFirstRange = nsstring
.NSRangeToByteRange(start: firstRange.location, length: firstRange.length),
let matchByteSecondRange = nsstring
.NSRangeToByteRange(start: secondRange.location, length: secondRange.length)
else { return nil }
let kindsInFirstRange = syntaxMap.kinds(inByteRange: matchByteFirstRange)
// check first captured range
// If not empty, first captured range is comment, string, keyword or typeidentifier.
// We checks "not empty" because kinds may empty without filtering.
guard !kindsInFirstRange
.contains(where: ForceUnwrappingRule.excludingSyntaxKindsForFirstCapture.contains) else {
return nil
}
let violationRange = NSRange(location: NSMaxRange(firstRange), length: 0)
// if first captured range is identifier, generate violation
if kindsInFirstRange.contains(.identifier) {
return violationRange
}
// check if firstCapturedString is either ")" or "]"
// and '!' is not within comment or string
// and matchByteFirstRange is not a type annotation
let firstCapturedString = nsstring.substring(with: firstRange)
if [")", "]"].contains(firstCapturedString) {
// check second capture '!'
let kindsInSecondRange = syntaxMap.kinds(inByteRange: matchByteSecondRange)
let forceUnwrapNotInCommentOrString = !kindsInSecondRange
.contains(where: ForceUnwrappingRule.excludingSyntaxKindsForSecondCapture.contains)
if forceUnwrapNotInCommentOrString &&
!isTypeAnnotation(in: file, contents: nsstring, byteRange: matchByteFirstRange) {
return violationRange
}
}
return nil
}
// check deepest kind matching range in structure is a typeAnnotation
private func isTypeAnnotation(in file: File, contents: NSString, byteRange: NSRange) -> Bool {
let kinds = file.structure.kinds(forByteOffset: byteRange.location)
guard let lastKind = kinds.last else {
return false
}
switch lastKind.kind {
// range is in some "source.lang.swift.decl.var.*"
case SwiftDeclarationKind.varClass.rawValue: fallthrough
case SwiftDeclarationKind.varGlobal.rawValue: fallthrough
case SwiftDeclarationKind.varInstance.rawValue: fallthrough
case SwiftDeclarationKind.varParameter.rawValue: fallthrough
case SwiftDeclarationKind.varLocal.rawValue: fallthrough
case SwiftDeclarationKind.varStatic.rawValue:
let byteOffset = lastKind.byteRange.location
let byteLength = byteRange.location - byteOffset
if let varDeclarationString = contents
.substringWithByteRange(start: byteOffset, length: byteLength),
varDeclarationString.contains("=") {
// if declarations contains "=", range is not type annotation
return false
}
// range is type annotation of declaration
return true
default:
break
}
return false
}
}