SwiftLint/Source/SwiftLintFramework/Rules/Idiomatic/NoFallthroughOnlyRule.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
}
}