Skip to content

Commit 8da26ce

Browse files
LaurenWhiteallevato
authored andcommitted
Implement use synthesized initializer. (swiftlang#87)
1 parent 9ee781f commit 8da26ce

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,85 @@
11
import SwiftSyntax
22

33
extension ModifierListSyntax {
4+
45
func has(modifier: String) -> Bool {
56
return contains { $0.name.text == modifier }
67
}
8+
9+
/// Returns the declaration's access level modifier, if present.
10+
var accessLevelModifier: DeclModifierSyntax? {
11+
for modifier in self {
12+
switch modifier.name.tokenKind {
13+
case .publicKeyword, .privateKeyword, .fileprivateKeyword, .internalKeyword:
14+
return modifier
15+
default:
16+
continue
17+
}
18+
}
19+
return nil
20+
}
21+
22+
/// Returns modifier list without the given modifier.
23+
func remove(name: String) -> ModifierListSyntax {
24+
guard has(modifier: name) else { return self }
25+
for mod in self {
26+
if mod.name.text == name {
27+
return removing(childAt: mod.indexInParent)
28+
}
29+
}
30+
return self
31+
}
32+
33+
/// Returns a foramatted declaration modifier token with the given name.
34+
func createModifierToken(name: String) -> DeclModifierSyntax {
35+
let id = SyntaxFactory.makeIdentifier(name, trailingTrivia: .spaces(1))
36+
let newModifier = SyntaxFactory.makeDeclModifier(name: id,
37+
detailLeftParen: nil, detail: nil, detailRightParen: nil)
38+
return newModifier
39+
}
40+
41+
/// Returns modifiers with the given modifier inserted at the given index.
42+
/// Preserves existing trivia and formats new trivia, given true for 'formatTrivia.'
43+
func insert(modifier: DeclModifierSyntax, at index: Int,
44+
formatTrivia: Bool = true) -> ModifierListSyntax {
45+
guard index >= 0, index <= count else { return self }
46+
47+
var newModifiers: [DeclModifierSyntax] = []
48+
newModifiers.append(contentsOf: self)
49+
50+
let modifier = formatTrivia ?
51+
replaceTrivia(on: modifier,
52+
token: modifier.name,
53+
trailingTrivia: .spaces(1)) as! DeclModifierSyntax : modifier
54+
55+
if index == 0 {
56+
guard formatTrivia else { return inserting(modifier, at: index) }
57+
guard let firstMod = first, let firstTok = firstMod.firstToken else {
58+
return inserting(modifier, at: index)
59+
}
60+
let formattedMod = replaceTrivia(on: modifier,
61+
token: modifier.firstToken,
62+
leadingTrivia: firstTok.leadingTrivia) as! DeclModifierSyntax
63+
newModifiers[0] = replaceTrivia(on: firstMod,
64+
token: firstTok,
65+
leadingTrivia: [],
66+
trailingTrivia: .spaces(1)) as! DeclModifierSyntax
67+
newModifiers.insert(formattedMod, at: 0)
68+
return SyntaxFactory.makeModifierList(newModifiers)
69+
} else {
70+
return inserting(modifier, at: index)
71+
}
72+
}
73+
74+
/// Returns modifier list with the given modifier at the end.
75+
/// Trivia manipulation optional by 'formatTrivia'
76+
func append(modifier: DeclModifierSyntax, formatTrivia: Bool = true) -> ModifierListSyntax {
77+
return insert(modifier: modifier, at: count, formatTrivia: formatTrivia)
78+
}
79+
80+
/// Returns modifier list with the given modifier at the beginning.
81+
/// Trivia manipulation optional by 'formatTrivia'
82+
func prepend(modifier: DeclModifierSyntax, formatTrivia: Bool = true) -> ModifierListSyntax {
83+
return insert(modifier: modifier, at: 0, formatTrivia: formatTrivia)
84+
}
785
}

Sources/Rules/UseSynthesizedInitializer.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,109 @@ import SwiftSyntax
1212
///
1313
/// - SeeAlso: https://google.github.io/swift#initializers-2
1414
public final class UseSynthesizedInitializer: SyntaxLintRule {
15+
public override func visit(_ node: StructDeclSyntax) {
16+
var storedProperties: [VariableDeclSyntax] = []
17+
var initializers: [InitializerDeclSyntax] = []
1518

19+
for member in node.members.members {
20+
// Collect all stored variables into a list
21+
if let varDecl = member.decl as? VariableDeclSyntax {
22+
guard let modifiers = varDecl.modifiers else {
23+
storedProperties.append(varDecl)
24+
continue
25+
}
26+
guard !modifiers.has(modifier: "static") else { continue }
27+
storedProperties.append(varDecl)
28+
// Collect any possible redundant initializers into a list
29+
} else if let initDecl = member.decl as? InitializerDeclSyntax {
30+
guard initDecl.modifiers == nil ||
31+
initDecl.modifiers!.has(modifier: "internal") else { continue }
32+
guard initDecl.optionalMark == nil else { continue }
33+
guard initDecl.throwsOrRethrowsKeyword == nil else { continue }
34+
initializers.append(initDecl)
35+
}
36+
}
37+
38+
for initializer in initializers {
39+
guard matchesPropertyList(parameters: initializer.parameters.parameterList,
40+
properties: storedProperties) else { continue }
41+
guard matchesAssignmentBody(variables: storedProperties,
42+
initBody: initializer.body) else { continue }
43+
diagnose(.removeRedundantInitializer, on: initializer)
44+
}
45+
}
46+
47+
// Compares initializer parameters to stored properties of the struct
48+
func matchesPropertyList(parameters: FunctionParameterListSyntax,
49+
properties: [VariableDeclSyntax]) -> Bool {
50+
guard parameters.count == properties.count else { return false }
51+
for (idx, parameter) in parameters.enumerated() {
52+
53+
guard let paramId = parameter.firstName, parameter.secondName == nil else { return false }
54+
guard let paramType = parameter.type else { return false }
55+
56+
let property = properties[idx]
57+
let propertyId = property.firstIdentifier
58+
guard let propertyType = property.firstType else { return false }
59+
60+
// Sythesized initializer only keeps default argument if the declaration uses 'var'
61+
if property.letOrVarKeyword.tokenKind == .varKeyword {
62+
if let initializer = property.firstInitializer {
63+
guard let defaultArg = parameter.defaultArgument else { return false }
64+
guard initializer.value.description == defaultArg.value.description else { return false }
65+
}
66+
}
67+
68+
if propertyId.identifier.text != paramId.text ||
69+
propertyType.description.trimmingCharacters(in: .whitespaces) !=
70+
paramType.description.trimmingCharacters(in: .whitespacesAndNewlines) { return false }
71+
}
72+
return true
73+
}
74+
75+
// Evaluates if all, and only, the stored properties are initialized in the body
76+
func matchesAssignmentBody(variables: [VariableDeclSyntax],
77+
initBody: CodeBlockSyntax?) -> Bool {
78+
guard let initBody = initBody else { return false }
79+
guard variables.count == initBody.statements.count else { return false }
80+
81+
var statements: [String] = []
82+
for statement in initBody.statements {
83+
guard let exp = statement.item as? SequenceExprSyntax else { return false }
84+
var leftName = ""
85+
var rightName = ""
86+
87+
for element in exp.elements {
88+
switch element {
89+
case let element as MemberAccessExprSyntax:
90+
guard let base = element.base else { return false }
91+
guard base.description.trimmingCharacters(in: .whitespacesAndNewlines) == "self" else {
92+
return false
93+
}
94+
leftName = element.name.text
95+
case let element as AssignmentExprSyntax:
96+
guard element.assignToken.tokenKind == .equal else { return false }
97+
case let element as IdentifierExprSyntax:
98+
rightName = element.identifier.text
99+
default:
100+
return false
101+
}
102+
}
103+
guard leftName == rightName else { return false }
104+
statements.append(leftName)
105+
}
106+
107+
for variable in variables {
108+
let id = variable.firstIdentifier.identifier.text
109+
guard statements.contains(id) else { return false }
110+
guard let idx = statements.firstIndex(of: id) else { return false }
111+
statements.remove(at: idx)
112+
}
113+
return statements.isEmpty
114+
}
115+
}
116+
117+
extension Diagnostic.Message {
118+
static let removeRedundantInitializer = Diagnostic.Message(.warning,
119+
"initializer is the same as synthesized initializer")
16120
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import SwiftSyntax
2+
3+
extension VariableDeclSyntax {
4+
5+
/// Returns array of all identifiers listed in the declaration.
6+
var identifiers: [IdentifierPatternSyntax] {
7+
var ids: [IdentifierPatternSyntax] = []
8+
for binding in bindings {
9+
guard let id = binding.pattern as? IdentifierPatternSyntax else { continue }
10+
ids.append(id)
11+
}
12+
return ids
13+
}
14+
15+
/// Returns the first identifier.
16+
var firstIdentifier: IdentifierPatternSyntax {
17+
return identifiers[0]
18+
}
19+
20+
/// Returns the first type explicitly stated in the declaration, if present.
21+
var firstType: TypeSyntax? {
22+
return bindings.first?.typeAnnotation?.type
23+
}
24+
25+
/// Returns the first initializer clause, if present.
26+
var firstInitializer: InitializerClauseSyntax? {
27+
return bindings.first?.initializer
28+
}
29+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import SwiftSyntax
3+
import XCTest
4+
5+
@testable import Rules
6+
7+
public class UseSynthesizedInitializerTests: DiagnosingTestCase {
8+
public func testRedundantCustomInitializer() {
9+
let input =
10+
"""
11+
public struct Person {
12+
13+
public var name: String = "John Doe"
14+
let phoneNumber: String?
15+
private let address: String = "123 Happy St"
16+
17+
init(name: String = "John Doe", phoneNumber: String?, address: String) {
18+
self.name = name
19+
self.address = address
20+
self.phoneNumber = phoneNumber
21+
}
22+
23+
init(name: String, phoneNumber: String?, address: String) {
24+
self.name = name
25+
self.phoneNumber = "1234578910"
26+
self.address = address
27+
}
28+
init(name: String, phoneNumber: String? = "123456789", address: String) {
29+
self.name = name
30+
self.phoneNumber = phoneNumber
31+
self.address = address
32+
}
33+
public init(name: String, phoneNumber: String?, address: String) {
34+
self.name = name
35+
self.phoneNumber = phoneNumber
36+
self.address = address
37+
}
38+
init?(name: String, phoneNumber: String?, address: String) {
39+
self.name = name
40+
self.phoneNumber = phoneNumber
41+
self.address = address
42+
}
43+
init(name: String, phoneNumber: String?, address: String) throws {
44+
self.name = name
45+
self.phoneNumber = phoneNumber
46+
self.address = address
47+
}
48+
}
49+
"""
50+
performLint(UseSynthesizedInitializer.self, input: input)
51+
XCTAssertDiagnosed(.removeRedundantInitializer)
52+
XCTAssertNotDiagnosed(.removeRedundantInitializer)
53+
}
54+
55+
#if !os(macOS)
56+
static let allTests = [
57+
UseSynthesizedInitializerTests.testRedundantCustomInitializer,
58+
]
59+
#endif
60+
}

0 commit comments

Comments
 (0)