Skip to content

Commit 9f33cf3

Browse files
committed
[Lint] Add a rule to detect and transform [<Type>]() into literal array init
Instead of using an archaic initializer call syntax, let's replace it with an empty array literal with a type annotation (if there isn't one).
1 parent b50fd28 commit 9f33cf3

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-0
lines changed

Sources/SwiftFormat/Core/Pipelines+Generated.swift

+2
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class LintPipeline: SyntaxVisitor {
141141
}
142142

143143
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
144+
visitIfEnabled(AlwaysUseLiteralForEmptyArrayInit.visit, for: node)
144145
visitIfEnabled(NoEmptyTrailingClosureParentheses.visit, for: node)
145146
visitIfEnabled(OnlyOneTrailingClosureArgument.visit, for: node)
146147
visitIfEnabled(ReplaceForEachWithForLoop.visit, for: node)
@@ -356,6 +357,7 @@ extension FormatPipeline {
356357

357358
func rewrite(_ node: Syntax) -> Syntax {
358359
var node = node
360+
node = AlwaysUseLiteralForEmptyArrayInit(context: context).rewrite(node)
359361
node = DoNotUseSemicolons(context: context).rewrite(node)
360362
node = FileScopedDeclarationPrivacy(context: context).rewrite(node)
361363
node = FullyIndirectEnum(context: context).rewrite(node)

Sources/SwiftFormat/Core/RuleNameCache+Generated.swift

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
@_spi(Testing)
1717
public let ruleNameCache: [ObjectIdentifier: String] = [
1818
ObjectIdentifier(AllPublicDeclarationsHaveDocumentation.self): "AllPublicDeclarationsHaveDocumentation",
19+
ObjectIdentifier(AlwaysUseLiteralForEmptyArrayInit.self): "AlwaysUseLiteralForEmptyArrayInit",
1920
ObjectIdentifier(AlwaysUseLowerCamelCase.self): "AlwaysUseLowerCamelCase",
2021
ObjectIdentifier(AmbiguousTrailingClosureOverload.self): "AmbiguousTrailingClosureOverload",
2122
ObjectIdentifier(BeginDocumentationCommentWithOneLineSummary.self): "BeginDocumentationCommentWithOneLineSummary",

Sources/SwiftFormat/Core/RuleRegistry+Generated.swift

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
enum RuleRegistry {
1616
static let rules: [String: Bool] = [
1717
"AllPublicDeclarationsHaveDocumentation": false,
18+
"AlwaysUseLiteralForEmptyArrayInit": true,
1819
"AlwaysUseLowerCamelCase": true,
1920
"AmbiguousTrailingClosureOverload": true,
2021
"BeginDocumentationCommentWithOneLineSummary": false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftSyntax
15+
import SwiftParser
16+
17+
/// Never use `[<Type>]()` syntax. In call sites that should be replaced with `[]`,
18+
/// for initializations use explicit type combined with empty array literal `let _: [<Type>] = []`
19+
/// Static properties of a type that return that type should not include a reference to their type.
20+
///
21+
/// Lint: Non-literal empty array initialization will yield a lint error.
22+
/// Format: All invalid use sites would be related with empty literal (with or without explicit type annotation).
23+
@_spi(Rules)
24+
public final class AlwaysUseLiteralForEmptyArrayInit : SyntaxFormatRule {
25+
public override func visit(_ node: PatternBindingSyntax) -> PatternBindingSyntax {
26+
guard let initializer = node.initializer else {
27+
return node
28+
}
29+
30+
// Check whether the initializer is `[<Type>]()`
31+
guard let initCall = initializer.value.as(FunctionCallExprSyntax.self),
32+
var arrayLiteral = initCall.calledExpression.as(ArrayExprSyntax.self),
33+
initCall.arguments.isEmpty else {
34+
return node
35+
}
36+
37+
guard let elementType = getElementType(arrayLiteral) else {
38+
return node
39+
}
40+
41+
var replacement = node
42+
43+
var withFixIt = "[]"
44+
if replacement.typeAnnotation == nil {
45+
withFixIt = ": [\(elementType)] = []"
46+
}
47+
48+
diagnose(.refactorEmptyArrayInit(replace: "\(initCall)", with: withFixIt), on: initCall)
49+
50+
if replacement.typeAnnotation == nil {
51+
// Drop trailing trivia after pattern because ':' has to appear connected to it.
52+
replacement.pattern = node.pattern.with(\.trailingTrivia, [])
53+
// Add explicit type annotiation: ': [<Type>]`
54+
replacement.typeAnnotation = .init(type: ArrayTypeSyntax(leadingTrivia: .space,
55+
element: elementType,
56+
trailingTrivia: .space))
57+
}
58+
59+
// Replace initializer call with empty array literal: `[<Type>]()` -> `[]`
60+
arrayLiteral.elements = ArrayElementListSyntax.init([])
61+
replacement.initializer = initializer.with(\.value, ExprSyntax(arrayLiteral))
62+
63+
return replacement
64+
}
65+
66+
private func getElementType(_ arrayLiteral: ArrayExprSyntax) -> TypeSyntax? {
67+
guard let elementExpr = arrayLiteral.elements.firstAndOnly?.as(ArrayElementSyntax.self) else {
68+
return nil
69+
}
70+
71+
var parser = Parser(elementExpr.description)
72+
let elementType = TypeSyntax.parse(from: &parser)
73+
return elementType.hasError ? nil : elementType
74+
}
75+
}
76+
77+
extension Finding.Message {
78+
public static func refactorEmptyArrayInit(replace: String, with: String) -> Finding.Message {
79+
"replace '\(replace)' with '\(with)'"
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import _SwiftFormatTestSupport
2+
3+
@_spi(Rules) import SwiftFormat
4+
5+
final class AlwaysUseLiteralForEmptyArrayInitTests: LintOrFormatRuleTestCase {
6+
func testPatternBindings() {
7+
assertFormatting(
8+
AlwaysUseLiteralForEmptyArrayInit.self,
9+
input: """
10+
public struct Test {
11+
var value1 = 1️⃣[Int]()
12+
13+
func test(v: [Double] = [Double]()) {
14+
let _ = 2️⃣[String]()
15+
}
16+
}
17+
18+
var _: [Category<Int>] = 3️⃣[Category<Int>]()
19+
let _ = 4️⃣[(Int, Array<String>)]()
20+
let _: [(String, Int, Float)] = 5️⃣[(String, Int, Float)]()
21+
22+
let _ = [(1, 2, String)]()
23+
""",
24+
expected: """
25+
public struct Test {
26+
var value1: [Int] = []
27+
28+
func test(v: [Double] = [Double]()) {
29+
let _: [String] = []
30+
}
31+
}
32+
33+
var _: [Category<Int>] = []
34+
let _: [(Int, Array<String>)] = []
35+
let _: [(String, Int, Float)] = []
36+
37+
let _ = [(1, 2, String)]()
38+
""",
39+
findings: [
40+
FindingSpec("1️⃣", message: "replace '[Int]()' with ': [Int] = []'"),
41+
FindingSpec("2️⃣", message: "replace '[String]()' with ': [String] = []'"),
42+
FindingSpec("3️⃣", message: "replace '[Category<Int>]()' with '[]'"),
43+
FindingSpec("4️⃣", message: "replace '[(Int, Array<String>)]()' with ': [(Int, Array<String>)] = []'"),
44+
FindingSpec("5️⃣", message: "replace '[(String, Int, Float)]()' with '[]'"),
45+
]
46+
)
47+
}
48+
}

0 commit comments

Comments
 (0)