Skip to content

Commit c00a2ee

Browse files
authored
Merge pull request swiftlang#155 from google/no-indirect-caseless-enum
Various updates to FullyIndirectEnum.
2 parents b0c4925 + 3d9fa73 commit c00a2ee

File tree

2 files changed

+117
-78
lines changed

2 files changed

+117
-78
lines changed

Sources/SwiftFormatRules/FullyIndirectEnum.swift

+63-46
Original file line numberDiff line numberDiff line change
@@ -27,78 +27,95 @@ public final class FullyIndirectEnum: SyntaxFormatRule {
2727

2828
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
2929
let enumMembers = node.members.members
30-
guard allAreIndirectCases(members: enumMembers) else { return node }
31-
diagnose(.reassignIndirectKeyword(name: node.identifier.text), on: node.identifier)
30+
guard let enumModifiers = node.modifiers,
31+
!enumModifiers.has(modifier: "indirect"),
32+
allCasesAreIndirect(in: enumMembers)
33+
else {
34+
return node
35+
}
36+
37+
diagnose(.moveIndirectKeywordToEnumDecl(name: node.identifier.text), on: node.identifier)
3238

3339
// Removes 'indirect' keyword from cases, reformats
34-
var newMembers: [DeclSyntax] = []
35-
for member in enumMembers {
36-
if let caseMember = member as? EnumCaseDeclSyntax {
37-
guard let caseModifiers = caseMember.modifiers else { continue }
38-
guard let firstModifier = caseModifiers.first else { continue }
39-
let newCase = caseMember.withModifiers(caseModifiers.remove(name: "indirect"))
40-
let formattedCase = formatCase(unformattedCase: newCase,
41-
leadingTrivia: firstModifier.leadingTrivia)
42-
newMembers.append(formattedCase)
43-
} else {
44-
newMembers.append(member)
40+
let newMembers = enumMembers.map { (member: DeclSyntax) -> DeclSyntax in
41+
guard let caseMember = member as? EnumCaseDeclSyntax,
42+
let modifiers = caseMember.modifiers,
43+
modifiers.has(modifier: "indirect"),
44+
let firstModifier = modifiers.first
45+
else {
46+
return member
4547
}
48+
49+
let newCase = caseMember.withModifiers(modifiers.remove(name: "indirect"))
50+
let formattedCase = formatCase(
51+
unformattedCase: newCase, leadingTrivia: firstModifier.leadingTrivia)
52+
return formattedCase
4653
}
4754

48-
let newMemberBlock = SyntaxFactory.makeMemberDeclBlock(
49-
leftBrace: node.members.leftBrace,
50-
members: SyntaxFactory.makeDeclList(newMembers),
51-
rightBrace: node.members.rightBrace)
55+
// If the `indirect` keyword being added would be the first token in the decl, we need to move
56+
// the leading trivia from the `enum` keyword to the new modifier to preserve the existing
57+
// line breaks/comments/indentation.
58+
let firstTok = node.firstToken!
59+
let leadingTrivia: Trivia
60+
let newEnumDecl: EnumDeclSyntax
5261

53-
// Format indirect keyword and following token, if necessary
54-
guard let firstTok = node.firstToken else { return node }
55-
var leadingTrivia: Trivia = []
56-
var newDecl = node
5762
if firstTok.tokenKind == .enumKeyword {
5863
leadingTrivia = firstTok.leadingTrivia
59-
newDecl = replaceTrivia(on: node,
60-
token: node.firstToken,
61-
leadingTrivia: []) as! EnumDeclSyntax
64+
newEnumDecl = replaceTrivia(
65+
on: node, token: node.firstToken, leadingTrivia: []) as! EnumDeclSyntax
66+
} else {
67+
leadingTrivia = []
68+
newEnumDecl = node
6269
}
6370

6471
let newModifier = SyntaxFactory.makeDeclModifier(
65-
name: SyntaxFactory.makeIdentifier("indirect",
66-
leadingTrivia: leadingTrivia,
67-
trailingTrivia: .spaces(1)),
68-
detail: nil)
72+
name: SyntaxFactory.makeIdentifier(
73+
"indirect", leadingTrivia: leadingTrivia, trailingTrivia: .spaces(1)),
74+
detail: nil)
6975

70-
return newDecl.addModifier(newModifier).withMembers(newMemberBlock)
76+
let newMemberBlock = node.members.withMembers(SyntaxFactory.makeDeclList(newMembers))
77+
return newEnumDecl.addModifier(newModifier).withMembers(newMemberBlock)
7178
}
7279

73-
// Determines if all given cases are indirect
74-
func allAreIndirectCases(members: DeclListSyntax) -> Bool {
80+
/// Returns a value indicating whether all enum cases in the given list are indirect.
81+
///
82+
/// Note that if the enum has no cases, this returns false.
83+
private func allCasesAreIndirect(in members: DeclListSyntax) -> Bool {
84+
var hadCases = false
7585
for member in members {
7686
if let caseMember = member as? EnumCaseDeclSyntax {
77-
guard let caseModifiers = caseMember.modifiers else { return false }
78-
if caseModifiers.has(modifier: "indirect") { continue }
79-
else { return false }
87+
hadCases = true
88+
guard let modifiers = caseMember.modifiers, modifiers.has(modifier: "indirect") else {
89+
return false
90+
}
8091
}
8192
}
82-
return true
93+
return hadCases
8394
}
8495

85-
// Transfers given leading trivia to the first token in the case declaration
86-
func formatCase(unformattedCase: EnumCaseDeclSyntax,
87-
leadingTrivia: Trivia?) -> EnumCaseDeclSyntax {
96+
/// Transfers given leading trivia to the first token in the case declaration.
97+
private func formatCase(
98+
unformattedCase: EnumCaseDeclSyntax,
99+
leadingTrivia: Trivia?
100+
) -> EnumCaseDeclSyntax {
88101
if let modifiers = unformattedCase.modifiers, let first = modifiers.first {
89-
return replaceTrivia(on: unformattedCase,
90-
token: first.firstToken,
91-
leadingTrivia: leadingTrivia) as! EnumCaseDeclSyntax
102+
return replaceTrivia(
103+
on: unformattedCase, token: first.firstToken, leadingTrivia: leadingTrivia
104+
) as! EnumCaseDeclSyntax
92105
} else {
93-
return replaceTrivia(on: unformattedCase,
94-
token: unformattedCase.caseKeyword,
95-
leadingTrivia: leadingTrivia) as! EnumCaseDeclSyntax
106+
return replaceTrivia(
107+
on: unformattedCase, token: unformattedCase.caseKeyword, leadingTrivia: leadingTrivia
108+
) as! EnumCaseDeclSyntax
96109
}
97110
}
98111
}
99112

100113
extension Diagnostic.Message {
101-
static func reassignIndirectKeyword(name: String) -> Diagnostic.Message {
102-
return .init(.warning, "move 'indirect' to \(name) enum declaration")
114+
115+
static func moveIndirectKeywordToEnumDecl(name: String) -> Diagnostic.Message {
116+
return .init(
117+
.warning,
118+
"move 'indirect' to \(name) enum declaration when all cases are indirect"
119+
)
103120
}
104121
}

Tests/SwiftFormatRulesTests/FullyIndirectEnumTests.swift

+54-32
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,63 @@ import XCTest
55
@testable import SwiftFormatRules
66

77
public class FullyIndirectEnumTests: DiagnosingTestCase {
8-
public func testIndirectEnumReassignment() {
8+
9+
public func testAllIndirectCases() {
910
XCTAssertFormatting(
1011
FullyIndirectEnum.self,
1112
input: """
12-
// Comment 1
13-
public enum DependencyGraphNode {
14-
internal indirect case userDefined(dependencies: [DependencyGraphNode])
15-
// Comment 2
16-
indirect case synthesized(dependencies: [DependencyGraphNode])
17-
indirect case other(dependencies: [DependencyGraphNode])
18-
var x: Int
19-
}
20-
public enum CompassPoint {
21-
case north
22-
indirect case south
23-
case east
24-
case west
25-
}
26-
""",
13+
// Comment 1
14+
public enum DependencyGraphNode {
15+
internal indirect case userDefined(dependencies: [DependencyGraphNode])
16+
// Comment 2
17+
indirect case synthesized(dependencies: [DependencyGraphNode])
18+
indirect case other(dependencies: [DependencyGraphNode])
19+
var x: Int
20+
}
21+
""",
2722
expected: """
28-
// Comment 1
29-
public indirect enum DependencyGraphNode {
30-
internal case userDefined(dependencies: [DependencyGraphNode])
31-
// Comment 2
32-
case synthesized(dependencies: [DependencyGraphNode])
33-
case other(dependencies: [DependencyGraphNode])
34-
var x: Int
35-
}
36-
public enum CompassPoint {
37-
case north
38-
indirect case south
39-
case east
40-
case west
41-
}
42-
"""
43-
)
23+
// Comment 1
24+
public indirect enum DependencyGraphNode {
25+
internal case userDefined(dependencies: [DependencyGraphNode])
26+
// Comment 2
27+
case synthesized(dependencies: [DependencyGraphNode])
28+
case other(dependencies: [DependencyGraphNode])
29+
var x: Int
30+
}
31+
""")
32+
}
33+
34+
public func testNotAllIndirectCases() {
35+
let input = """
36+
public enum CompassPoint {
37+
case north
38+
indirect case south
39+
case east
40+
case west
41+
}
42+
"""
43+
XCTAssertFormatting(FullyIndirectEnum.self, input: input, expected: input)
44+
}
45+
46+
public func testAlreadyIndirectEnum() {
47+
let input = """
48+
indirect enum CompassPoint {
49+
case north
50+
case south
51+
case east
52+
case west
53+
}
54+
"""
55+
XCTAssertFormatting(FullyIndirectEnum.self, input: input, expected: input)
56+
}
57+
58+
public func testCaselessEnum() {
59+
let input = """
60+
public enum Constants {
61+
public static let foo = 5
62+
public static let bar = "bar"
63+
}
64+
"""
65+
XCTAssertFormatting(FullyIndirectEnum.self, input: input, expected: input)
4466
}
4567
}

0 commit comments

Comments
 (0)