Skip to content

Commit d395f44

Browse files
authored
Implement AmbiguousTrailingClosureOverload rule. (swiftlang#82)
1 parent 54f73c1 commit d395f44

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

tools/swift-format/Sources/Rules/AmbiguousTrailingClosureOverload.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,63 @@ import SwiftSyntax
99
///
1010
/// - SeeAlso: https://google.github.io/swift#trailing-closures
1111
public final class AmbiguousTrailingClosureOverload: SyntaxLintRule {
12+
func diagnoseBadOverloads(_ overloads: [String: [FunctionDeclSyntax]]) {
13+
for (_, decls) in overloads where decls.count > 1 {
14+
let decl = decls[0]
15+
diagnose(.ambiguousTrailingClosureOverload(decl.fullDeclName), on: decl.identifier) {
16+
for decl in decls.dropFirst() {
17+
$0.note(
18+
.otherAmbiguousOverloadHere(decl.fullDeclName),
19+
location: decl.identifier.startLocation(in: self.context.fileURL)
20+
)
21+
}
22+
}
23+
}
24+
}
1225

26+
func discoverAndDiagnoseOverloads(_ functions: [FunctionDeclSyntax]) {
27+
var overloads = [String: [FunctionDeclSyntax]]()
28+
var staticOverloads = [String: [FunctionDeclSyntax]]()
29+
for fn in functions {
30+
let params = fn.signature.input.parameterList
31+
guard params.count == 1 else { continue }
32+
let firstParam = params[0]
33+
guard firstParam.type is FunctionTypeSyntax else { continue }
34+
if let mods = fn.modifiers, mods.has(modifier: "static") || mods.has(modifier: "class") {
35+
staticOverloads[fn.identifier.text, default: []].append(fn)
36+
} else {
37+
overloads[fn.identifier.text, default: []].append(fn)
38+
}
39+
}
40+
41+
diagnoseBadOverloads(overloads)
42+
diagnoseBadOverloads(staticOverloads)
43+
}
44+
45+
public override func visit(_ node: SourceFileSyntax) {
46+
let functions = node.statements.compactMap { $0.item as? FunctionDeclSyntax }
47+
discoverAndDiagnoseOverloads(functions)
48+
super.visit(node)
49+
}
50+
51+
public override func visit(_ node: CodeBlockSyntax) {
52+
let functions = node.statements.compactMap { $0.item as? FunctionDeclSyntax }
53+
discoverAndDiagnoseOverloads(functions)
54+
super.visit(node)
55+
}
56+
57+
public override func visit(_ decls: MemberDeclBlockSyntax) {
58+
let functions = decls.members.compactMap { $0.decl as? FunctionDeclSyntax }
59+
discoverAndDiagnoseOverloads(functions)
60+
super.visit(decls)
61+
}
62+
}
63+
64+
extension Diagnostic.Message {
65+
static func ambiguousTrailingClosureOverload(_ decl: String) -> Diagnostic.Message {
66+
return .init(.warning, "rename '\(decl)' so it is no longer ambiguous with a trailing closure")
67+
}
68+
static func otherAmbiguousOverloadHere(_ decl: String) -> Diagnostic.Message {
69+
return .init(.note, "ambiguous overload '\(decl)' is here")
70+
}
1371
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Foundation
2+
import SwiftSyntax
3+
import XCTest
4+
5+
@testable import Rules
6+
7+
public class AmbiguousTrailingClosureOverloadTests: DiagnosingTestCase {
8+
public func testAmbiguousOverloads() {
9+
performLint(
10+
AmbiguousTrailingClosureOverload.self,
11+
input: """
12+
func strong(mad: () -> Int) {}
13+
func strong(bad: (Bool) -> Bool) {}
14+
func strong(sad: (String) -> Bool) {}
15+
16+
class A {
17+
static func the(cheat: (Int) -> Void) {}
18+
class func the(sneak: (Int) -> Void) {}
19+
func the(kingOfTown: () -> Void) {}
20+
func the(cheatCommandos: (Bool) -> Void) {}
21+
func the(brothersStrong: (String) -> Void) {}
22+
}
23+
24+
struct B {
25+
func hom(estar: () -> Int) {}
26+
func hom(sar: () -> Bool) {}
27+
28+
static func baleeted(_ f: () -> Void) {}
29+
func baleeted(_ f: () -> Void) {}
30+
}
31+
"""
32+
)
33+
34+
XCTAssertDiagnosed(.ambiguousTrailingClosureOverload("strong(mad:)"))
35+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("strong(bad:)"))
36+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("strong(sad:)"))
37+
38+
XCTAssertDiagnosed(.ambiguousTrailingClosureOverload("the(cheat:)"))
39+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("the(sneak:)"))
40+
41+
XCTAssertDiagnosed(.ambiguousTrailingClosureOverload("the(kingOfTown:)"))
42+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("the(cheatCommandos:)"))
43+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("the(brothersStrong:)"))
44+
45+
XCTAssertDiagnosed(.ambiguousTrailingClosureOverload("hom(estar:)"))
46+
XCTAssertDiagnosed(.otherAmbiguousOverloadHere("hom(sar:)"))
47+
48+
XCTAssertNotDiagnosed(.ambiguousTrailingClosureOverload("baleeted(_:)"))
49+
XCTAssertNotDiagnosed(.otherAmbiguousOverloadHere("baleeted(_:)"))
50+
}
51+
52+
#if !os(macOS)
53+
static let allTests = [
54+
AmbiguousTrailingClosureOverloadTests.testAmbiguousOverloads,
55+
]
56+
#endif
57+
}

tools/swift-format/Tests/SwiftFormatTests/DiagnosingTestCase.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public class DiagnosingTestCase: XCTestCase {
1616
var registeredDiagnostics = [String]()
1717
func handle(_ diagnostic: Diagnostic) {
1818
registeredDiagnostics.append(diagnostic.message.text)
19+
for note in diagnostic.notes {
20+
registeredDiagnostics.append(note.message.text)
21+
}
1922
}
2023
func finalize() {}
2124
}

0 commit comments

Comments
 (0)