97 lines
3.5 KiB
Swift
97 lines
3.5 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct NoFallthroughOnlyRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "no_fallthrough_only",
|
|
name: "No Fallthrough Only",
|
|
description: "Fallthroughs can only be used if the `case` contains at least one other statement.",
|
|
kind: .idiomatic,
|
|
nonTriggeringExamples: NoFallthroughOnlyRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: NoFallthroughOnlyRuleExamples.triggeringExamples
|
|
)
|
|
|
|
public func validate(file: SwiftLintFile,
|
|
kind: StatementKind,
|
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
guard kind == .case,
|
|
let byteRange = dictionary.byteRange,
|
|
case let contents = file.stringView,
|
|
let range = contents.byteRangeToNSRange(byteRange),
|
|
let colonLocation = findCaseColon(text: file.stringView.nsString, range: range)
|
|
else {
|
|
return []
|
|
}
|
|
|
|
let caseBodyRange = NSRange(location: colonLocation,
|
|
length: range.length + range.location - colonLocation)
|
|
let nonCommentCaseBody = file.match(pattern: "\\w+", range: caseBodyRange).filter { _, syntaxKinds in
|
|
return !Set(syntaxKinds).subtracting(SyntaxKind.commentKinds).isEmpty
|
|
}
|
|
|
|
guard nonCommentCaseBody.count == 1 else {
|
|
return []
|
|
}
|
|
|
|
let nsRange = nonCommentCaseBody[0].0
|
|
if contents.substring(with: nsRange) == "fallthrough" && nonCommentCaseBody[0].1 == [.keyword] &&
|
|
!isNextTokenUnknownAttribute(afterOffset: byteRange.upperBound, file: file) {
|
|
return [StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, characterOffset: nsRange.location))]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
private func isNextTokenUnknownAttribute(afterOffset offset: ByteCount, file: SwiftLintFile) -> Bool {
|
|
let nextNonCommentToken = file.syntaxMap.tokens
|
|
.first { token in
|
|
guard let kind = token.kind, !kind.isCommentLike else {
|
|
return false
|
|
}
|
|
return token.offset > offset
|
|
}
|
|
|
|
return nextNonCommentToken?.kind == .attributeID &&
|
|
nextNonCommentToken.flatMap(file.contents(for:)) == "@unknown"
|
|
}
|
|
|
|
// Find the first colon that exists outside of all enclosing delimiters
|
|
private func findCaseColon(text: NSString, range: NSRange) -> Int? {
|
|
var nParen = 0
|
|
var nBrace = 0
|
|
var nBrack = 0
|
|
for index in range.location..<(range.location + range.length) {
|
|
let char = text.substring(with: NSRange(location: index, length: 1))
|
|
if char == "(" {
|
|
nParen += 1
|
|
}
|
|
if char == ")" {
|
|
nParen -= 1
|
|
}
|
|
if char == "[" {
|
|
nBrack += 1
|
|
}
|
|
if char == "]" {
|
|
nBrack -= 1
|
|
}
|
|
if char == "{" {
|
|
nBrace += 1
|
|
}
|
|
if char == "}" {
|
|
nBrace -= 1
|
|
}
|
|
|
|
if nParen == 0 && nBrack == 0 && nBrace == 0 && char == ":" {
|
|
return index
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|