diff --git a/Rules.md b/Rules.md
index 049de576..3258125e 100644
--- a/Rules.md
+++ b/Rules.md
@@ -1221,14 +1221,30 @@ Remove redundant type from variable declarations.
Option | Description
--- | ---
-`--redundanttype` | Keep "inferred" (default) or "explicit" type annotation
+`--redundanttype` | "inferred" (default), "explicit", or "infer-locals-only"
Examples
```diff
+// inferred
- let view: UIView = UIView()
+ let view = UIView()
+
+// explicit
+- let view: UIView = UIView()
++ let view: UIView = .init()
+
+// infer-locals-only
+ class Foo {
+- let view: UIView = UIView()
++ let view: UIView = .init()
+
+ func method() {
+- let view: UIView = UIView()
++ let view = UIView()
+ }
+ }
```
diff --git a/Sources/Examples.swift b/Sources/Examples.swift
index 25853e47..65e6718b 100644
--- a/Sources/Examples.swift
+++ b/Sources/Examples.swift
@@ -777,8 +777,24 @@ private struct Examples {
let redundantType = """
```diff
+ // inferred
- let view: UIView = UIView()
+ let view = UIView()
+
+ // explicit
+ - let view: UIView = UIView()
+ + let view: UIView = .init()
+
+ // infer-locals-only
+ class Foo {
+ - let view: UIView = UIView()
+ + let view: UIView = .init()
+
+ func method() {
+ - let view: UIView = UIView()
+ + let view = UIView()
+ }
+ }
```
"""
diff --git a/Sources/OptionDescriptor.swift b/Sources/OptionDescriptor.swift
index f6130ac6..cd181762 100644
--- a/Sources/OptionDescriptor.swift
+++ b/Sources/OptionDescriptor.swift
@@ -807,7 +807,7 @@ struct _Descriptors {
let redundantType = OptionDescriptor(
argumentName: "redundanttype",
displayName: "Redundant Type",
- help: "Keep \"inferred\" (default) or \"explicit\" type annotation",
+ help: "\"inferred\" (default), \"explicit\", or \"infer-locals-only\"",
keyPath: \.redundantType
)
let emptyBracesSpacing = OptionDescriptor(
diff --git a/Sources/Options.swift b/Sources/Options.swift
index cd84991a..a11d59f2 100644
--- a/Sources/Options.swift
+++ b/Sources/Options.swift
@@ -112,8 +112,20 @@ public enum WrapReturnType: String, CaseIterable {
/// Annotation which should be kept when removing a redundant type
public enum RedundantType: String, CaseIterable {
+ /// Preserves the type as a part of the property definition:
+ /// `let foo: Foo = Foo()` becomes `let foo: Foo = .init()`
case explicit
+
+ /// Uses type inference to omit the type in the property definition:
+ /// `let foo: Foo = Foo()` becomes `let foo = Foo()`
case inferred
+
+ /// Uses `.inferred` for properties within local scopes (method bodies, etc.),
+ /// but `.explicit` for globals and properties within types.
+ /// - This is because type checking for globals and type properties
+ /// using inferred types can be more expensive.
+ /// https://twitter.com/uint_min/status/1441448033988722691?s=21
+ case inferLocalsOnly = "infer-locals-only"
}
/// Argument type for empty brace spacing behavior
diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift
index dcff20f2..f449bd21 100644
--- a/Sources/ParsingHelpers.swift
+++ b/Sources/ParsingHelpers.swift
@@ -1441,6 +1441,67 @@ extension Formatter {
}
}
+ /// The type of scope that a declaration is contained within
+ enum DeclarationScope {
+ /// The declaration is a top-level global
+ case global
+
+ /// The declaration is a member of some type
+ case type
+
+ /// The declaration is within some local scope,
+ /// like a function body.
+ case local
+ }
+
+ /// The declaration scope (global, type, or local) that the
+ /// given token index is contained by.
+ func declarationScope(at i: Int) -> DeclarationScope {
+ /// Declarations which have `DeclarationScope.type`
+ let typeDeclararions = Set(["class", "struct", "enum", "actor", "extension"])
+
+ /// Declarations which have `DeclarationScope.local`
+ let localDeclararions = Set(["let", "var", "func", "subscript", "init", "deinit"])
+
+ let allDeclarationScopes = typeDeclararions.union(localDeclararions)
+
+ // back track through tokens until we find a startOfScope("{") that isDeclarationTypeKeyword
+ // - we have to skip scopes that sit between this token and the its actual start of scope,
+ // so we have to keep track of the number of unpaired end scope tokens we have encountered
+ var unpairedEndScopeCount = 0
+ var currentIndex = i
+ var startOfScope: Int?
+
+ while startOfScope == nil, currentIndex > 0 {
+ currentIndex -= 1
+
+ if tokens[currentIndex] == .endOfScope("}") {
+ unpairedEndScopeCount += 1
+ } else if tokens[currentIndex] == .startOfScope("{") {
+ if unpairedEndScopeCount == 0 {
+ startOfScope = currentIndex
+ } else {
+ unpairedEndScopeCount -= 1
+ }
+ }
+ }
+
+ // If this declaration isn't within any scope,
+ // it must be a global.
+ guard
+ let startOfScopeIndex = startOfScope,
+ let declarationTypeKeyword = lastToken(before: startOfScopeIndex, where: { allDeclarationScopes.contains($0.string) })
+ else {
+ return .global
+ }
+
+ if typeDeclararions.contains(declarationTypeKeyword.string) {
+ return .type
+ } else {
+ return .local
+ }
+ }
+
// Swift modifier keywords, in preferred order
var modifierOrder: [String] {
var priorities = [String: Int]()
diff --git a/Sources/Rules.swift b/Sources/Rules.swift
index 1cc9a03e..d9082eb5 100644
--- a/Sources/Rules.swift
+++ b/Sources/Rules.swift
@@ -682,7 +682,30 @@ public struct _FormatRules {
return
}
+ /// The implementation of RedundantType uses inferred or explicit,
+ /// potentially depending on the context.
+ enum RedundantTypeImplementation {
+ case inferred
+ case explicit
+ }
+
+ let implementation: RedundantTypeImplementation
+
switch formatter.options.redundantType {
+ case .inferred:
+ implementation = .inferred
+ case .explicit:
+ implementation = .explicit
+ case .inferLocalsOnly:
+ switch formatter.declarationScope(at: i) {
+ case .global, .type:
+ implementation = .explicit
+ case .local:
+ implementation = .inferred
+ }
+ }
+
+ switch implementation {
case .inferred:
formatter.removeTokens(in: colonIndex ... typeEndIndex)
if formatter.tokens[colonIndex - 1].isSpace {
diff --git a/Tests/ParsingHelpersTests.swift b/Tests/ParsingHelpersTests.swift
index 73687bbe..6b56e2b3 100644
--- a/Tests/ParsingHelpersTests.swift
+++ b/Tests/ParsingHelpersTests.swift
@@ -1416,6 +1416,55 @@ class ParsingHelpersTests: XCTestCase {
_ = Formatter(tokens).parseDeclarations()
}
+ // MARK: declarationScope
+
+ func testDeclarationScope_classAndGlobals() {
+ let input = """
+ let foo = Foo()
+
+ class Foo {
+ let instanceMember = Bar()
+ }
+
+ let bar = Bar()
+ """
+
+ let tokens = tokenize(input)
+ let formatter = Formatter(tokens)
+
+ XCTAssertEqual(formatter.declarationScope(at: 3), .global) // foo
+ XCTAssertEqual(formatter.declarationScope(at: 20), .type) // instanceMember
+ XCTAssertEqual(formatter.declarationScope(at: 33), .global) // bar
+ }
+
+ func testDeclarationScope_classAndLocal() {
+ let input = """
+ class Foo {
+ let instanceMember1 = Bar()
+
+ var instanceMember2: Bar = {
+ Bar()
+ }
+
+ func instanceMethod() {
+ let localMember1 = Bar()
+ }
+
+ let instanceMember3 = Bar()
+ }
+ """
+
+ let tokens = tokenize(input)
+ let formatter = Formatter(tokens)
+
+ XCTAssertEqual(formatter.declarationScope(at: 9), .type) // instanceMember1
+ XCTAssertEqual(formatter.declarationScope(at: 21), .type) // instanceMember2
+ XCTAssertEqual(formatter.declarationScope(at: 31), .local) // Bar()
+ XCTAssertEqual(formatter.declarationScope(at: 42), .type) // instanceMethod
+ XCTAssertEqual(formatter.declarationScope(at: 51), .local) // localMember1
+ XCTAssertEqual(formatter.declarationScope(at: 66), .type) // instanceMember3
+ }
+
// MARK: spaceEquivalentToWidth
func testSpaceEquivalentToWidth() {
diff --git a/Tests/RulesTests+Redundancy.swift b/Tests/RulesTests+Redundancy.swift
index 472c4053..bf7b1ec6 100644
--- a/Tests/RulesTests+Redundancy.swift
+++ b/Tests/RulesTests+Redundancy.swift
@@ -926,46 +926,59 @@ class RedundancyTests: RulesTests {
func testVarRedundantTypeRemoval() {
let input = "var view: UIView = UIView()"
let output = "var view = UIView()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testVarRedundantArrayTypeRemoval() {
let input = "var foo: [String] = [String]()"
let output = "var foo = [String]()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testVarRedundantDictionaryTypeRemoval() {
let input = "var foo: [String: Int] = [String: Int]()"
let output = "var foo = [String: Int]()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testLetRedundantGenericTypeRemoval() {
let input = "let relay: BehaviourRelay = BehaviourRelay(value: nil)"
let output = "let relay = BehaviourRelay(value: nil)"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testVarNonRedundantTypeDoesNothing() {
let input = "var view: UIView = UINavigationBar()"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testLetRedundantTypeRemoval() {
let input = "let view: UIView = UIView()"
let output = "let view = UIView()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testLetNonRedundantTypeDoesNothing() {
let input = "let view: UIView = UINavigationBar()"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testTypeNoRedundancyDoesNothing() {
let input = "let foo: Bar = 5"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testClassTwoVariablesNoRedundantTypeDoesNothing() {
@@ -975,7 +988,8 @@ class RedundancyTests: RulesTests {
var timeoutIntervalForRequest: TimeInterval = LGCoreKitConstants.websocketTimeOutTimeInterval
}
"""
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testRedundantTypeRemovedIfValueOnNextLine() {
@@ -987,7 +1001,9 @@ class RedundancyTests: RulesTests {
let view
= UIView()
"""
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovedIfValueOnNextLine2() {
@@ -999,24 +1015,31 @@ class RedundancyTests: RulesTests {
let view =
UIView()
"""
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithComment() {
let input = "var view: UIView /* view */ = UIView()"
let output = "var view /* view */ = UIView()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithComment2() {
let input = "var view: UIView = /* view */ UIView()"
let output = "var view = /* view */ UIView()"
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testNonRedundantTernaryConditionTypeNotRemoved() {
let input = "let foo: Bar = Bar.baz() ? .bar1 : .bar2"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testTernaryConditionAfterLetNotTreatedAsPartOfExpression() {
@@ -1028,41 +1051,49 @@ class RedundancyTests: RulesTests {
let foo = Bar.baz()
baz ? bar2() : bar2()
"""
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testNoRemoveRedundantTypeIfVoid() {
let input = "let foo: Void = Void()"
+ let options = FormatOptions(redundantType: .inferred)
testFormatting(for: input, rule: FormatRules.redundantType,
- exclude: ["void"])
+ options: options, exclude: ["void"])
}
func testNoRemoveRedundantTypeIfVoid2() {
let input = "let foo: () = ()"
+ let options = FormatOptions(redundantType: .inferred)
testFormatting(for: input, rule: FormatRules.redundantType,
- exclude: ["void"])
+ options: options, exclude: ["void"])
}
func testNoRemoveRedundantTypeIfVoid3() {
let input = "let foo: [Void] = [Void]()"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testNoRemoveRedundantTypeIfVoid4() {
let input = "let foo: Array = Array()"
+ let options = FormatOptions(redundantType: .inferred)
testFormatting(for: input, rule: FormatRules.redundantType,
- exclude: ["typeSugar"])
+ options: options, exclude: ["typeSugar"])
}
func testNoRemoveRedundantTypeIfVoid5() {
let input = "let foo: Void? = Void?.none"
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
func testNoRemoveRedundantTypeIfVoid6() {
let input = "let foo: Optional = Optional.none"
+ let options = FormatOptions(redundantType: .inferred)
testFormatting(for: input, rule: FormatRules.redundantType,
- exclude: ["typeSugar"])
+ options: options, exclude: ["typeSugar"])
}
func testRedundantTypeWithLiterals() {
@@ -1104,7 +1135,9 @@ class RedundancyTests: RulesTests {
let f1 = ["foo": 5]
let f2: [String: Int?] = ["foo": nil]
"""
- testFormatting(for: input, output, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypePreservesLiteralRepresentableTypes() {
@@ -1116,7 +1149,8 @@ class RedundancyTests: RulesTests {
let e: MyArrayRepresentable = ["bar"]
let f: MyDictionaryRepresentable = ["baz": 1]
"""
- testFormatting(for: input, rule: FormatRules.redundantType)
+ let options = FormatOptions(redundantType: .inferred)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options)
}
// --redundanttype explicit
@@ -1125,14 +1159,16 @@ class RedundancyTests: RulesTests {
let input = "var view: UIView = UIView()"
let output = "var view: UIView = .init()"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testLetRedundantGenericTypeRemovalExplicitType() {
let input = "let relay: BehaviourRelay = BehaviourRelay(value: nil)"
let output = "let relay: BehaviourRelay = .init(value: nil)"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testVarNonRedundantTypeDoesNothingExplicitType() {
@@ -1145,7 +1181,8 @@ class RedundancyTests: RulesTests {
let input = "let view: UIView = UIView()"
let output = "let view: UIView = .init()"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovedIfValueOnNextLineExplicitType() {
@@ -1158,7 +1195,8 @@ class RedundancyTests: RulesTests {
= .init()
"""
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovedIfValueOnNextLine2ExplicitType() {
@@ -1171,21 +1209,24 @@ class RedundancyTests: RulesTests {
.init()
"""
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithCommentExplicitType() {
let input = "var view: UIView /* view */ = UIView()"
let output = "var view: UIView /* view */ = .init()"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithComment2ExplicitType() {
let input = "var view: UIView = /* view */ UIView()"
let output = "var view: UIView = /* view */ .init()"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithStaticMember() {
@@ -1206,7 +1247,8 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeRemovalWithStaticFunc() {
@@ -1227,7 +1269,8 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeDoesNothingWithStaticMemberMakingCopy() {
@@ -1264,14 +1307,58 @@ class RedundancyTests: RulesTests {
let foo: Foo = .init()
"""
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
func testRedundantTypeIfVoid() {
let input = "let foo: [Void] = [Void]()"
let output = "let foo: [Void] = .init()"
let options = FormatOptions(redundantType: .explicit)
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
+ }
+
+ // --redundanttype infer-locals-only
+
+ func testRedundantTypeinferLocalsOnly() {
+ let input = """
+ let globalFoo: Foo = Foo()
+
+ struct SomeType {
+ let instanceFoo: Foo = Foo()
+
+ func method() {
+ let localFoo: Foo = Foo()
+ let localString: String = "foo"
+ }
+
+ let instanceString: String = "foo"
+ }
+
+ let globalString: String = "foo"
+ """
+
+ let output = """
+ let globalFoo: Foo = .init()
+
+ struct SomeType {
+ let instanceFoo: Foo = .init()
+
+ func method() {
+ let localFoo = Foo()
+ let localString = "foo"
+ }
+
+ let instanceString: String = "foo"
+ }
+
+ let globalString: String = "foo"
+ """
+
+ let options = FormatOptions(redundantType: .inferLocalsOnly)
+ testFormatting(for: input, output, rule: FormatRules.redundantType,
+ options: options)
}
// MARK: - redundantNilInit