Skip to content

Commit

Permalink
Migrate no_fallthrough_only rule to SwiftSyntax (realm#4266)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelofabri authored Oct 3, 2022
1 parent ac91f7b commit f37ea8a
Showing 1 changed file with 25 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax

public struct NoFallthroughOnlyRule: ASTRule, ConfigurationProviderRule {
public struct NoFallthroughOnlyRule: SwiftSyntaxRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)

public init() {}
Expand All @@ -15,82 +14,36 @@ public struct NoFallthroughOnlyRule: ASTRule, ConfigurationProviderRule {
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 []
}
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
Visitor(viewMode: .sourceAccurate)
}
}

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
}
private extension NoFallthroughOnlyRule {
final class Visitor: SyntaxVisitor, ViolationsSyntaxVisitor {
private(set) var violationPositions: [AbsolutePosition] = []

guard nonCommentCaseBody.count == 1 else {
return []
}
override func visitPost(_ node: SwitchCaseListSyntax) {
let cases = node.compactMap { $0.as(SwitchCaseSyntax.self) }

let nsRange = nonCommentCaseBody[0].0
if contents.substring(with: nsRange) == "fallthrough" && nonCommentCaseBody[0].1 == [.keyword] &&
!isNextTokenUnknownAttribute(afterOffset: byteRange.upperBound, file: file) {
return [StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: nsRange.location))]
}
let violations = cases.enumerated()
.compactMap { index, element -> AbsolutePosition? in
guard element.statements.count == 1,
let fallthroughStmt = element.statements.first?.item.as(FallthroughStmtSyntax.self) else {
return nil
}

return []
}
if case let nextCaseIndex = cases.index(after: index),
nextCaseIndex < cases.endIndex,
case let nextCase = cases[nextCaseIndex],
nextCase.unknownAttr != nil {
return nil
}

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 fallthroughStmt.positionAfterSkippingLeadingTrivia
}
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
}
violationPositions.append(contentsOf: violations)
}
return nil
}
}

0 comments on commit f37ea8a

Please sign in to comment.