forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathForceUnwrappingRule.swift
157 lines (137 loc) · 6.86 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
//
// ForceUnwrappingRule.swift
// SwiftLint
//
// Created by Benjamin Otto on 14/01/16.
// Copyright (c) 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.",
nonTriggeringExamples: [
"if let url = NSURL(string: query)",
"navigationController?.pushViewController(viewController, animated: true)",
"let s as! Test",
"try! canThrowErrors()",
"let object: AnyObject!",
"@IBOutlet var constraints: [NSLayoutConstraint]!",
"setEditing(!editing, animated: true)",
"navigationController.setNavigationBarHidden(!navigationController." +
"navigationBarHidden, animated: true)",
"if addedToPlaylist && (!self.selectedFilters.isEmpty || " +
"self.searchBar?.text?.isEmpty == false) {}",
],
triggeringExamples: [
"let url = NSURL(string: query)↓!",
"navigationController↓!.pushViewController(viewController, animated: true)",
"let unwrapped = optional↓!",
"return cell↓!",
"let url = NSURL(string: \"http://www.google.com\")↓!"
]
)
public func validateFile(file: File) -> [StyleViolation] {
return violationRangesInFile(file).map {
return StyleViolation(ruleDescription: self.dynamicType.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
// capture previous and next of "!"
// http://userguide.icu-project.org/strings/regexp
private static let pattern = "(\\S)!(.?)"
// swiftlint:disable:next force_try
private static let regularExpression = try! NSRegularExpression(pattern: pattern,
options: [.DotMatchesLineSeparators])
private static let excludingSyntaxKindsForFirstCapture = SyntaxKind
.commentKeywordStringAndTypeidentifierKinds().map { $0.rawValue }
private static let excludingSyntaxKindsForSecondCapture = [SyntaxKind.Identifier.rawValue]
private func violationRangesInFile(file: File) -> [NSRange] {
let contents = file.contents
let nsstring = contents as NSString
let range = NSRange(location: 0, length: contents.utf16.count)
let syntaxMap = file.syntaxMap
return ForceUnwrappingRule.regularExpression
.matchesInString(contents, options: [], range: range)
.flatMap { match -> NSRange? in
if match.numberOfRanges < 2 { return nil }
// check first captured range
let firstRange = match.rangeAtIndex(1)
let violationRange = NSRange(location: NSMaxRange(firstRange), length: 0)
guard let matchByteFirstRange = contents
.NSRangeToByteRange(start: firstRange.location, length: firstRange.length)
else { return nil }
let tokensInFirstRange = syntaxMap.tokensIn(matchByteFirstRange)
// If not empty, first captured range is comment, string, keyword or typeidentifier.
// We checks "not empty" because tokens may empty without filtering.
let tokensInFirstRangeExcludingSyntaxKindsOnly = tokensInFirstRange.filter({
ForceUnwrappingRule.excludingSyntaxKindsForFirstCapture.contains($0.type)
})
if !tokensInFirstRangeExcludingSyntaxKindsOnly.isEmpty { return nil }
// if first captured range is identifier, generate violation
if tokensInFirstRange.map({ $0.type }).contains(SyntaxKind.Identifier.rawValue) {
return violationRange
}
// check firstCapturedString is ")"
let firstCapturedString = nsstring.substringWithRange(firstRange)
if firstCapturedString == ")" { return violationRange }
// check second capture
if match.numberOfRanges == 3 {
// check second captured range
let secondRange = match.rangeAtIndex(2)
guard let matchByteSecondRange = contents
.NSRangeToByteRange(start: secondRange.location, length: secondRange.length)
else { return nil }
let tokensInSecondRange = syntaxMap.tokensIn(matchByteSecondRange).filter {
ForceUnwrappingRule.excludingSyntaxKindsForSecondCapture.contains($0.type)
}
// If not empty, second captured range is identifier.
// "!" is "operator prefix !".
if !tokensInSecondRange.isEmpty { return nil }
}
// check structure
if checkStructure(file, byteRange: matchByteFirstRange) {
return violationRange
} else {
return nil
}
}
}
// Returns if range should generate violation
// check deepest kind matching range in structure
private func checkStructure(file: File, byteRange: NSRange) -> Bool {
let nsstring = file.contents as NSString
let kinds = file.structure.kindsFor(byteRange.location)
if let lastKind = kinds.last {
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.VarStatic.rawValue:
let byteOffset = lastKind.byteRange.location
let byteLength = byteRange.location - byteOffset
if let varDeclarationString = nsstring
.substringWithByteRange(start: byteOffset, length: byteLength)
where varDeclarationString.containsString("=") {
// if declarations contains "=", range is not type annotation
return true
} else {
// range is type annotation of declaration
return false
}
// followings have invalid "key.length" returned from SourceKitService w/ Xcode 7.2.1
// case SwiftDeclarationKind.VarParameter.rawValue: fallthrough
// case SwiftDeclarationKind.VarLocal.rawValue: fallthrough
default:
break
}
}
return false
}
}