Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @Metadata and @DeprecationSummary in documentation comments #1107

Merged

Conversation

franklinsch
Copy link
Contributor

Bug/issue #, if applicable: rdar://138395778

Summary

This PR adds the following behavior changes:

  1. (First commit) Support for authoring @Metadata directives in documentation comments. Some notes:
  • 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:

    Screenshot 2024-11-28 at 18 08 43
  • 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.

    Screenshot 2024-11-28 at 18 08 58
  1. (Second commit) 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.

  2. (Third commit) Support for @Comment in documentation comments. They are innocuous, so may as well support them.

Dependencies

None.

Testing

In a Swift package, define @Metadata, @DeprecationSummary, and @Comment directives in documentation comments and verify that the rendered output matches your expectations.

Also, author invalid uses of these directives to trigger the diagnostics described above.

Checklist

  • Added tests
  • Ran the ./bin/test script and it succeeded
  • Updated documentation if necessary

@franklinsch
Copy link
Contributor Author

@swift-ci please test

@grynspan
Copy link

Woo! Do the directives apply recursively? For instance, if I have:

/// @Metadata {
///   @Available(Swift, introduced: 6.1)
/// }
struct S {
  var x
}

Will x be documented as introduced in Swift 6.1, or does it also need the directive?

@franklinsch
Copy link
Contributor Author

Good question. As of this PR, the changes don't get applied recursively. You need to annotate every member. Longer term I think it would make sense for availability to be carried over to child members (like in Swift), but we'd want a way to support annotating an API as unavailable so that you can opt out of the inferred availability. Currently the Available directive only supports introduced and deprecated versions.

@grynspan
Copy link

I think for our own purposes (Swift Testing) we can just make sure all symbols are explicitly annotated, but let's make sure to track that enhancement somewhere as it's probably not a scalable solution long-term. 🤔

bundle: bundle,
context: context,
problems: &problems
) as! (Metadata?, [BlockDirective])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the need to force cast here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was because the method returned any Markup and we needed to force cast the second element of the tuple so that it can be assigned to docCommentDirectives. But with your suggestion of making the markupElements argument inout, this is no longer needed.

import Markdown

/// A utlity type for parsing directives from markup.
struct DirectiveParser<Directive: AutomaticDirectiveConvertible> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: If this doesn't have any state, should it be an enum with static methods instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I typically prefer set these as structs in case we want to add configuration arguments to the initialiser, e.g., to customise the parsing behaviour. In this case it doesn't matter though, because the API is not public and we can switch it from an enum to a struct if/when we need to.

Comment on lines +341 to +342
bundle: DocumentationBundle,
context: DocumentationContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding the bundle and context parameters here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to pass them all the way down to DirectiveParser because the Metadata initialiser needs it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Franklin and I talked about this some more offline. Ideally the directives wouldn't need the full context because it creates a cyclic dependency. They really only need some mechanism to load data but changing that is way out of scope for this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this. I think there should be a better way to do it without having to pass the full context and bundle. This does not look correct for initialising a symbol.

