mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Rewrite return_arrow_whitespace with SwiftSyntax (#4413)
This commit is contained in:
@@ -142,6 +142,7 @@
|
||||
- `redundant_objc_attribute`
|
||||
- `redundant_string_enum_value`
|
||||
- `required_deinit`
|
||||
- `return_arrow_whitespace`
|
||||
- `self_in_property_initialization`
|
||||
- `shorthand_operator`
|
||||
- `single_test_class`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import SourceKittenFramework
|
||||
import SwiftSyntax
|
||||
|
||||
public struct ReturnArrowWhitespaceRule: CorrectableRule, ConfigurationProviderRule {
|
||||
public struct ReturnArrowWhitespaceRule: SwiftSyntaxRule, CorrectableRule, ConfigurationProviderRule {
|
||||
public var configuration = SeverityConfiguration(.warning)
|
||||
|
||||
public init() {}
|
||||
@@ -19,6 +19,12 @@ public struct ReturnArrowWhitespaceRule: CorrectableRule, ConfigurationProviderR
|
||||
Example("var abc = {(param: Int) -> Void in }\n"),
|
||||
Example("func abc() ->\n Int {}\n"),
|
||||
Example("func abc()\n -> Int {}\n"),
|
||||
Example("""
|
||||
func reallyLongFunctionMethods<T>(withParam1: Int, param2: String, param3: Bool) where T: AGenericConstraint
|
||||
-> Int {
|
||||
return 1
|
||||
}
|
||||
"""),
|
||||
Example("typealias SuccessBlock = ((Data) -> Void)")
|
||||
],
|
||||
triggeringExamples: [
|
||||
@@ -26,95 +32,134 @@ public struct ReturnArrowWhitespaceRule: CorrectableRule, ConfigurationProviderR
|
||||
Example("func abc()↓->[Int] {}\n"),
|
||||
Example("func abc()↓->(Int, Int) {}\n"),
|
||||
Example("func abc()↓-> Int {}\n"),
|
||||
Example("func abc()↓-> Int {}\n"),
|
||||
Example("func abc()↓ ->Int {}\n"),
|
||||
Example("func abc()↓ -> Int {}\n"),
|
||||
Example("var abc = {(param: Int)↓ ->Bool in }\n"),
|
||||
Example("var abc = {(param: Int)↓->Bool in }\n"),
|
||||
Example("typealias SuccessBlock = ((Data)↓->Void)")
|
||||
Example("typealias SuccessBlock = ((Data)↓->Void)"),
|
||||
Example("func abc()\n ↓-> Int {}\n"),
|
||||
Example("func abc()\n ↓-> Int {}\n"),
|
||||
Example("func abc()↓ ->\n Int {}\n"),
|
||||
Example("func abc()↓ ->\nInt {}\n")
|
||||
],
|
||||
corrections: [
|
||||
Example("func abc()↓->Int {}\n"): Example("func abc() -> Int {}\n"),
|
||||
Example("func abc()↓-> Int {}\n"): Example("func abc() -> Int {}\n"),
|
||||
Example("func abc()↓ ->Int {}\n"): Example("func abc() -> Int {}\n"),
|
||||
Example("func abc()↓ -> Int {}\n"): Example("func abc() -> Int {}\n"),
|
||||
Example("func abc()↓\n -> Int {}\n"): Example("func abc()\n -> Int {}\n"),
|
||||
Example("func abc()↓\n-> Int {}\n"): Example("func abc()\n-> Int {}\n"),
|
||||
Example("func abc()\n ↓-> Int {}\n"): Example("func abc()\n -> Int {}\n"),
|
||||
Example("func abc()\n ↓-> Int {}\n"): Example("func abc()\n -> Int {}\n"),
|
||||
Example("func abc()↓ ->\n Int {}\n"): Example("func abc() ->\n Int {}\n"),
|
||||
Example("func abc()↓ ->\nInt {}\n"): Example("func abc() ->\nInt {}\n")
|
||||
]
|
||||
)
|
||||
|
||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||
return violationRanges(in: file, skipParentheses: true).map {
|
||||
StyleViolation(ruleDescription: Self.description,
|
||||
severity: configuration.severity,
|
||||
location: Location(file: file, characterOffset: $0.location))
|
||||
}
|
||||
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||
Visitor(viewMode: .sourceAccurate)
|
||||
}
|
||||
|
||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
||||
let violationsRanges = violationRanges(in: file, skipParentheses: false)
|
||||
let matches = file.ruleEnabled(violatingRanges: violationsRanges, for: self)
|
||||
if matches.isEmpty { return [] }
|
||||
let regularExpression = regex(pattern)
|
||||
let violations = Visitor(viewMode: .sourceAccurate)
|
||||
.walk(file: file, handler: \.corrections)
|
||||
.compactMap { violation in
|
||||
file.stringView.NSRange(start: violation.start, end: violation.end).map { range in
|
||||
(range: range, correction: violation.correction)
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
file.ruleEnabled(violatingRange: $0.range, for: self) != nil
|
||||
}
|
||||
|
||||
guard violations.isNotEmpty else { return [] }
|
||||
|
||||
let description = Self.description
|
||||
var corrections = [Correction]()
|
||||
var contents = file.contents
|
||||
|
||||
let results = matches.reversed().compactMap { range in
|
||||
return regularExpression.firstMatch(in: contents, options: [], range: range)
|
||||
}
|
||||
|
||||
let replacementsByIndex = [2: " -> ", 4: " -> ", 6: " ", 7: " "]
|
||||
|
||||
for result in results {
|
||||
guard result.numberOfRanges > (replacementsByIndex.keys.max() ?? 0) else { break }
|
||||
|
||||
for (index, string) in replacementsByIndex {
|
||||
if let range = contents.nsrangeToIndexRange(result.range(at: index)) {
|
||||
contents.replaceSubrange(range, with: string)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// skip the parentheses when reporting correction
|
||||
let location = Location(file: file, characterOffset: result.range.location + 1)
|
||||
for violation in violations.sorted(by: { $0.range.location > $1.range.location }) {
|
||||
let contentsNSString = contents.bridge()
|
||||
contents = contentsNSString.replacingCharacters(in: violation.range, with: violation.correction)
|
||||
let location = Location(file: file, characterOffset: violation.range.location)
|
||||
corrections.append(Correction(ruleDescription: description, location: location))
|
||||
}
|
||||
|
||||
file.write(contents)
|
||||
return corrections
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private extension ReturnArrowWhitespaceRule {
|
||||
final class Visitor: ViolationsSyntaxVisitor {
|
||||
private(set) var corrections: [ArrowViolation] = []
|
||||
|
||||
private let pattern: String = {
|
||||
// Just horizontal spacing so that "func abc()->\n" can pass validation
|
||||
let space = "[ \\f\\r\\t]"
|
||||
override func visitPost(_ node: FunctionTypeSyntax) {
|
||||
guard let violation = node.arrow.arrowViolation else {
|
||||
return
|
||||
}
|
||||
|
||||
// Either 0 space characters or 2+
|
||||
let incorrectSpace = "(\(space){0}|\(space){2,})"
|
||||
|
||||
// The possible combinations of whitespace around the arrow
|
||||
let patterns = [
|
||||
"(\(incorrectSpace)\\->\(space)*)",
|
||||
"(\(space)\\->\(incorrectSpace))",
|
||||
"\\n\(space)*\\->\(incorrectSpace)",
|
||||
"\(incorrectSpace)\\->\\n\(space)*"
|
||||
]
|
||||
|
||||
// ex: `func abc()-> Int {` & `func abc() ->Int {`
|
||||
return "\\)(\(patterns.joined(separator: "|")))\\S+"
|
||||
}()
|
||||
|
||||
private func violationRanges(in file: SwiftLintFile, skipParentheses: Bool) -> [NSRange] {
|
||||
let matches = file.match(pattern: pattern, with: [.typeidentifier])
|
||||
guard skipParentheses else {
|
||||
return matches
|
||||
violations.append(violation.start)
|
||||
corrections.append(violation)
|
||||
}
|
||||
|
||||
return matches.map {
|
||||
// skip first (
|
||||
NSRange(location: $0.location + 1, length: $0.length - 1)
|
||||
override func visitPost(_ node: FunctionSignatureSyntax) {
|
||||
guard let output = node.output, let violation = output.arrow.arrowViolation else {
|
||||
return
|
||||
}
|
||||
|
||||
violations.append(violation.start)
|
||||
corrections.append(violation)
|
||||
}
|
||||
|
||||
override func visitPost(_ node: ClosureSignatureSyntax) {
|
||||
guard let output = node.output, let violation = output.arrow.arrowViolation else {
|
||||
return
|
||||
}
|
||||
|
||||
violations.append(violation.start)
|
||||
corrections.append(violation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ArrowViolation {
|
||||
let start: AbsolutePosition
|
||||
let end: AbsolutePosition
|
||||
let correction: String
|
||||
}
|
||||
|
||||
private extension TokenSyntax {
|
||||
var arrowViolation: ArrowViolation? {
|
||||
guard let previousToken = previousToken, let nextToken = nextToken else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var start: AbsolutePosition?
|
||||
var end: AbsolutePosition?
|
||||
var correction = " -> "
|
||||
|
||||
if previousToken.trailingTrivia != .space && !leadingTrivia.containsNewlines() {
|
||||
start = previousToken.endPositionBeforeTrailingTrivia
|
||||
end = endPosition
|
||||
|
||||
if nextToken.leadingTrivia.containsNewlines() {
|
||||
correction = " ->"
|
||||
}
|
||||
}
|
||||
|
||||
if trailingTrivia != .space && !nextToken.leadingTrivia.containsNewlines() {
|
||||
if leadingTrivia.containsNewlines() {
|
||||
start = positionAfterSkippingLeadingTrivia
|
||||
correction = "-> "
|
||||
} else {
|
||||
start = previousToken.endPositionBeforeTrailingTrivia
|
||||
}
|
||||
end = endPosition
|
||||
}
|
||||
|
||||
guard let start = start, let end = end else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ArrowViolation(start: start, end: end, correction: correction)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user