Rewrite `implicit_return` rule with SwiftSyntax

This commit is contained in:
Marcelo Fabri 2022-11-06 23:06:29 -08:00
parent eaf7db1250
commit d5f12cbb05
4 changed files with 167 additions and 63 deletions

View File

@ -113,6 +113,7 @@
- `ibinspectable_in_extension`
- `identical_operands`
- `implicit_getter`
- `implicit_return`
- `implicitly_unwrapped_optional`
- `inclusive_language`
- `inert_defer`

View File

@ -1,4 +1,4 @@
public struct ImplicitReturnConfiguration: RuleConfiguration, Equatable {
public struct ImplicitReturnConfiguration: SeverityBasedRuleConfiguration, Equatable {
public enum ReturnKind: String, CaseIterable {
case closure
case function
@ -7,7 +7,7 @@ public struct ImplicitReturnConfiguration: RuleConfiguration, Equatable {
public static let defaultIncludedKinds = Set(ReturnKind.allCases)
private(set) var severityConfiguration = SeverityConfiguration(.warning)
public private(set) var severityConfiguration = SeverityConfiguration(.warning)
private(set) var includedKinds = Self.defaultIncludedKinds

View File

@ -1,7 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax
public struct ImplicitReturnRule: ConfigurationProviderRule, SubstitutionCorrectableRule, OptInRule {
public struct ImplicitReturnRule: ConfigurationProviderRule, SwiftSyntaxCorrectableRule, OptInRule {
public var configuration = ImplicitReturnConfiguration()
public init() {}
@ -16,72 +15,176 @@ public struct ImplicitReturnRule: ConfigurationProviderRule, SubstitutionCorrect
corrections: ImplicitReturnRuleExamples.corrections
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
return violationRanges(in: file).compactMap {
StyleViolation(ruleDescription: Self.description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, characterOffset: $0.location))
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(includedKinds: configuration.includedKinds)
}
public func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
includedKinds: configuration.includedKinds,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
return (violationRange, "")
private extension ImplicitReturnRule {
final class Visitor: ViolationsSyntaxVisitor {
private let includedKinds: Set<ImplicitReturnConfiguration.ReturnKind>
init(includedKinds: Set<ImplicitReturnConfiguration.ReturnKind>) {
self.includedKinds = includedKinds
super.init(viewMode: .sourceAccurate)
}
public func violationRanges(in file: SwiftLintFile) -> [NSRange] {
let pattern = "(?:\\bin|\\{)\\s+(return\\s+)"
let contents = file.stringView
return file.matchesAndSyntaxKinds(matching: pattern).compactMap { result, kinds in
let range = result.range
guard kinds == [.keyword, .keyword] || kinds == [.keyword],
let byteRange = contents.NSRangeToByteRange(start: range.location, length: range.length),
case let kinds = file.structureDictionary.kinds(forByteOffset: byteRange.location),
let outerKindString = kinds.lastExcludingBrace()?.kind
else {
return nil
override func visitPost(_ node: ClosureExprSyntax) {
guard includedKinds.contains(.closure),
let statement = node.statements.onlyElement,
statement.item.is(ReturnStmtSyntax.self) else {
return
}
func isKindIncluded(_ kind: ImplicitReturnConfiguration.ReturnKind) -> Bool {
return self.configuration.isKindIncluded(kind)
violations.append(statement.item.positionAfterSkippingLeadingTrivia)
}
if let outerKind = SwiftExpressionKind(rawValue: outerKindString),
isKindIncluded(.closure),
[.call, .argument, .closure].contains(outerKind) {
return result.range(at: 1)
override func visitPost(_ node: FunctionDeclSyntax) {
guard includedKinds.contains(.function),
let statement = node.body?.statements.onlyElement,
statement.item.is(ReturnStmtSyntax.self) else {
return
}
if let outerKind = SwiftDeclarationKind(rawValue: outerKindString),
(isKindIncluded(.function) && SwiftDeclarationKind.functionKinds.contains(outerKind)) ||
(isKindIncluded(.getter) && SwiftDeclarationKind.variableKinds.contains(outerKind)) {
return result.range(at: 1)
violations.append(statement.item.positionAfterSkippingLeadingTrivia)
}
return nil
override func visitPost(_ node: VariableDeclSyntax) {
guard includedKinds.contains(.getter) else {
return
}
for binding in node.bindings {
switch binding.accessor {
case nil:
continue
case .accessors(let accessors):
if let statement = accessors.getAccessor?.body?.statements.onlyElement,
let returnStmt = statement.item.as(ReturnStmtSyntax.self) {
violations.append(returnStmt.positionAfterSkippingLeadingTrivia)
}
case .getter(let getter):
if let returnStmt = getter.statements.onlyElement?.item.as(ReturnStmtSyntax.self) {
violations.append(returnStmt.positionAfterSkippingLeadingTrivia)
}
}
}
}
}
private extension Array where Element == (kind: String, byteRange: ByteRange) {
func lastExcludingBrace() -> Element? {
guard SwiftVersion.current >= .fiveDotFour else {
return last
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
private let includedKinds: Set<ImplicitReturnConfiguration.ReturnKind>
private let locationConverter: SourceLocationConverter
private let disabledRegions: [SourceRange]
init(includedKinds: Set<ImplicitReturnConfiguration.ReturnKind>,
locationConverter: SourceLocationConverter,
disabledRegions: [SourceRange]) {
self.includedKinds = includedKinds
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
guard let last = last else {
return nil
override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
guard includedKinds.contains(.closure),
let statement = node.statements.onlyElement,
let returnStmt = statement.item.as(ReturnStmtSyntax.self),
let expr = returnStmt.expression,
!returnStmt.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
guard last.kind == "source.lang.swift.stmt.brace", count > 1 else {
return last
correctionPositions.append(returnStmt.positionAfterSkippingLeadingTrivia)
let newNode = node.withStatements([
statement
.withItem(.expr(expr))
.withLeadingTrivia(returnStmt.leadingTrivia ?? .zero)
])
return super.visit(newNode)
}
let secondLast = self[endIndex - 2]
if SwiftExpressionKind(rawValue: secondLast.kind) == .closure {
return secondLast
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
guard includedKinds.contains(.function),
let statement = node.body?.statements.onlyElement,
let returnStmt = statement.item.as(ReturnStmtSyntax.self),
let expr = returnStmt.expression,
!returnStmt.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
return super.visit(node)
}
return last
correctionPositions.append(returnStmt.positionAfterSkippingLeadingTrivia)
let newNode = node.withBody(node.body?.withStatements([
statement
.withItem(.expr(expr))
.withLeadingTrivia(returnStmt.leadingTrivia ?? .zero)
]))
return super.visit(newNode)
}
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
guard includedKinds.contains(.getter) else {
return super.visit(node)
}
let updatedBindings: [PatternBindingSyntax] = node.bindings.map { binding in
switch binding.accessor {
case nil:
return binding
case .accessors(let accessorBlock):
guard let getAccessor = accessorBlock.getAccessor,
let statement = accessorBlock.getAccessor?.body?.statements.onlyElement,
let returnStmt = statement.item.as(ReturnStmtSyntax.self),
!returnStmt.isContainedIn(regions: disabledRegions, locationConverter: locationConverter),
let expr = returnStmt.expression else {
break
}
correctionPositions.append(returnStmt.positionAfterSkippingLeadingTrivia)
let updatedGetAcessor = getAccessor.withBody(getAccessor.body?.withStatements([
statement
.withItem(.expr(expr))
.withLeadingTrivia(returnStmt.leadingTrivia ?? .zero)
]))
let updatedAccessors = accessorBlock
.withAccessors(
accessorBlock.accessors.replacing(
childAt: getAccessor.indexInParent,
with: updatedGetAcessor
)
)
return binding.withAccessor(.accessors(updatedAccessors))
case .getter(let getter):
guard let statement = getter.statements.onlyElement,
let returnStmt = statement.item.as(ReturnStmtSyntax.self),
!returnStmt.isContainedIn(regions: disabledRegions, locationConverter: locationConverter),
let expr = returnStmt.expression else {
break
}
correctionPositions.append(returnStmt.positionAfterSkippingLeadingTrivia)
return binding.withAccessor(.getter(getter.withStatements([
statement
.withItem(.expr(expr))
.withLeadingTrivia(returnStmt.leadingTrivia ?? .zero)
])))
}
return binding
}
return super.visit(node.withBindings(PatternBindingListSyntax(updatedBindings)))
}
}
}

View File

@ -1,9 +1,9 @@
internal struct ImplicitReturnRuleExamples {
internal struct GenericExamples {
internal enum GenericExamples {
static let nonTriggeringExamples = [Example("if foo {\n return 0\n}")]
}
internal struct ClosureExamples {
internal enum ClosureExamples {
static let nonTriggeringExamples = [
Example("foo.map { $0 + 1 }"),
Example("foo.map({ $0 + 1 })"),
@ -42,7 +42,7 @@ internal struct ImplicitReturnRuleExamples {
]
}
internal struct FunctionExamples {
internal enum FunctionExamples {
static let nonTriggeringExamples = [
Example("""
func foo() -> Int {
@ -85,7 +85,7 @@ internal struct ImplicitReturnRuleExamples {
]
}
internal struct GetterExamples {
internal enum GetterExamples {
static let nonTriggeringExamples = [
Example("var foo: Bool { true }"),
Example("""