Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PerformanceTests/PerformanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ class PerformanceTests: XCTestCase {
hoistPatternLet: false,
elseOnNextLine: true,
explicitSelf: .insert,
experimentalRules: true
experimentalRules: true,
breakLineAtEndOfTypes: true
)
measure {
_ = tokens.map { try! format($0, options: options) }
Expand Down
26 changes: 24 additions & 2 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,11 @@ Option | Description

## blankLinesAtEndOfScope

Remove trailing blank line at the end of a scope.
Remove or insert trailing blank line at the end of a scope.

Option | Description
--- | ---
`--typeblanklines` | breakLine: "remove" (default), "insert", or "preserve"

<details>
<summary>Examples</summary>
Expand Down Expand Up @@ -440,6 +444,15 @@ Remove trailing blank line at the end of a scope.
]
```

With `--typeblanklines insert`:

```diff
struct Foo {
let bar: Bar
+
}
```

</details>
<br/>

Expand All @@ -449,7 +462,7 @@ Remove leading blank line at the start of a scope.

Option | Description
--- | ---
`--typeblanklines` | "remove" (default) or "preserve" blank lines from types
`--typeblanklines` | breakLine: "remove" (default), "insert", or "preserve"

<details>
<summary>Examples</summary>
Expand Down Expand Up @@ -480,6 +493,15 @@ Option | Description
]
```

With `--typeblanklines insert`:

```diff
struct Foo {
+
let bar: Bar
}
```

</details>
<br/>

Expand Down
34 changes: 3 additions & 31 deletions Sources/Declaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,43 +387,15 @@ extension Declaration {
}

/// Ensures that this declaration ends with at least one trailing blank line,
/// by a blank like to the end of this declaration if not already present.
/// by adding a blank line to the end of this declaration if not already present.
func addTrailingBlankLineIfNeeded() {
while tokens.numberOfTrailingLinebreaks() < 2 {
formatter.insertLinebreak(at: range.upperBound)
}
formatter.addTrailingBlankLineIfNeeded(in: range)
}

/// Ensures that this declaration doesn't end with a trailing blank line
/// by removing any trailing blank lines.
func removeTrailingBlankLinesIfPresent() {
while tokens.numberOfTrailingLinebreaks() > 1 {
guard let lastNewlineIndex = formatter.lastIndex(of: .linebreak, in: Range(range)) else { break }
formatter.removeTokens(in: lastNewlineIndex ... range.upperBound)
}
}
}

extension RandomAccessCollection where Element == Token, Index == Int {
// The number of trailing newlines in this array of tokens,
// taking into account any spaces that may be between the linebreaks.
func numberOfTrailingLinebreaks() -> Int {
guard !isEmpty else { return 0 }

var numberOfTrailingLinebreaks = 0
var searchIndex = indices.last!

while searchIndex >= indices.first!,
self[searchIndex].isSpaceOrLinebreak
{
if self[searchIndex].isLinebreak {
numberOfTrailingLinebreaks += 1
}

searchIndex -= 1
}

return numberOfTrailingLinebreaks
formatter.removeTrailingBlankLinesIfPresent(in: range)
}
}

Expand Down
83 changes: 83 additions & 0 deletions Sources/FormattingHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2920,6 +2920,89 @@ extension Formatter {
usingDynamicLookup: false, classOrStatic: false,
isTypeRoot: false, isInit: false)
}

/// Ensures that the given range ends with at least one trailing blank line,
/// by adding a blank line to the end of this declaration if not already present.
func addTrailingBlankLineIfNeeded(in range: ClosedRange<Int>) {
let range = range.autoUpdating(in: self)
while tokens[range.range].numberOfTrailingLinebreaks() < 2 {
insertLinebreak(at: range.upperBound)
}
}

/// Ensures that given range doesn't end with a trailing blank line
/// by removing any trailing blank lines.
func removeTrailingBlankLinesIfPresent(in range: ClosedRange<Int>) {
let range = range.autoUpdating(in: self)
while tokens[range.range].numberOfTrailingLinebreaks() > 1 {
guard let lastNewlineIndex = lastIndex(of: .linebreak, in: Range(range.range)) else { break }

removeToken(at: lastNewlineIndex)
}
}

/// Ensures that given range starts with at least one leading blank line,
/// by adding blank like to the start of this declaration if not already present.
func addLeadingBlankLineIfNeeded(in range: ClosedRange<Int>) {
let range = range.autoUpdating(in: self)
while tokens[range.range].numberOfLeadingLinebreaks() < 2 {
insertLinebreak(at: range.lowerBound)
}
}

