Skip to content

Commit 6f583dd

Browse files
committed
Make ParametersDocumentationExtractor members top-level & remove firstTextContent parameter
1 parent 83e1522 commit 6f583dd

File tree

4 files changed

+261
-271
lines changed

4 files changed

+261
-271
lines changed

Sources/SwiftLanguageService/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ add_library(SwiftLanguageService STATIC
4646
GeneratedInterfaceManager.swift
4747
SignatureHelp.swift
4848
AdjustPositionToStartOfArgument.swift
49-
ParametersDocumentationExtractor.swift
49+
ExtractParametersDocumentation.swift
5050
)
5151
set_target_properties(SwiftLanguageService PROPERTIES
5252
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 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+
13+
import Foundation
14+
import Markdown
15+
16+
private struct Parameter {
17+
let name: String
18+
let documentation: String
19+
}
20+
21+
/// Extracts parameter documentation from a Doxygen parameter command.
22+
private func extractParameter(from doxygenParameter: DoxygenParameter) -> Parameter {
23+
return Parameter(
24+
name: doxygenParameter.name,
25+
documentation: Document(doxygenParameter.blockChildren).format(),
26+
)
27+
}
28+
29+
/// Extracts parameter documentation from an unordered list.
30+
///
31+
/// - Returns: A new UnorderedList with the items that were not added to the parameters if any.
32+
private func extractParameters(
33+
from unorderedList: UnorderedList
34+
) -> (remaining: UnorderedList?, parameters: [Parameter]) {
35+
var parameters: [Parameter] = []
36+
var newItems: [ListItem] = []
37+
38+
for item in unorderedList.listItems {
39+
if let param = extractSingleParameter(from: item) {
40+
parameters.append(param)
41+
} else if let params = extractParametersOutline(from: item) {
42+
parameters.append(contentsOf: params)
43+
} else {
44+
newItems.append(item)
45+
}
46+
}
47+
48+
if newItems.isEmpty {
49+
return (remaining: nil, parameters: parameters)
50+
}
51+
52+
return (remaining: UnorderedList(newItems), parameters: parameters)
53+
}
54+
55+
/// Parameter documentation from a `Parameters:` outline.
56+
///
57+
/// Example:
58+
/// ```markdown
59+
/// - Parameters:
60+
/// - param: description
61+
/// ```
62+
///
63+
/// - Returns: True if the list item has parameter outline documentation, false otherwise.
64+
private func extractParametersOutline(from listItem: ListItem) -> [Parameter]? {
65+
guard
66+
let firstChild = listItem.child(at: 0) as? Paragraph,
67+
let headingText = firstChild.child(at: 0) as? Text,
68+
headingText.string.trimmingCharacters(in: .whitespaces).lowercased().hasPrefix("parameters:")
69+
else {
70+
return nil
71+
}
72+
73+
return listItem.children.flatMap { child in
74+
guard let nestedList = child as? UnorderedList else {
75+
return [] as [Parameter]
76+
}
77+
78+
return nestedList.listItems.compactMap { extractParameter(listItem: $0, single: false) }
79+
}
80+
}
81+
82+
/// Extracts parameter documentation from a single parameter.
83+
///
84+
/// Example:
85+
/// ```markdown
86+
/// - Parameter param: description
87+
/// ```
88+
///
89+
/// - Returns: True if the list item has single parameter documentation, false otherwise.
90+
private func extractSingleParameter(from listItem: ListItem) -> Parameter? {
91+
guard let paragraph = listItem.child(at: 0) as? Paragraph,
92+
let paragraphText = paragraph.child(at: 0) as? Text
93+
else {
94+
return nil
95+
}
96+
97+
let parameterPrefix = "parameter "
98+
let paragraphContent = paragraphText.string
99+
100+
guard
101+
let prefixEnd = paragraphContent.index(
102+
paragraphContent.startIndex,
103+
offsetBy: parameterPrefix.count,
104+
limitedBy: paragraphContent.endIndex
105+
)
106+
else {
107+
return nil
108+
}
109+
110+
let potentialMatch = paragraphContent[..<prefixEnd].lowercased()
111+
112+
guard potentialMatch == parameterPrefix else {
113+
return nil
114+
}
115+
116+
let remainingContent = String(paragraphContent[prefixEnd...]).trimmingCharacters(in: .whitespaces)
117+
118+
var remainingParagraph = paragraph
119+
remainingParagraph.replaceChildrenInRange(0..<1, with: [Text(remainingContent)])
120+
121+
var remainingListItem = listItem
122+
remainingListItem.replaceChildrenInRange(0..<1, with: [remainingParagraph])
123+
124+
return extractParameter(listItem: remainingListItem, single: true)
125+
}
126+
127+
/// Extracts a parameter field from a list item provided the relevant first text content allowing reuse in ``extractParametersOutline`` and ``extractSingleParameter``
128+
///
129+
/// - Parameters:
130+
/// - listItem: The list item to extract the parameter from
131+
/// - single: Whether the parameter is a single parameter or part of a parameter outline
132+
///
133+
/// - Returns: A tuple containing the parameter name and documentation if a parameter was found, nil otherwise.
134+
private func extractParameter(listItem: ListItem, single: Bool) -> Parameter? {
135+
guard let paragraph = listItem.child(at: 0) as? Paragraph else {
136+
return nil
137+
}
138+
139+
guard let firstText = paragraph.child(at: 0) as? Text else {
140+
return extractParameterWithRawIdentifier(from: listItem, single: single)
141+
}
142+
143+
let components = firstText.string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
144+
145+
guard components.count == 2 else {
146+
return extractParameterWithRawIdentifier(from: listItem, single: single)
147+
}
148+
149+
let name = String(components[0]).trimmingCharacters(in: .whitespaces)
150+
guard !name.isEmpty else {
151+
return nil
152+
}
153+
154+
let remainingFirstTextContent = String(components[1]).trimmingCharacters(in: .whitespaces)
155+
let remainingParagraphChildren = [Text(remainingFirstTextContent)] + paragraph.inlineChildren.dropFirst()
156+
let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst()
157+
let documentation = Document(remainingChildren).format()
158+
159+
return Parameter(name: name, documentation: documentation)
160+
}
161+
162+
/// Extracts a parameter with its name as a raw identifier.
163+
///
164+
/// Example:
165+
/// ```markdown
166+
/// - Parameter `foo bar`: documentation
167+
/// - Parameters:
168+
/// - `foo bar`: documentation
169+
/// ```
170+
///
171+
/// - Parameters:
172+
/// - listItem: The list item to extract the parameter from
173+
/// - single: Whether the parameter is a single parameter or part of a parameter outline
174+
private func extractParameterWithRawIdentifier(from listItem: ListItem, single: Bool) -> Parameter? {
175+
/// The index of ``InlineCode`` for the raw identifier parameter name in the first paragraph of ``listItem``
176+
let inlineCodeIndex = single ? 1 : 0
177+
178+
guard let paragraph = listItem.child(at: 0) as? Paragraph,
179+
let rawIdentifier = paragraph.child(at: inlineCodeIndex) as? InlineCode,
180+
let text = paragraph.child(at: inlineCodeIndex + 1) as? Text
181+
else {
182+
return nil
183+
}
184+
185+
let textContent = text.string.trimmingCharacters(in: .whitespaces)
186+
187+
guard textContent.hasPrefix(":") else {
188+
return nil
189+
}
190+
191+
let remainingTextContent = String(textContent.dropFirst()).trimmingCharacters(in: .whitespaces)
192+
let remainingParagraphChildren =
193+
[Text(remainingTextContent)] + paragraph.inlineChildren.dropFirst(inlineCodeIndex + 2)
194+
let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst(1)
195+
let documentation = Document(remainingChildren).format()
196+
197+
return Parameter(name: rawIdentifier.code, documentation: documentation)
198+
}
199+
200+
/// Extracts parameter documentation from markdown text.
201+
///
202+
/// The parameter extraction implementation is almost ported from the implementation in the Swift compiler codebase.
203+
///
204+
/// The problem with doing that in the Swift compiler codebase is that once you parse a the comment as markdown into
205+
/// a `Document` you cannot easily convert it back into markdown (we'd need to write our own markdown formatter).
206+
/// Besides, `cmark` doesn't handle Doxygen commands.
207+
///
208+
/// We considered using `swift-docc` but we faced some problems with it:
209+
///
210+
/// 1. We would need to refactor existing use of `swift-docc` in SourceKit-LSP to reuse some of that logic here besides
211+
/// providing the required arguments.
212+
/// 2. The result returned by DocC can't be directly converted to markdown, we'd need to provide our own DocC markdown renderer.
213+
///
214+
/// Implementing this using `swift-markdown` allows us to easily parse the comment, process it, convert it back to markdown.
215+
/// It also provides minimal parsing for Doxygen commands (we're only interested in `\param`) allowing us to use the same
216+
/// implementation for Clang-based declarations.
217+
///
218+
/// Although this approach involves code duplication, it's simple enough for the initial implementation. We should consider
219+
/// `swift-docc` in the future.
220+
///
221+
/// - Parameter markdown: The markdown text to extract parameters from
222+
/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text
223+
package func extractParametersDocumentation(
224+
from markdown: String
225+
) -> (parameters: [String: String], remaining: String) {
226+
let document = Document(parsing: markdown, options: [.parseBlockDirectives, .parseMinimalDoxygen])
227+
228+
var parameters: [String: String] = [:]
229+
var remainingBlocks: [any BlockMarkup] = []
230+
231+
for block in document.blockChildren {
232+
switch block {
233+
case let unorderedList as UnorderedList:
234+
let (newUnorderedList, params) = extractParameters(from: unorderedList)
235+
if let newUnorderedList {
236+
remainingBlocks.append(newUnorderedList)
237+
}
238+
239+
for param in params {
240+
// If duplicate parameter documentation is found, keep the first one following swift-docc's behavior
241+
parameters[param.name] = parameters[param.name] ?? param.documentation
242+
}
243+
244+
case let doxygenParameter as DoxygenParameter:
245+
let param = extractParameter(from: doxygenParameter)
246+
// If duplicate parameter documentation is found, keep the first one following swift-docc's behavior
247+
parameters[param.name] = parameters[param.name] ?? param.documentation
248+
249+
default:
250+
remainingBlocks.append(block)
251+
}
252+
}
253+
254+
let remaining = Document(remainingBlocks).format()
255+
256+
return (parameters: parameters, remaining: remaining)
257+
}

0 commit comments

Comments
 (0)