Skip to content

Commit

Permalink
Rewrite empty_parentheses_with_trailing_closure with SwiftSyntax (rea…
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelofabri authored Sep 25, 2022
1 parent 4fb5ebe commit 734ecea
Showing 1 changed file with 54 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax

public struct EmptyParenthesesWithTrailingClosureRule: SubstitutionCorrectableASTRule, ConfigurationProviderRule {
public struct EmptyParenthesesWithTrailingClosureRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)

public init() {}
Expand Down Expand Up @@ -47,67 +46,74 @@ public struct EmptyParenthesesWithTrailingClosureRule: SubstitutionCorrectableAS
]
)

private static let emptyParenthesesRegex = regex("^\\s*\\(\\s*\\)")
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
Visitor()
}

public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
return violationRanges(in: file, kind: kind, dictionary: dictionary).map {
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
public func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
file.locationConverter.map { locationConverter in
Rewriter(
locationConverter: locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
}

public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
return (violationRange, "")
}
private extension EmptyParenthesesWithTrailingClosureRule {
final class Visitor: SyntaxVisitor, ViolationsSyntaxVisitor {
private(set) var violationPositions: [AbsolutePosition] = []

public func violationRanges(in file: SwiftLintFile, kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [NSRange] {
guard kind == .call else {
return []
}
override func visitPost(_ node: FunctionCallExprSyntax) {
guard let position = node.violationPosition else {
return
}

guard let offset = dictionary.offset,
let length = dictionary.length,
let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let bodyLength = dictionary.bodyLength,
bodyLength > 0 else {
return []
violationPositions.append(position)
}
}

// avoid the more expensive regex match if there's no trailing closure in the substructure
if !dictionary.hasTrailingClosure {
return []
private final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]

init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}

let rangeStart = nameOffset + nameLength
let rangeLength = (offset + length) - (nameOffset + nameLength)
let byteRange = ByteRange(location: rangeStart, length: rangeLength)
let regex = Self.emptyParenthesesRegex
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
guard let violationPosition = node.violationPosition else {
return super.visit(node)
}

guard let range = file.stringView.byteRangeToNSRange(byteRange),
let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range,
match.location == range.location
else {
return []
}
let isInDisabledRegion = disabledRegions.contains { region in
region.contains(node.positionAfterSkippingLeadingTrivia, locationConverter: locationConverter)
}

guard !isInDisabledRegion else {
return super.visit(node)
}

return [match]
let newNode = node
.withLeftParen(nil)
.withRightParen(nil)
.withTrailingClosure(node.trailingClosure?.withLeadingTrivia(.spaces(1)))
correctionPositions.append(violationPosition)
return super.visit(newNode)
}
}
}

private extension SourceKittenDictionary {
var hasTrailingClosure: Bool {
guard let lastStructure = substructure.last else {
return false
private extension FunctionCallExprSyntax {
var violationPosition: AbsolutePosition? {
guard trailingClosure != nil,
let leftParen = leftParen,
argumentList.isEmpty else {
return nil
}

if SwiftVersion.current >= .fiveDotSix, lastStructure.expressionKind == .argument {
return lastStructure.substructure.last?.expressionKind == .closure
} else {
return lastStructure.expressionKind == .closure
}
return leftParen.positionAfterSkippingLeadingTrivia
}
}

0 comments on commit 734ecea

Please sign in to comment.