Skip to content

Commit c23571a

Browse files
authored
Support @Metadata and @DeprecationSummary in documentation comments (#1107) (#1127)
1 parent 7564732 commit c23571a

File tree

12 files changed

+476
-51
lines changed

12 files changed

+476
-51
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public extension Diagnostic {
6969
mutating func offsetWithRange(_ docRange: SymbolGraph.LineList.SourceRange) {
7070
// If there is no location information in the source diagnostic, the diagnostic might be removed for safety reasons.
7171
range?.offsetWithRange(docRange)
72-
72+
}
73+
74+
/// Returns the diagnostic with its range offset by the given documentation comment range.
75+
func withRangeOffset(by docRange: SymbolGraph.LineList.SourceRange) -> Self {
76+
var diagnostic = self
77+
diagnostic.range?.offsetWithRange(docRange)
78+
return diagnostic
7379
}
7480
}

Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ extension Problem {
4242
}
4343
}
4444
}
45+
46+
/// Returns the diagnostic with its range offset by the given documentation comment range.
47+
func withRangeOffset(by docRange: SymbolGraph.LineList.SourceRange) -> Self {
48+
var problem = self
49+
problem.offsetWithRange(docRange)
50+
return problem
51+
}
4552
}
4653

4754
extension Sequence<Problem> {

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,15 +1048,21 @@ public class DocumentationContext {
10481048
/// A lookup of resolved references based on the reference's absolute string.
10491049
private(set) var referenceIndex = [String: ResolvedTopicReference]()
10501050

1051-
private func nodeWithInitializedContent(reference: ResolvedTopicReference, match foundDocumentationExtension: DocumentationContext.SemanticResult<Article>?) -> DocumentationNode {
1051+
private func nodeWithInitializedContent(
1052+
reference: ResolvedTopicReference,
1053+
match foundDocumentationExtension: DocumentationContext.SemanticResult<Article>?,
1054+
bundle: DocumentationBundle
1055+
) -> DocumentationNode {
10521056
guard var updatedNode = documentationCache[reference] else {
10531057
fatalError("A topic reference that has already been resolved should always exist in the cache.")
10541058
}
10551059

10561060
// Pull a matched article out of the cache and attach content to the symbol
10571061
updatedNode.initializeSymbolContent(
10581062
documentationExtension: foundDocumentationExtension?.value,
1059-
engine: diagnosticEngine
1063+
engine: diagnosticEngine,
1064+
bundle: bundle,
1065+
context: self
10601066
)
10611067

10621068
// After merging the documentation extension into the symbol, warn about deprecation summary for non-deprecated symbols.
@@ -1399,7 +1405,11 @@ public class DocumentationContext {
13991405
Array(documentationCache.symbolReferences).concurrentMap { finalReference in
14001406
// Match the symbol's documentation extension and initialize the node content.
14011407
let match = uncuratedDocumentationExtensions[finalReference]
1402-
let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match)
1408+
let updatedNode = nodeWithInitializedContent(
1409+
reference: finalReference,
1410+
match: match,
1411+
bundle: bundle
1412+
)
14031413

14041414
return ((
14051415
node: updatedNode,

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,19 @@ public struct DocumentationNode {
335335
/// - Parameters:
336336
/// - article: An optional documentation extension article.
337337
/// - engine: A diagnostics engine.
338-
mutating func initializeSymbolContent(documentationExtension: Article?, engine: DiagnosticEngine) {
338+
mutating func initializeSymbolContent(
339+
documentationExtension: Article?,
340+
engine: DiagnosticEngine,
341+
bundle: DocumentationBundle,
342+
context: DocumentationContext
343+
) {
339344
precondition(unifiedSymbol != nil && symbol != nil, "You can only call initializeSymbolContent() on a symbol node.")
340345

341-
let (markup, docChunks) = Self.contentFrom(
346+
let (markup, docChunks, metadataFromDocumentationComment) = Self.contentFrom(
342347
documentedSymbol: unifiedSymbol?.documentedSymbol,
343348
documentationExtension: documentationExtension,
349+
bundle: bundle,
350+
context: context,
344351
engine: engine
345352
)
346353

@@ -469,7 +476,27 @@ public struct DocumentationNode {
469476
}
470477

471478
options = documentationExtension?.options[.local]
472-
self.metadata = documentationExtension?.metadata
479+
480+
if documentationExtension?.metadata != nil && metadataFromDocumentationComment != nil {
481+
var problem = Problem(
482+
diagnostic: Diagnostic(
483+
source: unifiedSymbol?.documentedSymbol?.docComment?.url,
484+
severity: .warning,
485+
range: metadataFromDocumentationComment?.originalMarkup.range,
486+
identifier: "org.swift.docc.DuplicateMetadata",
487+
summary: "Redeclaration of '@Metadata' for this symbol; this directive will be skipped",
488+
explanation: "A '@Metadata' directive is already declared in this symbol's documentation extension file"
489+
)
490+
)
491+
492+
if let range = unifiedSymbol?.documentedSymbol?.docComment?.lines.first?.range {
493+
problem.offsetWithRange(range)
494+
}
495+
496+
engine.emit(problem)
497+
}
498+
499+
self.metadata = documentationExtension?.metadata ?? metadataFromDocumentationComment
473500

474501
updateAnchorSections()
475502
}
@@ -483,11 +510,19 @@ public struct DocumentationNode {
483510
static func contentFrom(
484511
documentedSymbol: SymbolGraph.Symbol?,
485512
documentationExtension: Article?,
513+
bundle: DocumentationBundle? = nil,
514+
context: DocumentationContext? = nil,
486515
engine: DiagnosticEngine
487-
) -> (markup: Markup, docChunks: [DocumentationChunk]) {
516+
) -> (
517+
markup: Markup,
518+
docChunks: [DocumentationChunk],
519+
metadata: Metadata?
520+
) {
488521
let markup: Markup
489522
var documentationChunks: [DocumentationChunk]
490523

524+
var metadata: Metadata?
525+
491526
// We should ignore the symbol's documentation comment if it wasn't provided
492527
// or if the documentation extension was set to override.
493528
let ignoreDocComment = documentedSymbol?.docComment == nil
@@ -512,7 +547,36 @@ public struct DocumentationNode {
512547
let docCommentMarkup = Document(parsing: docCommentString, source: docCommentLocation?.url, options: documentOptions)
513548
let offset = symbol.docComment?.lines.first?.range
514549

515-
let docCommentDirectives = docCommentMarkup.children.compactMap({ $0 as? BlockDirective })
550+
var docCommentMarkupElements = Array(docCommentMarkup.children)
551+
552+
var problems = [Problem]()
553+
554+
if let bundle, let context {
555+
metadata = DirectiveParser()
556+
.parseSingleDirective(
557+
Metadata.self,
558+
from: &docCommentMarkupElements,
559+
parentType: Symbol.self,
560+
source: docCommentLocation?.url,
561+
bundle: bundle,
562+
context: context,
563+
problems: &problems
564+
)
565+
566+
metadata?.validateForUseInDocumentationComment(
567+
symbolSource: symbol.docComment?.url,
568+
problems: &problems
569+
)
570+
}
571+
572+
if let offset {
573+
problems = problems.map { $0.withRangeOffset(by: offset) }
574+
}
575+
576+
engine.emit(problems)
577+
578+
let docCommentDirectives = docCommentMarkupElements.compactMap { $0 as? BlockDirective }
579+
516580
if !docCommentDirectives.isEmpty {
517581
let location = symbol.mixins.getValueIfPresent(
518582
for: SymbolGraph.Symbol.Location.self
@@ -529,9 +593,7 @@ public struct DocumentationNode {
529593
continue
530594
}
531595

532-
// Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.)
533-
// and so are inherently supported in doc comments.
534-
guard DirectiveIndex.shared.renderableDirectives[directive.name] == nil else {
596+
guard !directive.isSupportedInDocumentationComment else {
535597
continue
536598
}
537599

@@ -579,7 +641,7 @@ public struct DocumentationNode {
579641
documentationChunks = [DocumentationChunk(source: .sourceCode(location: nil, offset: nil), markup: markup)]
580642
}
581643

582-
return (markup: markup, docChunks: documentationChunks)
644+
return (markup: markup, docChunks: documentationChunks, metadata: metadata)
583645
}
584646

585647
/// Returns a documentation node kind for the given symbol kind.
@@ -667,7 +729,7 @@ public struct DocumentationNode {
667729
// Prefer content sections coming from an article (documentation extension file)
668730
var deprecated: DeprecatedSection?
669731

670-
let (markup, docChunks) = Self.contentFrom(documentedSymbol: symbol, documentationExtension: article, engine: engine)
732+
let (markup, docChunks, _) = Self.contentFrom(documentedSymbol: symbol, documentationExtension: article, engine: engine)
671733
self.markup = markup
672734
self.docChunks = docChunks
673735

@@ -784,3 +846,18 @@ public struct DocumentationNode {
784846
/// These tags contain information about the symbol's return values, potential errors, and parameters.
785847
public var tags: Tags = (returns: [], throws: [], parameters: [])
786848
}
849+
850+
private let directivesSupportedInDocumentationComments = [
851+
Comment.directiveName,
852+
Metadata.directiveName,
853+
DeprecationSummary.directiveName,
854+
]
855+
// Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.)
856+
// and so are inherently supported in doc comments.
857+
+ DirectiveIndex.shared.renderableDirectives.keys
858+
859+
private extension BlockDirective {
860+
var isSupportedInDocumentationComment: Bool {
861+
directivesSupportedInDocumentationComments.contains(name)
862+
}
863+
}

Sources/SwiftDocC/Semantics/Article/Article.swift

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,16 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
129129
return Redirect(from: childDirective, source: source, for: bundle, in: context, problems: &problems)
130130
}
131131

132-
let metadata: [Metadata]
133-
(metadata, remainder) = remainder.categorize { child -> Metadata? in
134-
guard let childDirective = child as? BlockDirective, childDirective.name == Metadata.directiveName else {
135-
return nil
136-
}
137-
return Metadata(from: childDirective, source: source, for: bundle, in: context)
138-
}
139-
140-
for extraMetadata in metadata.dropFirst() {
141-
problems.append(Problem(diagnostic: Diagnostic(source: source, severity: .warning, range: extraMetadata.originalMarkup.range, identifier: "org.swift.docc.HasAtMostOne<\(Article.self), \(Metadata.self)>.DuplicateChildren", summary: "Duplicate \(Metadata.directiveName.singleQuoted) child directive", explanation: nil, notes: []), possibleSolutions: []))
142-
}
143-
144-
var optionalMetadata = metadata.first
132+
var optionalMetadata = DirectiveParser()
133+
.parseSingleDirective(
134+
Metadata.self,
135+
from: &remainder,
136+
parentType: Article.self,
137+
source: source,
138+
bundle: bundle,
139+
context: context,
140+
problems: &problems
141+
)
145142

146143
// Append any redirects found in the metadata to the redirects
147144
// found in the main content.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import Markdown
13+
14+
/// A utlity type for parsing directives from markup.
15+
struct DirectiveParser<Directive: AutomaticDirectiveConvertible> {
16+
17+
/// Returns a directive of the given type if found in the given sequence of markup elements and the remaining markup.
18+
///
19+
/// If there are multiple instances of the same directive type, this functions returns the first instance
20+
/// and diagnoses subsequent instances.
21+
func parseSingleDirective(
22+
_ directiveType: Directive.Type,
23+
from markupElements: inout [any Markup],
24+
parentType: Semantic.Type,
25+
source: URL?,
26+
bundle: DocumentationBundle,
27+
context: DocumentationContext,
28+
problems: inout [Problem]
29+
) -> Directive? {
30+
let (directiveElements, remainder) = markupElements.categorize { markup -> Directive? in
31+
guard let childDirective = markup as? BlockDirective,
32+
childDirective.name == Directive.directiveName
33+
else {
34+
return nil
35+
}
36+
return Directive(
37+
from: childDirective,
38+
source: source,
39+
for: bundle,
40+
in: context,
41+
problems: &problems
42+
)
43+
}
44+
45+
let directive = directiveElements.first
46+
47+
for extraDirective in directiveElements.dropFirst() {
48+
problems.append(
49+
Problem(
50+
diagnostic: Diagnostic(
51+
source: source,
52+
severity: .warning,
53+
range: extraDirective.originalMarkup.range,
54+
identifier: "org.swift.docc.HasAtMostOne<\(parentType), \(Directive.self)>.DuplicateChildren",
55+
summary: "Duplicate \(Metadata.directiveName.singleQuoted) child directive",
56+
explanation: nil,
57+
notes: []
58+
),
59+
possibleSolutions: []
60+
)
61+
)
62+
}
63+
64+
markupElements = remainder
65+
66+
return directive
67+
}
68+
}

Sources/SwiftDocC/Semantics/Metadata/Metadata.swift

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,52 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {
192192

193193
return true
194194
}
195+
196+
/// Validates the use of this Metadata directive in a documentation comment.
197+
///
198+
/// Some configuration options of Metadata are invalid in documentation comments. This function
199+
/// emits warnings for illegal uses and sets their values to `nil`.
200+
func validateForUseInDocumentationComment(
201+
symbolSource: URL?,
202+
problems: inout [Problem]
203+
) {
204+
let invalidDirectives: [(any AutomaticDirectiveConvertible)?] = [
205+
documentationOptions,
206+
technologyRoot,
207+
displayName,
208+
callToAction,
209+
pageKind,
210+
_pageColor,
211+
titleHeading,
212+
] + (redirects ?? [])
213+
+ supportedLanguages
214+
+ pageImages
215+
216+
let namesAndRanges = invalidDirectives
217+
.compactMap { $0 }
218+
.map { (type(of: $0).directiveName, $0.originalMarkup.range) }
219+
220+
problems.append(
221+
contentsOf: namesAndRanges.map { (name, range) in
222+
Problem(
223+
diagnostic: Diagnostic(
224+
source: symbolSource,
225+
severity: .warning,
226+
range: range,
227+
identifier: "org.swift.docc.\(Metadata.directiveName).Invalid\(name)InDocumentationComment",
228+
summary: "Invalid use of \(name.singleQuoted) directive in documentation comment; configuration will be ignored",
229+
explanation: "Specify this configuration in a documentation extension file"
230+
231+
// TODO: It would be nice to offer a solution here that removes the directive for you (#1111, rdar://140846407)
232+
)
233+
)
234+
}
235+
)
236+
237+
documentationOptions = nil
238+
technologyRoot = nil
239+
displayName = nil
240+
pageKind = nil
241+
_pageColor = nil
242+
}
195243
}
196-

Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import Markdown
2424
/// }
2525
/// ```
2626
///
27-
/// You can use the `@DeprecationSummary` directive top-level in both articles and documentation extension files.
27+
/// You can use the `@DeprecationSummary` directive top-level in articles, documentation extension files, or documentation comments.
28+
///
29+
/// > Earlier versions: Before Swift-DocC 6.1, `@DeprecationSummary` was not supported in documentation comments.
2830
///
2931
/// > Tip:
3032
/// > If you are writing a custom deprecation summary message for an API or documentation page that isn't already deprecated,

0 commit comments

Comments
 (0)