Skip to content

Commit afaa91d

Browse files
committed
[Lint/Format] Extend empty literal initialization rule to support dictionaries
1 parent cef2c6e commit afaa91d

File tree

2 files changed

+123
-23
lines changed

2 files changed

+123
-23
lines changed

Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyArrayInit.swift

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,59 +25,116 @@ public final class AlwaysUseLiteralForEmptyArrayInit : SyntaxFormatRule {
2525
public override class var isOptIn: Bool { return true }
2626

2727
public override func visit(_ node: PatternBindingSyntax) -> PatternBindingSyntax {
28-
guard let initializer = node.initializer else {
29-
return node
30-
}
31-
3228
// Check whether the initializer is `[<Type>]()`
33-
guard let initCall = initializer.value.as(FunctionCallExprSyntax.self),
34-
var arrayLiteral = initCall.calledExpression.as(ArrayExprSyntax.self),
29+
guard let initializer = node.initializer,
30+
let initCall = initializer.value.as(FunctionCallExprSyntax.self),
3531
initCall.arguments.isEmpty else {
3632
return node
3733
}
3834

39-
guard let elementType = getElementType(arrayLiteral) else {
40-
return node
35+
if let arrayLiteral = initCall.calledExpression.as(ArrayExprSyntax.self),
36+
let type = getLiteralType(arrayLiteral) {
37+
return rewrite(node, type: type)
38+
}
39+
40+
if let dictLiteral = initCall.calledExpression.as(DictionaryExprSyntax.self),
41+
let type = getLiteralType(dictLiteral) {
42+
return rewrite(node, type: type)
4143
}
4244

45+
return node
46+
}
47+
48+
private func rewrite(_ node: PatternBindingSyntax,
49+
type: ArrayTypeSyntax) -> PatternBindingSyntax {
4350
var replacement = node
4451

45-
var withFixIt = "[]"
52+
diagnose(node, type: type)
53+
4654
if replacement.typeAnnotation == nil {
47-
withFixIt = ": [\(elementType)] = []"
55+
// Drop trailing trivia after pattern because ':' has to appear connected to it.
56+
replacement.pattern = node.pattern.with(\.trailingTrivia, [])
57+
// Add explicit type annotiation: ': [<Type>]`
58+
replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space)
59+
.with(\.trailingTrivia, .space))
4860
}
4961

50-
diagnose(.refactorEmptyArrayInit(replace: "\(initCall)", with: withFixIt), on: initCall)
62+
let initializer = node.initializer!
63+
let emptyArrayExpr = ArrayExprSyntax(elements: ArrayElementListSyntax.init([]))
64+
65+
// Replace initializer call with empty array literal: `[<Type>]()` -> `[]`
66+
replacement.initializer = initializer.with(\.value, ExprSyntax(emptyArrayExpr))
67+
68+
return replacement
69+
}
70+
71+
private func rewrite(_ node: PatternBindingSyntax,
72+
type: DictionaryTypeSyntax) -> PatternBindingSyntax {
73+
var replacement = node
74+
75+
diagnose(node, type: type)
5176

5277
if replacement.typeAnnotation == nil {
5378
// Drop trailing trivia after pattern because ':' has to appear connected to it.
5479
replacement.pattern = node.pattern.with(\.trailingTrivia, [])
5580
// Add explicit type annotiation: ': [<Type>]`
56-
replacement.typeAnnotation = .init(type: ArrayTypeSyntax(leadingTrivia: .space,
57-
element: elementType,
58-
trailingTrivia: .space))
81+
replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space)
82+
.with(\.trailingTrivia, .space))
5983
}
6084

61-
// Replace initializer call with empty array literal: `[<Type>]()` -> `[]`
62-
arrayLiteral.elements = ArrayElementListSyntax.init([])
63-
replacement.initializer = initializer.with(\.value, ExprSyntax(arrayLiteral))
85+
let initializer = node.initializer!
86+
let emptyDictExpr = DictionaryExprSyntax(content: .colon(.colonToken()))
87+
88+
// Replace initializer call with empty dictionary literal: `[<Type>]()` -> `[]`
89+
replacement.initializer = initializer.with(\.value, ExprSyntax(emptyDictExpr))
6490

6591
return replacement
6692
}
6793