/// Ensures that the given range doesn't end with a trailing blank line
/// by removing any trailing blank lines.
func removeLeadingBlankLinesIfPresent(in range: ClosedRange<Int>) {
let range = range.autoUpdating(in: self)
while tokens[range.range].numberOfLeadingLinebreaks() > 1 {
guard let firstNewlineIndex = index(of: .linebreak, in: Range(range.range)) else { break }
removeTokens(in: range.lowerBound ... firstNewlineIndex)
}
}
}

extension RandomAccessCollection where Element == Token, Index == Int {
// The number of trailing newlines in this array of tokens,
// taking into account any spaces that may be between the linebreaks.
func numberOfLeadingLinebreaks() -> Int {
guard !isEmpty else { return 0 }

var numberOfLeadingLinebreaks = 0
var searchIndex = indices.first!

while searchIndex <= indices.last!,
self[searchIndex].isSpaceOrLinebreak
{
if self[searchIndex].isLinebreak {
numberOfLeadingLinebreaks += 1
}

searchIndex += 1
}

return numberOfLeadingLinebreaks
}

// The number of trailing newlines in this array of tokens,
// taking into account any spaces that may be between the linebreaks.
func numberOfTrailingLinebreaks() -> Int {
guard !isEmpty else { return 0 }

var numberOfTrailingLinebreaks = 0
var searchIndex = indices.last!

while searchIndex >= indices.first!,
self[searchIndex].isSpaceOrLinebreak
{
if self[searchIndex].isLinebreak {
numberOfTrailingLinebreaks += 1
}

searchIndex -= 1
}

return numberOfTrailingLinebreaks
}
}

