Skip to content

Commit 63b8089

Browse files
committed
Add a StandardizeDocumentationComments rule
This adds a formatting rule that rewraps and restructures documentation comments to be in a consistent order and format. Other comments are left unchanged.
1 parent cc1911b commit 63b8089

File tree

7 files changed

+658
-5
lines changed

7 files changed

+658
-5
lines changed

Documentation/RuleDocumentation.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Use the rules below in the `rules` block of your `.swift-format`
66
configuration file, as described in
7-
[Configuration](Configuration.md). All of these rules can be
7+
[Configuration](Documentation/Configuration.md). All of these rules can be
88
applied in the linter, but only some of them can format your source code
99
automatically.
1010

@@ -43,6 +43,7 @@ Here's the list of available rules:
4343
- [OrderedImports](#OrderedImports)
4444
- [ReplaceForEachWithForLoop](#ReplaceForEachWithForLoop)
4545
- [ReturnVoidInsteadOfEmptyTuple](#ReturnVoidInsteadOfEmptyTuple)
46+
- [StandardizeDocumentationComments](#StandardizeDocumentationComments)
4647
- [TypeNamesShouldBeCapitalized](#TypeNamesShouldBeCapitalized)
4748
- [UseEarlyExits](#UseEarlyExits)
4849
- [UseExplicitNilCheckInConditions](#UseExplicitNilCheckInConditions)
@@ -440,6 +441,22 @@ Format: `-> ()` is replaced with `-> Void`
440441

441442
`ReturnVoidInsteadOfEmptyTuple` rule can format your code automatically.
442443

444+
### StandardizeDocumentationComments
445+
446+
Reformats documentation comments to a standard structure.
447+
448+
Format: Documentation is reflowed in a standard format:
449+
- All documentation comments are rendered as `///`-prefixed.
450+
- Documentation comments are re-wrapped to the preferred line length.
451+
- The order of elements in a documentation comment is standard:
452+
- Abstract
453+
- Discussion w/ paragraphs, code samples, lists, etc.
454+
- Param docs (outlined if > 1)
455+
- Return docs
456+
- Throw docs
457+
458+
`StandardizeDocumentationComments` rule can format your code automatically.
459+
443460
### TypeNamesShouldBeCapitalized
444461

445462
`struct`, `class`, `enum` and `protocol` declarations should have a capitalized name.

Sources/SwiftFormat/Core/DocumentationComment.swift

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ public struct DocumentationComment {
8888
}
8989

9090
// Disable smart quotes and dash conversion since we want to preserve the original content of
91-
// the comments instead of doing documentation generation.
92-
let doc = Document(parsing: commentInfo.text, options: [.disableSmartOpts])
91+
// the comments instead of doing documentation generation. For the same reason, parse
92+
// symbol links to preserve the double-backtick delimiters.
93+
let doc = Document(parsing: commentInfo.text, options: [.disableSmartOpts, .parseSymbolLinks])
9394
self.init(markup: doc)
9495
}
9596

@@ -129,8 +130,11 @@ public struct DocumentationComment {
129130

130131
extractSimpleFields(from: &list)
131132

132-
// If the list is now empty, don't add it to the body nodes below.
133-
guard !list.isEmpty else { continue }
133+
// Add the list if non-empty, then `continue` so that we don't add the original node.
134+
if !list.isEmpty {
135+
bodyNodes.append(list)
136+
}
137+
continue
134138
}
135139

136140
bodyNodes.append(child.detachedFromParent)
@@ -344,3 +348,137 @@ private struct SimpleFieldMarkupRewriter: MarkupRewriter {
344348
return Text(String(nameAndRemainder[1]))
345349
}
346350
}
351+
352+
extension DocumentationComment {
353+
/// Returns a trivia collection containing this documentation comment,
354+
/// formatted and rewrapped to the given line width.
355+
///
356+
/// - Parameters:
357+
/// - lineWidth: The expected line width, including leading spaces, the
358+
/// triple-slash prefix, and the documentation text.
359+
/// - joiningTrivia: The trivia to put between each line of documentation
360+
/// text. `joiningTrivia` must include a `.newlines` trivia piece.
361+
/// - Returns: A trivia collection that represents this documentation comment
362+
/// in standardized form.
363+
func renderForSource(lineWidth: Int, joiningTrivia: some Collection<TriviaPiece>) -> Trivia {
364+
// The width of the prefix is 4 (`/// `) plus the number of spaces in `joiningTrivia`.
365+
let prefixWidth =
366+
4
367+
+ joiningTrivia.map {
368+
switch $0 {
369+
case .spaces(let n): n
370+
default: 0
371+
}
372+
}.reduce(0, +)
373+
374+
let options = MarkupFormatter.Options(
375+
orderedListNumerals: .incrementing(start: 1),
376+
preferredLineLimit: .init(maxLength: lineWidth - prefixWidth, breakWith: .softBreak)
377+
)
378+
379+
var strings: [String] = []
380+
if let briefSummary {
381+
strings.append(
382+
contentsOf: briefSummary.formatForSource(options: options)
383+
)
384+
}
385+
386+
if !bodyNodes.isEmpty {
387+
if !strings.isEmpty { strings.append("") }
388+
389+
let renderedBody = bodyNodes.map {
390+
$0.formatForSource(options: options)
391+
}.joined(separator: [""])
392+
strings.append(contentsOf: renderedBody)
393+
}
394+
395+
// Empty line between discussion and the params/returns/throws documentation.
396+
if !strings.isEmpty && (!parameters.isEmpty || returns != nil || `throws` != nil) {
397+
strings.append("")
398+
}
399+
400+
// FIXME: Need to recurse rather than only using the `briefSummary`
401+
switch parameters.count {
402+
case 0: break
403+
case 1:
404+
// Output a single parameter item.
405+
let summary = parameters[0].comment.briefSummary ?? Paragraph()
406+
let summaryWithLabel =
407+
summary
408+
.prefixed(with: "Parameter \(parameters[0].name):")
409+
let list = UnorderedList([ListItem(summaryWithLabel)])
410+
strings.append(contentsOf: list.formatForSource(options: options))
411+
412+
default:
413+
// Build the list of parameters.
414+
let paramItems = parameters.map { parameter in
415+
let summary = parameter.comment.briefSummary ?? Paragraph()
416+
let summaryWithLabel =
417+
summary
418+
.prefixed(with: "\(parameter.name):")
419+
return ListItem(summaryWithLabel)
420+
}
421+
let paramList = UnorderedList(paramItems)
422+
423+
// Create a list with a single item: the label, followed by the list of parameters.
424+
let listItem = ListItem(
425+
Paragraph(Text("Parameters:")),
426+
paramList
427+
)
428+
strings.append(
429+
contentsOf: UnorderedList(listItem).formatForSource(options: options)
430+
)
431+
}
432+
433+
if let returns {
434+
let returnsWithLabel = returns.prefixed(with: "Returns:")
435+
let list = UnorderedList([ListItem(returnsWithLabel)])
436+
strings.append(contentsOf: list.formatForSource(options: options))
437+
}
438+
439+
if let `throws` {
440+
let throwsWithLabel = `throws`.prefixed(with: "Throws:")
441+
let list = UnorderedList([ListItem(throwsWithLabel)])
442+
strings.append(contentsOf: list.formatForSource(options: options))
443+
}
444+
445+
// Convert the pieces into trivia, then join them with the provided spacing.
446+
let pieces = strings.map {
447+
$0.isEmpty
448+
? TriviaPiece.docLineComment("///")
449+
: TriviaPiece.docLineComment("/// " + $0)
450+
}
451+
let spacedPieces: [TriviaPiece] = pieces.reduce(into: []) { result, piece in
452+
result.append(piece)
453+
result.append(contentsOf: joiningTrivia)
454+
}
455+
456+
return Trivia(pieces: spacedPieces)
457+
}
458+
}
459+
460+
extension Markup {
461+
func formatForSource(options: MarkupFormatter.Options) -> [String] {
462+
format(options: options)
463+
.split(separator: "\n", omittingEmptySubsequences: false)
464+
.map { $0.trimmingTrailingWhitespace() }
465+
}
466+
}
467+
468+
extension Paragraph {
469+
func prefixed(with str: String) -> Paragraph {
470+
struct ParagraphPrefixMarkupRewriter: MarkupRewriter {
471+
/// The list item to which the rewriter will be applied.
472+
let prefix: String
473+
474+
mutating func visitText(_ text: Text) -> Markup? {
475+
// Only manipulate the first text node (of the first paragraph).
476+
guard text.indexInParent == 0 else { return text }
477+
return Text(String(prefix + text.string))
478+
}
479+
}
480+
481+
var rewriter = ParagraphPrefixMarkupRewriter(prefix: str)
482+
return self.accept(&rewriter) as? Paragraph ?? self
483+
}
484+
}

0 commit comments

Comments
 (0)