Skip to content

Commit 9362431

Browse files
authored
Merge pull request #617 from xedin/upstream-literal-array-init-rewrite
[Lint] Add a rule to detect and transform `[<Type>]()` into literal …
2 parents e1b18cf + 9ab323e commit 9362431

5 files changed

+386
-0
lines changed

Sources/SwiftFormat/Core/Pipelines+Generated.swift

+3
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ class LintPipeline: SyntaxVisitor {
159159
}
160160

161161
override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind {
162+
visitIfEnabled(AlwaysUseLiteralForEmptyCollectionInit.visit, for: node)
162163
visitIfEnabled(NoLeadingUnderscores.visit, for: node)
163164
return .visitChildren
164165
}
@@ -243,6 +244,7 @@ class LintPipeline: SyntaxVisitor {
243244
}
244245

245246
override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
247+
visitIfEnabled(AlwaysUseLiteralForEmptyCollectionInit.visit, for: node)
246248
visitIfEnabled(OmitExplicitReturns.visit, for: node)
247249
visitIfEnabled(UseSingleLinePropertyGetter.visit, for: node)
248250
return .visitChildren
@@ -356,6 +358,7 @@ extension FormatPipeline {
356358

357359
func rewrite(_ node: Syntax) -> Syntax {
358360
var node = node
361+
node = AlwaysUseLiteralForEmptyCollectionInit(context: context).rewrite(node)
359362
node = DoNotUseSemicolons(context: context).rewrite(node)
360363
node = FileScopedDeclarationPrivacy(context: context).rewrite(node)
361364
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(AlwaysUseLiteralForEmptyCollectionInit.self): "AlwaysUseLiteralForEmptyCollectionInit",
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
@_spi(Internal) public enum RuleRegistry {
1616
public static let rules: [String: Bool] = [
1717
"AllPublicDeclarationsHaveDocumentation": false,
18+
"AlwaysUseLiteralForEmptyCollectionInit": false,
1819
"AlwaysUseLowerCamelCase": true,
1920
"AmbiguousTrailingClosureOverload": true,
2021
"BeginDocumentationCommentWithOneLineSummary": false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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 AlwaysUseLiteralForEmptyCollectionInit : SyntaxFormatRule {
25+
public override class var isOptIn: Bool { return true }
26+
27+
public override func visit(_ node: PatternBindingSyntax) -> PatternBindingSyntax {
28+
guard let initializer = node.initializer,
29+
let type = isRewritable(initializer) else {
30+
return node
31+
}
32+
33+
if let type = type.as(ArrayTypeSyntax.self) {
34+
return rewrite(node, type: type)
35+
}
36+
37+
if let type = type.as(DictionaryTypeSyntax.self) {
38+
return rewrite(node, type: type)
39+
}
40+
41+
return node
42+
}
43+
44+
public override func visit(_ param: FunctionParameterSyntax) -> FunctionParameterSyntax {
45+
guard let initializer = param.defaultValue,
46+
let type = isRewritable(initializer) else {
47+
return param
48+
}
49+
50+
if let type = type.as(ArrayTypeSyntax.self) {
51+
return rewrite(param, type: type)
52+
}
53+
54+
if let type = type.as(DictionaryTypeSyntax.self) {
55+
return rewrite(param, type: type)
56+
}
57+
58+
return param
59+
}
60+
61+
/// Check whether the initializer is `[<Type>]()` and, if so, it could be rewritten to use an empty collection literal.
62+
/// Return a type of the collection.
63+
public func isRewritable(_ initializer: InitializerClauseSyntax) -> TypeSyntax? {
64+
guard let initCall = initializer.value.as(FunctionCallExprSyntax.self),
65+
initCall.arguments.isEmpty else {
66+
return nil
67+
}
68+
69+
if let arrayLiteral = initCall.calledExpression.as(ArrayExprSyntax.self) {
70+
return getLiteralType(arrayLiteral)
71+
}
72+
73+
if let dictLiteral = initCall.calledExpression.as(DictionaryExprSyntax.self) {
74+
return getLiteralType(dictLiteral)
75+
}
76+
77+
return nil
78+
}
79+
80+
private func rewrite(_ node: PatternBindingSyntax,
81+
type: ArrayTypeSyntax) -> PatternBindingSyntax {
82+
var replacement = node
83+
84+
diagnose(node, type: type)
85+
86+
if replacement.typeAnnotation == nil {
87+
// Drop trailing trivia after pattern because ':' has to appear connected to it.
88+
replacement.pattern = node.pattern.with(\.trailingTrivia, [])
89+
// Add explicit type annotiation: ': [<Type>]`
90+
replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space)
91+
.with(\.trailingTrivia, .space))
92+
}
93+
94+
let initializer = node.initializer!
95+
let emptyArrayExpr = ArrayExprSyntax(elements: ArrayElementListSyntax.init([]))
96+
97+
// Replace initializer call with empty array literal: `[<Type>]()` -> `[]`
98+
replacement.initializer = initializer.with(\.value, ExprSyntax(emptyArrayExpr))
99+
100+
return replacement
101+
}
102+
103+
private func rewrite(_ node: PatternBindingSyntax,
104+
type: DictionaryTypeSyntax) -> PatternBindingSyntax {
105+
var replacement = node
106+
107+
diagnose(node, type: type)
108+
109+
if replacement.typeAnnotation == nil {
110+
// Drop trailing trivia after pattern because ':' has to appear connected to it.
111+
replacement.pattern = node.pattern.with(\.trailingTrivia, [])
112+
// Add explicit type annotiation: ': [<Type>]`
113+
replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space)
114+
.with(\.trailingTrivia, .space))
115+
}
116+
117+
let initializer = node.initializer!
118+
// Replace initializer call with empty dictionary literal: `[<Type>]()` -> `[]`
119+
replacement.initializer = initializer.with(\.value, ExprSyntax(getEmptyDictionaryLiteral()))
120+
121+
return replacement
122+
}
123+
124+
private func rewrite(_ param: FunctionParameterSyntax,
125+
type: ArrayTypeSyntax) -> FunctionParameterSyntax {
126+
guard let initializer = param.defaultValue else {
127+
return param
128+
}
129+
130+
emitDiagnostic(replace: "\(initializer.value)", with: "[]", on: initializer.value)
131+
return param.with(\.defaultValue, initializer.with(\.value, getEmptyArrayLiteral()))
132+
}
133+
134+
private func rewrite(_ param: FunctionParameterSyntax,
135+
type: DictionaryTypeSyntax) -> FunctionParameterSyntax {
136+
guard let initializer = param.defaultValue else {
137+
return param
138+
}
139+
140+
emitDiagnostic(replace: "\(initializer.value)", with: "[:]", on: initializer.value)
141+
return param.with(\.defaultValue, initializer.with(\.value, getEmptyDictionaryLiteral()))
142+
}
143+
144+
private func diagnose(_ node: PatternBindingSyntax, type: ArrayTypeSyntax) {
145+
var withFixIt = "[]"
146+
if node.typeAnnotation == nil {
147+
withFixIt = ": \(type) = []"
148+
}
149+
150+
let initCall = node.initializer!.value
151+
emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall)
152+
}
153+
154+
private func diagnose(_ node: PatternBindingSyntax, type: DictionaryTypeSyntax) {
155+
var withFixIt = "[:]"
156+
if node.typeAnnotation == nil {
157+
withFixIt = ": \(type) = [:]"
158+
}
159+
160+
let initCall = node.initializer!.value
161+
emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall)
162+
}
163+
164+
private func emitDiagnostic(replace: String, with fixIt: String, on: ExprSyntax?) {
165+
diagnose(.refactorIntoEmptyLiteral(replace: replace, with: fixIt), on: on)
166+
}
167+
168+
private func getLiteralType(_ arrayLiteral: ArrayExprSyntax) -> TypeSyntax? {
169+
guard let elementExpr = arrayLiteral.elements.firstAndOnly,
170+
elementExpr.is(ArrayElementSyntax.self) else {
171+
return nil
172+
}
173+
174+
var parser = Parser(arrayLiteral.description)
175+
let elementType = TypeSyntax.parse(from: &parser)
176+
177+
guard !elementType.hasError, elementType.is(ArrayTypeSyntax.self) else {
178+
return nil
179+
}
180+
181+
return elementType
182+
}
183+
184+
private func getLiteralType(_ dictLiteral: DictionaryExprSyntax) -> TypeSyntax? {
185+
var parser = Parser(dictLiteral.description)
186+
let elementType = TypeSyntax.parse(from: &parser)
187+
188+
guard !elementType.hasError, elementType.is(DictionaryTypeSyntax.self) else {
189+
return nil
190+
}
191+
192+
return elementType
193+
}
194+
195+
private func getEmptyArrayLiteral() -> ExprSyntax {
196+
ExprSyntax(ArrayExprSyntax(elements: ArrayElementListSyntax.init([])))
197+
}
198+
199+
private func getEmptyDictionaryLiteral() -> ExprSyntax {
200+
ExprSyntax(DictionaryExprSyntax(content: .colon(.colonToken())))
201+
}
202+
}
203+
204+
extension Finding.Message {
205+
public static func refactorIntoEmptyLiteral(replace: String, with: String) -> Finding.Message {
206+
"replace '\(replace)' with '\(with)'"
207+
}
208+
}

0 commit comments

Comments
 (0)