1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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 Markdown
import SwiftSyntax
/// A structured representation of information extracted from a documentation comment.
///
/// This type represents both the top-level content of a documentation comment on a declaration and
/// also the nested information that can be provided on a parameter. For example, when a parameter
/// is a function type, it can provide not only a brief summary but also its own parameter and
/// return value descriptions.
@_spi(Testing)
public struct DocumentationComment {
/// A description of a parameter in a documentation comment.
public struct Parameter {
/// The name of the parameter.
public var name: String
/// 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.
public var comment: DocumentationComment
}
/// Describes the structural layout of the parameter descriptions in the comment.
public enum ParameterLayout {
/// All parameters were written under a single `Parameters` outline section at the top level of
/// the comment.
case outline
/// All parameters were written as individual `Parameter` items at the top level of the comment.
case separated
/// Parameters were written as a combination of one or more `Parameters` outlines and individual
/// `Parameter` items.
case mixed
}
/// A single paragraph representing a brief summary of the declaration, if present.
public var briefSummary: Paragraph? = nil
/// A collection of otherwise uncategorized 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.
public var bodyNodes: [Markup] = []
/// The structural layout of the parameter descriptions in the comment.
public var parameterLayout: ParameterLayout? = nil
/// Descriptions of parameters to a function, if any.
public var parameters: [Parameter] = []
/// A description of the return value of a function.
///
/// If present, this value is a copy of the `Paragraph` node from the comment but with the
/// `Returns:` prefix removed for convenience.
public var returns: Paragraph? = nil
/// A description of an error thrown by a function.
///
/// If present, this value is a copy of the `Paragraph` node from the comment but with the
/// `Throws:` prefix removed for convenience.
public var `throws`: Paragraph? = nil
/// Creates a new `DocumentationComment` with information extracted from the leading trivia of the
/// given syntax node.
///
/// If the syntax node does not have a preceding documentation comment, this initializer returns
/// `nil`.
///
/// - Parameter node: The syntax node from which the documentation comment should be extracted.
public init?<Node: SyntaxProtocol>(extractedFrom node: Node) {
guard let commentInfo = DocumentationCommentText(extractedFrom: node.leadingTrivia) else {
return nil
}
// 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])
self.init(markup: doc)
}
/// Creates a new `DocumentationComment` from the given `Markup` node.
private init(markup: Markup) {
// Extract the first paragraph as the brief summary. It will *not* be included in the body
// nodes.
let remainingChildren: DropFirstSequence<MarkupChildren>
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)
}
for child in remainingChildren {
if var list = child.detachedFromParent as? UnorderedList {
// An unordered list could be one of the following:
//
// 1. A parameter outline:
// - Parameters:
// - x: ...
// - y: ...
//
// 2. An exploded parameter list:
// - Parameter x: ...
// - Parameter y: ...
//
// 3. Some other simple field, like `Returns:`.
//
// Note that the order of execution of these two functions matters for the correct value of
// `parameterLayout` to be computed. If these ever change, make sure to update that
// computation inside the functions.
extractParameterOutline(from: &list)
extractSeparatedParameters(from: &list)
extractSimpleFields(from: &list)
// If the list is now empty, don't add it to the body nodes below.
guard !list.isEmpty else { continue }
}
bodyNodes.append(child.detachedFromParent)
}
}
/// Extracts parameter fields in an outlined parameters list (i.e., `- Parameters:` containing a
/// nested list of parameter fields) from the given unordered list.
///
/// If parameters were successfully extracted, the provided list is mutated to remove them as a
/// side effect of this function.
private mutating func extractParameterOutline(from list: inout UnorderedList) {
var unprocessedChildren: [Markup] = []
for child in list.children {
guard
let listItem = child as? ListItem,
let firstText = listItem.child(through: [
(0, Paragraph.self),
(0, Text.self),
]) as? Text,
firstText.string.trimmingCharacters(in: .whitespaces).lowercased() == "parameters:"
else {
unprocessedChildren.append(child.detachedFromParent)
continue
}
for index in 1..<listItem.childCount {
let listChild = listItem.child(at: index)
guard let sublist = listChild as? UnorderedList else { continue }
for sublistItem in sublist.listItems {
guard
let paramField = parameterField(extractedFrom: sublistItem, expectParameterLabel: false)
else {
continue
}
self.parameters.append(paramField)
self.parameterLayout = .outline
}
}
}
list = list.withUncheckedChildren(unprocessedChildren) as! UnorderedList
}
/// Extracts parameter fields in separated form (i.e., individual `- Parameter <name>:` items in
/// a top-level list in the comment text) from the given unordered list.
///
/// If parameters were successfully extracted, the provided list is mutated to remove them as a
/// side effect of this function.
private mutating func extractSeparatedParameters(from list: inout UnorderedList) {
var unprocessedChildren: [Markup] = []
for child in list.children {
guard
let listItem = child as? ListItem,
let paramField = parameterField(extractedFrom: listItem, expectParameterLabel: true)
else {
unprocessedChildren.append(child.detachedFromParent)
continue
}
self.parameters.append(paramField)
switch self.parameterLayout {
case nil:
self.parameterLayout = .separated
case .outline:
self.parameterLayout = .mixed
default:
break
}
}
list = list.withUncheckedChildren(unprocessedChildren) as! UnorderedList
}
/// Returns a new `ParameterField` containing parameter information extracted from the given list
/// item, or `nil` if it was not a valid parameter field.
private func parameterField(
extractedFrom listItem: ListItem,
expectParameterLabel: Bool
) -> Parameter? {
var rewriter = ParameterOutlineMarkupRewriter(
origin: listItem,
expectParameterLabel: expectParameterLabel)
guard
let newListItem = listItem.accept(&rewriter) as? ListItem,
let name = rewriter.parameterName
else { return nil }
return Parameter(name: name, comment: DocumentationComment(markup: newListItem))
}
/// Extracts simple fields like `- Returns:` and `- Throws:` from the top-level list in the
/// comment text.
///
/// If fields were successfully extracted, the provided list is mutated to remove them.
private mutating func extractSimpleFields(from list: inout UnorderedList) {
var unprocessedChildren: [Markup] = []
for child in list.children {
guard
let listItem = child as? ListItem,
case var rewriter = SimpleFieldMarkupRewriter(origin: listItem),
listItem.accept(&rewriter) as? ListItem != nil,
let name = rewriter.fieldName,
let paragraph = rewriter.paragraph
else {
unprocessedChildren.append(child)
continue
}
switch name.lowercased() {
case "returns":
self.returns = paragraph
case "throws":
self.throws = paragraph
default:
unprocessedChildren.append(child)
}
}
list = list.withUncheckedChildren(unprocessedChildren) as! UnorderedList
}
}
/// Visits a list item representing a parameter in a documentation comment and rewrites it to remove
/// any `Parameter` tag (if present), the name of the parameter, and the subsequent colon.
private struct ParameterOutlineMarkupRewriter: MarkupRewriter {
/// The list item to which the rewriter will be applied.
let origin: ListItem
/// If true, the `Parameter` prefix is expected on the list item content and it should be dropped.
let expectParameterLabel: Bool
/// Populated if the list item to which this is applied represents a valid parameter field.
private(set) var parameterName: String? = nil
mutating func visitListItem(_ listItem: ListItem) -> Markup? {
// Only recurse into the exact list item we're applying this to; otherwise, return it unchanged.
guard listItem.isIdentical(to: origin) else { return listItem }
return defaultVisit(listItem)
}
mutating func visitParagraph(_ paragraph: Paragraph) -> Markup? {
// Only recurse into the first paragraph in the list item.
guard paragraph.indexInParent == 0 else { return paragraph }
return defaultVisit(paragraph)
}
mutating func visitText(_ text: Text) -> Markup? {
// Only manipulate the first text node (of the first paragraph).
guard text.indexInParent == 0 else { return text }
let parameterPrefix = "parameter "
if expectParameterLabel && !text.string.lowercased().hasPrefix(parameterPrefix) { return text }
let string =
expectParameterLabel ? text.string.dropFirst(parameterPrefix.count) : text.string[...]
let nameAndRemainder = string.split(separator: ":", maxSplits: 1)
guard nameAndRemainder.count == 2 else { return text }
let name = nameAndRemainder[0].trimmingCharacters(in: .whitespaces)
guard !name.isEmpty else { return text }
self.parameterName = name
return Text(String(nameAndRemainder[1]))
}
}
/// Visits a list item representing a simple field in a documentation comment and rewrites it to
/// extract the field name, removing it and the subsequent colon from the item.
private struct SimpleFieldMarkupRewriter: MarkupRewriter {
/// The list item to which the rewriter will be applied.
let origin: ListItem
/// Populated if the list item to which this is applied represents a valid simple field.
private(set) var fieldName: String? = nil
/// Populated if the list item to which this is applied represents a valid simple field.
private(set) var paragraph: Paragraph? = nil
mutating func visitListItem(_ listItem: ListItem) -> Markup? {
// Only recurse into the exact list item we're applying this to; otherwise, return it unchanged.
guard listItem.isIdentical(to: origin) else { return listItem }
return defaultVisit(listItem)
}
mutating func visitParagraph(_ paragraph: Paragraph) -> Markup? {
// Only recurse into the first paragraph in the list item.
guard paragraph.indexInParent == 0 else { return paragraph }
guard let newNode = defaultVisit(paragraph) else { return nil }
guard let newParagraph = newNode as? Paragraph else { return newNode }
self.paragraph = newParagraph.detachedFromParent as? Paragraph
return newParagraph
}
mutating func visitText(_ text: Text) -> Markup? {
// Only manipulate the first text node (of the first paragraph).
guard text.indexInParent == 0 else { return text }
let nameAndRemainder = text.string.split(separator: ":", maxSplits: 1)
guard nameAndRemainder.count == 2 else { return text }
let name = nameAndRemainder[0].trimmingCharacters(in: .whitespaces)
guard !name.isEmpty else { return text }
self.fieldName = name
return Text(String(nameAndRemainder[1]))
}
}
|