Skip to content

Commit 9501173

Browse files
committed
Don't use shorthand syntax for Optional<T> if all of the following are true:
* It is the type of a variable (file-, function-, or type-scoped) * It is mutable (`var`, not `let`) * It is not a computed property * It does not have an initializer expression If all of these are true, shortening it to `T?` would introduce a useless nil-initialization that can be problematic in situations that want to optimize performance (see https://forums.swift.org/t/what-is-a-variable-initialization-expression/52132). Fixes #304.
1 parent dc5564e commit 9501173

File tree

2 files changed

+180
-20
lines changed

2 files changed

+180
-20
lines changed

Sources/SwiftFormatRules/UseShorthandTypeNames.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public final class UseShorthandTypeNames: SyntaxFormatRule {
6868
trailingTrivia: trailingTrivia)
6969

7070
case "Optional":
71+
guard !isTypeOfUninitializedStoredVar(node) else {
72+
newNode = nil
73+
break
74+
}
7175
guard let typeArgument = genericArgumentList.firstAndOnly else {
7276
newNode = nil
7377
break
@@ -496,6 +500,72 @@ public final class UseShorthandTypeNames: SyntaxFormatRule {
496500
trailingTrivia: node.lastToken?.trailingTrivia ?? []
497501
)
498502
}
503+
504+
/// Returns true if the given pattern binding represents a stored property/variable (as opposed to
505+
/// a computed property/variable).
506+
private func isStoredProperty(_ node: PatternBindingSyntax) -> Bool {
507+
guard let accessor = node.accessor else {
508+
// If it has no accessors at all, it is definitely a stored property.
509+
return true
510+
}
511+
512+
guard let accessorBlock = accessor.as(AccessorBlockSyntax.self) else {
513+
// If the accessor isn't an `AccessorBlockSyntax`, then it is a `CodeBlockSyntax`; i.e., the
514+
// accessor an implicit `get`. So, it is definitely not a stored property.
515+
assert(accessor.is(CodeBlockSyntax.self))
516+
return false
517+
}
518+
519+
for accessorDecl in accessorBlock.accessors {
520+
// Look for accessors that indicate that this is a computed property. If none are found, then
521+
// it is a stored property (e.g., having only observers like `willSet/didSet`).
522+
switch accessorDecl.accessorKind.tokenKind {
523+
case .contextualKeyword("get"),
524+
.contextualKeyword("set"),
525+
.contextualKeyword("unsafeAddress"),
526+
.contextualKeyword("unsafeMutableAddress"),
527+
.contextualKeyword("_read"),
528+
.contextualKeyword("_modify"):
529+
return false
530+
default:
531+
return true
532+
}
533+
}
534+
535+
// This should be unreachable.
536+
assertionFailure("Should not have an AccessorBlock with no AccessorDecls")
537+
return false
538+
}
539+
540+
/// Returns true if the given type identifier node represents the type of a mutable variable or
541+
/// stored property that does not have an initializer clause.
542+
private func isTypeOfUninitializedStoredVar(_ node: SimpleTypeIdentifierSyntax) -> Bool {
543+
if let typeAnnotation = node.parent?.as(TypeAnnotationSyntax.self),
544+
let patternBinding = nearestAncestor(of: typeAnnotation, type: PatternBindingSyntax.self),
545+
isStoredProperty(patternBinding),
546+
patternBinding.initializer == nil,
547+
let variableDecl = nearestAncestor(of: patternBinding, type: VariableDeclSyntax.self),
548+
variableDecl.letOrVarKeyword.tokenKind == .varKeyword
549+
{
550+
return true
551+
}
552+
return false
553+
}
554+
555+
/// Returns the node's nearest ancestor of the given type, if found; otherwise, returns nil.
556+
private func nearestAncestor<NodeType: SyntaxProtocol, AncestorType: SyntaxProtocol>(
557+
of node: NodeType,
558+
type: AncestorType.Type = AncestorType.self
559+
) -> AncestorType? {
560+
var parent: Syntax? = Syntax(node)
561+
while let existingParent = parent {
562+
if existingParent.is(type) {
563+
return existingParent.as(type)
564+
}
565+
parent = existingParent.parent
566+
}
567+
return nil
568+
}
499569
}
500570

