Skip to content

Commit e54de2e

Browse files
authored
@DeprecationSummary not having effect for some symbols (#982)
When specifying the @DeprecationSummary directive in a symbol extension markdown file, the directive wasn't taking effect if the symbol had multiple lines of documentation comments. This was happening because `DocumentationMarkup.deprecation` was only being populated in the abstract. This has now been updated to take an effect in the Discussion section as well. * Make `@DeprecationSummary` have an effect in the Discussion section * Add tests to verify `@DeprecationSummary` has effect in Discussion section * Return early when `@DeprecationSummary` is detected. When we've parsed the `@DeprecationSummary` directive, no need to continue parsing, we can exit early. * Use `XCTUnwrap` in DeprecationSummaryTests. `XCTUnwrap` is preferred over force-unwrapping or using `XCTFail` in tests. * Remove `internal` keyword. It's not needed, `internal` is the default access level. Resolves rdar://70056350.
1 parent 563b958 commit e54de2e

File tree

5 files changed

+178
-54
lines changed

5 files changed

+178
-54
lines changed

Sources/SwiftDocC/Model/DocumentationMarkup.swift

+15-6
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,17 @@ struct DocumentationMarkup {
7474
}
7575

7676
/// Directives which are removed from the markdown content after being parsed.
77-
private static let directivesRemovedFromContent = [
77+
static let directivesRemovedFromContent = [
7878
Comment.directiveName,
7979
Metadata.directiveName,
8080
Options.directiveName,
8181
Redirect.directiveName,
8282
]
83+
84+
private static let allowedSectionsForDeprecationSummary = [
85+
ParserSection.abstract,
86+
ParserSection.discussion,
87+
]
8388

8489
// MARK: - Parsed Data
8590

@@ -144,17 +149,21 @@ struct DocumentationMarkup {
144149
}
145150
}
146151

152+
// The deprecation summary directive is allowed to have an effect in multiple sections of the content.
153+
if let directive = child as? BlockDirective,
154+
directive.name == DeprecationSummary.directiveName,
155+
Self.allowedSectionsForDeprecationSummary.contains(currentSection) {
156+
deprecation = MarkupContainer(directive.children)
157+
return
158+
}
159+
147160
// Parse an abstract, if found
148161
if currentSection == .abstract {
149162
if abstractSection == nil, let firstParagraph = child as? Paragraph {
150163
abstractSection = AbstractSection(paragraph: firstParagraph)
151164
return
152165
} else if let directive = child as? BlockDirective {
153-
if directive.name == DeprecationSummary.directiveName {
154-
// Found deprecation notice in the abstract.
155-
deprecation = MarkupContainer(directive.children)
156-
return
157-
} else if Self.directivesRemovedFromContent.contains(directive.name) {
166+
if Self.directivesRemovedFromContent.contains(directive.name) {
158167
// These directives don't affect content so they shouldn't break us out of
159168
// the automatic abstract section.
160169
return

Tests/SwiftDocCTests/Model/DocumentationMarkupTests.swift

+63-1
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,28 @@ class DocumentationMarkupTests: XCTestCase {
8989
XCTAssertNil(model.abstractSection)
9090
XCTAssertEqual(expected, model.discussionSection?.content.map({ $0.detachedFromParent.debugDescription() }).joined(separator: "\n"))
9191
}
92+
93+
// Directives which shouldn't break us out of the automatic abstract section.
94+
for allowedDirective in DocumentationMarkup.directivesRemovedFromContent {
95+
do {
96+
let source = """
97+
# Title
98+
@\(allowedDirective)
99+
My abstract __content__.
100+
"""
101+
let expected = """
102+
Text "My abstract "
103+
Strong
104+
└─ Text "content"
105+
Text "."
106+
"""
107+
let model = DocumentationMarkup(markup: Document(parsing: source, options: .parseBlockDirectives))
108+
XCTAssertEqual(expected, model.abstractSection?.content.map({ $0.detachedFromParent.debugDescription() }).joined(separator: "\n"))
109+
XCTAssertNil(model.discussionSection)
110+
}
111+
}
92112

93-
// Directives in between sections
113+
// Directives in between sections should go into the discussion section.
94114
do {
95115
let source = """
96116
# Title
@@ -328,6 +348,48 @@ class DocumentationMarkupTests: XCTestCase {
328348
Deprecated!
329349
}
330350
"""
351+
let expected = """
352+
Deprecated!
353+
"""
354+
let model = DocumentationMarkup(markup: Document(parsing: source, options: .parseBlockDirectives))
355+
XCTAssertEqual(expected, model.deprecation?.elements.map({ $0.format() }).joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines))
356+
}
357+
358+
// Deprecation in the topics
359+
do {
360+
let source = """
361+
# Title
362+
My abstract __content__.
363+
364+
Discussion __content__.
365+
## Topics
366+
367+
@DeprecationSummary {
368+
Deprecated!
369+
}
370+
371+
### Basics
372+
- <doc:link>
373+
"""
374+
let model = DocumentationMarkup(markup: Document(parsing: source, options: .parseBlockDirectives))
375+
XCTAssertNil(model.deprecation)
376+
}
377+
378+
// Deprecation in the SeeAlso
379+
do {
380+
let source = """
381+
# Title
382+
My abstract __content__.
383+
384+
Discussion __content__.
385+
## See Also
386+
387+
@DeprecationSummary {
388+
Deprecated!
389+
}
390+
391+
- <doc:link>
392+
"""
331393
let model = DocumentationMarkup(markup: Document(parsing: source, options: .parseBlockDirectives))
332394
XCTAssertNil(model.deprecation)
333395
}

Tests/SwiftDocCTests/Rendering/DeprecationSummaryTests.swift

+78-47
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import XCTest
1414

1515
class DeprecationSummaryTests: XCTestCase {
1616
func testDecodeDeprecatedSymbol() throws {
17-
let deprecatedSymbolURL = Bundle.module.url(
17+
let deprecatedSymbolURL = try XCTUnwrap(Bundle.module.url(
1818
forResource: "deprecated-symbol", withExtension: "json",
19-
subdirectory: "Rendering Fixtures")!
19+
subdirectory: "Rendering Fixtures"))
2020

2121
let data = try Data(contentsOf: deprecatedSymbolURL)
2222
let symbol = try RenderNode.decode(fromJSON: data)
@@ -35,14 +35,10 @@ class DeprecationSummaryTests: XCTestCase {
3535
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideClass/init()", sourceLanguage: .swift))
3636

3737
// Compile docs and verify contents
38-
let symbol = node.semantic as! Symbol
38+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
3939
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
4040

41-
guard let renderNode = translator.visit(symbol) as? RenderNode else {
42-
XCTFail("Could not compile the node")
43-
return
44-
}
45-
41+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
4642
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [.text("This initializer has been deprecated.")])
4743
}
4844

@@ -70,14 +66,10 @@ class DeprecationSummaryTests: XCTestCase {
7066
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift))
7167

7268
// Compile docs and verify contents
73-
let symbol = node.semantic as! Symbol
69+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
7470
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
7571

76-
guard let renderNode = translator.visit(symbol) as? RenderNode else {
77-
XCTFail("Could not compile the node")
78-
return
79-
}
80-
72+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
8173
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [.text("This class has been deprecated.")])
8274

8375
// Verify that the in-abstract directive didn't make the context overflow into the discussion
@@ -98,15 +90,15 @@ class DeprecationSummaryTests: XCTestCase {
9890
)
9991

10092
// Compile docs and verify contents
101-
let symbol = node.semantic as! Symbol
93+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
10294
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
10395

10496
guard let renderNode = translator.visit(symbol) as? RenderNode else {
10597
XCTFail("Could not compile the node")
10698
return
10799
}
108100

109-
let expected: [RenderInlineContent] = [
101+
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [
110102
.text("Use the "),
111103
SwiftDocC.RenderInlineContent.reference(
112104
identifier: SwiftDocC.RenderReferenceIdentifier("doc://org.swift.docc.example/documentation/CoolFramework/CoolClass/coolFunc()"),
@@ -116,9 +108,7 @@ class DeprecationSummaryTests: XCTestCase {
116108
),
117109
SwiftDocC.RenderInlineContent.text(" "),
118110
SwiftDocC.RenderInlineContent.text("initializer instead."),
119-
]
120-
121-
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, expected)
111+
])
122112
}
123113

124114
func testSymbolDeprecatedSummary() throws {
@@ -132,20 +122,15 @@ class DeprecationSummaryTests: XCTestCase {
132122
)
133123

134124
// Compile docs and verify contents
135-
let symbol = node.semantic as! Symbol
125+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
136126
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
137127

138-
guard let renderNode = translator.visit(symbol) as? RenderNode else {
139-
XCTFail("Could not compile the node")
140-
return
141-
}
142-
128+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
129+
143130
// `doUncoolThings(with:)` has a blanket deprecation notice from the class, but no curated article - verify that the deprecation notice from the class still shows up on the rendered page
144-
let expected: [RenderInlineContent] = [
131+
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [
145132
.text("This class is deprecated."),
146-
]
147-
148-
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, expected)
133+
])
149134
}
150135

