Add --redundanttype infer-locals-only option

This commit is contained in:
Cal Stephens
2021-11-06 16:09:02 +00:00
committed by Nick Lockwood
parent 7c20b950c4
commit aba7db2db2
8 changed files with 300 additions and 36 deletions
+17 -1
View File
@@ -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"
<details>
<summary>Examples</summary>
```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()
}
}
```
</details>
+16
View File
@@ -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()
}
}
```
"""
+1 -1
View File
@@ -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(
+12
View File
@@ -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
+61
View File
@@ -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]()
+23
View File
@@ -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 {
+49
View File
@@ -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() {
+121 -34
View File
@@ -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<Int?> = BehaviourRelay<Int?>(value: nil)"
let output = "let relay = BehaviourRelay<Int?>(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<Void> = Array<Void>()"
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<Void> = Optional<Void>.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<Int?> = BehaviourRelay<Int?>(value: nil)"
let output = "let relay: BehaviourRelay<Int?> = .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