diff --git a/Rules.md b/Rules.md index 692574b2..1d2627ab 100644 --- a/Rules.md +++ b/Rules.md @@ -2888,7 +2888,7 @@ Insert/remove explicit `self` where applicable. Option | Description --- | --- `--self` | Explicit self: "insert", "remove" (default) or "init-only" -`--self-required` | Comma-delimited list of functions with @autoclosure arguments +`--self-required` | Comma-delimited list of functions / types with @autoclosure arguments
Examples diff --git a/Sources/FormattingHelpers.swift b/Sources/FormattingHelpers.swift index 066daf6b..9d6cdc44 100644 --- a/Sources/FormattingHelpers.swift +++ b/Sources/FormattingHelpers.swift @@ -130,10 +130,59 @@ extension Formatter { } index = scopeStart } + if !staticSelf, + isAssignedToSelfRequiredType(at: index) + { + return false + } removeTokens(in: i ..< nextIndex) return true } + /// Whether the expression at the given index is on the RHS of an assignment + /// whose LHS has a type annotation matching a `selfRequired` type. + /// e.g. `let _: OSLogMessage = "\(self.bar)"` or `let _: OSLogMessage = foo(self.bar)` + func isAssignedToSelfRequiredType(at i: Int) -> Bool { + guard !options.selfRequired.isEmpty else { return false } + // Walk backwards from start of expression to find `=` operator + var index = i + while let prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index) { + if tokens[prevIndex] == .operator("=", .infix) { + return isSelfRequiredType(beforeAssignment: prevIndex) + } + switch tokens[prevIndex] { + case .identifier, .operator(".", .infix), + .keyword("try"), .keyword("await"), + .operator("?", .postfix), .operator("!", .postfix): + index = prevIndex + default: + return false + } + } + return false + } + + /// Whether the type annotation before an `=` operator is a `selfRequired` type. + func isSelfRequiredType(beforeAssignment equalsIndex: Int) -> Bool { + var typeIndex = equalsIndex + while let prevTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: typeIndex) { + if tokens[prevTypeIndex].isUnwrapOperator { + typeIndex = prevTypeIndex + } else if tokens[prevTypeIndex] == .endOfScope(">"), + let matchingStart = startOfScope(at: prevTypeIndex) + { + typeIndex = matchingStart + } else { + typeIndex = prevTypeIndex + break + } + } + guard tokens[typeIndex].isIdentifier else { + return false + } + return options.selfRequired.contains(tokens[typeIndex].unescaped()) + } + /// gather declared variable names, starting at index after let/var keyword func processDeclaredVariables(at index: inout Int, names: inout Set, removeSelfKeyword: String?, onlyLocal: Bool, diff --git a/Sources/OptionDescriptor.swift b/Sources/OptionDescriptor.swift index 14af6d19..cc77d4a4 100644 --- a/Sources/OptionDescriptor.swift +++ b/Sources/OptionDescriptor.swift @@ -838,7 +838,7 @@ struct _Descriptors { let selfRequired = OptionDescriptor( argumentName: "self-required", displayName: "Self Required", - help: "Comma-delimited list of functions with @autoclosure arguments", + help: "Comma-delimited list of functions / types with @autoclosure arguments", keyPath: \FormatOptions.selfRequired ) let throwCapturing = OptionDescriptor( diff --git a/Tests/Rules/RedundantSelfTests.swift b/Tests/Rules/RedundantSelfTests.swift index ec726726..148c3a83 100644 --- a/Tests/Rules/RedundantSelfTests.swift +++ b/Tests/Rules/RedundantSelfTests.swift @@ -879,6 +879,111 @@ final class RedundantSelfTests: XCTestCase { testFormatting(for: input, rule: .redundantSelf, options: options, exclude: [.propertyTypes]) } + func testNoRemoveSelfInStringInterpolationWithSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = NSObject() + func f() { + let _: OSLogMessage = "\\(self.bar)" + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + + func testNoRemoveSelfInStringInterpolationWithOptionalSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = NSObject() + func f() { + let x: OSLogMessage? = "\\(self.bar)" + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + + func testRemoveSelfInStringInterpolationWithNonSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = "test" + func f() { + let x: String = "\\(self.bar)" + } + } + """ + let output = """ + class C { + let bar = "test" + func f() { + let x: String = "\\(bar)" + } + } + """ + testFormatting(for: input, output, rule: .redundantSelf, + exclude: [.propertyTypes, .redundantType]) + } + + func testNoRemoveSelfInFunctionCallWithSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = NSObject() + func f() { + let _: OSLogMessage = foo(self.bar) + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + + func testNoRemoveSelfInMethodChainWithSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = NSObject() + func f() { + let _: OSLogMessage = Foo.bar(self.bar) + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + + func testNoRemoveSelfInDirectAssignmentWithSelfRequiredTypeAnnotation() { + let input = """ + class C { + let bar = NSObject() + func f() { + let _: OSLogMessage = self.bar + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + + func testNoRemoveSelfInSelfRequiredTypeInitializer() { + let input = """ + class C { + let bar = NSObject() + func f() { + let msg = OSLogMessage(self.bar) + } + } + """ + let options = FormatOptions(selfRequired: ["OSLogMessage"]) + testFormatting(for: input, rule: .redundantSelf, options: options, + exclude: [.propertyTypes]) + } + func testSelfRemovedFromSwitchCaseWhere() { let input = """ class Foo {