126 lines
5.7 KiB
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
|
|
}
|
|
}
|
|
}
|