SwiftLint/Source/SwiftLintFramework/Rules/Style/CommaRule.swift

126 lines
5.7 KiB
Swift

import Foundation
import SourceKittenFramework
public struct CommaRule: SubstitutionCorrectableRule, ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "comma",
name: "Comma Spacing",
description: "There should be no space before and one after any comma.",
kind: .style,
nonTriggeringExamples: [
"func abc(a: String, b: String) { }",
"abc(a: \"string\", b: \"string\"",
"enum a { case a, b, c }",
"func abc(\n a: String, // comment\n bcd: String // comment\n) {\n}\n",
"func abc(\n a: String,\n bcd: String\n) {\n}\n",
"#imageLiteral(resourceName: \"foo,bar,baz\")"
],
triggeringExamples: [
"func abc(a: String↓ ,b: String) { }",
"func abc(a: String↓ ,b: String↓ ,c: String↓ ,d: String) { }",
"abc(a: \"string\"↓,b: \"string\"",
"enum a { case a↓ ,b }",
"let result = plus(\n first: 3↓ , // #683\n second: 4\n)\n"
],
corrections: [
"func abc(a: String↓,b: String) {}\n": "func abc(a: String, b: String) {}\n",
"abc(a: \"string\"↓,b: \"string\"\n": "abc(a: \"string\", b: \"string\"\n",
"abc(a: \"string\"↓ , b: \"string\"\n": "abc(a: \"string\", b: \"string\"\n",
"enum a { case a↓ ,b }\n": "enum a { case a, b }\n",
"let a = [1↓,1]\nlet b = 1\nf(1, b)\n": "let a = [1, 1]\nlet b = 1\nf(1, b)\n",
"let a = [1↓,1↓,1↓,1]\n": "let a = [1, 1, 1, 1]\n"
]
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
return violationRanges(in: file).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String) {
return (violationRange, ", ")
}
// captures spaces and comma only
// http://userguide.icu-project.org/strings/regexp
private static let mainPatternGroups =
"(" + // start first capure
"\\s+" + // followed by whitespace
"," + // to the left of a comma
"[\\t\\p{Z}]*" + // followed by any amount of tab or space.
"|" + // or
"," + // immediately followed by a comma
"(?:[\\t\\p{Z}]{0}|" + // followed by 0
"[\\t\\p{Z}]{2,})" + // or 2+ tab or space characters.
")" + // end capture
"(\\S)" // second capture is not whitespace.
private static let pattern =
"\\S\(mainPatternGroups)" + // Regexp will match if expression not begin with comma
"|" + // or
"\(mainPatternGroups)" // Regexp will match if expression begins with comma
private static let regularExpression = regex(pattern, options: [])
private static let excludingSyntaxKindsForFirstCapture = SyntaxKind.commentAndStringKinds.union([.objectLiteral])
private static let excludingSyntaxKindsForSecondCapture = SyntaxKind.commentKinds.union([.objectLiteral])
public func violationRanges(in file: SwiftLintFile) -> [NSRange] {
let contents = file.stringView
let range = contents.range
let syntaxMap = file.syntaxMap
return CommaRule.regularExpression
.matches(in: contents, options: [], range: range)
.compactMap { match -> NSRange? in
if match.numberOfRanges != 5 { return nil } // Number of Groups in regexp
var indexStartRange = 1
if match.range(at: indexStartRange).location == NSNotFound {
indexStartRange += 2
}
// check first captured range
let firstRange = match.range(at: indexStartRange)
guard let matchByteFirstRange = contents
.NSRangeToByteRange(start: firstRange.location, length: firstRange.length)
else { return nil }
// first captured range won't match kinds if it is not comment neither string
let firstCaptureIsCommentOrString = syntaxMap.kinds(inByteRange: matchByteFirstRange)
.contains(where: CommaRule.excludingSyntaxKindsForFirstCapture.contains)
if firstCaptureIsCommentOrString {
return nil
}
// If the first range does not start with comma, it already violates this rule
// no matter what is contained in the second range.
if !contents.substring(with: firstRange).hasPrefix(", ") {
return firstRange
}
// check second captured range
let secondRange = match.range(at: indexStartRange + 1)
guard let matchByteSecondRange = contents
.NSRangeToByteRange(start: secondRange.location, length: secondRange.length)
else { return nil }
// second captured range won't match kinds if it is not comment
let secondCaptureIsComment = syntaxMap.kinds(inByteRange: matchByteSecondRange)
.contains(where: CommaRule.excludingSyntaxKindsForSecondCapture.contains)
if secondCaptureIsComment {
return nil
}
// return first captured range
return firstRange
}
}
}