diff --git a/Rules.md b/Rules.md index 03b013cd5..bf6c62559 100644 --- a/Rules.md +++ b/Rules.md @@ -12,6 +12,7 @@ * [duplicateImports](#duplicateImports) * [elseOnSameLine](#elseOnSameLine) * [emptyBraces](#emptyBraces) +* [enumNamespaces](#enumNamespaces) * [fileHeader](#fileHeader) * [hoistPatternLet](#hoistPatternLet) * [indent](#indent) @@ -433,6 +434,10 @@ Remove whitespace inside empty braces.
+## enumNamespaces + +Converts types used for hosting only static members into enums. + ## fileHeader Use specified source file header template for all files. diff --git a/Snapshots/Layout/Layout/LayoutConsole.swift b/Snapshots/Layout/Layout/LayoutConsole.swift index 8703c5357..05d248ce5 100755 --- a/Snapshots/Layout/Layout/LayoutConsole.swift +++ b/Snapshots/Layout/Layout/LayoutConsole.swift @@ -4,7 +4,7 @@ import Foundation import UIKit /// Singleton for managing the Layout debug console interface -public struct LayoutConsole { +public enum LayoutConsole { private static var errorView = LayoutErrorView() private static var warningView = LayoutWarningView() diff --git a/Snapshots/Layout/Layout/LayoutManaged.swift b/Snapshots/Layout/Layout/LayoutManaged.swift index 3ebd2e2ad..14e460009 100755 --- a/Snapshots/Layout/Layout/LayoutManaged.swift +++ b/Snapshots/Layout/Layout/LayoutManaged.swift @@ -3,7 +3,7 @@ import Foundation /// A protocol for view-type classes that can be configured using Layout -@objc protocol LayoutConfigurable: class { +@objc protocol LayoutConfigurable: enum { /// Expression names and types @objc static var expressionTypes: [String: RuntimeType] { get } } diff --git a/Snapshots/Layout/Layout/Utilities.swift b/Snapshots/Layout/Layout/Utilities.swift index 48b1bd467..fec06a942 100755 --- a/Snapshots/Layout/Layout/Utilities.swift +++ b/Snapshots/Layout/Layout/Utilities.swift @@ -140,11 +140,11 @@ struct UIntOptionSet: OptionSet { #if !swift(>=4) extension NSAttributedString { - struct DocumentType { + enum DocumentType { static let html = NSHTMLTextDocumentType } - struct DocumentReadingOptionKey { + enum DocumentReadingOptionKey { static let documentType = NSDocumentTypeDocumentAttribute static let characterEncoding = NSCharacterEncodingDocumentAttribute } @@ -169,7 +169,7 @@ struct UIntOptionSet: OptionSet { } extension UIFontDescriptor { - struct AttributeName { + enum AttributeName { static let traits = UIFontDescriptorTraitsAttribute } diff --git a/Sources/Rules.swift b/Sources/Rules.swift index d2698cdfa..484149e71 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -716,6 +716,67 @@ public struct _FormatRules { } } + // Converts types used for hosting only static members into enums to avoid instantiation. + public let enumNamespaces = FormatRule( + help: "Converts types used for hosting only static members into enums.", + options: [] + ) { formatter in + func rangeHostsOnlyStaticMembersAtTopLevel(start startIndex: Int, end endIndex: Int) -> Bool { + // exit for empty declarations + guard formatter.next(.nonSpaceOrCommentOrLinebreak, in: startIndex ..< endIndex) != nil else { return false } + + var j = startIndex + while j < endIndex, let token = formatter.token(at: j) { + if token == .startOfScope("{"), let skip = formatter.index(of: .endOfScope, + after: j, + if: { $0 == .endOfScope("}") }) + { + j = skip + continue + } + // exit if there's a explicit init + if token == .keyword("init") { + return false + } else if [.keyword("let"), + .keyword("var"), + .keyword("func")].contains(token), + !formatter.modifiersForType(at: j, contains: "static") + { + return false + } + j += 1 + } + return true + } + + formatter.forEachToken(where: { $0 == .keyword("class") || $0 == .keyword("struct") }) { i, _ in + guard formatter.last(.keyword, before: i) != .keyword("import") else { return } + // exit if class is a type modifier + guard let next = formatter.next(.nonSpaceOrCommentOrLinebreak, after: i), !next.isKeyword else { return } + + // exit for class as protocol conformance + guard formatter.last(.nonSpaceOrCommentOrLinebreak, before: i) != .delimiter(":") else { return } + + guard let braceIndex = formatter.index(after: i, where: { $0 == .startOfScope("{") }) else { return } + + // exit if type is conforming any types + guard !formatter.tokens[i ... braceIndex].contains(.delimiter(":")) else { return } + + guard let endIndex = formatter.index(after: braceIndex, where: { $0 == .endOfScope("}") }) else { return } + + if rangeHostsOnlyStaticMembersAtTopLevel(start: braceIndex + 1, end: endIndex) { + formatter.replaceToken(at: i, with: [.keyword("enum")]) + + let start = formatter.startOfModifiers(at: i) + if formatter.modifiersForType(at: i, contains: "final"), + let finalIndex = formatter.lastIndex(in: start ..< i, where: { $0 == .identifier("final") }) + { + formatter.removeTokens(in: finalIndex ... finalIndex + 1) + } + } + } + } + /// Remove trailing space from the end of lines, as it has no semantic /// meaning and leads to noise in commits. public let trailingSpace = FormatRule( diff --git a/Tests/RulesTests+Braces.swift b/Tests/RulesTests+Braces.swift index 4fc54f12f..09a85a8a6 100644 --- a/Tests/RulesTests+Braces.swift +++ b/Tests/RulesTests+Braces.swift @@ -336,7 +336,11 @@ extension RulesTests { } """ let options = FormatOptions(allmanBraces: true) - testFormatting(for: input, output, rule: FormatRules.braces, options: options) + testFormatting( + for: input, output, + rule: FormatRules.braces, + options: options + ) } func testAllmanBracesForInit() { diff --git a/Tests/RulesTests+Organization.swift b/Tests/RulesTests+Organization.swift index 10544f04c..cd8c2aeec 100644 --- a/Tests/RulesTests+Organization.swift +++ b/Tests/RulesTests+Organization.swift @@ -118,7 +118,7 @@ extension RulesTests { testFormatting( for: input, output, rule: FormatRules.organizeDeclarations, - exclude: ["blankLinesAtStartOfScope"] + exclude: ["blankLinesAtStartOfScope", "enumNamespaces"] ) } diff --git a/Tests/RulesTests+Redundancy.swift b/Tests/RulesTests+Redundancy.swift index f5df5f052..fb868d819 100644 --- a/Tests/RulesTests+Redundancy.swift +++ b/Tests/RulesTests+Redundancy.swift @@ -1425,7 +1425,7 @@ extension RulesTests { func testNoRemoveBackticksAroundTypeInsideType() { let input = "struct Foo {\n enum `Type` {}\n}" - testFormatting(for: input, rule: FormatRules.redundantBackticks) + testFormatting(for: input, rule: FormatRules.redundantBackticks, exclude: ["enumNamespaces"]) } func testNoRemoveBackticksAroundLetArgument() { @@ -1452,7 +1452,7 @@ extension RulesTests { func testNoRemoveBackticksAroundTypePropertyInsideType() { let input = "struct Foo {\n enum `Type` {}\n}" - testFormatting(for: input, rule: FormatRules.redundantBackticks) + testFormatting(for: input, rule: FormatRules.redundantBackticks, exclude: ["enumNamespaces"]) } func testNoRemoveBackticksAroundTrueProperty() { @@ -1855,7 +1855,7 @@ extension RulesTests { func testRemoveSelfInStaticFunction() { let input = "struct Foo {\n static func foo() {\n func bar() { self.foo() }\n }\n}" let output = "struct Foo {\n static func foo() {\n func bar() { foo() }\n }\n}" - testFormatting(for: input, output, rule: FormatRules.redundantSelf) + testFormatting(for: input, output, rule: FormatRules.redundantSelf, exclude: ["enumNamespaces"]) } func testRemoveSelfInClassFunctionWithModifiers() { diff --git a/Tests/RulesTests+Wrapping.swift b/Tests/RulesTests+Wrapping.swift index 93bc66efb..2ae3f1b23 100644 --- a/Tests/RulesTests+Wrapping.swift +++ b/Tests/RulesTests+Wrapping.swift @@ -2229,7 +2229,12 @@ extension RulesTests { class Foo {} """ let options = FormatOptions(typeAttributes: .prevLine) - testFormatting(for: input, output, rule: FormatRules.wrapAttributes, options: options) + testFormatting( + for: input, + output, + rule: FormatRules.wrapAttributes, + options: options + ) } func testTypeAttributeStaysWrapped() { diff --git a/Tests/RulesTests.swift b/Tests/RulesTests.swift index 9fc181f94..9cd3999d4 100644 --- a/Tests/RulesTests.swift +++ b/Tests/RulesTests.swift @@ -1204,6 +1204,237 @@ class RulesTests: XCTestCase { testFormatting(for: input, output, rule: FormatRules.hoistPatternLet, options: options) } + // MARK: - enumNamespaces + + func testEnumNamespacesClassAsProtocolRestriction() { + let input = """ + @objc protocol Foo: class { + @objc static var expressionTypes: [String: RuntimeType] { get } + } + """ + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesConformingOtherType() { + let input = "private final class CustomUITableViewCell: UITableViewCell {}" + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesImportClass() { + let input = "import class MyUIKit.AutoHeightTableView" + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesImportStruct() { + let input = "import struct Core.CurrencyFormatter" + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesClassFunction() { + let input = """ + class Container { + class func bar() {} + } + """ + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesRemovingExtraKeywords() { + let input = """ + final class MyNamespace { + static let bar = "bar" + } + """ + let output = """ + enum MyNamespace { + static let bar = "bar" + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesNestedTypes() { + let input = """ + enum Namespace {} + extension Namespace { + struct Constants { + static let bar = "bar" + } + } + """ + let output = """ + enum Namespace {} + extension Namespace { + enum Constants { + static let bar = "bar" + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesNestedTypes2() { + let input = """ + struct Namespace { + struct NestedNamespace { + static let foo: Int + static let bar: Int + } + } + """ + let output = """ + enum Namespace { + enum NestedNamespace { + static let foo: Int + static let bar: Int + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesNestedTypes3() { + let input = """ + struct Namespace { + struct TypeNestedInNamespace { + let foo: Int + let bar: Int + } + } + """ + let output = """ + enum Namespace { + struct TypeNestedInNamespace { + let foo: Int + let bar: Int + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesNestedTypes4() { + let input = """ + struct Namespace { + static func staticFunction() { + struct NestedType { + init() {} + } + } + } + """ + let output = """ + enum Namespace { + static func staticFunction() { + struct NestedType { + init() {} + } + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesNestedTypes5() { + let input = """ + struct Namespace { + static func staticFunction() { + func nestedFunction() { /* ... */ } + } + } + """ + let output = """ + enum Namespace { + static func staticFunction() { + func nestedFunction() { /* ... */ } + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesStaticVariable() { + let input = """ + struct Constants { + static let β = 0, 5 + } + """ + let output = """ + enum Constants { + static let β = 0, 5 + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesStaticAndInstanceVariable() { + let input = """ + struct Constants { + static let β = 0, 5 + let Ɣ = 0, 3 + } + """ + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesStaticFunction() { + let input = """ + struct Constants { + static func remoteConfig() -> Int { + return 10 + } + } + """ + let output = """ + enum Constants { + static func remoteConfig() -> Int { + return 10 + } + } + """ + testFormatting(for: input, output, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespacesStaticAndInstanceFunction() { + let input = """ + struct Constants { + static func remoteConfig() -> Int { + return 10 + } + + func instanceConfig(offset: Int) -> Int { + return offset + 10 + } + } + """ + + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespaceDoesNothing() { + let input = """ + struct Foo { + #if BAR + func something() {} + #else + func something() {} + #endif + } + """ + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + func testEnumNamespaceDoesNothingEmptyDeclaration() { + let input = """ + class Foo {} + """ + testFormatting(for: input, rule: FormatRules.enumNamespaces) + } + + // MARK: - trailingSpace + + // truncateBlankLines = true + func testUnhoistSingleCaseLet() { let input = "if case let .foo(bar) = quux {}" let output = "if case .foo(let bar) = quux {}" diff --git a/Tests/XCTestManifests.swift b/Tests/XCTestManifests.swift index 34637e627..5619ea451 100644 --- a/Tests/XCTestManifests.swift +++ b/Tests/XCTestManifests.swift @@ -701,6 +701,23 @@ extension RulesTests { ("testEnumCaseSplitOverMultipleLines", testEnumCaseSplitOverMultipleLines), ("testEnumCaseWrappedIfWithXcodeStyle", testEnumCaseWrappedIfWithXcodeStyle), ("testEnumIfCaseEndifIndenting", testEnumIfCaseEndifIndenting), + ("testEnumNamespaceDoesNothing", testEnumNamespaceDoesNothing), + ("testEnumNamespaceDoesNothingEmptyDeclaration", testEnumNamespaceDoesNothingEmptyDeclaration), + ("testEnumNamespacesClassAsProtocolRestriction", testEnumNamespacesClassAsProtocolRestriction), + ("testEnumNamespacesClassFunction", testEnumNamespacesClassFunction), + ("testEnumNamespacesConformingOtherType", testEnumNamespacesConformingOtherType), + ("testEnumNamespacesImportClass", testEnumNamespacesImportClass), + ("testEnumNamespacesImportStruct", testEnumNamespacesImportStruct), + ("testEnumNamespacesNestedTypes", testEnumNamespacesNestedTypes), + ("testEnumNamespacesNestedTypes2", testEnumNamespacesNestedTypes2), + ("testEnumNamespacesNestedTypes3", testEnumNamespacesNestedTypes3), + ("testEnumNamespacesNestedTypes4", testEnumNamespacesNestedTypes4), + ("testEnumNamespacesNestedTypes5", testEnumNamespacesNestedTypes5), + ("testEnumNamespacesRemovingExtraKeywords", testEnumNamespacesRemovingExtraKeywords), + ("testEnumNamespacesStaticAndInstanceFunction", testEnumNamespacesStaticAndInstanceFunction), + ("testEnumNamespacesStaticAndInstanceVariable", testEnumNamespacesStaticAndInstanceVariable), + ("testEnumNamespacesStaticFunction", testEnumNamespacesStaticFunction), + ("testEnumNamespacesStaticVariable", testEnumNamespacesStaticVariable), ("testExponentialGrouping", testExponentialGrouping), ("testExpressionCountEqualsZero", testExpressionCountEqualsZero), ("testExpressionCountNotEqualToZero", testExpressionCountNotEqualToZero),