Skip to content

Commit 6f7062d

Browse files
committed
use procedural implementation for typeIdentifier fragment mapping instead of state machine + add test for case with module prefix
1 parent 03af8e2 commit 6f7062d

File tree

2 files changed

+68
-75
lines changed

2 files changed

+68
-75
lines changed

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

+38-75
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,6 @@ extension DocumentationContentRenderer {
484484
/// Applies Swift symbol navigator titles rules to a title.
485485
/// Will strip the typeIdentifier's precise identifier.
486486
static func navigatorTitle(for tokens: [DeclarationRenderSection.Token], symbolTitle: String) -> [DeclarationRenderSection.Token] {
487-
// Replace kind "typeIdentifier" with "identifier" if the title matches the pattern:
488-
// [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=ancestor(Self)][.])*[typeIdentifier=Self]
489-
490487
return tokens.mapNameFragmentsToIdentifierKind(matching: symbolTitle)
491488
}
492489

@@ -498,8 +495,7 @@ extension DocumentationContentRenderer {
498495
static func subHeading(for tokens: [DeclarationRenderSection.Token], symbolTitle: String, symbolKind: String) -> [DeclarationRenderSection.Token] {
499496
var tokens = tokens
500497

501-
// 1. Replace kind "typeIdentifier" with "identifier" if the title matches the pattern:
502-
// [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=ancestor(Self)][.])*[typeIdentifier=Self]
498+
// 1. Map typeIdenifier tokens to identifier tokens where applicable
503499
tokens = tokens.mapNameFragmentsToIdentifierKind(matching: symbolTitle)
504500

505501

@@ -518,79 +514,46 @@ extension DocumentationContentRenderer {
518514

519515
private extension Array where Element == DeclarationRenderSection.Token {
520516
// Replaces kind "typeIdentifier" with "identifier" if the fragments matches the pattern:
521-
// [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=x_i)][.])*[typeIdentifier=x_i],
522-
// where the x_i joined with separator "." equal the `symbolTitle`
517+
// [keyword=_] [text=" "] [(typeIdentifier|identifier)=Name_0] ( [text="."] [typeIdentifier=Name_i] )*
518+
// where the Name_i from typeIdentifier tokens joined with separator "." equal the `symbolTitle`
523519
func mapNameFragmentsToIdentifierKind(matching symbolTitle: String) -> Self {
524-
let (includesTypeOrExtensionDeclaration, nameRange) = self.typeOrExtensionDeclaration()
525-
526-
if includesTypeOrExtensionDeclaration
527-
&& self[nameRange].map(\.text).joined() == symbolTitle {
528-
return self.enumerated().map { (index, token) -> DeclarationRenderSection.Token in
529-
530-
if nameRange.contains(index) && token.kind == .typeIdentifier {
531-
return DeclarationRenderSection.Token(
532-
text: token.text,
533-
kind: .identifier,
534-
preciseIdentifier: token.preciseIdentifier
535-
)
536-
}
537-
538-
return token
539-
}
520+
// Check that the first 3 tokens are: [keyword=_] [text=" "] [(typeIdentifier|identifier)=_]
521+
guard count >= 3,
522+
self[0].kind == .keyword,
523+
self[1].kind == .text, self[1].text == " ",
524+
self[2].kind == .typeIdentifier || self[2].kind == .identifier
525+
else { return self }
526+
527+
// If the first named token belongs to an identifier, this is a module prefix.
528+
// We store it for later comparison with the `combinedName`
529+
let modulePrefix = self[2].kind == .identifier ? self[2].text + "." : ""
530+
531+
var combinedName = self[2].text
532+
533+
var finalTypeIdentifierIndex = 2
534+
var remainder = self.dropFirst(3)
535+
// Continue checking for pairs of "." text tokens and typeIdentifier tokens: ( [text="."] [typeIdentifier=Name_i] )*
536+
while remainder.count >= 2 {
537+
let separator = remainder.removeFirst()
538+
guard separator.kind == .text, separator.text == "." else { break }
539+
let next = remainder.removeFirst()
540+
guard next.kind == .typeIdentifier else { break }
541+
542+
finalTypeIdentifierIndex += 2
543+
combinedName += "." + next.text
540544
}
541545

542-
return self
543-
}
544-
}
545-
546-
private extension Collection where Element == DeclarationRenderSection.Token, Index == Int {
547-
func typeOrExtensionDeclaration() -> (includesTypeOrExtensionDeclaration: Bool, name: Range<Index>) {
548-
self.reduce(into: TypeOrExtensionDeclarationNameExtractionSM(), { sm, token in sm.consume(token) }).result()
549-
}
550-
}
551-
552-
private enum TypeOrExtensionDeclarationNameExtractionSM {
553-
case initial
554-
case illegal
555-
case foundKeyword
556-
case foundIdentifier(Int)
557-
case expectIdentifier(Int)
558-
case done(Range<Int>)
559-
560-
init() {
561-
self = .initial
562-
}
563-
564-
static let expectedNameStartIndex = 2
565-
566-
mutating func consume(_ token: DeclarationRenderSection.Token) {
567-
switch (self, token.kind, token.text) {
568-
case (.initial, .keyword, _):
569-
self = .foundKeyword
570-
case (.foundKeyword, .text, " "):
571-
self = .expectIdentifier(Self.expectedNameStartIndex)
572-
case let (.expectIdentifier(index), .identifier, _),
573-
let (.expectIdentifier(index), .typeIdentifier, _):
574-
self = .foundIdentifier(index+1)
575-
case let (.foundIdentifier(index), .text, "."):
576-
self = .expectIdentifier(index+1)
577-
case let (.foundIdentifier(index), .text, _):
578-
self = .done(.init(uncheckedBounds: (Self.expectedNameStartIndex, index)))
579-
case let (.done(namerange), _, _):
580-
self = .done(namerange)
581-
default:
582-
self = .illegal
583-
}
584-
}
585-
586-
func result() -> (includesTypeOrExtensionDeclaration: Bool, name: Range<Int>) {
587-
switch self {
588-
case let .done(range):
589-
return (true, range)
590-
case let .foundIdentifier(index):
591-
return (true, .init(uncheckedBounds: (Self.expectedNameStartIndex, index)))
592-
default:
593-
return (false, .init(uncheckedBounds: (0, 0)))
546+
guard combinedName == modulePrefix + symbolTitle else { return self }
547+
548+
var mapped = self
549+
for index in stride(from: 2, to: finalTypeIdentifierIndex+1, by: 2) {
550+
let token = self[index]
551+
mapped[index] = DeclarationRenderSection.Token(
552+
text: token.text,
553+
kind: .identifier,
554+
preciseIdentifier: token.preciseIdentifier
555+
)
594556
}
557+
return mapped
595558
}
596559
}

Tests/SwiftDocCTests/Rendering/DocumentationContentRenderer+SwiftTests.swift

+30
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ class DocumentationContentRenderer_SwiftTests: XCTestCase {
2323
.init(text: "Object", kind: .typeIdentifier),
2424
]
2525

26+
// Tokens where the type name is incorrectly identified as "typeIdentifier", which
27+
// are additionally prefixed by a module name
28+
let typeIdentifierTokensWithModule: [DeclarationRenderSection.Token] = [
29+
.init(text: "class", kind: .keyword),
30+
.init(text: " ", kind: .text),
31+
.init(text: "MyModule", kind: .identifier),
32+
.init(text: ".", kind: .text),
33+
.init(text: "Test", kind: .typeIdentifier),
34+
.init(text: " : ", kind: .text),
35+
.init(text: "Object", kind: .typeIdentifier),
36+
]
37+
2638
// Tokens where the type name is correctly identified as "identifier"
2739
let identifierTokens: [DeclarationRenderSection.Token] = [
2840
.init(text: "class", kind: .keyword),
@@ -49,6 +61,15 @@ class DocumentationContentRenderer_SwiftTests: XCTestCase {
4961
XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier])
5062
XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"])
5163
}
64+
65+
do {
66+
// Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind even when prefixed with
67+
// a module "identifier".
68+
let mapped = DocumentationContentRenderer.Swift.navigatorTitle(for: typeIdentifierTokensWithModule, symbolTitle: "Test")
69+
70+
XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .identifier, .text, .typeIdentifier])
71+
XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "MyModule", ".", "Test", " : ", "Object"])
72+
}
5273
}
5374

5475
/// Test whether we map to "identifier" if we find an unexpected "typeIdentifier" token kind
@@ -68,6 +89,15 @@ class DocumentationContentRenderer_SwiftTests: XCTestCase {
6889
XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier])
6990
XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"])
7091
}
92+
93+
do {
94+
// Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind even when prefixed with
95+
// a module "identifier".
96+
let mapped = DocumentationContentRenderer.Swift.subHeading(for: typeIdentifierTokensWithModule, symbolTitle: "Test", symbolKind: "swift.class")
97+
98+
XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .identifier, .text, .typeIdentifier])
99+
XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "MyModule", ".", "Test", " : ", "Object"])
100+
}
71101
}
72102

73103
// Tokens for an "init" symbol

0 commit comments

Comments
 (0)