Rewrite return_arrow_whitespace with SwiftSyntax (#4413)

This commit is contained in:
Marcelo Fabri
2022-10-23 15:06:14 -07:00
committed by GitHub
parent cb79584c7d
commit 351c8be2e0
2 changed files with 106 additions and 60 deletions
+1
View File
@@ -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)
}
}