Skip to content

Commit 7cee6d3

Browse files
authored
Merge pull request swiftlang#15 from allevato/sr-11109
Preserve order of case elements in OneCasePerLine.
2 parents 2ce1c25 + 723445b commit 7cee6d3

File tree

4 files changed

+210
-100
lines changed

4 files changed

+210
-100
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,9 @@ private final class TokenStreamCreator: SyntaxVisitor {
834834

835835
func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
836836
before(node.firstToken, tokens: .open)
837+
838+
arrangeAttributeList(node.attributes)
839+
837840
after(node.caseKeyword, tokens: .break)
838841
after(node.lastToken, tokens: .close)
839842
return .visitChildren

Sources/SwiftFormatRules/OneCasePerLine.swift

Lines changed: 98 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,98 +14,125 @@ import Foundation
1414
import SwiftFormatCore
1515
import SwiftSyntax
1616

17-
/// Each enum case with associated values should appear on its own line.
17+
/// Each enum case with associated values or a raw value should appear in its own case declaration.
1818
///
1919
/// Lint: If a single `case` declaration declares multiple cases, and any of them have associated
20-
/// values, a lint error is raised.
20+
/// values or raw values, a lint error is raised.
2121
///
22-
/// Format: All case declarations with associated values will be moved to a new line.
22+
/// Format: All case declarations with associated values or raw values will be moved to their own
23+
/// case declarations.
2324
///
2425
/// - SeeAlso: https://google.github.io/swift#enum-cases
2526
public final class OneCasePerLine: SyntaxFormatRule {
2627

28+
/// A state machine that collects case elements encountered during visitation and allows new case
29+
/// declarations to be created with those elements.
30+
private struct CaseElementCollector {
31+
32+
/// The case declaration used as the source from which additional new declarations will be
33+
/// created; thus, all new cases will share the same attributes and modifiers as the basis.
34+
public private(set) var basis: EnumCaseDeclSyntax
35+
36+
/// Case elements collected so far.
37+
private var elements = [EnumCaseElementSyntax]()
38+
39+
/// Indicates whether the full leading trivia of basis case declaration should be preserved by
40+
/// the next case declaration that will be created by copying the basis declaration.
41+
///
42+
/// This is true for the first case (to preserve any leading comments on the original case
43+
/// declaration) and false for all subsequent cases (so that we don't repeat those comments).
44+
private var shouldKeepLeadingTrivia = true
45+
46+
/// Creates a new case element collector based on the given case declaration.
47+
init(basedOn basis: EnumCaseDeclSyntax) {
48+
self.basis = basis
49+
}
50+
51+
/// Adds a new case element to the collector.
52+
mutating func addElement(_ element: EnumCaseElementSyntax) {
53+
elements.append(element)
54+
}
55+
56+
/// Creates a new case declaration with the elements collected so far, then resets the internal
57+
/// state to start a new empty declaration again.
58+
///
59+
/// This will return nil if there are no elements collected since the last time this was called
60+
/// (or the collector was created).
61+
mutating func makeCaseDeclAndReset() -> EnumCaseDeclSyntax? {
62+
guard let last = elements.last else { return nil }
63+
64+
// Remove the trailing comma on the final element, if there was one.
65+
if last.trailingComma != nil {
66+
elements[elements.count - 1] = last.withTrailingComma(nil)
67+
}
68+
69+
defer { elements.removeAll() }
70+
return makeCaseDeclFromBasis(elements: elements)
71+
}
72+
73+
/// Creates and returns a new `EnumCaseDeclSyntax` with the given elements, based on the current
74+
/// basis declaration, and updates the comment preserving state if needed.
75+
mutating func makeCaseDeclFromBasis(elements: [EnumCaseElementSyntax]) -> EnumCaseDeclSyntax {
76+
let caseDecl = basis.withElements(SyntaxFactory.makeEnumCaseElementList(elements))
77+
78+
if shouldKeepLeadingTrivia {
79+
shouldKeepLeadingTrivia = false
80+
81+
// We don't bother preserving any indentation because the pretty printer will fix that up.
82+
// All we need to do here is ensure that there is a newline.
83+
basis = basis.withLeadingTrivia(Trivia.newlines(1))
84+
}
85+
86+
return caseDecl
87+
}
88+
}
89+
2790
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
28-
let enumMembers = node.members.members
2991
var newMembers: [MemberDeclListItemSyntax] = []
30-
var newIndx = 0
31-
32-
for member in enumMembers {
33-
var numNewMembers = 0
34-
if let caseMember = member.decl as? EnumCaseDeclSyntax {
35-
var otherDecl: EnumCaseDeclSyntax? = caseMember
36-
// Add and skip single element case declarations
37-
guard caseMember.elements.count > 1 else {
38-
newMembers.append(member.withDecl(caseMember))
39-
newIndx += 1
40-
continue
41-
}
42-
// Move all cases with associated/raw values to new declarations
43-
for element in caseMember.elements {
44-
if element.associatedValue != nil || element.rawValue != nil {
45-
diagnose(.moveAssociatedOrRawValueCase(name: element.identifier.text), on: element)
46-
let newRemovedDecl = createAssociateOrRawCaseDecl(
47-
fullDecl: caseMember,
48-
removedElement: element)
49-
otherDecl = removeAssociateOrRawCaseDecl(fullDecl: otherDecl)
50-
newMembers.append(member.withDecl(newRemovedDecl))
51-
numNewMembers += 1
52-
}
53-
}
54-
// Add case declaration of remaining elements without associated/raw values, if any
55-
if let otherDecl = otherDecl {
56-
newMembers.insert(member.withDecl(otherDecl), at: newIndx)
57-
newIndx += 1
58-
}
59-
// Add any member that isn't an enum case declaration
60-
} else {
92+
93+
for member in node.members.members {
94+
// If it's not a case declaration, or it's a case declaration with only one element, leave it
95+
// alone.
96+
guard let caseDecl = member.decl as? EnumCaseDeclSyntax, caseDecl.elements.count > 1 else {
6197
newMembers.append(member)
62-
newIndx += 1
98+
continue
6399
}
64-
newIndx += numNewMembers
65-
}
66100

67-
let newMemberBlock = SyntaxFactory.makeMemberDeclBlock(
68-
leftBrace: node.members.leftBrace,
69-
members: SyntaxFactory.makeMemberDeclList(newMembers),
70-
rightBrace: node.members.rightBrace)
71-
return node.withMembers(newMemberBlock)
72-
}
101+
var collector = CaseElementCollector(basedOn: caseDecl)
73102

74-
func createAssociateOrRawCaseDecl(
75-
fullDecl: EnumCaseDeclSyntax,
76-
removedElement: EnumCaseElementSyntax
77-
) -> EnumCaseDeclSyntax {
78-
let formattedElement = removedElement.withTrailingComma(nil)
79-
let newElementList = SyntaxFactory.makeEnumCaseElementList([formattedElement])
80-
let newDecl = SyntaxFactory.makeEnumCaseDecl(
81-
attributes: fullDecl.attributes,
82-
modifiers: fullDecl.modifiers,
83-
caseKeyword: fullDecl.caseKeyword,
84-
elements: newElementList)
85-
return newDecl
86-
}
103+
// Collect the elements of the case declaration until we see one that has either an associated
104+
// value or a raw value.
105+
for element in caseDecl.elements {
106+
if element.associatedValue != nil || element.rawValue != nil {
107+
// Once we reach one of these, we need to write out the ones we've collected so far, then
108+
// emit a separate case declaration with the associated/raw value element.
109+
diagnose(.moveAssociatedOrRawValueCase(name: element.identifier.text), on: element)
87110

88-
// Returns formatted declaration of cases without associated/raw values, or nil if all cases had
89-
// a raw or associate value
90-
func removeAssociateOrRawCaseDecl(fullDecl: EnumCaseDeclSyntax?) -> EnumCaseDeclSyntax? {
91-
guard let fullDecl = fullDecl else { return nil }
92-
var newList: [EnumCaseElementSyntax] = []
111+
if let caseDeclForCollectedElements = collector.makeCaseDeclAndReset() {
112+
newMembers.append(member.withDecl(caseDeclForCollectedElements))
113+
}
93114

94-
for element in fullDecl.elements {
95-
if element.associatedValue == nil && element.rawValue == nil { newList.append(element) }
96-
}
115+
let separatedCaseDecl =
116+
collector.makeCaseDeclFromBasis(elements: [element.withTrailingComma(nil)])
117+
newMembers.append(member.withDecl(separatedCaseDecl))
118+
} else {
119+
collector.addElement(element)
120+
}
121+
}
97122

98-
guard newList.count > 0 else { return nil }
99-
let (last, indx) = (newList[newList.count - 1], newList.count - 1)
100-
if last.trailingComma != nil {
101-
newList[indx] = last.withTrailingComma(nil)
123+
// Make sure to emit any trailing collected elements.
124+
if let caseDeclForCollectedElements = collector.makeCaseDeclAndReset() {
125+
newMembers.append(member.withDecl(caseDeclForCollectedElements))
126+
}
102127
}
103-
return fullDecl.withElements(SyntaxFactory.makeEnumCaseElementList(newList))
128+
129+
let newMemberBlock = node.members.withMembers(SyntaxFactory.makeMemberDeclList(newMembers))
130+
return node.withMembers(newMemberBlock)
104131
}
105132
}
106133

107134
extension Diagnostic.Message {
108135
static func moveAssociatedOrRawValueCase(name: String) -> Diagnostic.Message {
109-
return .init(.warning, "move \(name) case to a new line")
136+
return .init(.warning, "move '\(name)' to its own case declaration")
110137
}
111138
}

Tests/SwiftFormatRulesTests/OneCasePerLineTests.swift

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,112 @@ import XCTest
55
@testable import SwiftFormatRules
66

77
public class OneCasePerLineTests: DiagnosingTestCase {
8+
9+
// The inconsistent leading whitespace in the expected text is intentional. This transform does
10+
// not attempt to preserve leading indentation since the pretty printer will correct it when
11+
// running the full formatter.
12+
813
func testInvalidCasesOnLine() {
9-
XCTAssertFormatting(OneCasePerLine.self,
10-
input: """
11-
public enum Token {
12-
case arrow
13-
case comma, identifier(String), semicolon, stringSegment(String)
14-
case period
15-
case ifKeyword(String), forKeyword(String)
16-
indirect case guardKeyword, elseKeyword, contextualKeyword(String)
17-
var x: Bool
18-
case leftParen, rightParen = ")", leftBrace, rightBrace = "}"
19-
}
20-
""",
21-
expected: """
22-
public enum Token {
23-
case arrow
24-
case comma, semicolon
25-
case identifier(String)
26-
case stringSegment(String)
27-
case period
28-
case ifKeyword(String)
29-
case forKeyword(String)
30-
indirect case guardKeyword, elseKeyword
31-
indirect case contextualKeyword(String)
32-
var x: Bool
33-
case leftParen, leftBrace
34-
case rightParen = ")"
35-
case rightBrace = "}"
36-
}
37-
""")
14+
XCTAssertFormatting(
15+
OneCasePerLine.self,
16+
input:
17+
"""
18+
public enum Token {
19+
case arrow
20+
case comma, identifier(String), semicolon, stringSegment(String)
21+
case period
22+
case ifKeyword(String), forKeyword(String)
23+
indirect case guardKeyword, elseKeyword, contextualKeyword(String)
24+
var x: Bool
25+
case leftParen, rightParen = ")", leftBrace, rightBrace = "}"
26+
}
27+
""",
28+
expected:
29+
"""
30+
public enum Token {
31+
case arrow
32+
case comma
33+
case identifier(String)
34+
case semicolon
35+
case stringSegment(String)
36+
case period
37+
case ifKeyword(String)
38+
case forKeyword(String)
39+
indirect case guardKeyword, elseKeyword
40+
indirect case contextualKeyword(String)
41+
var x: Bool
42+
case leftParen
43+
case rightParen = ")"
44+
case leftBrace
45+
case rightBrace = "}"
46+
}
47+
""")
48+
}
49+
50+
func testElementOrderIsPreserved() {
51+
XCTAssertFormatting(
52+
OneCasePerLine.self,
53+
input:
54+
"""
55+
enum Foo: Int {
56+
case a = 0, b, c, d
57+
}
58+
""",
59+
expected:
60+
"""
61+
enum Foo: Int {
62+
case a = 0
63+
case b, c, d
64+
}
65+
""")
66+
}
67+
68+
func testCommentsAreNotRepeated() {
69+
XCTAssertFormatting(
70+
OneCasePerLine.self,
71+
input:
72+
"""
73+
enum Foo: Int {
74+
/// This should only be above `a`.
75+
case a = 0, b, c, d
76+
// This should only be above `e`.
77+
case e, f = 100
78+
}
79+
""",
80+
expected:
81+
"""
82+
enum Foo: Int {
83+
/// This should only be above `a`.
84+
case a = 0
85+
case b, c, d
86+
// This should only be above `e`.
87+
case e
88+
case f = 100
89+
}
90+
""")
91+
}
92+
93+
func testAttributesArePropagated() {
94+
XCTAssertFormatting(
95+
OneCasePerLine.self,
96+
input:
97+
"""
98+
enum Foo {
99+
@someAttr case a(String), b, c, d
100+
case e, f(Int)
101+
@anotherAttr case g, h(Float)
102+
}
103+
""",
104+
expected:
105+
"""
106+
enum Foo {
107+
@someAttr case a(String)
108+
@someAttr case b, c, d
109+
case e
110+
case f(Int)
111+
@anotherAttr case g
112+
@anotherAttr case h(Float)
113+
}
114+
""")
38115
}
39116
}

Tests/SwiftFormatRulesTests/XCTestManifests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ extension OneCasePerLineTests {
240240
// `swift test --generate-linuxmain`
241241
// to regenerate.
242242
static let __allTests__OneCasePerLineTests = [
243+
("testAttributesArePropagated", testAttributesArePropagated),
244+
("testCommentsAreNotRepeated", testCommentsAreNotRepeated),
245+
("testElementOrderIsPreserved", testElementOrderIsPreserved),
243246
("testInvalidCasesOnLine", testInvalidCasesOnLine),
244247
]
245248
}

0 commit comments

Comments
 (0)