From 013247c926a78ef4dff93b1e8a7fabcaba8fa775 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 2 Mar 2024 10:19:01 +0000 Subject: [PATCH] Add preliminary support for typed throws --- Sources/ParsingHelpers.swift | 5 +- Sources/Rules.swift | 2 +- Tests/ParsingHelpersTests.swift | 17 +++++ Tests/RulesTests+Indentation.swift | 11 +++ Tests/RulesTests+Redundancy.swift | 73 ++++++++++++++++++++ Tests/RulesTests+Spacing.swift | 12 ++++ Tests/RulesTests+Syntax.swift | 20 +++++- Tests/RulesTests+Wrapping.swift | 104 +++++++++++++++++++++++++++++ 8 files changed, 238 insertions(+), 6 deletions(-) diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift index d9aa4c38..935c96f7 100644 --- a/Sources/ParsingHelpers.swift +++ b/Sources/ParsingHelpers.swift @@ -594,10 +594,7 @@ extension Formatter { "func", "subscript", "catch": return false case "throws", "rethrows": - return next( - .nonSpaceOrLinebreak, - after: prevKeywordIndex - ) == .keyword("in") + return next(.keyword, after: prevKeywordIndex) == .keyword("in") default: return true } diff --git a/Sources/Rules.swift b/Sources/Rules.swift index be4e2ddd..8fc07e06 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -233,7 +233,7 @@ public struct _FormatRules { } return false case "private", "fileprivate", "internal", - "init", "subscript": + "init", "subscript", "throws": return false case "await": return formatter.options.swiftVersion >= "5.5" || diff --git a/Tests/ParsingHelpersTests.swift b/Tests/ParsingHelpersTests.swift index 11d31330..485ae910 100644 --- a/Tests/ParsingHelpersTests.swift +++ b/Tests/ParsingHelpersTests.swift @@ -550,6 +550,23 @@ class ParsingHelpersTests: XCTestCase { XCTAssert(formatter.isStartOfClosure(at: 6)) } + func testTypedThrowingClosure() { + let formatter = Formatter(tokenize(""" + let foo = { bar throws(Foo) in bar } + """)) + XCTAssert(formatter.isStartOfClosure(at: 6)) + } + + func testNestedTypedThrowingClosures() { + let formatter = Formatter(tokenize(""" + try! str.withCString(encodedAs: UTF8.self) { _ throws(Foo) in + try! str.withCString(encodedAs: UTF8.self) { _ throws(Foo) in } + } + """)) + XCTAssert(formatter.isStartOfClosure(at: 15)) + XCTAssert(formatter.isStartOfClosure(at: 42)) + } + func testTrailingClosureOnOptionalMethod() { let formatter = Formatter(tokenize(""" foo.bar? { print("") } diff --git a/Tests/RulesTests+Indentation.swift b/Tests/RulesTests+Indentation.swift index b05295d3..f3cf7ac3 100644 --- a/Tests/RulesTests+Indentation.swift +++ b/Tests/RulesTests+Indentation.swift @@ -3710,6 +3710,17 @@ class IndentTests: RulesTests { testFormatting(for: input, rule: FormatRules.indent, options: options) } + func testAsyncTypedThrowsNotUnindented() { + let input = """ + func multilineFunction( + foo _: String, + bar _: String) + async throws(Foo) -> String {} + """ + let options = FormatOptions(closingParenOnSameLine: true) + testFormatting(for: input, rule: FormatRules.indent, options: options) + } + func testIndentAsyncLet() { let input = """ func foo() async { diff --git a/Tests/RulesTests+Redundancy.swift b/Tests/RulesTests+Redundancy.swift index ab8ec373..45a5b06e 100644 --- a/Tests/RulesTests+Redundancy.swift +++ b/Tests/RulesTests+Redundancy.swift @@ -852,6 +852,19 @@ class RedundancyTests: RulesTests { testFormatting(for: input, rule: FormatRules.redundantInit, exclude: ["indent"]) } + func testNoRemoveInitInsideIfdef2() { + let input = """ + func myFunc() async throws(Foo) -> String { + #if DEBUG + .init("foo") + #else + "" + #endif + } + """ + testFormatting(for: input, rule: FormatRules.redundantInit, exclude: ["indent"]) + } + // MARK: - redundantLetError func testCatchLetError() { @@ -4089,6 +4102,18 @@ class RedundancyTests: RulesTests { testFormatting(for: input, rule: FormatRules.redundantSelf) } + func testTypedThrowingNestedClosureInNotMistakenForForLoop() { + let input = """ + func f() { + let str = "hello" + try! str.withCString(encodedAs: UTF8.self) { _ throws(Foo) in + try! str.withCString(encodedAs: UTF8.self) { _ throws(Foo) in } + } + } + """ + testFormatting(for: input, rule: FormatRules.redundantSelf) + } + func testRedundantSelfPreservesSelfInClosureWithExplicitStrongCaptureBefore5_3() { let input = """ class Foo { @@ -6135,6 +6160,27 @@ class RedundancyTests: RulesTests { exclude: ["hoistPatternLet"]) } + func testSelfRemovalParsingBug7() { + let input = """ + extension Dictionary where Key == String { + func requiredValue(for keyPath: String) throws(Foo) -> T { + return keyPath as! T + } + + func optionalValue(for keyPath: String) throws(Foo) -> T? { + guard let anyValue = self[keyPath] else { + return nil + } + guard let value = anyValue as? T else { + return nil + } + return value + } + } + """ + testFormatting(for: input, rule: FormatRules.redundantSelf) + } + func testSelfNotRemovedInCaseIfElse() { let input = """ class Foo { @@ -6929,6 +6975,11 @@ class RedundancyTests: RulesTests { testFormatting(for: input, rule: FormatRules.unusedArguments) } + func testNoRemoveClosureTypedThrows() { + let input = "let foo = { () throws(Foo) in }" + testFormatting(for: input, rule: FormatRules.unusedArguments) + } + func testNoRemoveClosureGenericReturnTypes() { let input = "let foo = { () -> Promise in bar }" testFormatting(for: input, rule: FormatRules.unusedArguments) @@ -7097,11 +7148,22 @@ class RedundancyTests: RulesTests { testFormatting(for: input, output, rule: FormatRules.unusedArguments) } + func testUnusedTypedThrowingClosureArgument() { + let input = "foo = { bar throws(Foo) in \"\" }" + let output = "foo = { _ throws(Foo) in \"\" }" + testFormatting(for: input, output, rule: FormatRules.unusedArguments) + } + func testUsedThrowingClosureArgument() { let input = "let foo = { bar throws in bar + \"\" }" testFormatting(for: input, rule: FormatRules.unusedArguments) } + func testUsedTypedThrowingClosureArgument() { + let input = "let foo = { bar throws(Foo) in bar + \"\" }" + testFormatting(for: input, rule: FormatRules.unusedArguments) + } + func testUnusedTrailingAsyncClosureArgument() { let input = """ app.get { foo async in @@ -7483,6 +7545,17 @@ class RedundancyTests: RulesTests { testFormatting(for: input, rule: FormatRules.unusedArguments) } + func testTypedTryAwaitArgumentNotMarkedUnused() { + let input = """ + func foo(bar: String) async throws(Foo) -> String? { + let bar = try + await parse(bar) + return bar + } + """ + testFormatting(for: input, rule: FormatRules.unusedArguments) + } + func testConditionalIfLetMarkedAsUnused() { let input = """ func foo(bar: UIViewController) { diff --git a/Tests/RulesTests+Spacing.swift b/Tests/RulesTests+Spacing.swift index 008a0b3e..f96b5981 100644 --- a/Tests/RulesTests+Spacing.swift +++ b/Tests/RulesTests+Spacing.swift @@ -199,6 +199,12 @@ class SpacingTests: RulesTests { testFormatting(for: input, output, rule: FormatRules.spaceAroundParens, exclude: ["redundantVoidReturnType"]) } + func testAddSpaceBetweenCaptureListAndArguments8() { + let input = "{ [weak self]() throws(Foo) -> Void in }" + let output = "{ [weak self] () throws(Foo) -> Void in }" + testFormatting(for: input, output, rule: FormatRules.spaceAroundParens, exclude: ["redundantVoidReturnType"]) + } + func testAddSpaceBetweenEscapingAndParenthesizedClosure() { let input = "@escaping(() -> Void)" let output = "@escaping (() -> Void)" @@ -316,6 +322,12 @@ class SpacingTests: RulesTests { testFormatting(for: input, rule: FormatRules.spaceAroundParens) } + func testNoAddSpaceAroundTypedThrowsFunctionType() { + let input = "func foo() throws (Bar) -> Baz {}" + let output = "func foo() throws(Bar) -> Baz {}" + testFormatting(for: input, output, rule: FormatRules.spaceAroundParens) + } + // MARK: - spaceInsideParens func testSpaceInsideParens() { diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift index de57ca2b..ee93c231 100644 --- a/Tests/RulesTests+Syntax.swift +++ b/Tests/RulesTests+Syntax.swift @@ -217,6 +217,12 @@ class SyntaxTests: RulesTests { testFormatting(for: input, output, rule: FormatRules.void) } + func testFunctionThatReturnsAFunctionThatHasTypedThrows() { + let input = "(Void) -> Void throws(Foo) -> ()" + let output = "(Void) -> () throws(Foo) -> Void" + testFormatting(for: input, output, rule: FormatRules.void) + } + func testChainOfFunctionsIsNotChanged() { let input = "() -> () -> () -> Void" testFormatting(for: input, rule: FormatRules.void) @@ -227,11 +233,21 @@ class SyntaxTests: RulesTests { testFormatting(for: input, rule: FormatRules.void) } + func testChainOfFunctionsWithTypedThrowsIsNotChanged() { + let input = "() -> () throws(Foo) -> () throws(Foo) -> Void" + testFormatting(for: input, rule: FormatRules.void) + } + func testVoidThrowsIsNotMangled() { let input = "(Void) throws -> Void" testFormatting(for: input, rule: FormatRules.void) } + func testVoidTypedThrowsIsNotMangled() { + let input = "(Void) throws(Foo) -> Void" + testFormatting(for: input, rule: FormatRules.void) + } + func testEmptyClosureArgsNotMangled() { let input = "{ () in }" testFormatting(for: input, rule: FormatRules.void) @@ -2965,8 +2981,10 @@ class SyntaxTests: RulesTests { let input = """ func foo(_: (Foo) -> Void) {} func bar(_: (Foo) throws -> Void) {} + func baz(_: (Foo) throws(Bar) -> Void) {} func baaz(_: (Foo) async -> Void) {} - func quux(_: (Foo) async throws -> Void) {} + func qux(_: (Foo) async throws -> Void) {} + func quux(_: (Foo) async throws(Bar) -> Void) {} func qaax(_: ([Foo]) -> Void) {} func qaax(_: ((Foo, Bar)) -> Void) {} """ diff --git a/Tests/RulesTests+Wrapping.swift b/Tests/RulesTests+Wrapping.swift index 8700b0a7..d8ffa801 100644 --- a/Tests/RulesTests+Wrapping.swift +++ b/Tests/RulesTests+Wrapping.swift @@ -873,6 +873,28 @@ class WrappingTests: RulesTests { testFormatting(for: input, [output, output2], rules: [FormatRules.wrap], options: options, exclude: ["wrapMultilineStatementBraces"]) } + func testWrapFunctionIfReturnTypeExceedsMaxWidth2WithXcodeIndentation2() { + let input = """ + func testFunc() throws(Foo) -> (ReturnType, ReturnType2) { + doSomething() + } + """ + let output = """ + func testFunc() throws(Foo) + -> (ReturnType, ReturnType2) { + doSomething() + } + """ + let output2 = """ + func testFunc() throws(Foo) + -> (ReturnType, ReturnType2) { + doSomething() + } + """ + let options = FormatOptions(xcodeIndentation: true, maxWidth: 35) + testFormatting(for: input, [output, output2], rules: [FormatRules.wrap], options: options, exclude: ["wrapMultilineStatementBraces"]) + } + func testWrapFunctionIfReturnTypeExceedsMaxWidth3() { let input = """ func testFunc() -> (Bool, String) -> String? { @@ -989,6 +1011,22 @@ class WrappingTests: RulesTests { testFormatting(for: input, output, rule: FormatRules.wrap, options: options, exclude: ["wrapMultilineStatementBraces"]) } + func testWrapTypedThrowingFunctionIfReturnTypeExceedsMaxWidth() { + let input = """ + func testFunc(_: () -> Void) throws(Foo) -> (Bool, String) -> String? { + doSomething() + } + """ + let output = """ + func testFunc(_: () -> Void) throws(Foo) + -> (Bool, String) -> String? { + doSomething() + } + """ + let options = FormatOptions(maxWidth: 42) + testFormatting(for: input, output, rule: FormatRules.wrap, options: options, exclude: ["wrapMultilineStatementBraces"]) + } + func testNoWrapInterpolatedStringLiteral() { let input = """ "a very long \\(string) literal" @@ -1783,6 +1821,28 @@ class WrappingTests: RulesTests { options: options) } + func testWrapParametersListBeforeFirstInTypedThrowingClosureType() { + let input = """ + var mathFunction: (Int, + Int, String) throws(Foo) -> Int = { _, _, _ in + 0 + } + """ + let output = """ + var mathFunction: ( + Int, + Int, + String + ) throws(Foo) -> Int = { _, _, _ in + 0 + } + """ + let options = FormatOptions(wrapParameters: .beforeFirst) + testFormatting(for: input, [output], + rules: [FormatRules.wrapArguments], + options: options) + } + func testWrapParametersListBeforeFirstInRethrowingClosureType() { let input = """ var mathFunction: (Int, @@ -2959,6 +3019,21 @@ class WrappingTests: RulesTests { testFormatting(for: input, rule: FormatRules.wrapArguments, options: options) } + func testDoesntWrapReturnAndTypedEffectOnSingleLineFunctionDeclaration() { + let input = """ + func singleLineFunction() async throws(Foo) -> String {} + """ + + let options = FormatOptions( + wrapArguments: .beforeFirst, + closingParenOnSameLine: true, + wrapReturnType: .ifMultiline, + wrapEffects: .ifMultiline + ) + + testFormatting(for: input, rule: FormatRules.wrapArguments, options: options) + } + func testWrapEffectOnMultilineFunctionDeclaration() { let input = """ func multilineFunction( @@ -3630,6 +3705,35 @@ class WrappingTests: RulesTests { ], options: options) } + func testWrapsMultilineStatementConsistently2_withTypedEffects() { + let input = """ + func aFunc( + one _: Int, + two _: Int) async throws(Foo) -> String { + "one" + } + """ + + let output = """ + func aFunc( + one _: Int, + two _: Int + ) async throws(Foo) -> String { + "one" + } + """ + + let options = FormatOptions( + wrapArguments: .beforeFirst, + closingParenOnSameLine: false, + wrapEffects: .never + ) + testFormatting(for: input, [output], rules: [ + FormatRules.wrapMultilineStatementBraces, + FormatRules.wrapArguments, + ], options: options) + } + func testWrapsMultilineStatementConsistently3() { let input = """ func aFunc(