From 63b8089e70abdf5a64db35367b34b10ab9941c0b Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 7 Mar 2025 03:10:42 -0600 Subject: [PATCH 01/11] 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. --- Documentation/RuleDocumentation.md | 19 +- .../Core/DocumentationComment.swift | 146 ++++++++++- .../Core/Pipelines+Generated.swift | 109 ++++++++ .../Core/RuleNameCache+Generated.swift | 1 + .../Core/RuleRegistry+Generated.swift | 1 + .../StandardizeDocumentationComments.swift | 239 ++++++++++++++++++ ...tandardizeDocumentationCommentsTests.swift | 148 +++++++++++ 7 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift create mode 100644 Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index 0b626d5cf..e0ebf7b5c 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -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. @@ -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) @@ -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. diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index d89b4c1c6..434a408fe 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -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) } @@ -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) @@ -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) -> 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 + } +} diff --git a/Sources/SwiftFormat/Core/Pipelines+Generated.swift b/Sources/SwiftFormat/Core/Pipelines+Generated.swift index 8b0906e3e..b5566c141 100644 --- a/Sources/SwiftFormat/Core/Pipelines+Generated.swift +++ b/Sources/SwiftFormat/Core/Pipelines+Generated.swift @@ -44,13 +44,23 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) } + override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: AccessorDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) return .visitChildren } override func visitPost(_ node: ActorDeclSyntax) { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) } @@ -65,12 +75,14 @@ class LintPipeline: SyntaxVisitor { override func visit(_ node: AssociatedTypeDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) return .visitChildren } override func visitPost(_ node: AssociatedTypeDeclSyntax) { onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) } @@ -87,6 +99,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(AlwaysUseLowerCamelCase.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren @@ -96,6 +109,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: AlwaysUseLowerCamelCase.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -165,15 +179,33 @@ class LintPipeline: SyntaxVisitor { override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren } override func visitPost(_ node: DeinitializerDeclSyntax) { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } + override func visit(_ node: EditorPlaceholderDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: EditorPlaceholderDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + + override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: EnumCaseDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AlwaysUseLowerCamelCase.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) @@ -198,6 +230,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(FullyIndirectEnum.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) visitIfEnabled(OneCasePerLine.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren @@ -208,6 +241,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: FullyIndirectEnum.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) onVisitPost(rule: OneCasePerLine.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -215,12 +249,14 @@ class LintPipeline: SyntaxVisitor { override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AvoidRetroactiveConformances.visit, for: node) visitIfEnabled(NoAccessLevelOnExtensionDeclaration.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren } override func visitPost(_ node: ExtensionDeclSyntax) { onVisitPost(rule: AvoidRetroactiveConformances.self, for: node) onVisitPost(rule: NoAccessLevelOnExtensionDeclaration.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -260,6 +296,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) visitIfEnabled(OmitExplicitReturns.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) visitIfEnabled(ValidateDocumentationComments.visit, for: node) return .visitChildren @@ -270,6 +307,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) onVisitPost(rule: OmitExplicitReturns.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) onVisitPost(rule: ValidateDocumentationComments.self, for: node) } @@ -342,6 +380,14 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: UseShorthandTypeNames.self, for: node) } + override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: IfConfigDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: IfExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoParensAroundConditions.visit, for: node) return .visitChildren @@ -350,6 +396,14 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoParensAroundConditions.self, for: node) } + override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: ImportDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoAssignmentInExpressions.visit, for: node) return .visitChildren @@ -361,6 +415,7 @@ class LintPipeline: SyntaxVisitor { override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) visitIfEnabled(ValidateDocumentationComments.visit, for: node) return .visitChildren @@ -368,6 +423,7 @@ class LintPipeline: SyntaxVisitor { override func visitPost(_ node: InitializerDeclSyntax) { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) onVisitPost(rule: ValidateDocumentationComments.self, for: node) } @@ -380,6 +436,22 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: GroupNumericLiterals.self, for: node) } + override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: MacroDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + + override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: MacroExpansionDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoPlaygroundLiterals.visit, for: node) return .visitChildren @@ -414,6 +486,22 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) } + override func visit(_ node: MissingDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: MissingDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + + override func visit(_ node: OperatorDeclSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: OperatorDeclSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AlwaysUseLowerCamelCase.visit, for: node) return .visitChildren @@ -434,20 +522,31 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: UseSingleLinePropertyGetter.self, for: node) } + override func visit(_ node: PoundSourceLocationSyntax) -> SyntaxVisitorContinueKind { + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) + return .visitChildren + } + override func visitPost(_ node: PoundSourceLocationSyntax) { + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) + } + override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) return .visitChildren } override func visitPost(_ node: PrecedenceGroupDeclSyntax) { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren @@ -456,6 +555,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -492,6 +592,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) visitIfEnabled(UseSynthesizedInitializer.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) @@ -501,6 +602,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) onVisitPost(rule: UseSynthesizedInitializer.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) @@ -510,6 +612,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(OmitExplicitReturns.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren } @@ -517,6 +620,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: OmitExplicitReturns.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -574,6 +678,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren @@ -582,6 +687,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: TypeNamesShouldBeCapitalized.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -592,6 +698,7 @@ class LintPipeline: SyntaxVisitor { visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) visitIfEnabled(DontRepeatTypeInStaticProperties.visit, for: node) visitIfEnabled(NeverUseImplicitlyUnwrappedOptionals.visit, for: node) + visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren } @@ -601,6 +708,7 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) onVisitPost(rule: DontRepeatTypeInStaticProperties.self, for: node) onVisitPost(rule: NeverUseImplicitlyUnwrappedOptionals.self, for: node) + onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } @@ -635,6 +743,7 @@ extension FormatPipeline { node = OneVariableDeclarationPerLine(context: context).rewrite(node) node = OrderedImports(context: context).rewrite(node) node = ReturnVoidInsteadOfEmptyTuple(context: context).rewrite(node) + node = StandardizeDocumentationComments(context: context).rewrite(node) node = UseEarlyExits(context: context).rewrite(node) node = UseExplicitNilCheckInConditions(context: context).rewrite(node) node = UseLetInEveryBoundCaseVariable(context: context).rewrite(node) diff --git a/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift b/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift index ed06b5577..b41928783 100644 --- a/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift +++ b/Sources/SwiftFormat/Core/RuleNameCache+Generated.swift @@ -48,6 +48,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [ ObjectIdentifier(OrderedImports.self): "OrderedImports", ObjectIdentifier(ReplaceForEachWithForLoop.self): "ReplaceForEachWithForLoop", ObjectIdentifier(ReturnVoidInsteadOfEmptyTuple.self): "ReturnVoidInsteadOfEmptyTuple", + ObjectIdentifier(StandardizeDocumentationComments.self): "StandardizeDocumentationComments", ObjectIdentifier(TypeNamesShouldBeCapitalized.self): "TypeNamesShouldBeCapitalized", ObjectIdentifier(UseEarlyExits.self): "UseEarlyExits", ObjectIdentifier(UseExplicitNilCheckInConditions.self): "UseExplicitNilCheckInConditions", diff --git a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift index d5c9c9ba1..29045408c 100644 --- a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift +++ b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift @@ -47,6 +47,7 @@ "OrderedImports": true, "ReplaceForEachWithForLoop": true, "ReturnVoidInsteadOfEmptyTuple": true, + "StandardizeDocumentationComments": false, "TypeNamesShouldBeCapitalized": true, "UseEarlyExits": false, "UseExplicitNilCheckInConditions": true, diff --git a/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift new file mode 100644 index 000000000..60ba63f40 --- /dev/null +++ b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift @@ -0,0 +1,239 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 Markdown +import SwiftSyntax + +/// 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 +@_spi(Rules) +public final class StandardizeDocumentationComments: SyntaxFormatRule { + public override class var isOptIn: Bool { return true } + + // For each kind of `DeclSyntax` node that we visit, if we modify the node we + // need to continue into that node's children, if any exist. These are + // different for different node types (e.g. an accessor has a `body`, while an + // actor has a `memberBlock`). + + public override func visit(_ node: AccessorDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.body = decl.body.map(visit) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: ActorDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: AssociatedTypeDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: ClassDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: DeinitializerDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: EditorPlaceholderDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: EnumCaseDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.body = decl.body.map(visit) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: IfConfigDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.clauses = visit(decl.clauses) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: ImportDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.body = decl.body.map(visit) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: MacroDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: MacroExpansionDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: MissingDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: OperatorDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: PoundSourceLocationSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: PrecedenceGroupDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: StructDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.memberBlock = visit(decl.memberBlock) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax { + if var decl = reformatDocumentation(node) { + decl.accessorBlock = decl.accessorBlock.map(visit) + return DeclSyntax(decl) + } + return super.visit(node) + } + + public override func visit(_ node: TypeAliasDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + public override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { + reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) + } + + private func reformatDocumentation( + _ node: T + ) -> T? { + guard let docComment = DocumentationComment(extractedFrom: node) + else { return nil } + + // Find the start of the documentation that is attached to this + // identifier, skipping over any trivia that doesn't actually + // attach (like `//` comments or full blank lines). + let docCommentTrivia = Array(node.leadingTrivia) + guard let startOfActualDocumentation = findStartOfDocComments(in: docCommentTrivia) + else { return node } + + // We need to preserve everything up to `startOfActualDocumentation`. + let preDocumentationTrivia = Trivia(pieces: node.leadingTrivia[.. Int? { + let startOfCommentSection = + trivia.lastIndex(where: { !$0.continuesDocComment }) + ?? trivia.startIndex + return trivia[startOfCommentSection...].firstIndex(where: \.isDocComment) +} + +extension TriviaPiece { + fileprivate var isDocComment: Bool { + switch self { + case .docBlockComment, .docLineComment: return true + default: return false + } + } + + fileprivate var continuesDocComment: Bool { + if isDocComment { return true } + switch self { + // Any amount of horizontal whitespace is okay + case .spaces, .tabs: + return true + // One line break is okay + case .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1): + return true + default: + return false + } + } +} diff --git a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift new file mode 100644 index 000000000..54adc4269 --- /dev/null +++ b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift @@ -0,0 +1,148 @@ +@_spi(Rules) import SwiftFormat +import _SwiftFormatTestSupport + +class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { + static var configuration: Configuration { + var c = Configuration() + c.lineLength = 80 + return c + } + + func testFunction() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// Returns a collection of subsequences, each with up to the specified length. + /// + /// If the number of elements in the + /// collection is evenly divided by `count`, + /// then every chunk will have a length equal to `count`. Otherwise, every chunk but the last will have a length equal to `count`, with the + /// remaining elements in the last chunk. + /// + /// let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + /// for chunk in numbers.chunks(ofCount: 5) { + /// print(chunk) + /// } + /// // [1, 2, 3, 4, 5] + /// // [6, 7, 8, 9, 10] + /// + /// - Parameter count: The desired size of each chunk. + /// - Parameter maxChunks: The total number of chunks that may not be exceeded, no matter how many would otherwise be produced. + /// - Returns: A collection of consescutive, non-overlapping subseqeunces of + /// this collection, where each subsequence (except possibly the last) has + /// the length `count`. + /// + /// - Complexity: O(1) if the collection conforms to `RandomAccessCollection`; + /// otherwise, O(*k*), where *k* is equal to `count`. + /// + public func chunks(ofCount count: Int, maxChunks: Int) -> [[SubSequence]] {} + """, + expected: """ + /// Returns a collection of subsequences, each with up to the specified length. + /// + /// If the number of elements in the collection is evenly divided by `count`, + /// then every chunk will have a length equal to `count`. Otherwise, every + /// chunk but the last will have a length equal to `count`, with the remaining + /// elements in the last chunk. + /// + /// ``` + /// let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + /// for chunk in numbers.chunks(ofCount: 5) { + /// print(chunk) + /// } + /// // [1, 2, 3, 4, 5] + /// // [6, 7, 8, 9, 10] + /// ``` + /// + /// - Complexity: O(1) if the collection conforms to `RandomAccessCollection`; + /// otherwise, O(*k*), where *k* is equal to `count`. + /// + /// - Parameters: + /// - count: The desired size of each chunk. + /// - maxChunks: The total number of chunks that may not be exceeded, no + /// matter how many would otherwise be produced. + /// - Returns: A collection of consescutive, non-overlapping subseqeunces of + /// this collection, where each subsequence (except possibly the last) has + /// the length `count`. + public func chunks(ofCount count: Int, maxChunks: Int) -> [[SubSequence]] {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testNestedFunction() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + // This comment helps verify that leading non-documentation trivia is preserved without changes. + + /// Provides a `chunks(ofCount:)` method, with some more information that should wrap. + extension Sequence { + /// Returns a collection of subsequences, each with up to the specified length. + /// + /// + /// - Parameter count: The desired size of each chunk. + /// - Returns: A collection of consescutive, non-overlapping subseqeunces of + /// this collection. + /// + public func chunks(ofCount count: Int) -> [[SubSequence]] {} + } + """, + expected: """ + // This comment helps verify that leading non-documentation trivia is preserved without changes. + + /// Provides a `chunks(ofCount:)` method, with some more information that + /// should wrap. + extension Sequence { + /// Returns a collection of subsequences, each with up to the specified + /// length. + /// + /// - Parameter count: The desired size of each chunk. + /// - Returns: A collection of consescutive, non-overlapping subseqeunces + /// of this collection. + public func chunks(ofCount count: Int) -> [[SubSequence]] {} + } + """, + findings: [], + configuration: Self.configuration + ) + } + + func testBlockDocumentation() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /** Provides an initializer that isn't actually possible to implement for all sequences. */ + extension Sequence { + /** + Creates a new sequence with the given element repeated the specified number of times. + - Parameter element: The element to repeat. + - Parameter count: The number of times to repeat `element`. `count` must be greater than or equal to zero. + - Complexity: O(1) + */ + public init(repeating element: Element, count: Int) {} + } + """, + expected: """ + /// Provides an initializer that isn't actually possible to implement for all + /// sequences. + extension Sequence { + /// Creates a new sequence with the given element repeated the specified + /// number of times. + /// + /// - Complexity: O(1) + /// + /// - Parameters: + /// - element: The element to repeat. + /// - count: The number of times to repeat `element`. `count` must be + /// greater than or equal to zero. + public init(repeating element: Element, count: Int) {} + } + """, + findings: [], + configuration: Self.configuration + ) + } + +} From 0b2adb95593da0e8a428cf873b82554fa400884e Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 00:20:01 -0500 Subject: [PATCH 02/11] Fix CI issues --- Sources/SwiftFormat/Core/DocumentationComment.swift | 6 ++---- .../Rules/StandardizeDocumentationComments.swift | 2 +- .../StandardizeDocumentationCommentsTests.swift | 12 ++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index 434a408fe..bd92c6658 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -365,10 +365,8 @@ extension DocumentationComment { let prefixWidth = 4 + joiningTrivia.map { - switch $0 { - case .spaces(let n): n - default: 0 - } + if case .spaces(let n) = $0 { return n } + else { return 0 } }.reduce(0, +) let options = MarkupFormatter.Options( diff --git a/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift index 60ba63f40..9ac562b09 100644 --- a/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift +++ b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2025 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 diff --git a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift index 54adc4269..d57b03282 100644 --- a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift +++ b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + @_spi(Rules) import SwiftFormat import _SwiftFormatTestSupport From 94f6f51d8ddeed5a7e90e054063a825b0671e504 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 09:25:34 -0500 Subject: [PATCH 03/11] Don't match special fields within parameter docs This behavior matches DocC, which treats all outline list items within a parameter's documentation as plain markup, rather than as special fields. --- .../Core/DocumentationComment.swift | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index bd92c6658..1d4896c2b 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -28,9 +28,9 @@ public struct DocumentationComment { /// The documentation comment of the parameter. /// - /// Typically, only the `briefSummary` field of this value will be populated. However, for more - /// complex cases like parameters whose types are functions, the grammar permits full - /// descriptions including `Parameter(s)`, `Returns`, and `Throws` fields to be present. + /// Typically, only the `briefSummary` field of this value will be populated. However, + /// parameters can also include a full discussion, although special fields like + /// `Parameter(s)`, `Returns`, and `Throws` are not specifically recognized. public var comment: DocumentationComment } @@ -141,6 +141,27 @@ public struct DocumentationComment { } } + /// Creates a new `DocumentationComment` from the given `Markup` node, treated as + /// the documentation for a parameter. + /// + /// Within the `parameterMarkup` node, only the brief summary is treated separately. + /// All other nodes are treated as body nodes, without special treatment for parameters + /// or return/throws documentation. + private init(parameterMarkup markup: Markup) { + // Extract the first paragraph as the brief summary. It will *not* be included in the body + // nodes. + let remainingChildren: DropFirstSequence + if let firstParagraph = markup.child(through: [(0, Paragraph.self)]) { + briefSummary = firstParagraph.detachedFromParent as? Paragraph + remainingChildren = markup.children.dropFirst() + } else { + briefSummary = nil + remainingChildren = markup.children.dropFirst(0) + } + + bodyNodes = remainingChildren.map(\.detachedFromParent) + } + /// Extracts parameter fields in an outlined parameters list (i.e., `- Parameters:` containing a /// nested list of parameter fields) from the given unordered list. /// @@ -227,7 +248,7 @@ public struct DocumentationComment { let name = rewriter.parameterName else { return nil } - return Parameter(name: name, comment: DocumentationComment(markup: newListItem)) + return Parameter(name: name, comment: DocumentationComment(parameterMarkup: newListItem)) } /// Extracts simple fields like `- Returns:` and `- Throws:` from the top-level list in the From cc09066f532e4dd42e20cc01ca8eda6e1f3d4185 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 09:26:39 -0500 Subject: [PATCH 04/11] Include full parameter documentation --- .../Core/DocumentationComment.swift | 24 +++--- ...tandardizeDocumentationCommentsTests.swift | 77 +++++++++++++++++++ 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index 1d4896c2b..6981643e9 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -421,22 +421,12 @@ extension DocumentationComment { 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)]) + let list = UnorderedList([parameters[0].listItem(asSingle: true)]) 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 paramItems = parameters.map { $0.listItem() } let paramList = UnorderedList(paramItems) // Create a list with a single item: the label, followed by the list of parameters. @@ -501,3 +491,13 @@ extension Paragraph { return self.accept(&rewriter) as? Paragraph ?? self } } + +extension DocumentationComment.Parameter { + func listItem(asSingle: Bool = false) -> ListItem { + let summary = comment.briefSummary ?? Paragraph() + let label = asSingle ? "Parameter \(name):" : "\(name):" + let summaryWithLabel = summary.prefixed(with: label) + return ListItem( + [summaryWithLabel] + comment.bodyNodes.map { $0 as! BlockMarkup }) + } +} diff --git a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift index d57b03282..4e299cf11 100644 --- a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift +++ b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift @@ -156,5 +156,82 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { configuration: Self.configuration ) } + + func testDetailedParameters() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// Creates an array with the specified capacity, then calls the given + /// closure with a buffer covering the array's uninitialized memory. + /// + /// Inside the closure, set the `initializedCount` parameter to the number of + /// elements that are initialized by the closure. The memory in the range + /// 'buffer[0.., + _ initializedCount: inout Int) throws -> Void + ) rethrows {} + """, + expected: """ + /// Creates an array with the specified capacity, then calls the given closure + /// with a buffer covering the array's uninitialized memory. + /// + /// Inside the closure, set the `initializedCount` parameter to the number of + /// elements that are initialized by the closure. The memory in the range + /// 'buffer[0.., + _ initializedCount: inout Int) throws -> Void + ) rethrows {} + """, + findings: [], + configuration: Self.configuration + ) + } + } From 498d9263a487540744c052286bb2662be7ac1209 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 09:28:07 -0500 Subject: [PATCH 05/11] Only visit syntax nodes for nominal declarations --- .../Core/Pipelines+Generated.swift | 60 ------------------- .../StandardizeDocumentationComments.swift | 44 -------------- 2 files changed, 104 deletions(-) diff --git a/Sources/SwiftFormat/Core/Pipelines+Generated.swift b/Sources/SwiftFormat/Core/Pipelines+Generated.swift index b5566c141..1e786e760 100644 --- a/Sources/SwiftFormat/Core/Pipelines+Generated.swift +++ b/Sources/SwiftFormat/Core/Pipelines+Generated.swift @@ -44,14 +44,6 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) } - override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: AccessorDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(StandardizeDocumentationComments.visit, for: node) @@ -179,25 +171,15 @@ class LintPipeline: SyntaxVisitor { override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node) visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node) - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node) return .visitChildren } override func visitPost(_ node: DeinitializerDeclSyntax) { onVisitPost(rule: AllPublicDeclarationsHaveDocumentation.self, for: node) onVisitPost(rule: BeginDocumentationCommentWithOneLineSummary.self, for: node) - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node) } - override func visit(_ node: EditorPlaceholderDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: EditorPlaceholderDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(StandardizeDocumentationComments.visit, for: node) return .visitChildren @@ -380,14 +362,6 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: UseShorthandTypeNames.self, for: node) } - override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: IfConfigDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: IfExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoParensAroundConditions.visit, for: node) return .visitChildren @@ -396,14 +370,6 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoParensAroundConditions.self, for: node) } - override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: ImportDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoAssignmentInExpressions.visit, for: node) return .visitChildren @@ -444,14 +410,6 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: StandardizeDocumentationComments.self, for: node) } - override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: MacroExpansionDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoPlaygroundLiterals.visit, for: node) return .visitChildren @@ -486,14 +444,6 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) } - override func visit(_ node: MissingDeclSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: MissingDeclSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: OperatorDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(StandardizeDocumentationComments.visit, for: node) return .visitChildren @@ -522,24 +472,14 @@ class LintPipeline: SyntaxVisitor { onVisitPost(rule: UseSingleLinePropertyGetter.self, for: node) } - override func visit(_ node: PoundSourceLocationSyntax) -> SyntaxVisitorContinueKind { - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) - return .visitChildren - } - override func visitPost(_ node: PoundSourceLocationSyntax) { - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) - } - override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind { visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node) visitIfEnabled(NoLeadingUnderscores.visit, for: node) - visitIfEnabled(StandardizeDocumentationComments.visit, for: node) return .visitChildren } override func visitPost(_ node: PrecedenceGroupDeclSyntax) { onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node) onVisitPost(rule: NoLeadingUnderscores.self, for: node) - onVisitPost(rule: StandardizeDocumentationComments.self, for: node) } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { diff --git a/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift index 9ac562b09..4d79bb8fa 100644 --- a/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift +++ b/Sources/SwiftFormat/Rules/StandardizeDocumentationComments.swift @@ -34,14 +34,6 @@ public final class StandardizeDocumentationComments: SyntaxFormatRule { // different for different node types (e.g. an accessor has a `body`, while an // actor has a `memberBlock`). - public override func visit(_ node: AccessorDeclSyntax) -> DeclSyntax { - if var decl = reformatDocumentation(node) { - decl.body = decl.body.map(visit) - return DeclSyntax(decl) - } - return super.visit(node) - } - public override func visit(_ node: ActorDeclSyntax) -> DeclSyntax { if var decl = reformatDocumentation(node) { decl.memberBlock = visit(decl.memberBlock) @@ -62,14 +54,6 @@ public final class StandardizeDocumentationComments: SyntaxFormatRule { return super.visit(node) } - public override func visit(_ node: DeinitializerDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - - public override func visit(_ node: EditorPlaceholderDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - public override func visit(_ node: EnumCaseDeclSyntax) -> DeclSyntax { reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) } @@ -98,18 +82,6 @@ public final class StandardizeDocumentationComments: SyntaxFormatRule { return super.visit(node) } - public override func visit(_ node: IfConfigDeclSyntax) -> DeclSyntax { - if var decl = reformatDocumentation(node) { - decl.clauses = visit(decl.clauses) - return DeclSyntax(decl) - } - return super.visit(node) - } - - public override func visit(_ node: ImportDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - public override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { if var decl = reformatDocumentation(node) { decl.body = decl.body.map(visit) @@ -122,26 +94,10 @@ public final class StandardizeDocumentationComments: SyntaxFormatRule { reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) } - public override func visit(_ node: MacroExpansionDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - - public override func visit(_ node: MissingDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - public override func visit(_ node: OperatorDeclSyntax) -> DeclSyntax { reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) } - public override func visit(_ node: PoundSourceLocationSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - - public override func visit(_ node: PrecedenceGroupDeclSyntax) -> DeclSyntax { - reformatDocumentation(DeclSyntax(node)) ?? super.visit(node) - } - public override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { if var decl = reformatDocumentation(node) { decl.memberBlock = visit(decl.memberBlock) From 3a375e6e945947a740185f4cbe16f22a9a3689b5 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 09:28:18 -0500 Subject: [PATCH 06/11] Add tests for all visited nodes --- ...tandardizeDocumentationCommentsTests.swift | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) diff --git a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift index 4e299cf11..5d7b4beb0 100644 --- a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift +++ b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift @@ -233,5 +233,374 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { ) } + // MARK: Nominal decl tests + + func testActorDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// An actor declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + package actor MyActor {} + """, + expected: """ + /// An actor declaration with documentation that needs to be rewrapped to the + /// correct width. + package actor MyActor {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testAssociatedTypeDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// An associated type declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + associatedtype MyAssociatedType = Int + """, + expected: """ + /// An associated type declaration with documentation that needs to be + /// rewrapped to the correct width. + associatedtype MyAssociatedType = Int + """, + findings: [], + configuration: Self.configuration + ) + } + + func testClassDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A class declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + public class MyClass {} + """, + expected: """ + /// A class declaration with documentation that needs to be rewrapped to the + /// correct width. + public class MyClass {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testEnumAndEnumCaseDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// An enum declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + public enum MyEnum { + /// An enum case declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + case myCase + } + """, + expected: """ + /// An enum declaration with documentation that needs to be rewrapped to the + /// correct width. + public enum MyEnum { + /// An enum case declaration with documentation that needs to be rewrapped to + /// the correct width. + case myCase + } + """, + findings: [], + configuration: Self.configuration + ) + } + + func testExtensionDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// An extension + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + extension MyClass {} + """, + expected: """ + /// An extension with documentation that needs to be rewrapped to the correct + /// width. + extension MyClass {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testFunctionDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A function declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + /// + /// - Returns: A value. + /// - Throws: An error. + /// + /// - Parameters: + /// - param: A single parameter. + /// - Parameter another: A second single parameter. + func myFunction(param: String, and another: Int) -> Value {} + """, + expected: """ + /// A function declaration with documentation that needs to be rewrapped to the + /// correct width. + /// + /// - Parameters: + /// - param: A single parameter. + /// - another: A second single parameter. + /// - Returns: A value. + /// - Throws: An error. + func myFunction(param: String, and another: Int) -> Value {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testInitializerDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// An initializer declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + /// + /// - Throws: An error. + /// + /// - Parameters: + /// - param: A single parameter. + /// - Parameter another: A second single parameter. + public init(param: String, and another: Int) {} + """, + expected: """ + /// An initializer declaration with documentation that needs to be rewrapped to + /// the correct width. + /// + /// - Parameters: + /// - param: A single parameter. + /// - another: A second single parameter. + /// - Throws: An error. + public init(param: String, and another: Int) {} + """, + findings: [], + configuration: Self.configuration + ) + } + func testMacroDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A macro declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + /// + /// - Throws: An error. + /// + /// - Parameters: + /// - param: A single parameter. + /// - Parameter another: A second single parameter. + @freestanding(expression) + public macro prohibitBinaryOperators(_ param: T, another: [String]) -> T = + #externalMacro(module: "ExampleMacros", type: "ProhibitBinaryOperators") + """, + expected: """ + /// A macro declaration with documentation that needs to be rewrapped to the + /// correct width. + /// + /// - Parameters: + /// - param: A single parameter. + /// - another: A second single parameter. + /// - Throws: An error. + @freestanding(expression) + public macro prohibitBinaryOperators(_ param: T, another: [String]) -> T = + #externalMacro(module: "ExampleMacros", type: "ProhibitBinaryOperators") + """, + findings: [], + configuration: Self.configuration + ) + } + + func testOperatorDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + extension Int { + /// An operator declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + /// + /// - Parameters: + /// - lhs: A single parameter. + /// - Parameter rhs: A second single parameter. + static func -+-(lhs: Int, rhs: Int) -> Int {} + } + """, + expected: """ + extension Int { + /// An operator declaration with documentation that needs to be rewrapped to + /// the correct width. + /// + /// - Parameters: + /// - lhs: A single parameter. + /// - rhs: A second single parameter. + static func -+-(lhs: Int, rhs: Int) -> Int {} + } + """, + findings: [], + configuration: Self.configuration + ) + } + + func testProtocolDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A protocol declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + protocol MyProto {} + """, + expected: """ + /// A protocol declaration with documentation that needs to be rewrapped to the + /// correct width. + protocol MyProto {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testStructDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A struct declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + struct MyStruct {} + """, + expected: """ + /// A struct declaration with documentation that needs to be rewrapped to the + /// correct width. + struct MyStruct {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testSubscriptDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A subscript declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + /// + /// - Returns: A value. + /// - Throws: An error. + /// + /// - Parameters: + /// - param: A single parameter. + /// - Parameter another: A second single parameter. + public subscript(param: String, and another: Int) -> Value {} + """, + expected: """ + /// A subscript declaration with documentation that needs to be rewrapped to + /// the correct width. + /// + /// - Parameters: + /// - param: A single parameter. + /// - another: A second single parameter. + /// - Returns: A value. + /// - Throws: An error. + public subscript(param: String, and another: Int) -> Value {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testTypeAliasDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A type alias declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + typealias MyAlias {} + """, + expected: """ + /// A type alias declaration with documentation that needs to be rewrapped to + /// the correct width. + typealias MyAlias {} + """, + findings: [], + configuration: Self.configuration + ) + } + + func testVariableDecl() { + assertFormatting( + StandardizeDocumentationComments.self, + input: """ + /// A variable declaration + /// with documentation + /// that needs to be + /// rewrapped to + /// the correct width. + var myVariable: Int = 5 + """, + expected: """ + /// A variable declaration with documentation that needs to be rewrapped to the + /// correct width. + var myVariable: Int = 5 + """, + findings: [], + configuration: Self.configuration + ) + } } From 932e7a4ac3208b4587a50b2d7e29f22ea6297d1d Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 11:43:25 -0500 Subject: [PATCH 07/11] Update nested parameter parsing test --- .../Core/DocumentationCommentTests.swift | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift b/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift index 55af641ef..3ce57994b 100644 --- a/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift +++ b/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift @@ -269,32 +269,24 @@ final class DocumentationCommentTests: XCTestCase { └─ Text " A function." """ ) - XCTAssertTrue(paramComment.bodyNodes.isEmpty) - XCTAssertEqual(paramComment.parameterLayout, .separated) - XCTAssertEqual(paramComment.parameters.count, 2) - XCTAssertEqual(paramComment.parameters[0].name, "x") + XCTAssertEqual(paramComment.bodyNodes.count, 1) XCTAssertEqual( - paramComment.parameters[0].comment.briefSummary?.debugDescription(), + paramComment.bodyNodes[0].debugDescription(), """ - Paragraph - └─ Text " A value." - """ - ) - XCTAssertEqual(paramComment.parameters[1].name, "y") - XCTAssertEqual( - paramComment.parameters[1].comment.briefSummary?.debugDescription(), - """ - Paragraph - └─ Text " Another value." - """ - ) - XCTAssertEqual( - paramComment.returns?.debugDescription(), - """ - Paragraph - └─ Text " A result." + UnorderedList + ├─ ListItem + │ └─ Paragraph + │ └─ Text "Parameter x: A value." + ├─ ListItem + │ └─ Paragraph + │ └─ Text "Parameter y: Another value." + └─ ListItem + └─ Paragraph + └─ Text "Returns: A result." """ ) + XCTAssertTrue(paramComment.parameters.isEmpty) + XCTAssertNil(paramComment.returns) XCTAssertNil(paramComment.throws) } } From a8776db12ac8281276d44de5d9b56ea40e8ffeed Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 11:45:37 -0500 Subject: [PATCH 08/11] Fix up formatting --- Sources/SwiftFormat/Core/DocumentationComment.swift | 6 +++--- .../Rules/StandardizeDocumentationCommentsTests.swift | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index 6981643e9..db4494bff 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -386,8 +386,7 @@ extension DocumentationComment { let prefixWidth = 4 + joiningTrivia.map { - if case .spaces(let n) = $0 { return n } - else { return 0 } + if case .spaces(let n) = $0 { return n } else { return 0 } }.reduce(0, +) let options = MarkupFormatter.Options( @@ -498,6 +497,7 @@ extension DocumentationComment.Parameter { let label = asSingle ? "Parameter \(name):" : "\(name):" let summaryWithLabel = summary.prefixed(with: label) return ListItem( - [summaryWithLabel] + comment.bodyNodes.map { $0 as! BlockMarkup }) + [summaryWithLabel] + comment.bodyNodes.map { $0 as! BlockMarkup } + ) } } diff --git a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift index 5d7b4beb0..ced7054ca 100644 --- a/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift +++ b/Tests/SwiftFormatTests/Rules/StandardizeDocumentationCommentsTests.swift @@ -156,7 +156,7 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { configuration: Self.configuration ) } - + func testDetailedParameters() { assertFormatting( StandardizeDocumentationComments.self, @@ -232,9 +232,9 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { configuration: Self.configuration ) } - + // MARK: Nominal decl tests - + func testActorDecl() { assertFormatting( StandardizeDocumentationComments.self, @@ -255,7 +255,7 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { configuration: Self.configuration ) } - + func testAssociatedTypeDecl() { assertFormatting( StandardizeDocumentationComments.self, @@ -276,7 +276,7 @@ class StandardizeDocumentationCommentsTests: LintOrFormatRuleTestCase { configuration: Self.configuration ) } - + func testClassDecl() { assertFormatting( StandardizeDocumentationComments.self, From 5863b752435c06cf75c66f0e9bc08c45cdb7f59d Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 11:52:10 -0500 Subject: [PATCH 09/11] Workaround for Swift 5.8 compiler failure --- Sources/SwiftFormat/Core/DocumentationComment.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index db4494bff..15a66cc58 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -150,16 +150,13 @@ public struct DocumentationComment { private init(parameterMarkup markup: Markup) { // Extract the first paragraph as the brief summary. It will *not* be included in the body // nodes. - let remainingChildren: DropFirstSequence if let firstParagraph = markup.child(through: [(0, Paragraph.self)]) { briefSummary = firstParagraph.detachedFromParent as? Paragraph - remainingChildren = markup.children.dropFirst() + bodyNodes = markup.children.dropFirst().map { $0.detachedFromParent } } else { briefSummary = nil - remainingChildren = markup.children.dropFirst(0) + bodyNodes = markup.children.map { $0.detachedFromParent } } - - bodyNodes = remainingChildren.map(\.detachedFromParent) } /// Extracts parameter fields in an outlined parameters list (i.e., `- Parameters:` containing a From 6e55e6cb8e22c16c72d5875ca7e125e10a3d910f Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 11:58:08 -0500 Subject: [PATCH 10/11] Revert "Update nested parameter parsing test" This reverts commit 932e7a4ac3208b4587a50b2d7e29f22ea6297d1d. --- .../Core/DocumentationCommentTests.swift | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift b/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift index 3ce57994b..55af641ef 100644 --- a/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift +++ b/Tests/SwiftFormatTests/Core/DocumentationCommentTests.swift @@ -269,24 +269,32 @@ final class DocumentationCommentTests: XCTestCase { └─ Text " A function." """ ) - XCTAssertEqual(paramComment.bodyNodes.count, 1) + XCTAssertTrue(paramComment.bodyNodes.isEmpty) + XCTAssertEqual(paramComment.parameterLayout, .separated) + XCTAssertEqual(paramComment.parameters.count, 2) + XCTAssertEqual(paramComment.parameters[0].name, "x") XCTAssertEqual( - paramComment.bodyNodes[0].debugDescription(), + paramComment.parameters[0].comment.briefSummary?.debugDescription(), """ - UnorderedList - ├─ ListItem - │ └─ Paragraph - │ └─ Text "Parameter x: A value." - ├─ ListItem - │ └─ Paragraph - │ └─ Text "Parameter y: Another value." - └─ ListItem - └─ Paragraph - └─ Text "Returns: A result." + Paragraph + └─ Text " A value." + """ + ) + XCTAssertEqual(paramComment.parameters[1].name, "y") + XCTAssertEqual( + paramComment.parameters[1].comment.briefSummary?.debugDescription(), + """ + Paragraph + └─ Text " Another value." + """ + ) + XCTAssertEqual( + paramComment.returns?.debugDescription(), + """ + Paragraph + └─ Text " A result." """ ) - XCTAssertTrue(paramComment.parameters.isEmpty) - XCTAssertNil(paramComment.returns) XCTAssertNil(paramComment.throws) } } From 8c51b69139204941b2ece8163b413289916ad1a5 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Mar 2025 12:01:02 -0500 Subject: [PATCH 11/11] Different approach for nested parameter docs Instead of parsing nested parameter documentation differently, which can have source-breaking effects, capture all the unfiltered body nodes when initializing a `DocumentationComment` from markup. This lets us choose whether to use the filtered body (at the top level of a doc comment) or the full list of body nodes (within a parameter's nested documentation). --- .../Core/DocumentationComment.swift | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftFormat/Core/DocumentationComment.swift b/Sources/SwiftFormat/Core/DocumentationComment.swift index 15a66cc58..3f4204e1a 100644 --- a/Sources/SwiftFormat/Core/DocumentationComment.swift +++ b/Sources/SwiftFormat/Core/DocumentationComment.swift @@ -75,6 +75,13 @@ public struct DocumentationComment { /// `Throws:` prefix removed for convenience. public var `throws`: Paragraph? = nil + /// A collection of _all_ body nodes at the top level of the comment text. + /// + /// If a brief summary paragraph was extracted from the comment, it will not be present in this + /// collection. Any special fields extracted (parameters, returns, and throws) from `bodyNodes` + /// will be present in this collection. + internal var allBodyNodes: [Markup] = [] + /// Creates a new `DocumentationComment` with information extracted from the leading trivia of the /// given syntax node. /// @@ -107,6 +114,9 @@ public struct DocumentationComment { remainingChildren = markup.children.dropFirst(0) } + // Capture all the body nodes before filtering out any special fields. + allBodyNodes = remainingChildren.map { $0.detachedFromParent } + for child in remainingChildren { if var list = child.detachedFromParent as? UnorderedList { // An unordered list could be one of the following: @@ -141,24 +151,6 @@ public struct DocumentationComment { } } - /// Creates a new `DocumentationComment` from the given `Markup` node, treated as - /// the documentation for a parameter. - /// - /// Within the `parameterMarkup` node, only the brief summary is treated separately. - /// All other nodes are treated as body nodes, without special treatment for parameters - /// or return/throws documentation. - private init(parameterMarkup markup: Markup) { - // Extract the first paragraph as the brief summary. It will *not* be included in the body - // nodes. - if let firstParagraph = markup.child(through: [(0, Paragraph.self)]) { - briefSummary = firstParagraph.detachedFromParent as? Paragraph - bodyNodes = markup.children.dropFirst().map { $0.detachedFromParent } - } else { - briefSummary = nil - bodyNodes = markup.children.map { $0.detachedFromParent } - } - } - /// Extracts parameter fields in an outlined parameters list (i.e., `- Parameters:` containing a /// nested list of parameter fields) from the given unordered list. /// @@ -245,7 +237,7 @@ public struct DocumentationComment { let name = rewriter.parameterName else { return nil } - return Parameter(name: name, comment: DocumentationComment(parameterMarkup: newListItem)) + return Parameter(name: name, comment: DocumentationComment(markup: newListItem)) } /// Extracts simple fields like `- Returns:` and `- Throws:` from the top-level list in the @@ -494,7 +486,7 @@ extension DocumentationComment.Parameter { let label = asSingle ? "Parameter \(name):" : "\(name):" let summaryWithLabel = summary.prefixed(with: label) return ListItem( - [summaryWithLabel] + comment.bodyNodes.map { $0 as! BlockMarkup } + [summaryWithLabel] + comment.allBodyNodes.map { $0 as! BlockMarkup } ) } }