diff --git a/Sources/SwiftFormat/Core/ModifierListSyntax+Convenience.swift b/Sources/SwiftFormat/Core/ModifierListSyntax+Convenience.swift index 2a544b9d2..b1e58cc12 100644 --- a/Sources/SwiftFormat/Core/ModifierListSyntax+Convenience.swift +++ b/Sources/SwiftFormat/Core/ModifierListSyntax+Convenience.swift @@ -17,7 +17,8 @@ extension DeclModifierListSyntax { var accessLevelModifier: DeclModifierSyntax? { for modifier in self { switch modifier.name.tokenKind { - case .keyword(.public), .keyword(.private), .keyword(.fileprivate), .keyword(.internal): + case .keyword(.public), .keyword(.private), .keyword(.fileprivate), .keyword(.internal), + .keyword(.package): return modifier default: continue diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 5d00dd005..048f0c866 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -2293,6 +2293,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: GenericParameterSyntax) -> SyntaxVisitorContinueKind { before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(node.eachKeyword, tokens: .break) after(node.colon, tokens: .break) if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) @@ -2312,6 +2313,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: PackElementExprSyntax) -> SyntaxVisitorContinueKind { + // `each` cannot be separated from the following token, or it is parsed as an identifier itself. + after(node.eachKeyword, tokens: .space) + return .visitChildren + } + + override func visit(_ node: PackElementTypeSyntax) -> SyntaxVisitorContinueKind { + // `each` cannot be separated from the following token, or it is parsed as an identifier itself. + after(node.eachKeyword, tokens: .space) + return .visitChildren + } + + override func visit(_ node: PackExpansionExprSyntax) -> SyntaxVisitorContinueKind { + after(node.repeatKeyword, tokens: .break) + return .visitChildren + } + + override func visit(_ node: PackExpansionTypeSyntax) -> SyntaxVisitorContinueKind { + after(node.repeatKeyword, tokens: .break) + return .visitChildren + } + override func visit(_ node: ExpressionPatternSyntax) -> SyntaxVisitorContinueKind { return .visitChildren } @@ -2481,6 +2504,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: ConsumeExprSyntax) -> SyntaxVisitorContinueKind { + // The `consume` keyword cannot be separated from the following token or it will be parsed as + // an identifier. + after(node.consumeKeyword, tokens: .space) + return .visitChildren + } + + override func visit(_ node: CopyExprSyntax) -> SyntaxVisitorContinueKind { + // The `copy` keyword cannot be separated from the following token or it will be parsed as an + // identifier. + after(node.copyKeyword, tokens: .space) + return .visitChildren + } + + override func visit(_ node: DiscardStmtSyntax) -> SyntaxVisitorContinueKind { + // The `discard` keyword cannot be separated from the following token or it will be parsed as + // an identifier. + after(node.discardKeyword, tokens: .space) + return .visitChildren + } + override func visit(_ node: InheritanceClauseSyntax) -> SyntaxVisitorContinueKind { // Normally, the open-break is placed before the open token. In this case, it's intentionally // ordered differently so that the inheritance list can start on the current line and only diff --git a/Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyCollectionInit.swift b/Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyCollectionInit.swift index 49b850a50..28c52d64a 100644 --- a/Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyCollectionInit.swift +++ b/Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyCollectionInit.swift @@ -166,8 +166,7 @@ public final class AlwaysUseLiteralForEmptyCollectionInit : SyntaxFormatRule { } private func getLiteralType(_ arrayLiteral: ArrayExprSyntax) -> TypeSyntax? { - guard let elementExpr = arrayLiteral.elements.firstAndOnly, - elementExpr.is(ArrayElementSyntax.self) else { + guard arrayLiteral.elements.count == 1 else { return nil } diff --git a/Sources/SwiftFormat/Rules/NoAccessLevelOnExtensionDeclaration.swift b/Sources/SwiftFormat/Rules/NoAccessLevelOnExtensionDeclaration.swift index b9be2483e..e16e82cf6 100644 --- a/Sources/SwiftFormat/Rules/NoAccessLevelOnExtensionDeclaration.swift +++ b/Sources/SwiftFormat/Rules/NoAccessLevelOnExtensionDeclaration.swift @@ -32,8 +32,8 @@ public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule { var result = node switch keyword { - // Public, private, or fileprivate keywords need to be moved to members - case .public, .private, .fileprivate: + // Public, private, fileprivate, or package keywords need to be moved to members + case .public, .private, .fileprivate, .package: // The effective access level of the members of a `private` extension is `fileprivate`, so // we have to update the keyword to ensure that the result is correct. var accessKeywordToAdd = accessKeyword diff --git a/Tests/SwiftFormatTests/PrettyPrint/ConsumeExprTests.swift b/Tests/SwiftFormatTests/PrettyPrint/ConsumeExprTests.swift new file mode 100644 index 000000000..30a1b6a1c --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/ConsumeExprTests.swift @@ -0,0 +1,14 @@ +final class ConsumeExprTests: PrettyPrintTestCase { + func testConsume() { + assertPrettyPrintEqual( + input: """ + let x = consume y + """, + expected: """ + let x = + consume y + + """, + linelength: 16) + } +} diff --git a/Tests/SwiftFormatTests/PrettyPrint/CopyExprSyntax.swift b/Tests/SwiftFormatTests/PrettyPrint/CopyExprSyntax.swift new file mode 100644 index 000000000..188121eef --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/CopyExprSyntax.swift @@ -0,0 +1,14 @@ +final class CopyExprTests: PrettyPrintTestCase { + func testCopy() { + assertPrettyPrintEqual( + input: """ + let x = copy y + """, + expected: """ + let x = + copy y + + """, + linelength: 13) + } +} diff --git a/Tests/SwiftFormatTests/PrettyPrint/DiscardStmtTests.swift b/Tests/SwiftFormatTests/PrettyPrint/DiscardStmtTests.swift new file mode 100644 index 000000000..63637da85 --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/DiscardStmtTests.swift @@ -0,0 +1,13 @@ +final class DiscardStmtTests: PrettyPrintTestCase { + func testDiscard() { + assertPrettyPrintEqual( + input: """ + discard self + """, + expected: """ + discard self + + """, + linelength: 9) + } +} diff --git a/Tests/SwiftFormatTests/PrettyPrint/ParameterPackTests.swift b/Tests/SwiftFormatTests/PrettyPrint/ParameterPackTests.swift new file mode 100644 index 000000000..c4615dfab --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/ParameterPackTests.swift @@ -0,0 +1,46 @@ +final class ParameterPackTests: PrettyPrintTestCase { + func testGenericPackArgument() { + assertPrettyPrintEqual( + input: """ + func someFunction() {} + struct SomeStruct {} + """, + expected: """ + func someFunction< + each P + >() {} + struct SomeStruct< + each P + > {} + + """, + linelength: 22) + } + + func testPackExpansionsAndElements() { + assertPrettyPrintEqual( + input: """ + repeat checkNilness(of: each value) + """, + expected: """ + repeat checkNilness( + of: each value) + + """, + linelength: 25) + + assertPrettyPrintEqual( + input: """ + repeat f(of: each v) + """, + expected: """ + repeat + f( + of: + each v + ) + + """, + linelength: 7) + } +} diff --git a/Tests/SwiftFormatTests/Rules/NoAccessLevelOnExtensionDeclarationTests.swift b/Tests/SwiftFormatTests/Rules/NoAccessLevelOnExtensionDeclarationTests.swift index 030fa707a..f020059cb 100644 --- a/Tests/SwiftFormatTests/Rules/NoAccessLevelOnExtensionDeclarationTests.swift +++ b/Tests/SwiftFormatTests/Rules/NoAccessLevelOnExtensionDeclarationTests.swift @@ -128,6 +128,31 @@ final class NoAccessLevelOnExtensionDeclarationTests: LintOrFormatRuleTestCase { ) } + func testPackageAccessLevel() { + assertFormatting( + NoAccessLevelOnExtensionDeclaration.self, + input: """ + 1️⃣package extension Foo { + 2️⃣func f() {} + } + """, + expected: """ + extension Foo { + package func f() {} + } + """, + findings: [ + FindingSpec( + "1️⃣", + message: "move this 'package' access modifier to precede each member inside this extension", + notes: [ + NoteSpec("2️⃣", message: "add 'package' access modifier to this declaration"), + ] + ), + ] + ) + } + func testPrivateIsEffectivelyFileprivate() { assertFormatting( NoAccessLevelOnExtensionDeclaration.self,