Skip to content

Commit

Permalink
Rewrite ForceUnwrappingRule
Browse files Browse the repository at this point in the history
Add capture previous of "!" for:
- checking whether SyntaxKinds is comment, string, keyword or type identifier
- checking whether character is ")"
- checking whether SyntaxKinds is identifier or not
- SwiftDeclarationKind is one of some "Var*" and declaration containing "="

Add capture next of "!" for:
- checking SyntaxKinds is identifier

By applying this, the duration of linting Carthage 0.14 increase from 300ms to 380ms.
  • Loading branch information
norio-nomura committed Feb 19, 2016
1 parent e4f76c8 commit bde39b5
Showing 1 changed file with 109 additions and 4 deletions.
113 changes: 109 additions & 4 deletions Source/SwiftLintFramework/Rules/ForceUnwrappingRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,116 @@ public struct ForceUnwrappingRule: OptInRule, ConfigurationProviderRule {
)

public func validateFile(file: File) -> [StyleViolation] {
return file.matchPattern("\\S!",
excludingSyntaxKinds: SyntaxKind.commentKeywordStringAndTypeidentifierKinds()).map {
StyleViolation(ruleDescription: self.dynamicType.description,
return violationRangesInFile(file).map {
return StyleViolation(ruleDescription: self.dynamicType.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: NSMaxRange($0) - 1))
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
}
}

0 comments on commit bde39b5

Please sign in to comment.