Skip to content

Add a StandardizeDocumentationComments rule #959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
19 changes: 18 additions & 1 deletion Documentation/RuleDocumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

Expand Down Expand Up @@ -43,6 +43,7 @@ Here's the list of available rules:
- [OrderedImports](#OrderedImports)
- [ReplaceForEachWithForLoop](#ReplaceForEachWithForLoop)
- [ReturnVoidInsteadOfEmptyTuple](#ReturnVoidInsteadOfEmptyTuple)
- [StandardizeDocumentationComments](#StandardizeDocumentationComments)
- [TypeNamesShouldBeCapitalized](#TypeNamesShouldBeCapitalized)
- [UseEarlyExits](#UseEarlyExits)
- [UseExplicitNilCheckInConditions](#UseExplicitNilCheckInConditions)
Expand Down Expand Up @@ -440,6 +441,22 @@ Format: `-> ()` is replaced with `-> Void`

`ReturnVoidInsteadOfEmptyTuple` rule can format your code automatically.

### StandardizeDocumentationComments

Reformats documentation comments to a standard structure.

Format: Documentation is reflowed in a standard format:
- All documentation comments are rendered as `///`-prefixed.
- Documentation comments are re-wrapped to the preferred line length.
- The order of elements in a documentation comment is standard:
- Abstract
- Discussion w/ paragraphs, code samples, lists, etc.
- Param docs (outlined if > 1)
- Return docs
- Throw docs

`StandardizeDocumentationComments` rule can format your code automatically.

### TypeNamesShouldBeCapitalized

`struct`, `class`, `enum` and `protocol` declarations should have a capitalized name.
Expand Down
146 changes: 142 additions & 4 deletions Sources/SwiftFormat/Core/DocumentationComment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ public struct DocumentationComment {
}

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

Expand Down Expand Up @@ -129,8 +130,11 @@ public struct DocumentationComment {

extractSimpleFields(from: &list)

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

bodyNodes.append(child.detachedFromParent)
Expand Down Expand Up @@ -344,3 +348,137 @@ private struct SimpleFieldMarkupRewriter: MarkupRewriter {
return Text(String(nameAndRemainder[1]))
}
}

extension DocumentationComment {
/// Returns a trivia collection containing this documentation comment,
/// formatted and rewrapped to the given line width.
///
/// - Parameters:
/// - lineWidth: The expected line width, including leading spaces, the
/// triple-slash prefix, and the documentation text.
/// - joiningTrivia: The trivia to put between each line of documentation
/// text. `joiningTrivia` must include a `.newlines` trivia piece.
/// - Returns: A trivia collection that represents this documentation comment
/// in standardized form.
func renderForSource(lineWidth: Int, joiningTrivia: some Collection<TriviaPiece>) -> Trivia {
// The width of the prefix is 4 (`/// `) plus the number of spaces in `joiningTrivia`.
let prefixWidth =
4
+ joiningTrivia.map {
switch $0 {
case .spaces(let n): n
default: 0
}
}.reduce(0, +)

let options = MarkupFormatter.Options(
orderedListNumerals: .incrementing(start: 1),
preferredLineLimit: .init(maxLength: lineWidth - prefixWidth, breakWith: .softBreak)
)

var strings: [String] = []
if let briefSummary {
strings.append(
contentsOf: briefSummary.formatForSource(options: options)
)
}

if !bodyNodes.isEmpty {
if !strings.isEmpty { strings.append("") }

let renderedBody = bodyNodes.map {
$0.formatForSource(options: options)
}.joined(separator: [""])
strings.append(contentsOf: renderedBody)
}

// Empty line between discussion and the params/returns/throws documentation.
if !strings.isEmpty && (!parameters.isEmpty || returns != nil || `throws` != nil) {
strings.append("")
}

// FIXME: Need to recurse rather than only using the `briefSummary`
switch parameters.count {
case 0: break
case 1:
// Output a single parameter item.
let summary = parameters[0].comment.briefSummary ?? Paragraph()
let summaryWithLabel =
summary
.prefixed(with: "Parameter \(parameters[0].name):")
let list = UnorderedList([ListItem(summaryWithLabel)])
strings.append(contentsOf: list.formatForSource(options: options))

default:
// Build the list of parameters.
let paramItems = parameters.map { parameter in
let summary = parameter.comment.briefSummary ?? Paragraph()
let summaryWithLabel =
summary
.prefixed(with: "\(parameter.name):")
return ListItem(summaryWithLabel)
}
let paramList = UnorderedList(paramItems)

// Create a list with a single item: the label, followed by the list of parameters.
let listItem = ListItem(
Paragraph(Text("Parameters:")),
paramList
)
strings.append(
contentsOf: UnorderedList(listItem).formatForSource(options: options)
)
}

if let returns {
let returnsWithLabel = returns.prefixed(with: "Returns:")
let list = UnorderedList([ListItem(returnsWithLabel)])
strings.append(contentsOf: list.formatForSource(options: options))
}

if let `throws` {
let throwsWithLabel = `throws`.prefixed(with: "Throws:")
let list = UnorderedList([ListItem(throwsWithLabel)])
strings.append(contentsOf: list.formatForSource(options: options))
}

// Convert the pieces into trivia, then join them with the provided spacing.
let pieces = strings.map {
$0.isEmpty
? TriviaPiece.docLineComment("///")
: TriviaPiece.docLineComment("/// " + $0)
}
let spacedPieces: [TriviaPiece] = pieces.reduce(into: []) { result, piece in
result.append(piece)
result.append(contentsOf: joiningTrivia)
}

return Trivia(pieces: spacedPieces)
}
}

extension Markup {
func formatForSource(options: MarkupFormatter.Options) -> [String] {
format(options: options)
.split(separator: "\n", omittingEmptySubsequences: false)
.map { $0.trimmingTrailingWhitespace() }
}
}

extension Paragraph {
func prefixed(with str: String) -> Paragraph {
struct ParagraphPrefixMarkupRewriter: MarkupRewriter {
/// The list item to which the rewriter will be applied.
let prefix: String

mutating func visitText(_ text: Text) -> Markup? {
// Only manipulate the first text node (of the first paragraph).
guard text.indexInParent == 0 else { return text }
return Text(String(prefix + text.string))
}
}

var rewriter = ParagraphPrefixMarkupRewriter(prefix: str)
return self.accept(&rewriter) as? Paragraph ?? self
}
}
Loading
Loading