mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
cb53af5cd6
Resolves #918 `force_unwrapping: false positive when using ! as boolean operator`.
175 lines
7.8 KiB
Swift
175 lines
7.8 KiB
Swift
//
|
|
// 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.",
|
|
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)"
|
|
],
|
|
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\"]↓! }"
|
|
]
|
|
)
|
|
|
|
public func validate(file: File) -> [StyleViolation] {
|
|
return violationRanges(in: file).map {
|
|
return StyleViolation(ruleDescription: type(of: self).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\\p{Ps}])(!)(.?)"
|
|
|
|
private static let regularExpression = regex(pattern, options: [.dotMatchesLineSeparators])
|
|
private static let excludingSyntaxKindsForFirstCapture = SyntaxKind
|
|
.commentKeywordStringAndTypeidentifierKinds().map { $0.rawValue }
|
|
private static let excludingSyntaxKindsForSecondCapture = SyntaxKind
|
|
.commentAndStringKinds().map { $0.rawValue }
|
|
private static let excludingSyntaxKindsForThirdCapture = [SyntaxKind.identifier.rawValue]
|
|
|
|
// swiftlint:disable:next function_body_length
|
|
private func violationRanges(in file: File) -> [NSRange] {
|
|
let contents = file.contents
|
|
let nsstring = contents.bridge()
|
|
let range = NSRange(location: 0, length: contents.utf16.count)
|
|
let syntaxMap = file.syntaxMap
|
|
return ForceUnwrappingRule.regularExpression
|
|
.matches(in: contents, options: [], range: range)
|
|
.flatMap { match -> NSRange? in
|
|
if match.numberOfRanges < 3 { return nil }
|
|
|
|
let firstRange = match.rangeAt(1)
|
|
let secondRange = match.rangeAt(2)
|
|
|
|
let violationRange = NSRange(location: NSMaxRange(firstRange), length: 0)
|
|
|
|
guard let matchByteFirstRange = contents.bridge()
|
|
.NSRangeToByteRange(start: firstRange.location, length: firstRange.length),
|
|
let matchByteSecondRange = contents.bridge()
|
|
.NSRangeToByteRange(start: secondRange.location, length: secondRange.length)
|
|
else { return nil }
|
|
|
|
let tokensInFirstRange = syntaxMap.tokens(inByteRange: matchByteFirstRange)
|
|
let tokensInSecondRange = syntaxMap.tokens(inByteRange: matchByteSecondRange)
|
|
|
|
// check first captured range
|
|
// If not empty, first captured range is comment, string, keyword or typeidentifier.
|
|
// We checks "not empty" because tokens may empty without filtering.
|
|
guard tokensInFirstRange.filter({
|
|
ForceUnwrappingRule.excludingSyntaxKindsForFirstCapture.contains($0.type)
|
|
}).isEmpty else { return nil }
|
|
|
|
// if first captured range is identifier, generate violation
|
|
if tokensInFirstRange.map({ $0.type }).contains(SyntaxKind.identifier.rawValue) {
|
|
return violationRange
|
|
}
|
|
|
|
// check second capture '!'
|
|
let forceUnwrapNotInCommentOrString = tokensInSecondRange.filter({
|
|
ForceUnwrappingRule.excludingSyntaxKindsForSecondCapture.contains($0.type)
|
|
}).isEmpty
|
|
|
|
// check firstCapturedString is ")" and '!' is not within comment or string
|
|
let firstCapturedString = nsstring.substring(with: firstRange)
|
|
if firstCapturedString == ")" &&
|
|
forceUnwrapNotInCommentOrString { return violationRange }
|
|
|
|
// check third capture
|
|
if match.numberOfRanges == 3 {
|
|
|
|
// check third captured range
|
|
let secondRange = match.rangeAt(3)
|
|
guard let matchByteThirdRange = contents.bridge()
|
|
.NSRangeToByteRange(start: secondRange.location, length: secondRange.length)
|
|
else { return nil }
|
|
|
|
let tokensInThirdRange = syntaxMap.tokens(inByteRange: matchByteThirdRange).filter {
|
|
ForceUnwrappingRule.excludingSyntaxKindsForThirdCapture.contains($0.type)
|
|
}
|
|
// If not empty, third captured range is identifier.
|
|
// "!" is "operator prefix !".
|
|
if !tokensInThirdRange.isEmpty { return nil }
|
|
}
|
|
|
|
// check structure
|
|
if checkStructure(in: file, byteRange: matchByteFirstRange) {
|
|
return violationRange
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns if range should generate violation
|
|
// check deepest kind matching range in structure
|
|
private func checkStructure(in file: File, byteRange: NSRange) -> Bool {
|
|
let nsstring = file.contents.bridge()
|
|
let kinds = file.structure.kinds(forByteOffset: 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),
|
|
varDeclarationString.contains("=") {
|
|
// 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
|
|
}
|
|
if lastKind.kind.hasPrefix("source.lang.swift.decl.function") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|