Skip to content

Commit 7a1d592

Browse files
committed
Refine ParametersDocumentationExtractor to avoid mutations
1 parent beb9e0c commit 7a1d592

File tree

1 file changed

+81
-46
lines changed

1 file changed

+81
-46
lines changed
Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,69 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
113
import Foundation
214
import Markdown
315

16+
/// Extracts parameter documentation from a markdown string.
17+
///
18+
/// The parameter extraction implementation is almost ported from the implementation in the Swift compiler codebase.
19+
///
20+
/// The problem with doing that in the Swift compiler codebase is that once you parse a the comment as markdown into
21+
/// a `Document` you cannot easily convert it back into markdown (we'd need to write our own markdown formatter).
22+
/// Besides, `cmark` doesn't handle Doxygen commands.
23+
///
24+
/// We considered using `swift-docc` but we faced some problems with it:
25+
///
26+
/// 1. We would need to refactor existing use of `swift-docc` in SourceKit-LSP to reuse some of that logic here besides
27+
/// providing the required arguments.
28+
/// 2. The result returned by DocC can't be directly converted to markdown, we'd need to provide our own DocC markdown renderer.
29+
///
30+
/// Implementing this using `swift-markdown` allows us to easily parse the comment, process it, convert it back to markdown.
31+
/// It also provides minimal parsing for Doxygen commands (we're only interested in `\param`) allowing us to use the same
32+
/// implementation for Clang-based declarations.
33+
///
34+
/// Although this approach involves code duplication, it's simple enough for the initial implementation. We should consider
35+
/// `swift-docc` in the future.
436
private struct ParametersDocumentationExtractor {
5-
private var parameters = [String: String]()
37+
struct Parameter {
38+
let name: String
39+
let documentation: String
40+
}
641

742
/// Extracts parameter documentation from a markdown string.
843
///
944
/// - Returns: A tuple containing the extracted parameters and the remaining markdown.
10-
mutating func extract(from markdown: String) -> (parameters: [String: String], remaining: String) {
45+
func extract(from markdown: String) -> (parameters: [String: String], remaining: String) {
1146
let document = Document(parsing: markdown, options: [.parseBlockDirectives, .parseMinimalDoxygen])
1247

13-
var remainingBlocks = [any BlockMarkup]()
48+
var parameters: [String: String] = [:]
49+
var remainingBlocks: [any BlockMarkup] = []
1450

1551
for block in document.blockChildren {
1652
switch block {
1753
case let unorderedList as UnorderedList:
18-
if let newUnorderedList = extract(from: unorderedList) {
54+
let (newUnorderedList, params) = extract(from: unorderedList)
55+
if let newUnorderedList {
1956
remainingBlocks.append(newUnorderedList)
2057
}
58+
59+
for param in params {
60+
parameters[param.name] = param.documentation
61+
}
62+
2163
case let doxygenParameter as DoxygenParameter:
22-
extract(from: doxygenParameter)
64+
let param = extract(from: doxygenParameter)
65+
parameters[param.name] = param.documentation
66+
2367
default:
2468
remainingBlocks.append(block)
2569
}
@@ -31,29 +75,35 @@ private struct ParametersDocumentationExtractor {
3175
}
3276

3377
/// Extracts parameter documentation from a Doxygen parameter command.
34-
private mutating func extract(from doxygenParameter: DoxygenParameter) {
35-
parameters[doxygenParameter.name] = Document(doxygenParameter.blockChildren).format()
78+
private func extract(from doxygenParameter: DoxygenParameter) -> Parameter {
79+
return Parameter(
80+
name: doxygenParameter.name,
81+
documentation: Document(doxygenParameter.blockChildren).format(),
82+
)
3683
}
3784

3885
/// Extracts parameter documentation from an unordered list.
3986
///
4087
/// - Returns: A new UnorderedList with the items that were not added to the parameters if any.
41-
private mutating func extract(from unorderedList: UnorderedList) -> UnorderedList? {
42-
var newItems = [ListItem]()
88+
private func extract(from unorderedList: UnorderedList) -> (remaining: UnorderedList?, parameters: [Parameter]) {
89+
var parameters: [Parameter] = []
90+
var newItems: [ListItem] = []
4391

4492
for item in unorderedList.listItems {
45-
if extractSingle(from: item) || extractOutline(from: item) {
46-
continue
93+
if let param = extractSingle(from: item) {
94+
parameters.append(param)
95+
} else if let params = extractOutline(from: item) {
96+
parameters.append(contentsOf: params)
97+
} else {
98+
newItems.append(item)
4799
}
48-
49-
newItems.append(item)
50100
}
51101

52102
if newItems.isEmpty {
53-
return nil
103+
return (remaining: nil, parameters: parameters)
54104
}
55105

56-
return UnorderedList(newItems)
106+
return (remaining: UnorderedList(newItems), parameters: parameters)
57107
}
58108

59109
/// Parameter documentation from a `Parameters:` outline.
@@ -65,33 +115,24 @@ private struct ParametersDocumentationExtractor {
65115
/// ```
66116
///
67117
/// - Returns: True if the list item has parameter outline documentation, false otherwise.
68-
private mutating func extractOutline(from listItem: ListItem) -> Bool {
118+
private func extractOutline(from listItem: ListItem) -> [Parameter]? {
69119
guard let firstChild = listItem.child(at: 0) as? Paragraph,
70120
let headingText = firstChild.child(at: 0) as? Text
71121
else {
72-
return false
122+
return nil
73123
}
74124

75-
let parametersPrefix = "parameters:"
76-
let headingContent = headingText.string.trimmingCharacters(in: .whitespaces)
77-
78-
guard headingContent.lowercased().hasPrefix(parametersPrefix) else {
79-
return false
125+
guard headingText.string.trimmingCharacters(in: .whitespaces).lowercased().hasPrefix("parameters:") else {
126+
return nil
80127
}
81128

82-
for child in listItem.children {
129+
return listItem.children.flatMap { child in
83130
guard let nestedList = child as? UnorderedList else {
84-
continue
131+
return [] as [Parameter]
85132
}
86133

87-
for nestedItem in nestedList.listItems {
88-
if let parameter = extractOutlineItem(from: nestedItem) {
89-
parameters[parameter.name] = parameter.documentation
90-
}
91-
}
134+
return nestedList.listItems.compactMap(extractOutlineItem)
92135
}
93-
94-
return true
95136
}
96137

97138
/// Extracts parameter documentation from a single parameter.
@@ -102,40 +143,34 @@ private struct ParametersDocumentationExtractor {
102143
/// ```
103144
///
104145
/// - Returns: True if the list item has single parameter documentation, false otherwise.
105-
private mutating func extractSingle(from listItem: ListItem) -> Bool {
146+
private func extractSingle(from listItem: ListItem) -> Parameter? {
106147
guard let paragraph = listItem.child(at: 0) as? Paragraph,
107148
let paragraphText = paragraph.child(at: 0) as? Text
108149
else {
109-
return false
150+
return nil
110151
}
111152

112153
let parameterPrefix = "parameter "
113154
let paragraphContent = paragraphText.string
114155

115156
guard paragraphContent.count >= parameterPrefix.count else {
116-
return false
157+
return nil
117158
}
118159

119160
let prefixEnd = paragraphContent.index(paragraphContent.startIndex, offsetBy: parameterPrefix.count)
120161
let potentialMatch = paragraphContent[..<prefixEnd].lowercased()
121162

122163
guard potentialMatch == parameterPrefix else {
123-
return false
164+
return nil
124165
}
125166

126167
let remainingContent = String(paragraphContent[prefixEnd...]).trimmingCharacters(in: .whitespaces)
127168

128-
guard let parameter = extractParam(firstTextContent: remainingContent, listItem: listItem) else {
129-
return false
130-
}
131-
132-
parameters[parameter.name] = parameter.documentation
133-
134-
return true
169+
return extractParam(firstTextContent: remainingContent, listItem: listItem)
135170
}
136171

137172
/// Extracts a parameter field from a list item (used for parameter outline items)
138-
private func extractOutlineItem(from listItem: ListItem) -> (name: String, documentation: String)? {
173+
private func extractOutlineItem(from listItem: ListItem) -> Parameter? {
139174
guard let paragraph = listItem.child(at: 0) as? Paragraph else {
140175
return nil
141176
}
@@ -157,7 +192,7 @@ private struct ParametersDocumentationExtractor {
157192
private func extractParam(
158193
firstTextContent: String,
159194
listItem: ListItem
160-
) -> (name: String, documentation: String)? {
195+
) -> Parameter? {
161196
guard let paragraph = listItem.child(at: 0) as? Paragraph else {
162197
return nil
163198
}
@@ -178,7 +213,7 @@ private struct ParametersDocumentationExtractor {
178213
let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst()
179214
let documentation = Document(remainingChildren).format()
180215

181-
return (name, documentation)
216+
return Parameter(name: name, documentation: documentation)
182217
}
183218
}
184219

@@ -187,6 +222,6 @@ private struct ParametersDocumentationExtractor {
187222
/// - Parameter markdown: The markdown text to extract parameters from
188223
/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text
189224
package func extractParametersDocumentation(from markdown: String) -> ([String: String], String) {
190-
var extractor = ParametersDocumentationExtractor()
225+
let extractor = ParametersDocumentationExtractor()
191226
return extractor.extract(from: markdown)
192227
}

0 commit comments

Comments
 (0)