501571
extension Finding.Message {

Tests/SwiftFormatRulesTests/UseShorthandTypeNamesTests.swift

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ final class UseShorthandTypeNamesTests: LintOrFormatRuleTestCase {
66
UseShorthandTypeNames.self,
77
input:
88
"""
9-
var a: Array<Int>
10-
var b: Dictionary<String, Int>
11-
var c: Optional<Foo>
9+
var a: Array<Int> = []
10+
var b: Dictionary<String, Int> = [:]
11+
var c: Optional<Foo> = nil
1212
""",
1313
expected:
1414
"""
15-
var a: [Int]
16-
var b: [String: Int]
17-
var c: Foo?
15+
var a: [Int] = []
16+
var b: [String: Int] = [:]
17+
var c: Foo? = nil
1818
""")
1919

2020
XCTAssertDiagnosed(.useTypeShorthand(type: "Array"))
@@ -41,13 +41,13 @@ final class UseShorthandTypeNamesTests: LintOrFormatRuleTestCase {
4141
var h: [String: Dictionary<String, Int>]
4242
var i: [Dictionary<String, Int>: Dictionary<String, Int>]
4343
44-
var a: Optional<Array<Int>>
45-
var b: Optional<Dictionary<String, Int>>
46-
var c: Optional<Optional<Int>>
47-
var d: Array<Int>?
48-
var e: Dictionary<String, Int>?
49-
var f: Optional<Int>?
50-
var g: Optional<Int?>
44+
let a: Optional<Array<Int>>
45+
let b: Optional<Dictionary<String, Int>>
46+
let c: Optional<Optional<Int>>
47+
let d: Array<Int>?
48+
let e: Dictionary<String, Int>?
49+
let f: Optional<Int>?
50+
let g: Optional<Int?>
5151
5252
var a: Array<Optional<Int>>
5353
var b: Dictionary<Optional<String>, Optional<Int>>
@@ -70,13 +70,13 @@ final class UseShorthandTypeNamesTests: LintOrFormatRuleTestCase {
7070
var h: [String: [String: Int]]
7171
var i: [[String: Int]: [String: Int]]
7272
73-
var a: [Int]?
74-
var b: [String: Int]?
75-
var c: Int??
76-
var d: [Int]?
77-
var e: [String: Int]?
78-
var f: Int??
79-
var g: Int??
73+
let a: [Int]?
74+
let b: [String: Int]?
75+
let c: Int??
76+
let d: [Int]?
77+
let e: [String: Int]?
78+
let f: Int??
79+
let g: Int??
8080
8181
var a: [Int?]
8282
var b: [String?: Int?]
@@ -323,4 +323,94 @@ final class UseShorthandTypeNamesTests: LintOrFormatRuleTestCase {
323323
var c: Sectional<Couch> = Sectional<Couch>(from: warehouse)
324324
""")
325325
}
326+
327+
func testOptionalStoredVarsWithoutInitializersAreNotChangedUnlessImmutable() {
328+
XCTAssertFormatting(
329+
UseShorthandTypeNames.self,
330+
input:
331+
"""
332+
var a: Optional<Int>
333+
var b: Optional<Int> {
334+
didSet {}
335+
}
336+
let c: Optional<Int>
337+
""",
338+
expected:
339+
"""
340+
var a: Optional<Int>
341+
var b: Optional<Int> {
342+
didSet {}
343+
}
344+
let c: Int?
345+
""")
346+
}
347+
348+
func testOptionalComputedVarsAreChanged() {
349+
XCTAssertFormatting(
350+
UseShorthandTypeNames.self,
351+
input:
352+
"""
353+
var a: Optional<Int> { nil }
354+
var b: Optional<Int> {
355+
get { 0 }
356+
}
357+
var c: Optional<Int> {
358+
_read {}
359+
}
360+
var d: Optional<Int> {
361+
unsafeAddress {}
362+
}
363+
""",
364+
expected:
365+
"""
366+
var a: Int? { nil }
367+
var b: Int? {
368+
get { 0 }
369+
}
370+
var c: Int? {
371+
_read {}
372+
}
373+
var d: Int? {
374+
unsafeAddress {}
375+
}
376+
""")
377+
}
378+
379+
func testOptionalStoredVarsWithInitializersAreChanged() {
380+
XCTAssertFormatting(
381+
UseShorthandTypeNames.self,
382+
input:
383+
"""
384+
var a: Optional<Int> = nil
385+
var b: Optional<Int> = nil {
386+
didSet {}
387+
}
388+
let c: Optional<Int> = nil
389+
""",
390+
expected:
391+
"""
392+
var a: Int? = nil
393+
var b: Int? = nil {
394+
didSet {}
395+
}
396+
let c: Int? = nil
397+
""")
398+
}
399+
400+
func testOptionalsNestedInOtherTypseInStoredVarsAreStillChanged() {
401+
XCTAssertFormatting(
402+
UseShorthandTypeNames.self,
403+
input:
404+
"""
405+
var c: Generic<Optional<Int>>
406+
var d: [Optional<Int>]
407+
var e: [String: Optional<Int>]
408+
""",
409+
expected:
410+
"""
411+
var c: Generic<Int?>
412+
var d: [Int?]
413+
var e: [String: Int?]
414+
""")
415+
}
326416
}

0 commit comments

Comments
 (0)