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