From aeb55a38967d5a159ec507fe13820e67cdef6d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6nnqvist?= Date: Tue, 10 Dec 2024 15:06:34 +0100 Subject: [PATCH 1/2] Document how to disambiguate symbol links using type signature information (#1095) * Indent table markup for readability * Add more details about how relative symbol links works * Move examples about unambiguous links before "Ambiguous Symbol Links" section. * Reorder list of supported disambiguation symbol types * Add missing supported disambiguation symbol types * Move the symbol type disambiguation example above the hash disambiguation example * Update `Sloth/update(_:)` example to describe type disambiguation rdar://136207820 * Add another more complicated type disambiguation example rdar://136207820 * Minor refinements to the new documentation * Fix 3 incomplete sentences in the new documentation * Add a table of link disambiguation examples * Apply suggestions from code review Co-authored-by: Maya Epps <53411851+mayaepps@users.noreply.github.com> --------- Co-authored-by: Maya Epps <53411851+mayaepps@users.noreply.github.com> --- .../linking-to-symbols-and-other-content.md | 272 +++++++++++++----- 1 file changed, 200 insertions(+), 72 deletions(-) diff --git a/Sources/docc/DocCDocumentation.docc/linking-to-symbols-and-other-content.md b/Sources/docc/DocCDocumentation.docc/linking-to-symbols-and-other-content.md index e9665bb07..c30e36de9 100644 --- a/Sources/docc/DocCDocumentation.docc/linking-to-symbols-and-other-content.md +++ b/Sources/docc/DocCDocumentation.docc/linking-to-symbols-and-other-content.md @@ -6,74 +6,122 @@ Facilitate navigation between pages using links. DocC supports the following link types to enable navigation between pages: -| Type | Usage | -| --- | --- | -| Symbol | Links to a symbol's reference page in your documentation. | -| Article | Links to an article or API collection in your documentation catalog. | -| Tutorial | Links to a tutorial in your documentation catalog. | -| Web | Links to an external URL. | +| Type | Usage +| -------- | ----- +| Symbol | Links to a symbol's reference page in your documentation. +| Article | Links to an article or API collection in your documentation catalog. +| Tutorial | Links to a tutorial in your documentation catalog. +| Web | Links to an external URL. ### Navigate to a Symbol -To add a link to a symbol, wrap the symbol's name in a set of double backticks -(\`\`). +To add a link to a symbol in your module, wrap the symbol's name in a set of double backticks (\`\`): ```markdown -``SlothCreator`` +``Sloth`` ``` -For nested symbols, include the path to the symbol in the link. +For links to member symbols or nested types, include the path to the symbol in the link: ```markdown -``SlothCreator/Sloth/eat(_:quantity:)`` +``Sloth/eat(_:quantity:)`` +``Sloth/Food`` ``` -DocC resolves symbol links relative to the context they appear in. For example, -a symbol link that appears inline in the `Sloth` class, and targets a -symbol in that class, can omit the `SlothCreator/Sloth/` portion of the symbol -path. +DocC resolves symbol links relative to the context that the link appears in. +This allows links in a type's documentation comment to omit the type's name from the symbol path when referring to its members. +For example, in the `Sloth` structure below, the `init(name:color:power:)` symbol link omits the `Sloth/` portion of the symbol path: -In some cases, a symbol's path isn't unique, such as with overloaded methods in -Swift. For example, consider the `Sloth` structure, which has multiple -`update(_:)` methods: +```swift +/// ... +/// You can create a sloth using ``init(name:color:power:)``. +public struct Sloth { // ╰──────────┬──────────╯ + /// ... // ╰─────refers-to────╮ + public init(name: String, color: Color, power: Power) { // ◁─╯ + /* ... */ + } +} +``` + +If DocC can't resolve a link in the current context, it gradually expands the search to the containing scope. +This allows links from one member to another member of the same type to omit the containing type's name from the symbol path. +For example, in the `Sloth` structure below, +the `eat(_:quantity:)` symbol link in the `energyLevel` property's documentation comment omits the `Sloth/` portion of the symbol path: ```swift -/// Updates the sloth's power. -/// -/// - Parameter power: The sloth's new power. -mutating public func update(_ power: Power) { - self.power = power +/// ... +public struct Sloth { + /// ... + /// Restore the sloth's energy using ``eat(_:quantity:)``. + public var energyLevel = 10 // ╰───────┬──────╯ + // │ + /// ... // ╰──────refers-to─────╮ + public mutating func eat(_ food: Food, quantity: Int) -> Int { // ◁─╯ + /* ... */ + } } +``` -/// Updates the sloth's energy level. -/// -/// - Parameter energyLevel: The sloth's new energy level. -mutating public func update(_ energyLevel: Int) { - self.energyLevel = energyLevel +> Note: +If you prefer absolute symbol links you can prefix the symbol path with a leading slash followed by the name of the module to which that symbol belongs: +> +> ```markdown +> ``/SlothCreator/Sloth`` +> ``/SlothCreator/Sloth/eat(_:quantity:)`` +> ``/SlothCreator/Sloth/Food`` +> ``` +> +> DocC resolves absolute symbol links from the module's scope instead of the context that the link appears in. + +Symbol paths are case-sensitive, meaning that symbols with the same name in different text casing are unambiguous. +For example, consider a `Sloth` structure with both a `color` property and a `Color` enumeration type: + +```swift +public struct Sloth { + public var color: Color + + public enum Color { + /* ... */ + } } ``` -Both methods have an identical symbol path of `SlothCreator/Sloth/update(_:)`. -In this scenario, and to ensure uniqueness, DocC uses the symbol's unique -identifier instead of its name to disambiguate. DocC's warnings about ambiguous -symbol links suggests one disambiguation for each of the symbols that match the -ambiguous symbol path. +A ` ``Sloth/color`` ` symbol link unambiguously refers to the `color` property and a ` ``Sloth/Color`` ` symbol link unambiguously refers to the inner `Color` type. + +#### Symbols with Multiple Language Representations + +Symbol links to symbols that have representations in more than one programming language can use symbol paths in either source language. +For example, consider a `Sloth` class with `@objc` attributes: + +```swift +@objc(TLASloth) public class Sloth: NSObject { + @objc public init(name: String, color: Color, power: Power) { + self.name = name + self.color = color + self.power = power + } +} +``` + +You can write a symbol link to the Sloth initializer using the symbol path in either source language: + +**Swift name** ```markdown -### Updating Sloths -- ``Sloth/update(_:)-4ko57`` -- ``Sloth/update(_:)-jixx`` +``Sloth/init(name:color:power:)`` +``` + +**Objective-C name** + +```markdown +``TLASloth/initWithName:color:power:`` ``` -In the example above, both symbols are functions, so you need the unique -identifiers to disambiguate the `Sloth/update(_:)` link. +#### Ambiguous Symbol Links -Unique identifiers aren't the only way to disambiguate symbol links. If a symbol -has a different type from the other symbols with the same symbol path, you can -use that symbol's type suffix to disambiguate the link and make the link refer -to that symbol. For example, consider a `Color` structure with `red`, `green`, -and `blue` properties for color components and static properties for a handful -of predefined color values: +In some cases a symbol's path isn't unique. +This makes it ambiguous what specific symbol a symbol link refers to. +For example, consider a `Color` structure with `red`, `green`, and `blue` properties for color components and static properties for a handful of predefined color values: ```swift public struct Color { @@ -87,9 +135,8 @@ extension Color { } ``` -Both the `red` property and the `red` static property have a symbol path of -`Color/red`. Because these are different types of symbols you can disambiguate -`Color/red` with symbol type suffixes instead of the symbols' unique identifiers. +Both the `red` property and the `red` static property have a symbol path of `Color/red`. +Because these are different types of symbols you can disambiguate ` ``Color/red`` ` with a suffix indicating the symbol's type. The following example shows a symbol link to the `red` property: @@ -103,65 +150,146 @@ The following example shows a symbol link to the `red` static property: ``Color/red-type.property`` ``` -DocC supports the following symbol types for use in symbol links: +DocC supports the following symbol types as disambiguation in symbol links: | Symbol type | Suffix | |-------------------|-------------------| | Enumeration | `-enum` | | Enumeration case | `-enum.case` | | Protocol | `-protocol` | -| Operator | `-func.op` | | Typealias | `-typealias` | -| Function | `-func` | +| Associated Type | `-associatedtype` | | Structure | `-struct` | | Class | `-class` | +| Function | `-func` | +| Operator | `-func.op` | +| Property | `-property` | | Type property | `-type.property` | +| Method | `-method` | | Type method | `-type.method` | +| Subscript | `-subscript` | | Type subscript | `-type.subscript` | -| Property | `-property` | | Initializer | `-init` | | Deinitializer | `-deinit` | -| Method | `-method` | -| Subscript | `-subscript` | +| Global variable | `-var` | | Instance variable | `-ivar` | | Macro | `-macro` | | Module | `-module` | +| Namespace | `-namespace` | +| HTTP Request | `-httpRequest` | +| HTTP Parameter | `-httpParameter` | +| HTTP Response | `-httpResponse` | +| HTTPBody | `-httpBody` | +| Dictionary | `-dictionary` | +| Dictionary Key | `-dictionaryKey` | -Symbol type suffixes can include a source language identifier prefix — for -example, `-swift.enum` instead of `-enum`. However, the language -identifier doesn't disambiguate the link. +You can discover these symbol type suffixes from DocC's warnings about ambiguous symbol links. +DocC suggests one disambiguation for each of the symbols that match the ambiguous symbol path. -Symbol paths are case-sensitive, meaning that symbols with the same name in -different text casing don't need disambiguation. +Symbol type suffixes can include a source language identifier prefix---for example, `-swift.enum` instead of `-enum`. +However, the language identifier doesn't disambiguate the link. -Symbols that have representations in both Swift and Objective-C can use -symbol paths in either source language. For example, consider a `Sloth` -class with `@objc` attributes: + +In the example above, both symbols that match the ambiguous symbol path were different types of symbol. +If the symbols that match the ambiguous symbol path have are the same type of symbol, +such as with overloaded methods in Swift, a symbol type suffix won't disambiguate the link. +In this scenario, DocC uses information from the symbols' type signatures to disambiguate. +For example, consider the `Sloth` structure---from the SlothCreator example---which has two different `update(_:)` methods: ```swift -@objc(TLASloth) public class Sloth: NSObject { - @objc public init(name: String, color: Color, power: Power) { - self.name = name - self.color = color - self.power = power - } +/// Updates the sloth's power. +/// +/// - Parameter power: The sloth's new power. +mutating public func update(_ power: Power) { + self.power = power +} + +/// Updates the sloth's energy level. +/// +/// - Parameter energyLevel: The sloth's new energy level. +mutating public func update(_ energyLevel: Int) { + self.energyLevel = energyLevel } ``` -You can write a symbol link to the Sloth initializer using the symbol path in either source language. +Both methods have an identical symbol path of `SlothCreator/Sloth/update(_:)`. +In this example there's only one parameter and its type is `Power` for the first overload and `Int` for the second overload. +DocC uses this parameter type information to suggest adding `(Power)` and `(Int)` to disambiguate each respective overload. -**Swift name** +The following example shows a topic group with disambiguated symbol links to both `Sloth/update(_:)` methods: ```markdown -``Sloth/init(name:color:power:)`` +### Updating Sloths + +- ``Sloth/update(_:)-(Power)`` +- ``Sloth/update(_:)-(Int)`` ``` -**Objective-C name** +If there are more overloads with more parameters and return values, +DocC may suggest a combination of parameter types and return value types to uniquely disambiguate each overload. +For example consider a hypothetical weather service with these three overloads---with different parameter types and different return types---for a `forecast(for:at:)` method: + +```swift +public func forecast(for days: DateInterval, at location: Location) -> HourByHourForecast { /* ... */ } +public func forecast(for day: Date, at location: Location) -> MinuteByMinuteForecast { /* ... */ } +public func forecast(for day: Date, at location: Location) -> HourByHourForecast { /* ... */ } +``` + +The first overload is the only one with where the first parameter has a `DateInterval` type. +The second parameter type isn't necessary to disambiguate the overload, and is the same in all three overloads, +so DocC suggests to add `(DateInterval,_)` to disambiguate the first overload. + +The second overload is the only one with where the return value has a `MinuteByMinuteForecast` type, +so DocC suggests to add `‑>MinuteByMinuteForecast` to disambiguate the second overload. + +The third overload has the same parameter types as the second overload and the same return value as the first overload, +so neither parameter types nor return types alone can uniquely disambiguate this overload. +In this scenario, DocC considers a combination of parameter types and return types to disambiguate the overload. +The first parameter type is different from the first overload and the return type is different from the second overload. +Together this information uniquely disambiguates the third overload, +so DocC suggests to add `(Date,_)‑>HourByHourForecast` to disambiguate the third overload. + +You can discover the minimal combination of parameter types and return types for each overload from DocC's warnings about ambiguous symbol links. + +The following example shows a topic group with disambiguated symbol links to the three `forecast(for:at:)` methods from before: ```markdown -``TLASloth/initWithName:color:power:`` +### Requesting weather forecasts + +- ``forecast(for:at:)-(DateInterval,_)`` +- ``forecast(for:at:)->MinuteByMinuteForecast`` +- ``forecast(for:at:)->(Date,_)->HourByHourForecast`` +``` + +> Earlier Versions: +> Before Swift-DocC 6.1, disambiguation using parameter types or return types isn't supported. + +If DocC can't disambiguate the symbol link using either a symbol type suffix or a combination parameter type names and return type names, +it will fall back to using a short hash of each symbol's unique identifier to disambiguate the symbol link. +You can discover these hashes from DocC's warnings about ambiguous symbol links. +The following example shows the same topic group with symbol links to the three `forecast(for:at:)` methods as before, +but using each symbol's unique identifier hash for disambiguation: + +```markdown +### Requesting weather forecasts + +- ``forecast(for:at:)-3brnk`` +- ``forecast(for:at:)-4gcpg`` +- ``forecast(for:at:)-7f3u`` ``` +The table below shows some examples of the types of link disambiguation suffixes that DocC supports: + +| Disambiguation type | Example | Meaning +| ------------------------------- | --------------- | ------------ +| Type of symbol | `-enum` | This symbol is an enumeration. +| Parameter type names | `-(Int,_,_)` | This symbol has three parameters and the first parameter is an `Int` value. +| ^ | `-()` | This symbol has no parameters. +| Return type names | `->String` | This symbol returns a `String` value. +| ^ | `->(_,_)` | This symbol returns a tuple with two elements. +| Parameter and return type names | `-(Bool)->()` | This symbol has one `Bool` parameter and no return value. +| Symbol identifier hash | `-4gcpg` | The hash of this symbol's unique identifier is "`4gcpg`". + ### Navigate to an Article To add a link to an article, use the less-than symbol (`<`), the `doc` keyword, From 62fb5d7c5a2e6387119e6ba07b726e5d4e777068 Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Wed, 11 Dec 2024 13:01:45 +0000 Subject: [PATCH 2/2] Support `@Metadata` and `@DeprecationSummary` in documentation comments (#1107) --- .../Diagnostics/Diagnostic.swift | 8 +- .../Infrastructure/Diagnostics/Problem.swift | 7 + .../Infrastructure/DocumentationContext.swift | 16 +- .../SwiftDocC/Model/DocumentationNode.swift | 97 +++++++- .../SwiftDocC/Semantics/Article/Article.swift | 23 +- .../SwiftDocC/Semantics/DirectiveParser.swift | 68 ++++++ .../Semantics/Metadata/Metadata.swift | 49 +++- .../Semantics/Symbol/DeprecationSummary.swift | 4 +- .../DocCDocumentation.docc/DocC.symbols.json | 8 +- .../API Reference Syntax/Metadata.md | 11 + .../Semantics/MetadataTests.swift | 15 +- .../Semantics/SymbolTests.swift | 221 ++++++++++++++++-- 12 files changed, 476 insertions(+), 51 deletions(-) create mode 100644 Sources/SwiftDocC/Semantics/DirectiveParser.swift diff --git a/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift b/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift index 5f9b75895..6658f0599 100644 --- a/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift +++ b/Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift @@ -69,6 +69,12 @@ public extension Diagnostic { mutating func offsetWithRange(_ docRange: SymbolGraph.LineList.SourceRange) { // If there is no location information in the source diagnostic, the diagnostic might be removed for safety reasons. range?.offsetWithRange(docRange) - + } + + /// Returns the diagnostic with its range offset by the given documentation comment range. + func withRangeOffset(by docRange: SymbolGraph.LineList.SourceRange) -> Self { + var diagnostic = self + diagnostic.range?.offsetWithRange(docRange) + return diagnostic } } diff --git a/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift b/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift index a460f4fd2..db6fedd71 100644 --- a/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift +++ b/Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift @@ -42,6 +42,13 @@ extension Problem { } } } + + /// Returns the diagnostic with its range offset by the given documentation comment range. + func withRangeOffset(by docRange: SymbolGraph.LineList.SourceRange) -> Self { + var problem = self + problem.offsetWithRange(docRange) + return problem + } } extension Sequence { diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index d76bfa412..c1495ab55 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -1048,7 +1048,11 @@ public class DocumentationContext { /// A lookup of resolved references based on the reference's absolute string. private(set) var referenceIndex = [String: ResolvedTopicReference]() - private func nodeWithInitializedContent(reference: ResolvedTopicReference, match foundDocumentationExtension: DocumentationContext.SemanticResult
?) -> DocumentationNode { + private func nodeWithInitializedContent( + reference: ResolvedTopicReference, + match foundDocumentationExtension: DocumentationContext.SemanticResult
?, + bundle: DocumentationBundle + ) -> DocumentationNode { guard var updatedNode = documentationCache[reference] else { fatalError("A topic reference that has already been resolved should always exist in the cache.") } @@ -1056,7 +1060,9 @@ public class DocumentationContext { // Pull a matched article out of the cache and attach content to the symbol updatedNode.initializeSymbolContent( documentationExtension: foundDocumentationExtension?.value, - engine: diagnosticEngine + engine: diagnosticEngine, + bundle: bundle, + context: self ) // After merging the documentation extension into the symbol, warn about deprecation summary for non-deprecated symbols. @@ -1399,7 +1405,11 @@ public class DocumentationContext { Array(documentationCache.symbolReferences).concurrentMap { finalReference in // Match the symbol's documentation extension and initialize the node content. let match = uncuratedDocumentationExtensions[finalReference] - let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match) + let updatedNode = nodeWithInitializedContent( + reference: finalReference, + match: match, + bundle: bundle + ) return (( node: updatedNode, diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 9139609c5..5d865b9f9 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -335,12 +335,19 @@ public struct DocumentationNode { /// - Parameters: /// - article: An optional documentation extension article. /// - engine: A diagnostics engine. - mutating func initializeSymbolContent(documentationExtension: Article?, engine: DiagnosticEngine) { + mutating func initializeSymbolContent( + documentationExtension: Article?, + engine: DiagnosticEngine, + bundle: DocumentationBundle, + context: DocumentationContext + ) { precondition(unifiedSymbol != nil && symbol != nil, "You can only call initializeSymbolContent() on a symbol node.") - let (markup, docChunks) = Self.contentFrom( + let (markup, docChunks, metadataFromDocumentationComment) = Self.contentFrom( documentedSymbol: unifiedSymbol?.documentedSymbol, documentationExtension: documentationExtension, + bundle: bundle, + context: context, engine: engine ) @@ -469,7 +476,27 @@ public struct DocumentationNode { } options = documentationExtension?.options[.local] - self.metadata = documentationExtension?.metadata + + if documentationExtension?.metadata != nil && metadataFromDocumentationComment != nil { + var problem = Problem( + diagnostic: Diagnostic( + source: unifiedSymbol?.documentedSymbol?.docComment?.url, + severity: .warning, + range: metadataFromDocumentationComment?.originalMarkup.range, + identifier: "org.swift.docc.DuplicateMetadata", + summary: "Redeclaration of '@Metadata' for this symbol; this directive will be skipped", + explanation: "A '@Metadata' directive is already declared in this symbol's documentation extension file" + ) + ) + + if let range = unifiedSymbol?.documentedSymbol?.docComment?.lines.first?.range { + problem.offsetWithRange(range) + } + + engine.emit(problem) + } + + self.metadata = documentationExtension?.metadata ?? metadataFromDocumentationComment updateAnchorSections() } @@ -483,11 +510,19 @@ public struct DocumentationNode { static func contentFrom( documentedSymbol: SymbolGraph.Symbol?, documentationExtension: Article?, + bundle: DocumentationBundle? = nil, + context: DocumentationContext? = nil, engine: DiagnosticEngine - ) -> (markup: Markup, docChunks: [DocumentationChunk]) { + ) -> ( + markup: Markup, + docChunks: [DocumentationChunk], + metadata: Metadata? + ) { let markup: Markup var documentationChunks: [DocumentationChunk] + var metadata: Metadata? + // We should ignore the symbol's documentation comment if it wasn't provided // or if the documentation extension was set to override. let ignoreDocComment = documentedSymbol?.docComment == nil @@ -512,7 +547,36 @@ public struct DocumentationNode { let docCommentMarkup = Document(parsing: docCommentString, source: docCommentLocation?.url, options: documentOptions) let offset = symbol.docComment?.lines.first?.range - let docCommentDirectives = docCommentMarkup.children.compactMap({ $0 as? BlockDirective }) + var docCommentMarkupElements = Array(docCommentMarkup.children) + + var problems = [Problem]() + + if let bundle, let context { + metadata = DirectiveParser() + .parseSingleDirective( + Metadata.self, + from: &docCommentMarkupElements, + parentType: Symbol.self, + source: docCommentLocation?.url, + bundle: bundle, + context: context, + problems: &problems + ) + + metadata?.validateForUseInDocumentationComment( + symbolSource: symbol.docComment?.url, + problems: &problems + ) + } + + if let offset { + problems = problems.map { $0.withRangeOffset(by: offset) } + } + + engine.emit(problems) + + let docCommentDirectives = docCommentMarkupElements.compactMap { $0 as? BlockDirective } + if !docCommentDirectives.isEmpty { let location = symbol.mixins.getValueIfPresent( for: SymbolGraph.Symbol.Location.self @@ -529,9 +593,7 @@ public struct DocumentationNode { continue } - // Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.) - // and so are inherently supported in doc comments. - guard DirectiveIndex.shared.renderableDirectives[directive.name] == nil else { + guard !directive.isSupportedInDocumentationComment else { continue } @@ -579,7 +641,7 @@ public struct DocumentationNode { documentationChunks = [DocumentationChunk(source: .sourceCode(location: nil, offset: nil), markup: markup)] } - return (markup: markup, docChunks: documentationChunks) + return (markup: markup, docChunks: documentationChunks, metadata: metadata) } /// Returns a documentation node kind for the given symbol kind. @@ -667,7 +729,7 @@ public struct DocumentationNode { // Prefer content sections coming from an article (documentation extension file) var deprecated: DeprecatedSection? - let (markup, docChunks) = Self.contentFrom(documentedSymbol: symbol, documentationExtension: article, engine: engine) + let (markup, docChunks, _) = Self.contentFrom(documentedSymbol: symbol, documentationExtension: article, engine: engine) self.markup = markup self.docChunks = docChunks @@ -784,3 +846,18 @@ public struct DocumentationNode { /// These tags contain information about the symbol's return values, potential errors, and parameters. public var tags: Tags = (returns: [], throws: [], parameters: []) } + +private let directivesSupportedInDocumentationComments = [ + Comment.directiveName, + Metadata.directiveName, + DeprecationSummary.directiveName, + ] + // Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.) + // and so are inherently supported in doc comments. + + DirectiveIndex.shared.renderableDirectives.keys + +private extension BlockDirective { + var isSupportedInDocumentationComment: Bool { + directivesSupportedInDocumentationComments.contains(name) + } +} diff --git a/Sources/SwiftDocC/Semantics/Article/Article.swift b/Sources/SwiftDocC/Semantics/Article/Article.swift index d42106528..c18b350d2 100644 --- a/Sources/SwiftDocC/Semantics/Article/Article.swift +++ b/Sources/SwiftDocC/Semantics/Article/Article.swift @@ -129,19 +129,16 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected, return Redirect(from: childDirective, source: source, for: bundle, in: context, problems: &problems) } - let metadata: [Metadata] - (metadata, remainder) = remainder.categorize { child -> Metadata? in - guard let childDirective = child as? BlockDirective, childDirective.name == Metadata.directiveName else { - return nil - } - return Metadata(from: childDirective, source: source, for: bundle, in: context) - } - - for extraMetadata in metadata.dropFirst() { - 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: [])) - } - - var optionalMetadata = metadata.first + var optionalMetadata = DirectiveParser() + .parseSingleDirective( + Metadata.self, + from: &remainder, + parentType: Article.self, + source: source, + bundle: bundle, + context: context, + problems: &problems + ) // Append any redirects found in the metadata to the redirects // found in the main content. diff --git a/Sources/SwiftDocC/Semantics/DirectiveParser.swift b/Sources/SwiftDocC/Semantics/DirectiveParser.swift new file mode 100644 index 000000000..24c3f00fa --- /dev/null +++ b/Sources/SwiftDocC/Semantics/DirectiveParser.swift @@ -0,0 +1,68 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 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 Swift project authors +*/ + +import Foundation +import Markdown + +/// A utlity type for parsing directives from markup. +struct DirectiveParser { + + /// Returns a directive of the given type if found in the given sequence of markup elements and the remaining markup. + /// + /// If there are multiple instances of the same directive type, this functions returns the first instance + /// and diagnoses subsequent instances. + func parseSingleDirective( + _ directiveType: Directive.Type, + from markupElements: inout [any Markup], + parentType: Semantic.Type, + source: URL?, + bundle: DocumentationBundle, + context: DocumentationContext, + problems: inout [Problem] + ) -> Directive? { + let (directiveElements, remainder) = markupElements.categorize { markup -> Directive? in + guard let childDirective = markup as? BlockDirective, + childDirective.name == Directive.directiveName + else { + return nil + } + return Directive( + from: childDirective, + source: source, + for: bundle, + in: context, + problems: &problems + ) + } + + let directive = directiveElements.first + + for extraDirective in directiveElements.dropFirst() { + problems.append( + Problem( + diagnostic: Diagnostic( + source: source, + severity: .warning, + range: extraDirective.originalMarkup.range, + identifier: "org.swift.docc.HasAtMostOne<\(parentType), \(Directive.self)>.DuplicateChildren", + summary: "Duplicate \(Metadata.directiveName.singleQuoted) child directive", + explanation: nil, + notes: [] + ), + possibleSolutions: [] + ) + ) + } + + markupElements = remainder + + return directive + } +} diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 83189a3f6..da3d129cd 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -192,5 +192,52 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { return true } + + /// Validates the use of this Metadata directive in a documentation comment. + /// + /// Some configuration options of Metadata are invalid in documentation comments. This function + /// emits warnings for illegal uses and sets their values to `nil`. + func validateForUseInDocumentationComment( + symbolSource: URL?, + problems: inout [Problem] + ) { + let invalidDirectives: [(any AutomaticDirectiveConvertible)?] = [ + documentationOptions, + technologyRoot, + displayName, + callToAction, + pageKind, + _pageColor, + titleHeading, + ] + (redirects ?? []) + + supportedLanguages + + pageImages + + let namesAndRanges = invalidDirectives + .compactMap { $0 } + .map { (type(of: $0).directiveName, $0.originalMarkup.range) } + + problems.append( + contentsOf: namesAndRanges.map { (name, range) in + Problem( + diagnostic: Diagnostic( + source: symbolSource, + severity: .warning, + range: range, + identifier: "org.swift.docc.\(Metadata.directiveName).Invalid\(name)InDocumentationComment", + summary: "Invalid use of \(name.singleQuoted) directive in documentation comment; configuration will be ignored", + explanation: "Specify this configuration in a documentation extension file" + + // TODO: It would be nice to offer a solution here that removes the directive for you (#1111, rdar://140846407) + ) + ) + } + ) + + documentationOptions = nil + technologyRoot = nil + displayName = nil + pageKind = nil + _pageColor = nil + } } - diff --git a/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift b/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift index fe34b034c..ca44f7571 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift @@ -24,7 +24,9 @@ import Markdown /// } /// ``` /// -/// You can use the `@DeprecationSummary` directive top-level in both articles and documentation extension files. +/// You can use the `@DeprecationSummary` directive top-level in articles, documentation extension files, or documentation comments. +/// +/// > Earlier versions: Before Swift-DocC 6.1, `@DeprecationSummary` was not supported in documentation comments. /// /// > Tip: /// > If you are writing a custom deprecation summary message for an API or documentation page that isn't already deprecated, diff --git a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json index 8ea9c2b8a..75b7a14bf 100644 --- a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json +++ b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json @@ -2033,7 +2033,13 @@ "text" : "" }, { - "text" : "You can use the `@DeprecationSummary` directive top-level in both articles and documentation extension files." + "text" : "You can use the `@DeprecationSummary` directive top-level in articles, documentation extension files, or documentation comments." + }, + { + "text" : "" + }, + { + "text" : "> Earlier versions: Before Swift-DocC 6.1, `@DeprecationSummary` was not supported in documentation comments." }, { "text" : "" diff --git a/Sources/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md b/Sources/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md index ec6769cb8..02ba29c88 100644 --- a/Sources/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md +++ b/Sources/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md @@ -56,6 +56,17 @@ to add additional URLs for a page. > Note: Starting with version 6.0, @Redirected is supported as a child directive of @Metadata. In previous versions, @Redirected must be used as a top level directive. +### Usage in documentation comments + +You can specify `@Metadata` configuration in documentation comments to specify ``Available`` directives. Other metadata directives are +not supported in documentation comments. + +> Note: Don't specify an `@Metadata` directive in both the symbol's documentation comment and its documentation extension file. +If you have one in each, DocC will pick the one in the documentation extension and discard the one in the documentation +comment. + +> Earlier versions: Before Swift-DocC 6.1, `@Metadata` was not supported in documentation comments. + ## Topics ### Extending or Overriding Source Documentation diff --git a/Tests/SwiftDocCTests/Semantics/MetadataTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataTests.swift index 888695f31..9d86ee288 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataTests.swift @@ -314,9 +314,13 @@ class MetadataTests: XCTestCase { XCTAssertNotNil(article?.metadata, "The Article has the parsed Metadata") XCTAssertNil(article?.metadata?.displayName, "The Article doesn't have the DisplayName") - XCTAssertEqual(1, problems.count) - XCTAssertEqual("org.swift.docc.HasAtMostOne.DuplicateChildren", problems.first?.diagnostic.identifier) - + XCTAssertEqual( + problems.map(\.diagnostic.identifier), + [ + "org.swift.docc.DocumentationExtension.NoConfiguration", + "org.swift.docc.HasAtMostOne.DuplicateChildren", + ] + ) } func testPageImageSupport() throws { @@ -416,10 +420,7 @@ class MetadataTests: XCTestCase { let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var analyzer = SemanticAnalyzer(source: nil, context: context, bundle: bundle) - _ = analyzer.visit(document) - var problems = analyzer.problems - + var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, in: context, problems: &problems) let problemIDs = problems.map { problem -> String in diff --git a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index d607e7936..09d3d9c2a 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -1198,22 +1198,183 @@ class SymbolTests: XCTestCase { // Declaration fragments should remain unchanged XCTAssertEqual(1, withoutArticle.declarationVariants[trait]!.count) } + + func testParsesMetadataDirectiveFromDocComment() throws { + let (node, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Metadata { + @Available(customOS, introduced: 1.2.3) + } + """, + articleContent: nil + ) + + XCTAssert(problems.isEmpty) + + let availability = try XCTUnwrap(node.metadata?.availability.first) + XCTAssertEqual(availability.platform, .other("customOS")) + XCTAssertEqual(availability.introduced.description, "1.2.3") + } + + func testEmitsWarningsInMetadataDirectives() throws { + let (_, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Metadata + """, + docCommentLineOffset: 12, + articleContent: nil, + diagnosticEngineFilterLevel: .information + ) + + XCTAssertEqual(problems.count, 1) + + let diagnostic = try XCTUnwrap(problems.first).diagnostic + XCTAssertEqual(diagnostic.identifier, "org.swift.docc.Metadata.NoConfiguration") + XCTAssertEqual(diagnostic.source?.absoluteString, "file:///tmp/File.swift") + XCTAssertEqual(diagnostic.range?.lowerBound.line, 15) + XCTAssertEqual(diagnostic.range?.lowerBound.column, 1) + } + + func testEmitsWarningForDuplicateMetadata() throws { + let (node, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Metadata { + @Available("Platform from doc comment", introduced: 1.2.3) + } + """, + docCommentLineOffset: 12, + articleContent: """ + # Title + + @Metadata { + @Available("Platform from documentation extension", introduced: 1.2.3) + } + """ + ) + + XCTAssertEqual(problems.count, 1) + + let diagnostic = try XCTUnwrap(problems.first).diagnostic + XCTAssertEqual(diagnostic.identifier, "org.swift.docc.DuplicateMetadata") + XCTAssertEqual(diagnostic.source?.absoluteString, "file:///tmp/File.swift") + XCTAssertEqual(diagnostic.range?.lowerBound.line, 15) + XCTAssertEqual(diagnostic.range?.lowerBound.column, 1) + + let availability = try XCTUnwrap(node.metadata?.availability.first) + XCTAssertEqual(availability.platform, .other("Platform from documentation extension")) + } + + func testEmitsWarningsForInvalidMetadataChildrenInDocumentationComments() throws { + let (_, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Metadata { + @Available("Platform from doc comment", introduced: 1.2.3) + @CustomMetadata(key: "key", value: "value") + + @Comment(The directives below this are invalid in documentation comments) + + @DocumentationExtension(mergeBehavior: override) + @TechnologyRoot + @DisplayName(Title) + @PageImage(source: test, purpose: icon) + @CallToAction(url: "https://example.com/sample.zip", purpose: download) + @PageKind(sampleCode) + @SupportedLanguage(swift) + @PageColor(orange) + @TitleHeading("Release Notes") + @Redirected(from: "old/path/to/this/page") + } + """, + articleContent: nil + ) + + XCTAssertEqual( + Set(problems.map(\.diagnostic.identifier)), + [ + "org.swift.docc.Metadata.InvalidDocumentationExtensionInDocumentationComment", + "org.swift.docc.Metadata.InvalidTechnologyRootInDocumentationComment", + "org.swift.docc.Metadata.InvalidDisplayNameInDocumentationComment", + "org.swift.docc.Metadata.InvalidPageImageInDocumentationComment", + "org.swift.docc.Metadata.InvalidCallToActionInDocumentationComment", + "org.swift.docc.Metadata.InvalidPageKindInDocumentationComment", + "org.swift.docc.Metadata.InvalidSupportedLanguageInDocumentationComment", + "org.swift.docc.Metadata.InvalidPageColorInDocumentationComment", + "org.swift.docc.Metadata.InvalidTitleHeadingInDocumentationComment", + "org.swift.docc.Metadata.InvalidRedirectedInDocumentationComment", + ] + ) + } + + func testParsesDeprecationSummaryDirectiveFromDocComment() throws { + let (node, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @DeprecationSummary { + This is the deprecation summary. + } + """, + articleContent: nil + ) + + XCTAssert(problems.isEmpty) + + XCTAssertEqual( + (node.semantic as? Symbol)? + .deprecatedSummary? + .content + .first? + .format() + .trimmingCharacters(in: .whitespaces) + , + "This is the deprecation summary." + ) + } + + func testAllowsCommentDirectiveInDocComment() throws { + let (_, problems) = try makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Comment(This is a comment) + """, + articleContent: nil + ) + + XCTAssert(problems.isEmpty) + } // MARK: - Helpers - func makeDocumentationNodeSymbol(docComment: String, articleContent: String?, file: StaticString = #file, line: UInt = #line) throws -> (Symbol, [Problem]) { + func makeDocumentationNodeForSymbol( + docComment: String, + docCommentLineOffset: Int = 0, + articleContent: String?, + diagnosticEngineFilterLevel: DiagnosticSeverity = .warning, + file: StaticString = #file, + line: UInt = #line + ) throws -> (DocumentationNode, [Problem]) { let myFunctionUSR = "s:5MyKit0A5ClassC10myFunctionyyF" let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: url.appendingPathComponent("mykit-iOS.symbols.json"))) - let newDocComment = SymbolGraph.LineList(docComment.components(separatedBy: .newlines).enumerated().map { arg -> SymbolGraph.LineList.Line in - let (index, line) = arg - let range = SymbolGraph.LineList.SourceRange( - start: .init(line: index, character: 0), - end: .init(line: index, character: line.utf8.count) - ) - return .init(text: line, range: range) - }) + let newDocComment = self.makeLineList( + docComment: docComment, + startOffset: .init( + line: docCommentLineOffset, + character: 0 + ), + url: URL(string: "file:///tmp/File.swift")! + ) + // The `guard` statement` below will handle the `nil` case by failing the test and graph.symbols[myFunctionUSR]?.docComment = newDocComment @@ -1221,7 +1382,10 @@ class SymbolTests: XCTestCase { try newGraphData.write(to: url.appendingPathComponent("mykit-iOS.symbols.json")) } - guard let original = context.documentationCache[myFunctionUSR], let symbol = original.symbol, let symbolSemantic = original.semantic as? Symbol else { + guard let original = context.documentationCache[myFunctionUSR], + let unifiedSymbol = original.unifiedSymbol, + let symbolSemantic = original.semantic as? Symbol + else { XCTFail("Couldn't find the expected symbol", file: (file), line: line) enum TestHelperError: Error { case missingExpectedMyFuctionSymbol } throw TestHelperError.missingExpectedMyFuctionSymbol @@ -1232,14 +1396,43 @@ class SymbolTests: XCTestCase { var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, in: context, problems: &problems) XCTAssertNotNil(article, "The sidecar Article couldn't be created.", file: (file), line: line) - XCTAssert(problems.isEmpty, "Unexpectedly found problems: \(DiagnosticConsoleWriter.formattedDescription(for: problems))", file: (file), line: line) return article } - let engine = DiagnosticEngine() - let node = DocumentationNode(reference: original.reference, symbol: symbol, platformName: symbolSemantic.platformName.map { $0.rawValue }, moduleReference: symbolSemantic.moduleReference, article: article, engine: engine) + let engine = DiagnosticEngine(filterLevel: diagnosticEngineFilterLevel) + + var node = DocumentationNode( + reference: original.reference, + unifiedSymbol: unifiedSymbol, + moduleData: unifiedSymbol.modules.first!.value, + moduleReference: symbolSemantic.moduleReference + ) + + node.initializeSymbolContent( + documentationExtension: article, + engine: engine, + bundle: bundle, + context: context + ) + + return (node, engine.problems) + } + + func makeDocumentationNodeSymbol( + docComment: String, + articleContent: String?, + file: StaticString = #file, + line: UInt = #line + ) throws -> (Symbol, [Problem]) { + let (node, problems) = try makeDocumentationNodeForSymbol( + docComment: docComment, + articleContent: articleContent, + file: file, + line: line + ) + let semantic = try XCTUnwrap(node.semantic as? Symbol) - return (semantic, engine.problems) + return (semantic, problems) } }