Skip to content

Commit 6c33035

Browse files
authored
Merge pull request swiftlang#18 from allevato/sr-11111
Don't try to convert classes that look like namespaces to enums.
2 parents 1f9186b + cf161bd commit 6c33035

File tree

2 files changed

+37
-88
lines changed

2 files changed

+37
-88
lines changed

Sources/SwiftFormatRules/UseEnumForNamespacing.swift

Lines changed: 34 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -16,122 +16,71 @@ import SwiftSyntax
1616

1717
/// Use caseless `enum`s for namespacing.
1818
///
19-
/// In practice, this means that any `class` or `struct` that consists of only `static let`s and
20-
/// `static func`s should be converted to an `enum`.
19+
/// In practice, this means that any `struct` that consists of only `static let`s and `static func`s
20+
/// should be converted to an `enum`.
2121
///
22-
/// Lint: `class`es or `struct`s consisting of only `static let/func`s will yield a lint error.
22+
/// This is **not** a safe transformation for `class` types because the user might pass the metatype
23+
/// as an `AnyClass` argument to an API, and changing the declaration to a non-`class` type breaks
24+
/// that (see https://bugs.swift.org/browse/SR-11111).
2325
///
24-
/// Format: Rewrite the `class` or `struct` as an `enum`.
25-
/// TODO(abl): This can get complicated to pattern-match correctly.
26-
// . TODO(b/78286392): Give this formatting pass a category that makes it not run on save.
26+
/// Lint: `struct`s consisting of only `static let/func`s will yield a lint error.
27+
///
28+
/// Format: Rewrite the `struct` as an `enum`.
2729
///
2830
/// - SeeAlso: https://google.github.io/swift#nesting-and-namespacing
2931
public final class UseEnumForNamespacing: SyntaxFormatRule {
32+
3033
public override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
31-
guard let newDecls = declsIfUsedAsNamespace(node.members.members),
32-
node.genericParameterClause == nil,
33-
node.inheritanceClause == nil
34+
guard node.genericParameterClause == nil, node.inheritanceClause == nil,
35+
let memberDecls = membersToKeepIfUsedAsNamespace(node.members.members)
3436
else {
3537
return node
3638
}
3739

3840
diagnose(.convertToEnum(kind: "struct", name: node.identifier), on: node)
3941

40-
return makeEnum(
41-
declarationKeyword: node.structKeyword,
42-
modifiers: node.modifiers,
43-
name: node.identifier,
44-
members: node.members.withMembers(newDecls)
45-
)
46-
}
47-
48-
public override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
49-
guard let newDecls = declsIfUsedAsNamespace(node.members.members),
50-
node.genericParameterClause == nil,
51-
node.inheritanceClause == nil
52-
else {
53-
return node
54-
}
55-
56-
diagnose(.convertToEnum(kind: "class", name: node.identifier), on: node)
57-
58-
return makeEnum(
59-
declarationKeyword: node.classKeyword,
60-
modifiers: node.modifiers,
61-
name: node.identifier,
62-
members: node.members.withMembers(newDecls)
63-
)
64-
}
65-
66-
func makeEnum(
67-
declarationKeyword: TokenSyntax,
68-
modifiers: ModifierListSyntax?,
69-
name: TokenSyntax,
70-
members: MemberDeclBlockSyntax
71-
) -> EnumDeclSyntax {
72-
// Since we remove the "final" modifier, we need to preserve its trivia if it is the first
73-
// modifier.
74-
var newLeadingTrivia: Trivia? = nil
75-
if let firstMod = modifiers?.first, firstMod.name.text == "final" {
76-
newLeadingTrivia = firstMod.leadingTrivia
77-
}
78-
79-
let newModifiers = modifiers?.remove(name: "final")
80-
81-
let outputEnum = EnumDeclSyntax {
82-
if let mods = newModifiers {
83-
for mod in mods { $0.addModifier(mod) }
84-
}
85-
$0.useEnumKeyword(declarationKeyword.withKind(.enumKeyword))
86-
$0.useIdentifier(name)
87-
$0.useMembers(members)
88-
}
89-
90-
if let trivia = newLeadingTrivia {
91-
return replaceTrivia(
92-
on: outputEnum,
93-
token: outputEnum.firstToken,
94-
leadingTrivia: trivia
95-
) as! EnumDeclSyntax
96-
} else {
97-
return outputEnum
42+
return EnumDeclSyntax { builder in
43+
node.modifiers?.forEach { builder.addModifier($0) }
44+
builder.useEnumKeyword(node.structKeyword.withKind(.enumKeyword))
45+
builder.useIdentifier(node.identifier)
46+
builder.useMembers(node.members.withMembers(memberDecls))
9847
}
9948
}
10049

101-
/// Determines if the set of declarations is consistent with a class or struct being used
102-
/// solely as a namespace for static functions. If there is a non-static private initializer
103-
/// with no arguments, that does not count against possibly being a namespace.
104-
func declsIfUsedAsNamespace(_ members: MemberDeclListSyntax) -> MemberDeclListSyntax? {
50+
/// Returns the list of members that should be retained if all of them satisfy conditions that
51+
/// make them effectively a namespace.
52+
///
53+
/// If there is a non-static private initializer with no arguments, that does not count against
54+
/// possibly being a namespace, since the user probably added it to prevent instantiation.
55+
///
56+
/// If any of the members causes the type to disqualify as a namespace, this method returns nil.
57+
func membersToKeepIfUsedAsNamespace(_ members: MemberDeclListSyntax) -> MemberDeclListSyntax? {
10558
if members.count == 0 { return nil }
10659
var declList = [MemberDeclListItemSyntax]()
60+
10761
for member in members {
10862
switch member.decl {
10963
case let decl as FunctionDeclSyntax:
110-
guard let modifiers = decl.modifiers,
111-
modifiers.has(modifier: "static")
112-
else {
113-
return nil
114-
}
64+
guard let modifiers = decl.modifiers, modifiers.has(modifier: "static") else { return nil }
11565
declList.append(member)
66+
11667
case let decl as VariableDeclSyntax:
117-
guard let modifiers = decl.modifiers,
118-
modifiers.has(modifier: "static")
119-
else {
120-
return nil
121-
}
68+
guard let modifiers = decl.modifiers, modifiers.has(modifier: "static") else { return nil }
12269
declList.append(member)
70+
12371
case let decl as InitializerDeclSyntax:
124-
guard let modifiers = decl.modifiers,
125-
modifiers.has(modifier: "private"),
126-
decl.parameters.parameterList.count == 0
72+
guard let modifiers = decl.modifiers, modifiers.has(modifier: "private"),
73+
decl.parameters.parameterList.isEmpty
12774
else {
12875
return nil
12976
}
130-
// Do not append private initializer
77+
// Do not append private initializer.
78+
13179
default:
13280
declList.append(member)
13381
}
13482
}
83+
13584
return SyntaxFactory.makeMemberDeclList(declList)
13685
}
13786
}

Tests/SwiftFormatRulesTests/UseEnumForNamespacingTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ public class UseEnumForNamespacingTests: DiagnosingTestCase {
3737
static func foo() {}
3838
private init() {}
3939
}
40-
enum C {
40+
class C {
4141
static func foo() {}
4242
}
43-
public enum D {
43+
public final class D {
4444
static func bar()
4545
}
46-
enum E {
46+
final class E {
4747
static let a = 123
4848
}
4949
""")

0 commit comments

Comments
 (0)