151136
func testDeprecationOverride() throws {
@@ -159,26 +144,72 @@ class DeprecationSummaryTests: XCTestCase {
159144
)
160145

161146
// Compile docs and verify contents
162-
let symbol = node.semantic as! Symbol
147+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
163148
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
164149

165-
guard let renderNode = translator.visit(symbol) as? RenderNode else {
166-
XCTFail("Could not compile the node")
167-
return
168-
}
169-
150+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
151+
170152
// `init()` has deprecation information in both the symbol graph and the documentation extension; when there are extra headings in an extension file, we need to make sure we correctly parse out the deprecation message from the extension and display that
171-
let expected: [RenderInlineContent] = [
172-
.text("Use the "),
173-
.reference(
174-
identifier: SwiftDocC.RenderReferenceIdentifier("doc://org.swift.docc.example/documentation/CoolFramework/CoolClass/init(config:cache:)"),
175-
isActive: true,
176-
overridingTitle: nil,
177-
overridingTitleInlineContent: nil
178-
),
179-
.text(" initializer instead."),
180-
]
181-
182-
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, expected)
153+
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [
154+
.text("Use the "),
155+
.reference(
156+
identifier: SwiftDocC.RenderReferenceIdentifier("doc://org.swift.docc.example/documentation/CoolFramework/CoolClass/init(config:cache:)"),
157+
isActive: true,
158+
overridingTitle: nil,
159+
overridingTitleInlineContent: nil
160+
),
161+
.text(" initializer instead."),
162+
])
183163
}
164+
165+
func testDeprecationSummaryInDiscussionSection() throws {
166+
let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective")
167+
let node = try context.entity(
168+
with: ResolvedTopicReference(
169+
bundleIdentifier: bundle.identifier,
170+
path: "/documentation/CoolFramework/CoolClass/coolFunc()",
171+
sourceLanguage: .swift
172+
)
173+
)
174+
175+
// Compile docs and verify contents
176+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
177+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
178+
179+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
180+
181+
// `coolFunc()` has deprecation information in both the symbol graph and the documentation extension; the deprecation information is part of the "Overview" section of the markup but it should still be parsed as expected.
182+
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [
183+
.text("Use the "),
184+
.reference(
185+
identifier: SwiftDocC.RenderReferenceIdentifier("doc://org.swift.docc.example/documentation/CoolFramework/CoolClass/init()"),
186+
isActive: true,
187+
overridingTitle: nil,
188+
overridingTitleInlineContent: nil
189+
),
190+
.text(" initializer instead."),
191+
])
192+
}
193+
194+
func testDeprecationSummaryWithMultiLineCommentSymbol() throws {
195+
let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective")
196+
let node = try context.entity(
197+
with: ResolvedTopicReference(
198+
bundleIdentifier: bundle.identifier,
199+
path: "/documentation/CoolFramework/CoolClass/init(config:cache:)",
200+
sourceLanguage: .swift
201+
)
202+
)
203+
204+
// Compile docs and verify contents
205+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
206+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference)
207+
208+
let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node")
209+
210+
// `init(config:cache:)` has deprecation information in both the symbol graph and the documentation extension; the symbol graph has multiple lines of documentation comments for the function, but adding deprecation information in the documentation extension should still work.
211+
XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [
212+
.text("This initializer is deprecated as of version 1.0.0."),
213+
])
214+
}
184215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# ``CoolFramework/CoolClass/coolFunc()``
2+
3+
This is a very cool (yet deprecated) function.
4+
5+
## Overview
6+
7+
We can also deprecate anywhere in the discussion section.
8+
9+
@DeprecationSummary {
10+
Use the ``CoolClass/init()`` initializer instead.
11+
}
12+
13+
<!-- Copyright (c) 2024 Apple Inc and the Swift Project authors. All Rights Reserved. -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# ``CoolFramework/CoolClass/init(config:cache:)``
2+
3+
@DeprecationSummary {
4+
This initializer is deprecated as of version 1.0.0.
5+
}
6+
7+
Overriding the deprecation summary of a symbol that has multiple lines of documentation comments also works!
8+
9+
<!-- Copyright (c) 2024 Apple Inc and the Swift Project authors. All Rights Reserved. -->

0 commit comments

Comments
 (0)