From d1622c63c2740d949624f0b3a0c849ead9ebfe5e Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Thu, 28 Nov 2024 17:42:02 +0000 Subject: [PATCH 1/3] Allow `@Metadata` directives in doc comments Adds support for authoring `@Metadata` directives in documentation comments, for convenience. For some children of `@Metadata`, like `@Available`, it's a bit inconvenient to need to create a documentation extension just to specify availability for a symbol. This commit allows you to do that in documentation comments instead. A few notes on the behavior: - Only the `@Available` and `@CustomMetadata` are supported as child directives of `@Metadata` in documentation comments. DocC ignores other directives and emits a warnings for their uses. - If you author a `@Metadata` directive both in the symbol's documentation comment and documentation extension file, DocC will pick the one in the documentation extension, and ignore the one in the documentation comment and emit a warning. On the implementation: - While `AutomaticDirectiveConvertible` provides nice utilities for parsing arguments and child directives, its benefits can't be leveraged when the directive you're parsing in a top-level document, e.g., an article or documentation comment. This commits adds a `DirectiveParser` utility type which takes a list of Markup elements and parses the first one that matches the directive type you're looking for. - Emitting a diagnostic for a directive in a documentation comment at the right source location is complicated. The codebase already has utilities to offset a diagnostic's source range which makes this computation easier, and this commit adds non-mutating variants of those APIs. rdar://138395778 --- .../Diagnostics/Diagnostic.swift | 8 +- .../Infrastructure/Diagnostics/Problem.swift | 7 + .../Infrastructure/DocumentationContext.swift | 16 +- .../SwiftDocC/Model/DocumentationNode.swift | 95 ++++++++- .../SwiftDocC/Semantics/Article/Article.swift | 23 +-- .../SwiftDocC/Semantics/DirectiveParser.swift | 68 +++++++ .../Semantics/Metadata/Metadata.swift | 49 ++++- .../API Reference Syntax/Metadata.md | 11 ++ .../Semantics/MetadataTests.swift | 15 +- .../Semantics/SymbolTests.swift | 182 ++++++++++++++++-- 10 files changed, 425 insertions(+), 49 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 5f9b75895a..6658f05994 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 a460f4fd2d..db6fedd713 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 d76bfa4126..c1495ab55a 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 9139609c5e..8539f1d976 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,16 @@ 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 = [ + Metadata.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 d421065288..c18b350d2e 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 0000000000..24c3f00faa --- /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 83189a3f67..da3d129cdc 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/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md b/Sources/docc/DocCDocumentation.docc/Reference Syntax/API Reference Syntax/Metadata.md index ec6769cb8a..02ba29c88e 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 888695f31a..9d86ee2887 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 d607e7936f..c2eac48582 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -1198,22 +1198,144 @@ 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", + ] + ) + } + // 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 +1343,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 +1357,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) } } From 7e9579a1fa857fa6a6e896ea6d94aa1852514edf Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Thu, 28 Nov 2024 17:57:17 +0000 Subject: [PATCH 2/3] Support `@DeprecationSummary` in doc comments Adds support for authoring `@DeprecationSummary` messages in documentation comments. This is useful when you want to provide a formatted documentation summary for an API that's deprecated, but don't want to create a documentation extension file in order to do just that. It turns out that DocC already supported this, but emitted a warning whenever you did so. This commit removes that warning. --- .../SwiftDocC/Model/DocumentationNode.swift | 1 + .../Semantics/Symbol/DeprecationSummary.swift | 4 ++- .../DocCDocumentation.docc/DocC.symbols.json | 8 +++++- .../Semantics/SymbolTests.swift | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 8539f1d976..1976c8b2df 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -849,6 +849,7 @@ public struct DocumentationNode { private let directivesSupportedInDocumentationComments = [ 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. diff --git a/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift b/Sources/SwiftDocC/Semantics/Symbol/DeprecationSummary.swift index fe34b034ca..ca44f7571b 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 8ea9c2b8a6..75b7a14bff 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/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index c2eac48582..a06c77ce9e 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -1313,6 +1313,32 @@ class SymbolTests: XCTestCase { ) } + 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." + ) + } + // MARK: - Helpers func makeDocumentationNodeForSymbol( From a5c523ec68c0b1278ba88a06faf3b29c973d6e23 Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Thu, 28 Nov 2024 17:57:34 +0000 Subject: [PATCH 3/3] Add support for `@Comment` in doc comments Adds support for `@Comment` directives in documentation comments. They don't hurt anyone, so might as well allow them. --- Sources/SwiftDocC/Model/DocumentationNode.swift | 1 + Tests/SwiftDocCTests/Semantics/SymbolTests.swift | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 1976c8b2df..5d865b9f9c 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -848,6 +848,7 @@ public struct DocumentationNode { } private let directivesSupportedInDocumentationComments = [ + Comment.directiveName, Metadata.directiveName, DeprecationSummary.directiveName, ] diff --git a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index a06c77ce9e..09d3d9c2a0 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -1338,6 +1338,19 @@ class SymbolTests: XCTestCase { "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