Skip to content

Commit c5c0695

Browse files
LaurenWhiteallevato
authored andcommitted
Implement FullyIndirectEnum rule and tests. (swiftlang#55)
1 parent e0ccb59 commit c5c0695

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

Sources/Rules/FullyIndirectEnum.swift

+93
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import Core
23
import Foundation
34
import SwiftSyntax
@@ -12,5 +13,97 @@ import SwiftSyntax
1213
///
1314
/// - SeeAlso: https://google.github.io/swift#enum-cases
1415
public final class FullyIndirectEnum: SyntaxFormatRule {
16+
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
17+
18+
let enumMembers = node.members.members
19+
guard allAreIndirectCases(members: enumMembers) else { return node }
20+
diagnose(.reassignIndirectKeyword(name: node.identifier.text), on: node.identifier)
21+
22+
// Removes 'indirect' keyword from cases, reformats
23+
var newMembers: [MemberDeclListItemSyntax] = []
24+
for member in enumMembers {
25+
if let caseMember = member.decl as? EnumCaseDeclSyntax {
26+
guard let caseModifiers = caseMember.modifiers else { continue }
27+
guard let firstModifier = caseModifiers.first else { continue }
28+
let newCase = caseMember.withModifiers(removeIndirectModifier(curModifiers: caseModifiers))
29+
let formattedCase = formatCase(unformattedCase: newCase,
30+
leadingTrivia: firstModifier.leadingTrivia)
31+
let newMember = SyntaxFactory.makeMemberDeclListItem(decl: formattedCase, semicolon: nil)
32+
newMembers.append(newMember)
33+
} else {
34+
newMembers.append(member)
35+
}
36+
}
37+
38+
let members = SyntaxFactory.makeMemberDeclList(newMembers)
39+
let newMemberBlock = SyntaxFactory.makeMemberDeclBlock(leftBrace: node.members.leftBrace,
40+
members: members,
41+
rightBrace: node.members.rightBrace)
42+
43+
// Format indirect keyword and following token, if necessary
44+
guard let firstTok = node.firstToken else { return node }
45+
var leadingTrivia: Trivia = []
46+
var newDecl = node
47+
if firstTok.tokenKind == .enumKeyword {
48+
leadingTrivia = firstTok.leadingTrivia
49+
newDecl = replaceTrivia(on: node,
50+
token: node.firstToken,
51+
leadingTrivia: []) as! EnumDeclSyntax
52+
}
53+
54+
let newModifier = SyntaxFactory.makeDeclModifier(
55+
name: SyntaxFactory.makeIdentifier("indirect",
56+
leadingTrivia: leadingTrivia,
57+
trailingTrivia: .spaces(1)),
58+
detailLeftParen: nil, detail: nil, detailRightParen: nil)
59+
60+
return newDecl.addModifier(newModifier).withMembers(newMemberBlock)
61+
}
62+
63+
// Determines if all given cases are indirect
64+
func allAreIndirectCases(members: MemberDeclListSyntax) -> Bool {
65+
for member in members {
66+
if let caseMember = member.decl as? EnumCaseDeclSyntax {
67+
guard let caseModifiers = caseMember.modifiers else { return false }
68+
if isIndirectCase(modifiers: caseModifiers) { continue }
69+
else { return false }
70+
}
71+
}
72+
return true
73+
}
74+
75+
func isIndirectCase(modifiers: ModifierListSyntax) -> Bool {
76+
for modifier in modifiers {
77+
if modifier.name.tokenKind == .identifier("indirect") { return true }
78+
}
79+
return false
80+
}
81+
82+
func removeIndirectModifier(curModifiers: ModifierListSyntax) -> ModifierListSyntax {
83+
var newMods: [DeclModifierSyntax] = []
84+
for modifier in curModifiers {
85+
if modifier.name.tokenKind != .identifier("indirect") { newMods.append(modifier) }
86+
}
87+
return SyntaxFactory.makeModifierList(newMods)
88+
}
89+
90+
// Transfers given leading trivia to the first token in the case declaration
91+
func formatCase(unformattedCase: EnumCaseDeclSyntax,
92+
leadingTrivia: Trivia?) -> EnumCaseDeclSyntax {
93+
if let modifiers = unformattedCase.modifiers, let first = modifiers.first {
94+
return replaceTrivia(on: unformattedCase,
95+
token: first.firstToken,
96+
leadingTrivia: leadingTrivia) as! EnumCaseDeclSyntax
97+
} else {
98+
return replaceTrivia(on: unformattedCase,
99+
token: unformattedCase.caseKeyword,
100+
leadingTrivia: leadingTrivia) as! EnumCaseDeclSyntax
101+
}
102+
}
103+
}
15104

105+
extension Diagnostic.Message {
106+
static func reassignIndirectKeyword(name: String) -> Diagnostic.Message {
107+
return .init(.warning, "move 'indirect' to \(name) enum declaration")
108+
}
16109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class FullyIndirectEnumTests: DiagnosingTestCase {
8+
public func testIndirectEnumReassignment() {
9+
XCTAssertFormatting(
10+
FullyIndirectEnum.self,
11+
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+
""",
27+
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+
)
44+
}
45+
}

0 commit comments

Comments
 (0)