mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
147 lines
5.5 KiB
Swift
147 lines
5.5 KiB
Swift
//
|
|
// SwitchCaseOnNewlineRule.swift
|
|
// SwiftLint
|
|
//
|
|
// Created by Marcelo Fabri on 10/15/2016.
|
|
// Copyright © 2016 Realm. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct SwitchCaseOnNewlineRule: ConfigurationProviderRule, Rule, OptInRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() { }
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "switch_case_on_newline",
|
|
name: "Switch Case on Newline",
|
|
description: "Cases inside a switch should always be on a newline",
|
|
nonTriggeringExamples: [
|
|
"case 1:\n return true",
|
|
"default:\n return true",
|
|
"case let value:\n return true",
|
|
"/*case 1: */return true",
|
|
"//case 1:\n return true",
|
|
"let x = [caseKey: value]",
|
|
"let x = [key: .default]",
|
|
"if case let .someEnum(value) = aFunction([key: 2]) {",
|
|
"guard case let .someEnum(value) = aFunction([key: 2]) {",
|
|
"for case let .someEnum(value) = aFunction([key: 2]) {",
|
|
"case .myCase: // error from network",
|
|
"case let .myCase(value) where value > 10:\n return false",
|
|
"enum Environment {\n case development\n}",
|
|
"enum Environment {\n case development(url: URL)\n}",
|
|
"enum Environment {\n case development(url: URL) // staging\n}"
|
|
],
|
|
triggeringExamples: [
|
|
"case 1: return true",
|
|
"case let value: return true",
|
|
"default: return true",
|
|
"case \"a string\": return false",
|
|
"case .myCase: return false // error from network",
|
|
"case let .myCase(value) where value > 10: return false"
|
|
]
|
|
)
|
|
|
|
public func validateFile(_ file: File) -> [StyleViolation] {
|
|
let pattern = "(case[^\n]*|default):[^\\S\n]*[^\n]"
|
|
return file.rangesAndTokensMatching(pattern).filter { range, tokens in
|
|
guard let firstToken = tokens.first, tokenIsKeyword(token: firstToken) else {
|
|
return false
|
|
}
|
|
|
|
let tokenString = contentForToken(token: firstToken, file: file)
|
|
guard ["case", "default"].contains(tokenString) else {
|
|
return false
|
|
}
|
|
|
|
// check if the first token in the line is `case`
|
|
let lineAndCharacter = file.contents.lineAndCharacter(forByteOffset: range.location)
|
|
guard let (lineNumber, _) = lineAndCharacter else {
|
|
return false
|
|
}
|
|
|
|
let line = file.lines[lineNumber - 1]
|
|
let allLineTokens = file.syntaxMap.tokensIn(line.byteRange)
|
|
let lineTokens = allLineTokens.filter(tokenIsKeyword)
|
|
|
|
guard let firstLineToken = lineTokens.first else {
|
|
return false
|
|
}
|
|
|
|
let firstTokenInLineString = contentForToken(token: firstLineToken, file: file)
|
|
guard firstTokenInLineString == tokenString else {
|
|
return false
|
|
}
|
|
|
|
return isViolation(lineTokens: allLineTokens, file: file, line: line)
|
|
}.map {
|
|
StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: self.configuration.severity,
|
|
location: Location(file: file, characterOffset: $0.0.location))
|
|
}
|
|
}
|
|
|
|
private func tokenIsKeyword(token: SyntaxToken) -> Bool {
|
|
return SyntaxKind(rawValue: token.type) == .keyword
|
|
}
|
|
|
|
private func tokenIsComment(token: SyntaxToken) -> Bool {
|
|
guard let kind = SyntaxKind(rawValue: token.type) else {
|
|
return false
|
|
}
|
|
|
|
return SyntaxKind.commentKinds().contains(kind)
|
|
}
|
|
|
|
private func contentForToken(token: SyntaxToken, file: File) -> String {
|
|
return contentForRange(start: token.offset, length: token.length, file: file)
|
|
}
|
|
|
|
private func contentForRange(start: Int, length: Int, file: File) -> String {
|
|
return file.contents.substringWithByteRange(start: start, length: length) ?? ""
|
|
}
|
|
|
|
private func trailingComments(tokens: [SyntaxToken]) -> [SyntaxToken] {
|
|
var lastWasComment = true
|
|
return tokens.reversed().filter { token in
|
|
let shouldRemove = lastWasComment && tokenIsComment(token: token)
|
|
if !shouldRemove {
|
|
lastWasComment = false
|
|
}
|
|
return shouldRemove
|
|
}.reversed()
|
|
}
|
|
|
|
private func isViolation(lineTokens: [SyntaxToken], file: File, line: Line) -> Bool {
|
|
let trailingCommentsTokens = trailingComments(tokens: lineTokens)
|
|
|
|
guard let firstToken = lineTokens.first, !isEnumCase(file, token: firstToken) else {
|
|
return false
|
|
}
|
|
|
|
guard let firstComment = trailingCommentsTokens.first,
|
|
let lastComment = trailingCommentsTokens.last else {
|
|
return true
|
|
}
|
|
|
|
let commentsLength = (lastComment.offset + lastComment.length) - firstComment.offset
|
|
let line = contentForRange(start: line.byteRange.location,
|
|
length: line.byteRange.length - commentsLength, file: file)
|
|
let cleaned = line.trimmingCharacters(in: .whitespaces)
|
|
|
|
return !cleaned.hasSuffix(":")
|
|
}
|
|
|
|
private func isEnumCase(_ file: File, token: SyntaxToken) -> Bool {
|
|
let kinds = file.structure.kindsFor(token.offset).flatMap {
|
|
SwiftDeclarationKind(rawValue: $0.kind)
|
|
}
|
|
|
|
// it's a violation unless it's actually an enum case declaration
|
|
return kinds.contains(.enumcase)
|
|
}
|
|
}
|