/// and diagnoses subsequent instances.
func parseSingleDirective<MarkupElements: Sequence<Markup>>(
_ directiveType: Directive.Type,
from markupElements: MarkupElements,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering how this is called, how do you feel about making the markup inout instead of returning remaining markup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great suggestion, thanks! Initially I though we'd run into trouble because we sometimes call this function with [any Markup] and sometimes with [any BlockDirective] and I thought that'd be problematic with type erasure and any Markup not conforming to Markup, but we can actually just make this function take inout [any Markup], so I did that.

in context: DocumentationContext,
problems: inout [Problem]
) {
let invalidDirectives: [(any AutomaticDirectiveConvertible)?] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to have a list of supported directives rather than the unsupported directives here? I imagine that that would need to be updated less frequently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this. If we were to specify the valid ones, we'd still need to somehow get the values of the invalid ones in order to produce a diagnostic. I couldn't find a good way to get an array of the valid ones easily. We could maybe use keyPaths.values and find a way to generically access the relevant properties, but that seems too complex.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having the valid ones, If not we will have to remember to add it to the list, which could be forgotten and lead to errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to list the valid ones as well anyway, unfortunately.

Comment on lines 21 to 23
func parseSingleDirective<MarkupElements: Sequence<Markup>>(
_ directiveType: Directive.Type,
from markupElements: MarkupElements,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: You can express this more concisely using some

Suggested change
func parseSingleDirective<MarkupElements: Sequence<Markup>>(
_ directiveType: Directive.Type,
from markupElements: MarkupElements,
func parseSingleDirective(
_ directiveType: Directive.Type,
from markupElements: some Sequence<Markup>,

@@ -416,10 +420,7 @@ class MetadataTests: XCTestCase {
let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks])
let (bundle, context) = try testBundleAndContext(named: "TestBundle")

var analyzer = SemanticAnalyzer(source: nil, context: context, bundle: bundle)
_ = analyzer.visit(document)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why removing the analysis in this test and any problems raised from that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were parsing the document twice for some reason, and producing the diagnostics twice. I didn't see a reason for us needing to do that.

Comment on lines 1368 to 1377
docComment
.components(
separatedBy: .newlines
)
.enumerated()
.map { arg -> SymbolGraph.LineList.Line in
var (
index,
line
) = arg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I find this excessive use of newlines hard to read. AFAICT not based on width—because .map { arg -> SymbolGraph.LineList.Line in is wider than .components(separatedBy: .newlines)—so I'm assuming that it's a purely stylistic choice.

Suggested change
docComment
.components(
separatedBy: .newlines
)
.enumerated()
.map { arg -> SymbolGraph.LineList.Line in
var (
index,
line
) = arg
docComment
.components(separatedBy: .newlines)
.enumerated()
.map { index, line in

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. I had used Xcode's multi-line format here but it's a bit excessive here.

XCTAssert(problems.isEmpty)

XCTAssertEqual(
try XCTUnwrap(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This XCTWrap isn't necessary because you can compare an optional value with a non-optional value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's right, thanks!

@@ -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

If you wish, you can specify `@Metadata` configuration in documentation comments for the following customizations:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we use "if you wish" in any other user-facing documentation

Suggested change
If you wish, you can specify `@Metadata` configuration in documentation comments for the following customizations:
You can specify the following `@Metadata` configuration in an in-source documentation comment:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I agree. I prefer your more concise version.

@franklinsch
Copy link
Contributor Author

@swift-ci please test

@@ -24,7 +24,7 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: you need to run swift run generate-symbol-graph for this change to be reflected in the published documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, thanks. I updated this in the latest commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be good to add an "Earlier Versions" aside, like on the @Available documentation to say that before Swift-DocC 6.2 (or 6.1 if we plan on cherry-picking this), @DeprecationSummary isn't supported in documentation comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Updated, thanks! I also realised that CustomMetadata is actually intentionally hidden from documentation (although I'm not sure why), so I removed its reference in this document.

@franklinsch franklinsch force-pushed the r138395778-metadata-in-doc-comments branch from f10b1a4 to 4f75d2f Compare December 4, 2024 16:45
Comment on lines +341 to +342
bundle: DocumentationBundle,
context: DocumentationContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this. I think there should be a better way to do it without having to pass the full context and bundle. This does not look correct for initialising a symbol.

in context: DocumentationContext,
problems: inout [Problem]
) {
let invalidDirectives: [(any AutomaticDirectiveConvertible)?] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having the valid ones, If not we will have to remember to add it to the list, which could be forgotten and lead to errors.

Comment on lines +211 to +214
titleHeading,
] + (redirects ?? [])
+ supportedLanguages
+ pageImages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the change on code style here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what you mean here? This is all new code

@franklinsch franklinsch force-pushed the r138395778-metadata-in-doc-comments branch from 4f75d2f to 8b4a0ea Compare December 6, 2024 16:09
@@ -492,7 +492,7 @@ class SymbolTests: XCTestCase {
XCTAssertEqual(withRedirectInArticle.redirects, nil)

XCTAssertEqual(problems.first?.diagnostic.identifier, "org.swift.docc.UnsupportedDocCommentDirective")
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 3)
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 15)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems like it's finding another directive in some other symbol. Please update the test to

  • don't build more content that it needs to
  • warn about some unsupported directive that's created in this test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's referring to the @Redirected directive in the test, but I can see how that's confusing. I had made makeDocumentationNodeForSymbol add an arbitrary offset of 12 to the doc comment's line numbers, but I changed it so that it takes the value as a parameter instead for the specific tests I wrote.

)
return .init(text: line, range: range)
})
let newDocComment = SymbolGraph.LineList(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use makeLineList(docComment:startOffset:url:) here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks!

> 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to add an "Earlier Versions" aside, like on the @Available documentation to say that before Swift-DocC 6.2 (or 6.1 if we plan on cherry-picking this), @metadata isn't supported in documentation comments.

Copy link
Contributor

@d-ronnqvist d-ronnqvist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@franklinsch franklinsch force-pushed the r138395778-metadata-in-doc-comments branch from 3bf6dd7 to 0fc804c Compare December 11, 2024 12:51
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
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.
Adds support for `@Comment` directives in documentation comments. They
don't hurt anyone, so might as well allow them.
@franklinsch franklinsch force-pushed the r138395778-metadata-in-doc-comments branch from 0fc804c to a5c523e Compare December 11, 2024 12:55
@franklinsch
Copy link
Contributor Author

@swift-ci please test

@franklinsch franklinsch merged commit 62fb5d7 into swiftlang:main Dec 11, 2024
2 checks passed
@franklinsch franklinsch deleted the r138395778-metadata-in-doc-comments branch December 11, 2024 13:01
franklinsch added a commit to franklinsch/swift-docc that referenced this pull request Dec 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants