diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 8eca259e4..5e30ffcff 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -82,6 +82,9 @@ top-level keys and values: * `spacesAroundRangeFormationOperators` _(boolean)_: Determines whether whitespace should be forced before and after the range formation operators `...` and `..<`. +* `multiElementCollectionTrailingCommas` _(boolean)_: Determines whether multi-element collection literals should have trailing commas. + Defaults to `true`. + > TODO: Add support for enabling/disabling specific syntax transformations in > the pipeline. diff --git a/Package.swift b/Package.swift index 124672571..51fe74bfe 100644 --- a/Package.swift +++ b/Package.swift @@ -154,7 +154,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { ), .package( url: "https://github.com/apple/swift-syntax.git", - branch: "main" + branch: "release/5.10" ), ] } else { diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 61ba05c34..3f0123fb4 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -37,5 +37,6 @@ extension Configuration { self.indentSwitchCaseLabels = false self.spacesAroundRangeFormationOperators = false self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + self.multiElementCollectionTrailingCommas = true } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 13064c6e7..6ef959695 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -42,6 +42,7 @@ public struct Configuration: Codable, Equatable { case rules case spacesAroundRangeFormationOperators case noAssignmentInExpressions + case multiElementCollectionTrailingCommas } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -162,6 +163,29 @@ public struct Configuration: Codable, Equatable { /// Contains exceptions for the `NoAssignmentInExpressions` rule. public var noAssignmentInExpressions: NoAssignmentInExpressionsConfiguration + /// Determines if multi-element collection literals should have trailing commas. + /// + /// When `true` (default), the correct form is: + /// ```swift + /// let MyCollection = [1, 2,] + /// ... + /// let MyCollection = [ + /// "a": 1, + /// "b": 2, + /// ] + /// ``` + /// + /// When `false`, the correct form is: + /// ```swift + /// let MyCollection = [1, 2] + /// ... + /// let MyCollection = [ + /// "a": 1, + /// "b": 2 + /// ] + /// ``` + public var multiElementCollectionTrailingCommas: Bool + /// Constructs a Configuration by loading it from a configuration file. public init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) @@ -239,6 +263,10 @@ public struct Configuration: Codable, Equatable { try container.decodeIfPresent( NoAssignmentInExpressionsConfiguration.self, forKey: .noAssignmentInExpressions) ?? defaults.noAssignmentInExpressions + self.multiElementCollectionTrailingCommas = + try container.decodeIfPresent( + Bool.self, forKey: .multiElementCollectionTrailingCommas) + ?? defaults.multiElementCollectionTrailingCommas // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been @@ -271,6 +299,7 @@ public struct Configuration: Codable, Equatable { try container.encode(fileScopedDeclarationPrivacy, forKey: .fileScopedDeclarationPrivacy) try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels) try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions) + try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas) try container.encode(rules, forKey: .rules) } 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/Core/Pipelines+Generated.swift b/Sources/SwiftFormat/Core/Pipelines+Generated.swift index 6603fcb72..01d2b1aaa 100644 --- a/Sources/SwiftFormat/Core/Pipelines+Generated.swift +++ b/Sources/SwiftFormat/Core/Pipelines+Generated.swift @@ -159,6 +159,7 @@ class LintPipeline: SyntaxVisitor { } override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(AlwaysUseLiteralForEmptyCollectionInit.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) return .visitChildren } @@ -243,6 +244,7 @@ class LintPipeline: SyntaxVisitor { } override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(AlwaysUseLiteralForEmptyCollectionInit.visit, for: node) visitIfEnabled(OmitExplicitReturns.visit, for: node) visitIfEnabled(UseSingleLinePropertyGetter.visit, for: node) return .visitChildren @@ -356,6 +358,7 @@ extension FormatPipeline { func rewrite(_ node: Syntax) -> Syntax { var node = node + node = AlwaysUseLiteralForEmptyCollectionInit(context: context).rewrite(node) node = DoNotUseSemicolons(context: context).rewrite(node) node = FileScopedDeclarationPrivacy(context: context).rewrite(node) node = FullyIndirectEnum(context: context).rewrite(node) diff --git a/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift b/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift index 453f82940..b4eb96438 100644 --- a/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift +++ b/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift @@ -16,6 +16,7 @@ @_spi(Testing) public let ruleNameCache: [ObjectIdentifier: String] = [ ObjectIdentifier(AllPublicDeclarationsHaveDocumentation.self): "AllPublicDeclarationsHaveDocumentation", + ObjectIdentifier(AlwaysUseLiteralForEmptyCollectionInit.self): "AlwaysUseLiteralForEmptyCollectionInit", ObjectIdentifier(AlwaysUseLowerCamelCase.self): "AlwaysUseLowerCamelCase", ObjectIdentifier(AmbiguousTrailingClosureOverload.self): "AmbiguousTrailingClosureOverload", ObjectIdentifier(BeginDocumentationCommentWithOneLineSummary.self): "BeginDocumentationCommentWithOneLineSummary", diff --git a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift index 7a0de7315..e3ad66c12 100644 --- a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift +++ b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift @@ -15,6 +15,7 @@ @_spi(Internal) public enum RuleRegistry { public static let rules: [String: Bool] = [ "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, "AlwaysUseLowerCamelCase": true, "AmbiguousTrailingClosureOverload": true, "BeginDocumentationCommentWithOneLineSummary": false, diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 97a1c5fc6..8447ff8d2 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -557,7 +557,7 @@ public class PrettyPrinter { // We never want to add a trailing comma in an initializer so we disable trailing commas on // single element collections. let shouldHaveTrailingComma = - startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement + startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement && configuration.multiElementCollectionTrailingCommas if shouldHaveTrailingComma && !hasTrailingComma { diagnose(.addTrailingComma, category: .trailingComma) } else if !shouldHaveTrailingComma && hasTrailingComma { diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 5d00dd005..0aa0533fc 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -1799,8 +1799,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: AvailabilityLabeledArgumentSyntax) -> SyntaxVisitorContinueKind { before(node.label, tokens: .open) - after(node.colon, tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) - after(node.value.lastToken(viewMode: .sourceAccurate), tokens: .close) + + let tokensAfterColon: [Token] + let endTokens: [Token] + + if case .string(let string) = node.value, + string.openingQuote.tokenKind == .multilineStringQuote + { + tokensAfterColon = + [.break(.open(kind: .block), newlines: .elective(ignoresDiscretionary: true))] + endTokens = [.break(.close(mustBreak: false), size: 0), .close] + } else { + tokensAfterColon = [.break(.continue, newlines: .elective(ignoresDiscretionary: true))] + endTokens = [.close] + } + + after(node.colon, tokens: tokensAfterColon) + after(node.value.lastToken(viewMode: .sourceAccurate), tokens: endTokens) return .visitChildren } @@ -2293,6 +2308,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 +2328,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 } @@ -2353,6 +2391,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: SimpleStringLiteralExprSyntax) -> SyntaxVisitorContinueKind { + if node.openingQuote.tokenKind == .multilineStringQuote { + after(node.openingQuote, tokens: .break(.same, size: 0, newlines: .hard(count: 1))) + if !node.segments.isEmpty { + before(node.closingQuote, tokens: .break(.same, newlines: .hard(count: 1))) + } + } + return .visitChildren + } + override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind { // Looks up the correct break kind based on prior context. func breakKind() -> BreakKind { @@ -2481,6 +2529,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 new file mode 100644 index 000000000..28c52d64a --- /dev/null +++ b/Sources/SwiftFormat/Rules/AlwaysUseLiteralForEmptyCollectionInit.swift @@ -0,0 +1,207 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftSyntax +import SwiftParser + +/// Never use `[]()` syntax. In call sites that should be replaced with `[]`, +/// for initializations use explicit type combined with empty array literal `let _: [] = []` +/// Static properties of a type that return that type should not include a reference to their type. +/// +/// Lint: Non-literal empty array initialization will yield a lint error. +/// Format: All invalid use sites would be related with empty literal (with or without explicit type annotation). +@_spi(Rules) +public final class AlwaysUseLiteralForEmptyCollectionInit : SyntaxFormatRule { + public override class var isOptIn: Bool { return true } + + public override func visit(_ node: PatternBindingSyntax) -> PatternBindingSyntax { + guard let initializer = node.initializer, + let type = isRewritable(initializer) else { + return node + } + + if let type = type.as(ArrayTypeSyntax.self) { + return rewrite(node, type: type) + } + + if let type = type.as(DictionaryTypeSyntax.self) { + return rewrite(node, type: type) + } + + return node + } + + public override func visit(_ param: FunctionParameterSyntax) -> FunctionParameterSyntax { + guard let initializer = param.defaultValue, + let type = isRewritable(initializer) else { + return param + } + + if let type = type.as(ArrayTypeSyntax.self) { + return rewrite(param, type: type) + } + + if let type = type.as(DictionaryTypeSyntax.self) { + return rewrite(param, type: type) + } + + return param + } + + /// Check whether the initializer is `[]()` and, if so, it could be rewritten to use an empty collection literal. + /// Return a type of the collection. + public func isRewritable(_ initializer: InitializerClauseSyntax) -> TypeSyntax? { + guard let initCall = initializer.value.as(FunctionCallExprSyntax.self), + initCall.arguments.isEmpty else { + return nil + } + + if let arrayLiteral = initCall.calledExpression.as(ArrayExprSyntax.self) { + return getLiteralType(arrayLiteral) + } + + if let dictLiteral = initCall.calledExpression.as(DictionaryExprSyntax.self) { + return getLiteralType(dictLiteral) + } + + return nil + } + + private func rewrite(_ node: PatternBindingSyntax, + type: ArrayTypeSyntax) -> PatternBindingSyntax { + var replacement = node + + diagnose(node, type: type) + + if replacement.typeAnnotation == nil { + // Drop trailing trivia after pattern because ':' has to appear connected to it. + replacement.pattern = node.pattern.with(\.trailingTrivia, []) + // Add explicit type annotiation: ': []` + replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space) + .with(\.trailingTrivia, .space)) + } + + let initializer = node.initializer! + let emptyArrayExpr = ArrayExprSyntax(elements: ArrayElementListSyntax.init([])) + + // Replace initializer call with empty array literal: `[]()` -> `[]` + replacement.initializer = initializer.with(\.value, ExprSyntax(emptyArrayExpr)) + + return replacement + } + + private func rewrite(_ node: PatternBindingSyntax, + type: DictionaryTypeSyntax) -> PatternBindingSyntax { + var replacement = node + + diagnose(node, type: type) + + if replacement.typeAnnotation == nil { + // Drop trailing trivia after pattern because ':' has to appear connected to it. + replacement.pattern = node.pattern.with(\.trailingTrivia, []) + // Add explicit type annotiation: ': []` + replacement.typeAnnotation = .init(type: type.with(\.leadingTrivia, .space) + .with(\.trailingTrivia, .space)) + } + + let initializer = node.initializer! + // Replace initializer call with empty dictionary literal: `[]()` -> `[]` + replacement.initializer = initializer.with(\.value, ExprSyntax(getEmptyDictionaryLiteral())) + + return replacement + } + + private func rewrite(_ param: FunctionParameterSyntax, + type: ArrayTypeSyntax) -> FunctionParameterSyntax { + guard let initializer = param.defaultValue else { + return param + } + + emitDiagnostic(replace: "\(initializer.value)", with: "[]", on: initializer.value) + return param.with(\.defaultValue, initializer.with(\.value, getEmptyArrayLiteral())) + } + + private func rewrite(_ param: FunctionParameterSyntax, + type: DictionaryTypeSyntax) -> FunctionParameterSyntax { + guard let initializer = param.defaultValue else { + return param + } + + emitDiagnostic(replace: "\(initializer.value)", with: "[:]", on: initializer.value) + return param.with(\.defaultValue, initializer.with(\.value, getEmptyDictionaryLiteral())) + } + + private func diagnose(_ node: PatternBindingSyntax, type: ArrayTypeSyntax) { + var withFixIt = "[]" + if node.typeAnnotation == nil { + withFixIt = ": \(type) = []" + } + + let initCall = node.initializer!.value + emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall) + } + + private func diagnose(_ node: PatternBindingSyntax, type: DictionaryTypeSyntax) { + var withFixIt = "[:]" + if node.typeAnnotation == nil { + withFixIt = ": \(type) = [:]" + } + + let initCall = node.initializer!.value + emitDiagnostic(replace: "\(initCall)", with: withFixIt, on: initCall) + } + + private func emitDiagnostic(replace: String, with fixIt: String, on: ExprSyntax?) { + diagnose(.refactorIntoEmptyLiteral(replace: replace, with: fixIt), on: on) + } + + private func getLiteralType(_ arrayLiteral: ArrayExprSyntax) -> TypeSyntax? { + guard arrayLiteral.elements.count == 1 else { + return nil + } + + var parser = Parser(arrayLiteral.description) + let elementType = TypeSyntax.parse(from: &parser) + + guard !elementType.hasError, elementType.is(ArrayTypeSyntax.self) else { + return nil + } + + return elementType + } + + private func getLiteralType(_ dictLiteral: DictionaryExprSyntax) -> TypeSyntax? { + var parser = Parser(dictLiteral.description) + let elementType = TypeSyntax.parse(from: &parser) + + guard !elementType.hasError, elementType.is(DictionaryTypeSyntax.self) else { + return nil + } + + return elementType + } + + private func getEmptyArrayLiteral() -> ExprSyntax { + ExprSyntax(ArrayExprSyntax(elements: ArrayElementListSyntax.init([]))) + } + + private func getEmptyDictionaryLiteral() -> ExprSyntax { + ExprSyntax(DictionaryExprSyntax(content: .colon(.colonToken()))) + } +} + +extension Finding.Message { + public static func refactorIntoEmptyLiteral(replace: String, with: String) -> Finding.Message { + "replace '\(replace)' with '\(with)'" + } +} 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/Sources/SwiftFormat/Rules/UseShorthandTypeNames.swift b/Sources/SwiftFormat/Rules/UseShorthandTypeNames.swift index a66aafb17..c93cb76c3 100644 --- a/Sources/SwiftFormat/Rules/UseShorthandTypeNames.swift +++ b/Sources/SwiftFormat/Rules/UseShorthandTypeNames.swift @@ -238,11 +238,12 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { ) -> TypeSyntax { var wrappedType = wrappedType - // Function types and some-or-any types must be wrapped in parentheses before using shorthand - // optional syntax, otherwise the "?" will bind incorrectly (in the function case it binds to - // only the result, and in the some-or-any case it only binds to the child protocol). Attach the - // leading trivia to the left-paren that we're adding in these cases. + // Certain types must be wrapped in parentheses before using shorthand optional syntax to avoid + // the "?" from binding incorrectly when re-parsed. Attach the leading trivia to the left-paren + // that we're adding in these cases. switch Syntax(wrappedType).as(SyntaxEnum.self) { + case .attributedType(let attributedType): + wrappedType = parenthesizedType(attributedType, leadingTrivia: leadingTrivia) case .functionType(let functionType): wrappedType = parenthesizedType(functionType, leadingTrivia: leadingTrivia) case .someOrAnyType(let someOrAnyType): @@ -319,12 +320,11 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { ) -> OptionalChainingExprSyntax? { guard var wrappedTypeExpr = expressionRepresentation(of: wrappedType) else { return nil } - // Function types and some-or-any types must be wrapped in parentheses before using shorthand - // optional syntax, otherwise the "?" will bind incorrectly (in the function case it binds to - // only the result, and in the some-or-any case it only binds to the child protocol). Attach the - // leading trivia to the left-paren that we're adding in these cases. + // Certain types must be wrapped in parentheses before using shorthand optional syntax to avoid + // the "?" from binding incorrectly when re-parsed. Attach the leading trivia to the left-paren + // that we're adding in these cases. switch Syntax(wrappedType).as(SyntaxEnum.self) { - case .functionType, .someOrAnyType: + case .attributedType, .functionType, .someOrAnyType: wrappedTypeExpr = parenthesizedExpr(wrappedTypeExpr, leadingTrivia: leadingTrivia) default: // Otherwise, the argument type can safely become an optional by simply appending a "?". If @@ -448,6 +448,9 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { case .someOrAnyType(let someOrAnyType): return ExprSyntax(TypeExprSyntax(type: someOrAnyType)) + case .attributedType(let attributedType): + return ExprSyntax(TypeExprSyntax(type: attributedType)) + default: return nil } diff --git a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift index 36cd2971d..e9ab39c34 100644 --- a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift +++ b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift @@ -40,6 +40,7 @@ extension Configuration { config.indentSwitchCaseLabels = false config.spacesAroundRangeFormationOperators = false config.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + config.multiElementCollectionTrailingCommas = true return config } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift index 57a12a3a7..06aa46c8f 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift @@ -356,4 +356,58 @@ final class AttributeTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 32) } + + func testMultilineStringLiteralInCustomAttribute() { + let input = + #""" + @CustomAttribute(message: """ + This is a + multiline + string + """) + public func f() {} + """# + + let expected = + #""" + @CustomAttribute( + message: """ + This is a + multiline + string + """) + public func f() {} + + """# + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) + } + + func testMultilineStringLiteralInAvailableAttribute() { + let input = + #""" + @available(*, deprecated, message: """ + This is a + multiline + string + """) + public func f() {} + """# + + let expected = + #""" + @available( + *, deprecated, + message: """ + This is a + multiline + string + """ + ) + public func f() {} + + """# + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) + } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift new file mode 100644 index 000000000..24bd0238e --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/CommaTests.swift @@ -0,0 +1,289 @@ +import SwiftFormat + +final class CommaTests: PrettyPrintTestCase { + func testArrayCommasAbsentEnabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasAbsentDisabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasPresentEnabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArrayCommasPresentDisabled() { + let input = + """ + let MyCollection = [ + 1, + 2, + 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + 1, + 2, + 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testArraySingleLineCommasPresentDisabled() { + let input = + """ + let MyCollection = [1, 2, 3,] + + """ + + // no effect expected + let expected = + """ + let MyCollection = [1, 2, 3] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testArraySingleLineCommasPresentEnabled() { + let input = + """ + let MyCollection = [1, 2, 3,] + + """ + + // no effect expected + let expected = + """ + let MyCollection = [1, 2, 3] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testDictionaryCommasAbsentEnabled() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasAbsentDisabled() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3 + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasPresentEnabled() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionaryCommasPresentDisabled() { + let input = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3, + ] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, + "b": 2, + "c": 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 20, configuration: configuration) + } + + func testDictionarySingleLineCommasPresentDisabled() { + let input = + """ + let MyCollection = ["a": 1, "b": 2, "c": 3,] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, "b": 2, "c": 3, + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } + + func testDictionarySingleLineCommasPresentEnabled() { + let input = + """ + let MyCollection = ["a": 1, "b": 2, "c": 3,] + + """ + + let expected = + """ + let MyCollection = [ + "a": 1, "b": 2, "c": 3 + ] + + """ + + var configuration = Configuration.forTesting + configuration.multiElementCollectionTrailingCommas = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40, configuration: configuration) + } +} 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/AlwaysUseLiteralForEmptyCollectionInitTests.swift b/Tests/SwiftFormatTests/Rules/AlwaysUseLiteralForEmptyCollectionInitTests.swift new file mode 100644 index 000000000..c6b6267ce --- /dev/null +++ b/Tests/SwiftFormatTests/Rules/AlwaysUseLiteralForEmptyCollectionInitTests.swift @@ -0,0 +1,173 @@ +import _SwiftFormatTestSupport + +@_spi(Rules) import SwiftFormat + +final class AlwaysUseLiteralForEmptyCollectionInitTests: LintOrFormatRuleTestCase { + func testArray() { + assertFormatting( + AlwaysUseLiteralForEmptyCollectionInit.self, + input: """ + public struct Test { + var value1 = 1️⃣[Int]() + + func test(v: [Double] = 2️⃣[Double]()) { + let _ = 3️⃣[String]() + } + } + + var _: [Category] = 4️⃣[Category]() + let _ = 5️⃣[(Int, Array)]() + let _: [(String, Int, Float)] = 6️⃣[(String, Int, Float)]() + + let _ = [(1, 2, String)]() + + class TestSubscript { + subscript(_: [A] = 7️⃣[A](), x: [(Int, B)] = 8️⃣[(Int, B)]()) { + } + } + + // All of the examples in this block could be re-written to use leading-dot syntax: `.init(...)` + do { + let _ = [Int](repeating: 0, count: 10) + let _: [Int] = [Int](repeating: 0, count: 10) + + func testDefault(_ x: [String] = [String](repeating: "a", count: 42)) { + } + + class TestSubscript { + subscript(_: Int = 42, x: [(Int, B)] = [(Int, B)](repeating: (0, B()), count: 1)) { + } + } + } + """, + expected: """ + public struct Test { + var value1: [Int] = [] + + func test(v: [Double] = []) { + let _: [String] = [] + } + } + + var _: [Category] = [] + let _: [(Int, Array)] = [] + let _: [(String, Int, Float)] = [] + + let _ = [(1, 2, String)]() + + class TestSubscript { + subscript(_: [A] = [], x: [(Int, B)] = []) { + } + } + + // All of the examples in this block could be re-written to use leading-dot syntax: `.init(...)` + do { + let _ = [Int](repeating: 0, count: 10) + let _: [Int] = [Int](repeating: 0, count: 10) + + func testDefault(_ x: [String] = [String](repeating: "a", count: 42)) { + } + + class TestSubscript { + subscript(_: Int = 42, x: [(Int, B)] = [(Int, B)](repeating: (0, B()), count: 1)) { + } + } + } + """, + findings: [ + FindingSpec("1️⃣", message: "replace '[Int]()' with ': [Int] = []'"), + FindingSpec("2️⃣", message: "replace '[Double]()' with '[]'"), + FindingSpec("3️⃣", message: "replace '[String]()' with ': [String] = []'"), + FindingSpec("4️⃣", message: "replace '[Category]()' with '[]'"), + FindingSpec("5️⃣", message: "replace '[(Int, Array)]()' with ': [(Int, Array)] = []'"), + FindingSpec("6️⃣", message: "replace '[(String, Int, Float)]()' with '[]'"), + FindingSpec("7️⃣", message: "replace '[A]()' with '[]'"), + FindingSpec("8️⃣", message: "replace '[(Int, B)]()' with '[]'"), + ] + ) + } + + func testDictionary() { + assertFormatting( + AlwaysUseLiteralForEmptyCollectionInit.self, + input: """ + public struct Test { + var value1 = 1️⃣[Int: String]() + + func test(v: [Double: Int] = 2️⃣[Double: Int]()) { + let _ = 3️⃣[String: Int]() + } + } + + var _: [Category: String] = 4️⃣[Category: String]() + let _ = 5️⃣[(Int, Array): Int]() + let _: [String: (String, Int, Float)] = 6️⃣[String: (String, Int, Float)]() + + let _ = [String: (1, 2, String)]() + + class TestSubscript { + subscript(_: [A: Int] = 7️⃣[A: Int](), x: [(Int, B): String] = 8️⃣[(Int, B): String]()) { + } + } + + // All of the examples in this block could be re-written to use leading-dot syntax: `.init(...)` + do { + let _ = [String: Int](minimumCapacity: 42) + let _: [String: Int] = [String: Int](minimumCapacity: 42) + + func testDefault(_ x: [Int: String] = [String](minimumCapacity: 1)) { + } + + class TestSubscript { + subscript(_: Int = 42, x: [String: (Int, B)] = [String: (Int, B)](minimumCapacity: 2)) { + } + } + } + """, + expected: """ + public struct Test { + var value1: [Int: String] = [:] + + func test(v: [Double: Int] = [:]) { + let _: [String: Int] = [:] + } + } + + var _: [Category: String] = [:] + let _: [(Int, Array): Int] = [:] + let _: [String: (String, Int, Float)] = [:] + + let _ = [String: (1, 2, String)]() + + class TestSubscript { + subscript(_: [A: Int] = [:], x: [(Int, B): String] = [:]) { + } + } + + // All of the examples in this block could be re-written to use leading-dot syntax: `.init(...)` + do { + let _ = [String: Int](minimumCapacity: 42) + let _: [String: Int] = [String: Int](minimumCapacity: 42) + + func testDefault(_ x: [Int: String] = [String](minimumCapacity: 1)) { + } + + class TestSubscript { + subscript(_: Int = 42, x: [String: (Int, B)] = [String: (Int, B)](minimumCapacity: 2)) { + } + } + } + """, + findings: [ + FindingSpec("1️⃣", message: "replace '[Int: String]()' with ': [Int: String] = [:]'"), + FindingSpec("2️⃣", message: "replace '[Double: Int]()' with '[:]'"), + FindingSpec("3️⃣", message: "replace '[String: Int]()' with ': [String: Int] = [:]'"), + FindingSpec("4️⃣", message: "replace '[Category: String]()' with '[:]'"), + FindingSpec("5️⃣", message: "replace '[(Int, Array): Int]()' with ': [(Int, Array): Int] = [:]'"), + FindingSpec("6️⃣", message: "replace '[String: (String, Int, Float)]()' with '[:]'"), + FindingSpec("7️⃣", message: "replace '[A: Int]()' with '[:]'"), + FindingSpec("8️⃣", message: "replace '[(Int, B): String]()' with '[:]'"), + ] + ) + } +} 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, diff --git a/Tests/SwiftFormatTests/Rules/UseShorthandTypeNamesTests.swift b/Tests/SwiftFormatTests/Rules/UseShorthandTypeNamesTests.swift index a54b98a9e..ce1f3d797 100644 --- a/Tests/SwiftFormatTests/Rules/UseShorthandTypeNamesTests.swift +++ b/Tests/SwiftFormatTests/Rules/UseShorthandTypeNamesTests.swift @@ -692,4 +692,44 @@ final class UseShorthandTypeNamesTests: LintOrFormatRuleTestCase { ] ) } + + func testAttributedTypesInOptionalsAreParenthesized() { + // If we need to insert parentheses, verify that we do, but also verify that we don't insert + // them unnecessarily. + assertFormatting( + UseShorthandTypeNames.self, + input: """ + var x: 1️⃣Optional = S() + var y: 2️⃣Optional<@Sendable (Int) -> Void> = S() + var z = [3️⃣Optional]([S()]) + var a = [4️⃣Optional<@Sendable (Int) -> Void>]([S()]) + + var x: 5️⃣Optional<(consuming P)> = S() + var y: 6️⃣Optional<(@Sendable (Int) -> Void)> = S() + var z = [7️⃣Optional<(consuming P)>]([S()]) + var a = [8️⃣Optional<(@Sendable (Int) -> Void)>]([S()]) + """, + expected: """ + var x: (consuming P)? = S() + var y: (@Sendable (Int) -> Void)? = S() + var z = [(consuming P)?]([S()]) + var a = [(@Sendable (Int) -> Void)?]([S()]) + + var x: (consuming P)? = S() + var y: (@Sendable (Int) -> Void)? = S() + var z = [(consuming P)?]([S()]) + var a = [(@Sendable (Int) -> Void)?]([S()]) + """, + findings: [ + FindingSpec("1️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("2️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("3️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("4️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("5️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("6️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("7️⃣", message: "use shorthand syntax for this 'Optional' type"), + FindingSpec("8️⃣", message: "use shorthand syntax for this 'Optional' type"), + ] + ) + } }