extension Date {
Expand Down
10 changes: 4 additions & 6 deletions Sources/OptionDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1154,13 +1154,11 @@ struct _Descriptors {
help: "Change type to enum: \"always\" (default) or \"structs-only\"",
keyPath: \.enumNamespaces
)
let removeStartOrEndBlankLinesFromTypes = OptionDescriptor(
let typeBlankLines = OptionDescriptor(
argumentName: "typeblanklines",
displayName: "Remove blank lines from types",
help: "\"remove\" (default) or \"preserve\" blank lines from types",
keyPath: \.removeStartOrEndBlankLinesFromTypes,
trueValues: ["remove"],
falseValues: ["preserve"]
displayName: "Blank lines types",
help: "breakLine: \"remove\" (default), \"insert\", or \"preserve\"",
keyPath: \.typeBlankLines
)
let genericTypes = OptionDescriptor(
argumentName: "generictypes",
Expand Down
15 changes: 11 additions & 4 deletions Sources/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,13 @@ public enum DeclarationOrganizationMode: String, CaseIterable {
case type
}

/// Whether to insert or remove blank lines from the start / end of type bodies
public enum TypeBlankLines: String, CaseIterable {
case remove
case insert
case preserve
}

/// Format to use when printing dates
public enum DateFormat: Equatable, RawRepresentable, CustomStringConvertible {
case dayMonthYear
Expand Down Expand Up @@ -731,7 +738,7 @@ public struct FormatOptions: CustomStringConvertible {
public var indentStrings: Bool
public var closureVoidReturn: ClosureVoidReturn
public var enumNamespaces: EnumNamespacesMode
public var removeStartOrEndBlankLinesFromTypes: Bool
public var typeBlankLines: TypeBlankLines
public var genericTypes: String
public var useSomeAny: Bool
public var wrapEffects: WrapEffects
Expand Down Expand Up @@ -863,7 +870,7 @@ public struct FormatOptions: CustomStringConvertible {
indentStrings: Bool = false,
closureVoidReturn: ClosureVoidReturn = .remove,
enumNamespaces: EnumNamespacesMode = .always,
removeStartOrEndBlankLinesFromTypes: Bool = true,
typeBlankLines: TypeBlankLines = .remove,
genericTypes: String = "",
useSomeAny: Bool = true,
wrapEffects: WrapEffects = .preserve,
Expand Down Expand Up @@ -898,7 +905,6 @@ public struct FormatOptions: CustomStringConvertible {
self.useVoid = useVoid
self.indentCase = indentCase
self.trailingCommas = trailingCommas
self.indentComments = indentComments
self.truncateBlankLines = truncateBlankLines
self.insertBlankLines = insertBlankLines
self.removeBlankLines = removeBlankLines
Expand Down Expand Up @@ -985,7 +991,7 @@ public struct FormatOptions: CustomStringConvertible {
self.indentStrings = indentStrings
self.closureVoidReturn = closureVoidReturn
self.enumNamespaces = enumNamespaces
self.removeStartOrEndBlankLinesFromTypes = removeStartOrEndBlankLinesFromTypes
self.typeBlankLines = typeBlankLines
self.genericTypes = genericTypes
self.useSomeAny = useSomeAny
self.wrapEffects = wrapEffects
Expand All @@ -1004,6 +1010,7 @@ public struct FormatOptions: CustomStringConvertible {
self.preferFileMacro = preferFileMacro
self.lineBetweenConsecutiveGuards = lineBetweenConsecutiveGuards
// Doesn't really belong here, but hard to put elsewhere
self.indentComments = indentComments
self.fragment = fragment
self.ignoreConflictMarkers = ignoreConflictMarkers
self.swiftVersion = swiftVersion
Expand Down
11 changes: 11 additions & 0 deletions Sources/ParsingHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,17 @@ extension Formatter {
}
}

/// Whether the given index is a `startOfScope("{")` that represents the start of a type body
func isStartOfTypeBody(at scopeIndex: Int) -> Bool {
guard tokens[scopeIndex] == .startOfScope("{") else { return false }

guard let lastKeyword = lastSignificantKeyword(at: scopeIndex, excluding: ["where"]) else {
return false
}

return Token.swiftTypeKeywords.contains(lastKeyword)
}

func isTrailingClosureLabel(at i: Int) -> Bool {
if case .identifier? = token(at: i),
last(.nonSpaceOrCommentOrLinebreak, before: i) == .endOfScope("}"),
Expand Down
63 changes: 30 additions & 33 deletions Sources/Rules/BlankLinesAtEndOfScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,39 @@ import Foundation
public extension FormatRule {
/// Remove blank lines immediately before a closing brace, bracket, paren or chevron
/// unless it's followed by more code on the same line (e.g. } else { )
/// Also insert blank lines before closing braces for type declarations if configured
static let blankLinesAtEndOfScope = FormatRule(
help: "Remove trailing blank line at the end of a scope.",
help: "Remove or insert trailing blank line at the end of a scope.",
options: ["typeblanklines"],
sharedOptions: ["typeblanklines"]
) { formatter in
formatter.forEach(.startOfScope) { startOfScopeIndex, _ in
guard let endOfScopeIndex = formatter.endOfScope(at: startOfScopeIndex) else { return }
let endOfScope = formatter.tokens[endOfScopeIndex]
formatter.forEach(.startOfScope) { startOfScope, token in
guard ["{", "(", "[", "<"].contains(token.string) else { return }

guard ["}", ")", "]", ">"].contains(endOfScope.string),
// If there is extra code after the closing scope on the same line, ignore it
(formatter.next(.nonSpaceOrComment, after: endOfScopeIndex).map(\.isLinebreak)) ?? true
guard let endOfScope = formatter.endOfScope(at: startOfScope),
formatter.index(of: .nonSpaceOrComment, after: startOfScope) != endOfScope
else { return }

// Consumers can choose whether or not this rule should apply to type bodies
if !formatter.options.removeStartOrEndBlankLinesFromTypes,
["class", "actor", "struct", "enum", "protocol", "extension"].contains(
formatter.lastSignificantKeyword(at: startOfScopeIndex, excluding: ["where"]))
// If there is extra code after the closing scope on the same line, ignore it
if let nextTokenAfterClosingScope = formatter.next(.nonSpaceOrComment, after: endOfScope),
!nextTokenAfterClosingScope.isLinebreak
{
return
}

// Find previous non-space token
var index = endOfScopeIndex - 1
var indexOfFirstLineBreak: Int?
var indexOfLastLineBreak: Int?
loop: while let token = formatter.token(at: index) {
switch token {
case .linebreak:
indexOfFirstLineBreak = index
if indexOfLastLineBreak == nil {
indexOfLastLineBreak = index
}
case .space:
let rangeInsideScope = ClosedRange(startOfScope + 1 ..< endOfScope)

if formatter.isStartOfTypeBody(at: startOfScope) {
switch formatter.options.typeBlankLines {
case .insert:
formatter.addTrailingBlankLineIfNeeded(in: rangeInsideScope)
case .remove:
formatter.removeTrailingBlankLinesIfPresent(in: rangeInsideScope)
case .preserve:
break
default:
break loop
}
index -= 1
}
if formatter.options.removeBlankLines,
let indexOfFirstLineBreak,
indexOfFirstLineBreak != indexOfLastLineBreak
{
formatter.removeTokens(in: indexOfFirstLineBreak ..< indexOfLastLineBreak!)
return
} else {
formatter.removeTrailingBlankLinesIfPresent(in: rangeInsideScope)
}
}
} examples: {
Expand Down Expand Up @@ -85,6 +73,15 @@ public extension FormatRule {
baz,
]
```

With `--typeblanklines insert`:

```diff
struct Foo {
let bar: Bar
+
}
```
"""
}
}
Loading