diff --git a/CHANGELOG.md b/CHANGELOG.md index 606c9922b4..03d91e3d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ #### Enhancements +* Rewrite `void_return` rule with SwiftSyntax. + [SimplyDanny](https://github.com/SimplyDanny) + * Print invalid keys when configuration parsing fails. [SimplyDanny](https://github.com/SimplyDanny) [#5347](https://github.com/realm/SwiftLint/pull/5347) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift index 60edbb2eb6..46fbc41117 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VoidReturnRule.swift @@ -1,7 +1,8 @@ -import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct VoidReturnRule: SubstitutionCorrectableRule { +@SwiftSyntaxRule(explicitRewriter: true) +struct VoidReturnRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -46,29 +47,37 @@ struct VoidReturnRule: SubstitutionCorrectableRule { Example("func foo() async throws -> ↓()"): Example("func foo() async throws -> Void") ] ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - return violationRanges(in: file).map { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, characterOffset: $0.location)) +private extension VoidReturnRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ReturnClauseSyntax) { + if node.violates { + violations.append(node.type.positionAfterSkippingLeadingTrivia) + } } } - func violationRanges(in file: SwiftLintFile) -> [NSRange] { - let kinds = SyntaxKind.commentAndStringKinds - let parensPattern = "\\(\\s*(?:Void)?\\s*\\)" - let pattern = "->\\s*\(parensPattern)\\s*(?!->)" - let excludingPattern = "(\(pattern))\\s*(async\\s+)?(throws\\s+)?->" - - return file.match(pattern: pattern, excludingSyntaxKinds: kinds, excludingPattern: excludingPattern, - exclusionMapping: { $0.range(at: 1) }).compactMap { - let parensRegex = regex(parensPattern) - return parensRegex.firstMatch(in: file.contents, options: [], range: $0)?.range + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: ReturnClauseSyntax) -> ReturnClauseSyntax { + if node.violates { + correctionPositions.append(node.type.positionAfterSkippingLeadingTrivia) + let node = node + .with(\.type, TypeSyntax(IdentifierTypeSyntax(name: "Void"))) + .with(\.trailingTrivia, node.type.trailingTrivia) + return super.visit(node) + } + return super.visit(node) } } +} - func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { - return (violationRange, "Void") +private extension ReturnClauseSyntax { + var violates: Bool { + if let type = type.as(TupleTypeSyntax.self) { + let elements = type.elements + return elements.isEmpty || elements.onlyElement?.type.as(IdentifierTypeSyntax.self)?.name.text == "Void" + } + return false } }