Skip to content

Commit 556736e

Browse files
Strip Leading Whitespace From Symbol Graph Doc Comments (#1190)
* Strip the minimum leading whitespace from all doc comments loaded from symbol graphs. This worksaround a Clang compiler issue with parsing comments like this that are missing the leading asterisk: /** Foo's brief description. Foo's discussion. */ ...as opposed to this which Clang does parse properly: /** * Foo's brief description. * * Foo's discussion. */ "Minumum leading whitespace" is defined as: - Find the doc comment line with least amount of leading whitespace. Ignore blank lines during this search. - Remove that number of whitespace chars from all the lines (including blank lines). * Update Sources/SwiftDocC/Model/DocumentationNode.swift Refactor a loop to use `contains(where:)` Co-authored-by: David Rönnqvist <[email protected]> * [String].linesWithoutLeadingWhitespace returns an array of substrings, not strings. * [String].linesWithoutLeadingWhitespace checks for an empty collection before continuing to parse all the lines * Tweak comments --------- Co-authored-by: David Rönnqvist <[email protected]>
1 parent 0ab704f commit 556736e

File tree

2 files changed

+193
-1
lines changed

2 files changed

+193
-1
lines changed

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,10 @@ public struct DocumentationNode {
523523
DocumentationChunk(source: .documentationExtension, markup: documentationExtensionMarkup)
524524
]
525525
} else if let symbol = documentedSymbol, let docComment = symbol.docComment {
526-
let docCommentString = docComment.lines.map { $0.text }.joined(separator: "\n")
526+
let docCommentString = docComment.lines
527+
.map(\.text)
528+
.linesWithoutLeadingWhitespace()
529+
.joined(separator: "\n")
527530

528531
let docCommentLocation: SymbolGraph.Symbol.Location? = {
529532
if let uri = docComment.uri, let position = docComment.lines.first?.range?.start {
@@ -849,3 +852,40 @@ private extension BlockDirective {
849852
directivesSupportedInDocumentationComments.contains(name)
850853
}
851854
}
855+
856+
extension [String] {
857+
858+
/// Strip the minimum leading whitespace from all the strings in this array, as follows:
859+
/// - Find the line with least amount of leading whitespace. Ignore blank lines during this search.
860+
/// - Remove that number of whitespace chars from all the lines (including blank lines).
861+
/// - Returns: An array of substrings of the original lines with the minimum leading whitespace removed.
862+
func linesWithoutLeadingWhitespace() -> [Substring] {
863+
864+
// Optimization for the common case: If any of the lines does not start
865+
// with whitespace, or if there are no lines, then return the original lines
866+
// as substrings.
867+
if isEmpty || contains(where: { $0.first?.isWhitespace == false }) {
868+
return self.map{ .init($0) }
869+
}
870+
871+
/// - Count the leading whitespace characters in the given string.
872+
/// - Returns: The count of leading whitespace characters, if the string is not blank,
873+
/// or `nil` if the string is empty or blank (contains only whitespace)
874+
func leadingWhitespaceCount(_ line: String) -> Int? {
875+
let count = line.prefix(while: \.isWhitespace).count
876+
guard count < line.count else { return nil }
877+
return count
878+
}
879+
880+
// Find the minimum count of leading whitespace. If there are no
881+
// leading whitespace counts (if all the lines were blank) then return
882+
// the original lines as substrings.
883+
guard let minimumWhitespaceCount = self.compactMap(leadingWhitespaceCount).min() else {
884+
return self.map{ .init($0) }
885+
}
886+
887+
// Drop the leading whitespace from all the lines and return the
888+
// modified lines as substrings of the original lines.
889+
return self.map { $0.dropFirst(minimumWhitespaceCount) }
890+
}
891+
}

Tests/SwiftDocCTests/Semantics/SymbolTests.swift

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,158 @@ class SymbolTests: XCTestCase {
13521352
XCTAssert(problems.isEmpty)
13531353
}
13541354

1355+
// MARK: - Leading Whitespace in Doc Comments
1356+
1357+
func testWithoutLeadingWhitespace() {
1358+
let lines = [
1359+
"One",
1360+
"Two Words",
1361+
"With Trailing Whitespace "
1362+
]
1363+
let linesWithoutLeadingWhitespace: [Substring] = [
1364+
"One",
1365+
"Two Words",
1366+
"With Trailing Whitespace "
1367+
]
1368+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1369+
}
1370+
1371+
func testWithLeadingWhitespace() {
1372+
let lines = [
1373+
" One",
1374+
" Two Words",
1375+
" With Trailing Whitespace "
1376+
]
1377+
let linesWithoutLeadingWhitespace: [Substring] = [
1378+
"One",
1379+
"Two Words",
1380+
"With Trailing Whitespace "
1381+
]
1382+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1383+
}
1384+
1385+
func testWithIncreasingLeadingWhitespace() {
1386+
let lines = [
1387+
" One",
1388+
" Two Words",
1389+
" With Trailing Whitespace "
1390+
]
1391+
let linesWithoutLeadingWhitespace: [Substring] = [
1392+
"One",
1393+
" Two Words",
1394+
" With Trailing Whitespace "
1395+
]
1396+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1397+
}
1398+
1399+
func testWithDecreasingLeadingWhitespace() {
1400+
let lines = [
1401+
" One",
1402+
" Two Words",
1403+
" With Trailing Whitespace "
1404+
]
1405+
let linesWithoutLeadingWhitespace: [Substring] = [
1406+
" One",
1407+
" Two Words",
1408+
"With Trailing Whitespace "
1409+
]
1410+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1411+
}
1412+
1413+
func testWithoutLeadingWhitespaceBlankLines() {
1414+
let lines = [
1415+
" One",
1416+
" ",
1417+
" Two Words",
1418+
" ",
1419+
" With Trailing Whitespace "
1420+
]
1421+
let linesWithoutLeadingWhitespace: [Substring] = [
1422+
"One",
1423+
" ",
1424+
"Two Words",
1425+
"",
1426+
"With Trailing Whitespace "
1427+
]
1428+
1429+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1430+
}
1431+
1432+
func testWithoutLeadingWhitespaceEmptyLines() {
1433+
let lines = [
1434+
" One",
1435+
"",
1436+
" Two Words",
1437+
"",
1438+
" With Trailing Whitespace "
1439+
]
1440+
let linesWithoutLeadingWhitespace: [Substring] = [
1441+
"One",
1442+
"",
1443+
"Two Words",
1444+
"",
1445+
"With Trailing Whitespace "
1446+
]
1447+
1448+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1449+
}
1450+
1451+
func testWithoutLeadingWhitespaceAllEmpty() {
1452+
let lines = [
1453+
"",
1454+
"",
1455+
]
1456+
let linesWithoutLeadingWhitespace: [Substring] = [
1457+
"",
1458+
"",
1459+
]
1460+
1461+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1462+
}
1463+
1464+
func testWithoutLeadingWhitespaceAllBlank() {
1465+
let lines = [
1466+
" ",
1467+
" ",
1468+
]
1469+
let linesWithoutLeadingWhitespace: [Substring] = [
1470+
" ",
1471+
" ",
1472+
]
1473+
1474+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1475+
}
1476+
1477+
func testWithoutLeadingWhitespaceEmpty() {
1478+
let lines = [String]()
1479+
let linesWithoutLeadingWhitespace = [Substring]()
1480+
1481+
XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace)
1482+
}
1483+
1484+
func testLeadingWhitespaceInDocComment() throws {
1485+
let (semanticWithLeadingWhitespace, problems) = try makeDocumentationNodeSymbol(
1486+
docComment: """
1487+
This is an abstract.
1488+
1489+
This is a multi-paragraph overview.
1490+
1491+
It continues here.
1492+
""",
1493+
articleContent: nil
1494+
)
1495+
XCTAssert(problems.isEmpty)
1496+
XCTAssertEqual(semanticWithLeadingWhitespace.abstract?.format(), "This is an abstract.")
1497+
let lines = semanticWithLeadingWhitespace.discussion?.content.map{ $0.format() } ?? []
1498+
let expectedDiscussion = """
1499+
This is a multi-paragraph overview.
1500+
1501+
It continues here.
1502+
"""
1503+
XCTAssertEqual(lines.joined(), expectedDiscussion)
1504+
}
1505+
1506+
13551507
// MARK: - Helpers
13561508

13571509
func makeDocumentationNodeForSymbol(

0 commit comments

Comments
 (0)