Skip to content

Commit b8e732f

Browse files
authored
Merge pull request #82 from kkebo/merge-upstream-main
2 parents 40aefde + 11b407a commit b8e732f

22 files changed

+970
-72
lines changed

Documentation/RuleDocumentation.md

+11
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Here's the list of available rules:
2929
- [NoAssignmentInExpressions](#NoAssignmentInExpressions)
3030
- [NoBlockComments](#NoBlockComments)
3131
- [NoCasesWithOnlyFallthrough](#NoCasesWithOnlyFallthrough)
32+
- [NoEmptyLinesOpeningClosingBraces](#NoEmptyLinesOpeningClosingBraces)
3233
- [NoEmptyTrailingClosureParentheses](#NoEmptyTrailingClosureParentheses)
3334
- [NoLabelsInCasePatterns](#NoLabelsInCasePatterns)
3435
- [NoLeadingUnderscores](#NoLeadingUnderscores)
@@ -271,6 +272,16 @@ Format: The fallthrough `case` is added as a prefix to the next case unless the
271272

272273
`NoCasesWithOnlyFallthrough` rule can format your code automatically.
273274

275+
### NoEmptyLinesOpeningClosingBraces
276+
277+
Empty lines are forbidden after opening braces and before closing braces.
278+
279+
Lint: Empty lines after opening braces and before closing braces yield a lint error.
280+
281+
Format: Empty lines after opening braces and before closing braces will be removed.
282+
283+
`NoEmptyLinesOpeningClosingBraces` rule can format your code automatically.
284+
274285
### NoEmptyTrailingClosureParentheses
275286

276287
Function calls with no arguments and a trailing closure should not have empty parentheses.

Sources/SwiftFormat/API/Configuration+Default.swift

+1
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ extension Configuration {
4040
self.spacesAroundRangeFormationOperators = false
4141
self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
4242
self.multiElementCollectionTrailingCommas = true
43+
self.reflowMultilineStringLiterals = .never
4344
}
4445
}

Sources/SwiftFormat/API/Configuration.swift

+71
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public struct Configuration: Codable, Equatable {
4545
case spacesAroundRangeFormationOperators
4646
case noAssignmentInExpressions
4747
case multiElementCollectionTrailingCommas
48+
case reflowMultilineStringLiterals
4849
}
4950

5051
/// A dictionary containing the default enabled/disabled states of rules, keyed by the rules'
@@ -194,6 +195,71 @@ public struct Configuration: Codable, Equatable {
194195
/// ```
195196
public var multiElementCollectionTrailingCommas: Bool
196197

198+
/// Determines how multiline string literals should reflow when formatted.
199+
public enum MultilineStringReflowBehavior: Codable {
200+
/// Never reflow multiline string literals.
201+
case never
202+
/// Reflow lines in string literal that exceed the maximum line length. For example with a line length of 10:
203+
/// ```swift
204+
/// """
205+
/// an escape\
206+
/// line break
207+
/// a hard line break
208+
/// """
209+
/// ```
210+
/// will be formatted as:
211+
/// ```swift
212+
/// """
213+
/// an esacpe\
214+
/// line break
215+
/// a hard \
216+
/// line break
217+
/// """
218+
/// ```
219+
/// The existing `\` is left in place, but the line over line length is broken.
220+
case onlyLinesOverLength
221+
/// Always reflow multiline string literals, this will ignore existing escaped newlines in the literal and reflow each line. Hard linebreaks are still respected.
222+
/// For example, with a line length of 10:
223+
/// ```swift
224+
/// """
225+
/// one \
226+
/// word \
227+
/// a line.
228+
/// this is too long.
229+
/// """
230+
/// ```
231+
/// will be formatted as:
232+
/// ```swift
233+
/// """
234+
/// one word \
235+
/// a line.
236+
/// this is \
237+
/// too long.
238+
/// """
239+
/// ```
240+
case always
241+
242+
var isNever: Bool {
243+
switch self {
244+
case .never:
245+
return true
246+
default:
247+
return false
248+
}
249+
}
250+
251+
var isAlways: Bool {
252+
switch self {
253+
case .always:
254+
return true
255+
default:
256+
return false
257+
}
258+
}
259+
}
260+
261+
public var reflowMultilineStringLiterals: MultilineStringReflowBehavior
262+
197263
/// Creates a new `Configuration` by loading it from a configuration file.
198264
public init(contentsOf url: URL) throws {
199265
let data = try Data(contentsOf: url)
@@ -287,6 +353,10 @@ public struct Configuration: Codable, Equatable {
287353
Bool.self, forKey: .multiElementCollectionTrailingCommas)
288354
?? defaults.multiElementCollectionTrailingCommas
289355

356+
self.reflowMultilineStringLiterals =
357+
try container.decodeIfPresent(MultilineStringReflowBehavior.self, forKey: .reflowMultilineStringLiterals)
358+
?? defaults.reflowMultilineStringLiterals
359+
290360
// If the `rules` key is not present at all, default it to the built-in set
291361
// so that the behavior is the same as if the configuration had been
292362
// default-initialized. To get an empty rules dictionary, one can explicitly
@@ -321,6 +391,7 @@ public struct Configuration: Codable, Equatable {
321391
try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels)
322392
try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions)
323393
try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas)
394+
try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals)
324395
try container.encode(rules, forKey: .rules)
325396
}
326397

Sources/SwiftFormat/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ add_library(SwiftFormat
7373
Rules/NoAssignmentInExpressions.swift
7474
Rules/NoBlockComments.swift
7575
Rules/NoCasesWithOnlyFallthrough.swift
76+
Rules/NoEmptyLineOpeningClosingBraces.swift
7677
Rules/NoEmptyTrailingClosureParentheses.swift
7778
Rules/NoLabelsInCasePatterns.swift
7879
Rules/NoLeadingUnderscores.swift

Sources/SwiftFormat/Core/Pipelines+Generated.swift

+19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class LintPipeline: SyntaxVisitor {
3636
super.init(viewMode: .sourceAccurate)
3737
}
3838

39+
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
40+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
41+
return .visitChildren
42+
}
43+
override func visitPost(_ node: AccessorBlockSyntax) {
44+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
45+
}
46+
3947
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
4048
visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node)
4149
visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node)
@@ -93,10 +101,12 @@ class LintPipeline: SyntaxVisitor {
93101
}
94102

95103
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
104+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
96105
visitIfEnabled(OmitExplicitReturns.visit, for: node)
97106
return .visitChildren
98107
}
99108
override func visitPost(_ node: ClosureExprSyntax) {
109+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
100110
onVisitPost(rule: OmitExplicitReturns.self, for: node)
101111
}
102112

@@ -134,10 +144,12 @@ class LintPipeline: SyntaxVisitor {
134144

135145
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
136146
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, for: node)
147+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
137148
return .visitChildren
138149
}
139150
override func visitPost(_ node: CodeBlockSyntax) {
140151
onVisitPost(rule: AmbiguousTrailingClosureOverload.self, for: node)
152+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
141153
}
142154