68-
private func getElementType(_ arrayLiteral: ArrayExprSyntax) -> TypeSyntax? {
69-
guard let elementExpr = arrayLiteral.elements.firstAndOnly?.as(ArrayElementSyntax.self) else {
94+
private func diagnose(_ node: PatternBindingSyntax, type: ArrayTypeSyntax) {
95+
var withFixIt = "[]"
96+
if node.typeAnnotation == nil {
97+
withFixIt = ": \(type) = []"
98+
}
99+
100+
let initCall = node.initializer!.value
101+
emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall)
102+
}
103+
104+
private func diagnose(_ node: PatternBindingSyntax, type: DictionaryTypeSyntax) {
105+
var withFixIt = "[:]"
106+
if node.typeAnnotation == nil {
107+
withFixIt = ": \(type) = [:]"
108+
}
109+
110+
let initCall = node.initializer!.value
111+
emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall)
112+
}
113+
114+
private func emitDiagnostic(replace: String, with fixIt: String, on: ExprSyntax?) {
115+
diagnose(.refactorIntoEmptyLiteral(replace: replace, with: fixIt), on: on)
116+
}
117+
118+
private func getLiteralType(_ arrayLiteral: ArrayExprSyntax) -> ArrayTypeSyntax? {
119+
guard let elementExpr = arrayLiteral.elements.firstAndOnly,
120+
elementExpr.is(ArrayElementSyntax.self) else {
70121
return nil
71122
}
72123

73-
var parser = Parser(elementExpr.description)
124+
var parser = Parser(arrayLiteral.description)
125+
let elementType = TypeSyntax.parse(from: &parser)
126+
return elementType.hasError ? nil : elementType.as(ArrayTypeSyntax.self)
127+
}
128+
129+
private func getLiteralType(_ dictLiteral: DictionaryExprSyntax) -> DictionaryTypeSyntax? {
130+
var parser = Parser(dictLiteral.description)
74131
let elementType = TypeSyntax.parse(from: &parser)
75-
return elementType.hasError ? nil : elementType
132+
return elementType.hasError ? nil : elementType.as(DictionaryTypeSyntax.self)
76133
}
77134
}
78135

79136
extension Finding.Message {
80-
public static func refactorEmptyArrayInit(replace: String, with: String) -> Finding.Message {
137+
public static func refactorIntoEmptyLiteral(replace: String, with: String) -> Finding.Message {
81138
"replace '\(replace)' with '\(with)'"
82139
}
83140
}

Tests/SwiftFormatTests/Rules/AlwaysUseLiteralForEmptyArrayInitTests.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import _SwiftFormatTestSupport
33
@_spi(Rules) import SwiftFormat
44

55
final class AlwaysUseLiteralForEmptyArrayInitTests: LintOrFormatRuleTestCase {
6-
func testPatternBindings() {
6+
func testArray() {
77
assertFormatting(
88
AlwaysUseLiteralForEmptyArrayInit.self,
99
input: """
@@ -45,4 +45,47 @@ final class AlwaysUseLiteralForEmptyArrayInitTests: LintOrFormatRuleTestCase {
4545
]
4646
)
4747
}
48+
49+
func testDictionary() {
50+
assertFormatting(
51+
AlwaysUseLiteralForEmptyArrayInit.self,
52+
input: """
53+
public struct Test {
54+
var value1 = 1️⃣[Int: String]()
55+
56+
func test(v: [Double: Int] = [Double: Int]()) {
57+
let _ = 2️⃣[String: Int]()
58+
}
59+
}
60+
61+
var _: [Category<Int>: String] = 3️⃣[Category<Int>: String]()
62+
let _ = 4️⃣[(Int, Array<String>): Int]()
63+
let _: [String: (String, Int, Float)] = 5️⃣[String: (String, Int, Float)]()
64+
65+
let _ = [String: (1, 2, String)]()
66+
""",
67+
expected: """
68+
public struct Test {
69+
var value1: [Int: String] = [:]
70+
71+
func test(v: [Double: Int] = [Double: Int]()) {
72+
let _: [String: Int] = [:]
73+
}
74+
}
75+
76+
var _: [Category<Int>: String] = [:]
77+
let _: [(Int, Array<String>): Int] = [:]
78+
let _: [String: (String, Int, Float)] = [:]
79+
80+
let _ = [String: (1, 2, String)]()
81+
""",
82+
findings: [
83+
FindingSpec("1️⃣", message: "replace '[Int: String]()' with ': [Int: String] = [:]'"),
84+
FindingSpec("2️⃣", message: "replace '[String: Int]()' with ': [String: Int] = [:]'"),
85+
FindingSpec("3️⃣", message: "replace '[Category<Int>: String]()' with '[:]'"),
86+
FindingSpec("4️⃣", message: "replace '[(Int, Array<String>): Int]()' with ': [(Int, Array<String>): Int] = [:]'"),
87+
FindingSpec("5️⃣", message: "replace '[String: (String, Int, Float)]()' with '[:]'"),
88+
]
89+
)
90+
}
4891
}

0 commit comments

Comments
 (0)