Skip to content

Commit cf161bd

Browse files
committed
Don't try to convert classes that look like namespaces to enums.
The class could potentially be used as an `AnyClass` argument to an API like `Bundle(for:)`, so converting it to a value type breaks that. Converting structs to enums should still be safe, however. I'm not convinced the rule holds its weight if it only handles structs, but I decided that leaving it here is better than removing it, which might cause someone to try to implement it again in the future and run into the same issue. Fixes SR-11111.
1 parent 5652419 commit cf161bd

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)