143155
override func visit(_ node: ConditionElementSyntax) -> SyntaxVisitorContinueKind {
@@ -384,10 +396,12 @@ class LintPipeline: SyntaxVisitor {
384396

385397
override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind {
386398
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, for: node)
399+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
387400
return .visitChildren
388401
}
389402
override func visitPost(_ node: MemberBlockSyntax) {
390403
onVisitPost(rule: AmbiguousTrailingClosureOverload.self, for: node)
404+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
391405
}
392406

393407
override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind {
@@ -411,10 +425,12 @@ class LintPipeline: SyntaxVisitor {
411425
}
412426

413427
override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind {
428+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
414429
visitIfEnabled(NoLeadingUnderscores.visit, for: node)
415430
return .visitChildren
416431
}
417432
override func visitPost(_ node: PrecedenceGroupDeclSyntax) {
433+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
418434
onVisitPost(rule: NoLeadingUnderscores.self, for: node)
419435
}
420436

@@ -511,10 +527,12 @@ class LintPipeline: SyntaxVisitor {
511527
}
512528

513529
override func visit(_ node: SwitchExprSyntax) -> SyntaxVisitorContinueKind {
530+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
514531
visitIfEnabled(NoParensAroundConditions.visit, for: node)
515532
return .visitChildren
516533
}
517534
override func visitPost(_ node: SwitchExprSyntax) {
535+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
518536
onVisitPost(rule: NoParensAroundConditions.self, for: node)
519537
}
520538

@@ -597,6 +615,7 @@ extension FormatPipeline {
597615
node = NoAccessLevelOnExtensionDeclaration(context: context).rewrite(node)
598616
node = NoAssignmentInExpressions(context: context).rewrite(node)
599617
node = NoCasesWithOnlyFallthrough(context: context).rewrite(node)
618+
node = NoEmptyLinesOpeningClosingBraces(context: context).rewrite(node)
600619
node = NoEmptyTrailingClosureParentheses(context: context).rewrite(node)
601620
node = NoLabelsInCasePatterns(context: context).rewrite(node)
602621
node = NoParensAroundConditions(context: context).rewrite(node)

Sources/SwiftFormat/Core/RuleNameCache+Generated.swift

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [
3434
ObjectIdentifier(NoAssignmentInExpressions.self): "NoAssignmentInExpressions",
3535
ObjectIdentifier(NoBlockComments.self): "NoBlockComments",
3636
ObjectIdentifier(NoCasesWithOnlyFallthrough.self): "NoCasesWithOnlyFallthrough",
37+
ObjectIdentifier(NoEmptyLinesOpeningClosingBraces.self): "NoEmptyLinesOpeningClosingBraces",
3738
ObjectIdentifier(NoEmptyTrailingClosureParentheses.self): "NoEmptyTrailingClosureParentheses",
3839
ObjectIdentifier(NoLabelsInCasePatterns.self): "NoLabelsInCasePatterns",
3940
ObjectIdentifier(NoLeadingUnderscores.self): "NoLeadingUnderscores",

Sources/SwiftFormat/Core/RuleRegistry+Generated.swift

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"NoAssignmentInExpressions": true,
3434
"NoBlockComments": true,
3535
"NoCasesWithOnlyFallthrough": true,
36+
"NoEmptyLinesOpeningClosingBraces": false,
3637
"NoEmptyTrailingClosureParentheses": true,
3738
"NoLabelsInCasePatterns": true,
3839
"NoLeadingUnderscores": false,

Sources/SwiftFormat/PrettyPrint/Comment.swift

+23-1
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ struct Comment {
5858
let kind: Kind
5959
var text: [String]
6060
var length: Int
61+
// what was the leading indentation, if any, that preceded this comment?
62+
var leadingIndent: Indent?
6163

62-
init(kind: Kind, text: String) {
64+
init(kind: Kind, leadingIndent: Indent?, text: String) {
6365
self.kind = kind
66+
self.leadingIndent = leadingIndent
6467

6568
switch kind {
6669
case .line, .docLine:
@@ -93,6 +96,25 @@ struct Comment {
9396
return kind.prefix + trimmedLines.joined(separator: separator)
9497
case .block, .docBlock:
9598
let separator = "\n"
99+
100+
// if all the lines after the first matching leadingIndent, replace that prefix with the
101+
// current indentation level
102+
if let leadingIndent {
103+
let rest = self.text.dropFirst()
104+
105+
let hasLeading = rest.allSatisfy {
106+
let result = $0.hasPrefix(leadingIndent.text) || $0.isEmpty
107+
return result
108+
}
109+
if hasLeading, let first = self.text.first, !rest.isEmpty {
110+
let restStr = rest.map {
111+
let stripped = $0.dropFirst(leadingIndent.text.count)
112+
return indent.indentation() + stripped
113+
}.joined(separator: separator)
114+
return kind.prefix + first + separator + restStr + "*/"
115+
}
116+
}
117+
96118
return kind.prefix + self.text.joined(separator: separator) + "*/"
97119
}
98120
}

Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

+45-10
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ public class PrettyPrinter {
214214
switch token {
215215
case .contextualBreakingStart:
216216
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber))
217-
218217
// Discard the last finished breaking context to keep it from effecting breaks inside of the
219218
// new context. The discarded context has already either had an impact on the contextual break
220219
// after it or there was no relevant contextual break, so it's safe to discard.
@@ -414,7 +413,7 @@ public class PrettyPrinter {
414413

415414
var overrideBreakingSuppressed = false
416415
switch newline {
417-
case .elective: break
416+
case .elective, .escaped: break
418417
case .soft(_, let discretionary):
419418
// A discretionary newline (i.e. from the source) should create a line break even if the
420419
// rules for breaking are disabled.
@@ -429,6 +428,10 @@ public class PrettyPrinter {
429428
let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed
430429
if !suppressBreaking && (!canFit(length) || mustBreak) {
431430
currentLineIsContinuation = isContinuationIfBreakFires
431+
if case .escaped = newline {
432+
outputBuffer.enqueueSpaces(size)
433+
outputBuffer.write("\\")
434+
}
432435
outputBuffer.writeNewlines(newline)
433436
lastBreak = true
434437
} else {
@@ -594,19 +597,51 @@ public class PrettyPrinter {
594597
// Break lengths are equal to its size plus the token or group following it. Calculate the
595598
// length of any prior break tokens.
596599
case .break(_, let size, let newline):
597-
if let index = delimIndexStack.last, case .break = tokens[index] {
598-
lengths[index] += total
600+
if let index = delimIndexStack.last, case .break(_, _, let lastNewline) = tokens[index] {
601+
/// If the last break and this break are both `.escaped` we add an extra 1 to the total for the last `.escaped` break.
602+
/// This is to handle situations where adding the `\` for an escaped line break would put us over the line length.
603+
/// For example, consider the token sequence:
604+
/// `[.syntax("this fits"), .break(.escaped), .syntax("this fits in line length"), .break(.escaped)]`
605+
/// The naive layout of these tokens will incorrectly print as:
606+
/// """
607+
/// this fits this fits in line length \
608+
/// """
609+
/// which will be too long because of the '\' character. Instead we have to print it as:
610+
/// """
611+
/// this fits \
612+
/// this fits in line length
613+
/// """
614+
///
615+
/// While not prematurely inserting a line in situations where a hard line break is occurring, such as:
616+
///
617+
/// `[.syntax("some text"), .break(.escaped), .syntax("this is exactly the right length"), .break(.hard)]`
618+
///
619+
/// We want this to print as:
620+
/// """
621+
/// some text this is exactly the right length
622+
/// """
623+
/// and not:
624+
/// """
625+
/// some text \
626+
/// this is exactly the right length
627+
/// """
628+
if case .escaped = newline, case .escaped = lastNewline {
629+
lengths[index] += total + 1
630+
} else {
631+
lengths[index] += total
632+
}
599633
delimIndexStack.removeLast()
600634
}
601635
lengths.append(-total)
602636
delimIndexStack.append(i)
603637

604-
if case .elective = newline {
605-
total += size
606-
} else {
607-
// `size` is never used in this case, because the break always fires. Use `maxLineLength`
608-
// to ensure enclosing groups are large enough to force preceding breaks to fire.
609-
total += maxLineLength
638+
switch newline {
639+
case .elective, .escaped:
640+
total += size
641+
default:
642+
// `size` is never used in this case, because the break always fires. Use `maxLineLength`
643+
// to ensure enclosing groups are large enough to force preceding breaks to fire.
644+
total += maxLineLength
610645
}
611646

612647
// Space tokens have a length equal to its size.

Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ struct PrettyPrintBuffer {
8181
numberToPrint = min(count, maximumBlankLines + 1) - consecutiveNewlineCount
8282
case .hard(let count):
8383
numberToPrint = count
84+
case .escaped:
85+
numberToPrint = 1
8486
}
8587

8688
guard numberToPrint > 0 else { return }

Sources/SwiftFormat/PrettyPrint/Token.swift

+4
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ enum NewlineBehavior {
147147
/// newlines and the configured maximum number of blank lines.
148148
case hard(count: Int)
149149

150+
/// Break onto a new line is allowed if neccessary. If a line break is emitted, it will be escaped with a '\', and this breaks whitespace will be printed prior to the
151+
/// escaped line break. This is useful in multiline strings where we don't want newlines printed in syntax to appear in the literal.
152+
case escaped
153+
150154
/// An elective newline that respects discretionary newlines from the user-entered text.
151155
static let elective = NewlineBehavior.elective(ignoresDiscretionary: false)
152156

0 commit comments

Comments
 (0)