Update redundantSelf rule to support functions with no body (#2315)

This commit is contained in:
Cal Stephens
2026-01-16 11:03:25 -08:00
parent f914b48583
commit 32f61584d0
4 changed files with 78 additions and 17 deletions
+32 -16
View File
@@ -2924,6 +2924,7 @@ extension Formatter {
usingDynamicLookup: Bool,
classOrStatic: Bool)
{
let funcKeywordIndex = index
let startToken = tokens[index]
var localNames = localNames
guard let startIndex = self.index(of: .startOfScope("("), after: index),
@@ -2952,23 +2953,38 @@ extension Formatter {
}
index = self.index(of: .delimiter(","), after: index) ?? endIndex
}
guard let bodyStartIndex = self.index(after: endIndex, where: {
switch $0 {
case .startOfScope("{"): // What we're looking for
return true
case .keyword("throws"),
.keyword("rethrows"),
.keyword("where"),
.keyword("is"),
.keyword("repeat"):
return false // Keep looking
case .keyword where !$0.isAttribute:
return true // Not valid between end of arguments and start of body
default:
return false // Keep looking
let bodyStartIndex: Int
if let functionDeclaration = parseFunctionDeclaration(keywordIndex: funcKeywordIndex) {
guard let validBodyStartIndex = functionDeclaration.bodyRange?.lowerBound else {
// Ensure we move the `redundantSelf` work index to the end of the function declaration
index = functionDeclaration.range.upperBound
return
}
}), tokens[bodyStartIndex] == .startOfScope("{") else {
return
bodyStartIndex = validBodyStartIndex
} else {
// If `parseFunctionDeclaration` fails due to some unsupported pattern, use a more permissive search.
guard let validBodyStartIndex = self.index(after: endIndex, where: {
switch $0 {
case .startOfScope("{"): // What we're looking for
return true
case .keyword("throws"),
.keyword("rethrows"),
.keyword("where"),
.keyword("is"),
.keyword("repeat"):
return false // Keep looking
case .keyword where !$0.isAttribute:
return true // Not valid between end of arguments and start of body
default:
return false // Keep looking
}
}), tokens[validBodyStartIndex] == .startOfScope("{") else {
return
}
bodyStartIndex = validBodyStartIndex
}
// Functions defined inside closures with `[weak self]` captures can
+1 -1
View File
@@ -3156,7 +3156,7 @@ extension Formatter {
/// The range of the `where` clause if present
let whereClauseRange: ClosedRange<Int>?
/// The range of the function body (`{ ... }`) if present.
/// A protocol method requirement doesn't have a body.
/// A protocol method requirement, or a function with a `@_silgen` attribute, doesn't have a body.
let bodyRange: ClosedRange<Int>?
/// The full range of this declaration
+16
View File
@@ -1180,6 +1180,9 @@ final class ParsingHelpersTests: XCTestCase {
private(set)
var instanceVar = "test" // trailing comment
@_silgen_name("__MARKER_functionWithNoBody")
func functionWithNoBody(_ x: String) -> Int?
@objc
private var computed: String {
get {
@@ -1304,6 +1307,9 @@ final class ParsingHelpersTests: XCTestCase {
private(set)
var instanceVar = "test" // trailing comment
@_silgen_name("__MARKER_functionWithNoBody")
func functionWithNoBody(_ x: String) -> Int?
@objc
private var computed: String {
get {
@@ -1363,6 +1369,16 @@ final class ParsingHelpersTests: XCTestCase {
XCTAssertEqual(
declarations[7].body?[2].tokens.string,
"""
@_silgen_name(\"__MARKER_functionWithNoBody\")
func functionWithNoBody(_ x: String) -> Int?
"""
)
XCTAssertEqual(
declarations[7].body?[3].tokens.string,
"""
@objc
private var computed: String {
+29
View File
@@ -2973,6 +2973,35 @@ final class RedundantSelfTests: XCTestCase {
testFormatting(for: input, rule: .redundantSelf, options: options)
}
func testFunctionWithNoBodyFollowedByStaticFunction() {
let input = """
struct Foo {
let foo: String
@_silgen_name("__MARKER_doIt")
func doIt(_ x: String) -> Int?
static func bar() {
print(self.foo)
}
}
"""
let output = """
struct Foo {
let foo: String
@_silgen_name("__MARKER_doIt")
func doIt(_ x: String) -> Int?
static func bar() {
print(foo)
}
}
"""
testFormatting(for: input, output, rule: .redundantSelf)
}
func testNoInsertSelfBeforeSet() {
let input = """
class Foo {