-
Notifications
You must be signed in to change notification settings - Fork 143
Centralize directive parsing and declaration logic #362
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
Centralize directive parsing and declaration logic #362
Conversation
2a24e61
to
925cf4e
Compare
@swift-ci please test |
I opened a PR on my fork to show how this new infrastructure would be used to create the new directives described here: https://forums.swift.org/t/supporting-more-dynamic-content-in-swift-docc-reference-documentation/59527. |
925cf4e
to
f6395ab
Compare
Introduces a new `AutomaticDirectiveConvertible` protocol along with several property wrappers to allow for declaring new markup directives with built-in parsing and diagnostics. The general architecture is based on `ArgumentParser`. Here’s an example directive: public final class Intro: Semantic, AutomaticDirectiveConvertible { public let originalMarkup: BlockDirective /// The title of the containing ``Tutorial``. @DirectiveArgumentWrapped public private(set) var title: String /// An optional video, displayed as a modal. @ChildDirective public private(set) var video: VideoMedia? = nil /// An optional standout image. @ChildDirective public private(set) var image: ImageMedia? = nil /// The child markup content. @ChildMarkup(numberOfParagraphs: .zeroOrMore) public private(set) var content: MarkupContainer static var keyPaths: [String : AnyKeyPath] = [ "title" : \Intro._title, "video" : \Intro._video, "image" : \Intro._image, "content" : \Intro._content ] init(originalMarkup: BlockDirective, title: String, image: ImageMedia?, video: VideoMedia?, content: MarkupContainer) { self.originalMarkup = originalMarkup super.init() self.content = content self.title = title self.image = image self.video = video } @available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.") init(originalMarkup: BlockDirective) { self.originalMarkup = originalMarkup } } This has some immediate benefits of reducing code duplication but longer term the main goal is to create a single source of truth for what Directives are allowed where and what arguments they expect. The next step here will be to expose this information to DocC’s own conceptual documentation so that directive documentation can be auto-generated based on this metadata. Longer term, this information can be used to power other integrations like code completion strategies. Resolves rdar://99065717
f6395ab
to
473e21a
Compare
@swift-ci please test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, this cleans things up significantly! It would be good to have an article that describes at a high-level what needs to be done to add a new directive to DocC, either as part of this PR or a future one.
precondition(directive.name == Self.directiveName) | ||
self.init(originalMarkup: directive) | ||
|
||
let reflectedDirective = DirectiveIndex.shared.reflection(of: type(of: self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you find any performance impact in using Mirror?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't run performance tests yet, but I don't expect any because of the DirectiveIndex
that caches each mirror creation so we only need to make one per directive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing a slight performance hit but nothing too significant. My guess is this is caused by the lock on the DirectiveIndex
. I'm planning on making the DirectiveIndex read-only in a future PR (the one that adds docs support) to remove the need for the lock so I'd prefer to land this as-is and make sure we address any performance hit then.
$ swift run make-test-bundle --output ~/Downloads/test-bundle.docc --sizeFactor 10
$ swift run --package-path bin/benchmark benchmark measure-commits main 473e21a82e14f475f5c20e4d1aa87ae85619b4d8 --repetitions 5 --docc-arguments convert ~/Downloads/test-bundle.docc/TestFramework/Docs.docc
$ swift run --package-path bin/benchmark benchmark diff benchmark-main.json benchmark-473e21a82e14f475f5c20e4d1aa87ae85619b4d8.json
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Metric │ Change │ main │ 473e21a82e14f475f5c20e4d1aa87ae85619b4d8 │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Duration for 'bundle-registration' │ no change¹,² │ 4.056 sec │ 4.141 sec │
│ Duration for 'convert-total-time' │ +2.321%³,⁴ │ 5.738 sec │ 5.871 sec │
│ Duration for 'documentation-processing' │ no change⁵ │ 1.516 sec │ 1.563 sec │
│ Duration for 'finalize-navigation-index' │ no change⁶ │ 0.089 sec │ 0.088 sec │
│ Peak memory footprint │ no change⁷,⁸ │ 428.9 MB │ 432.6 MB │
│ Data subdirectory size │ no change │ 111.7 MB │ 111.7 MB │
│ Index subdirectory size │ no change │ 994 KB │ 994 KB │
│ Total DocC archive size │ no change │ 117.8 MB │ 117.8 MB │
│ Topic Anchor Checksum │ no change │ c8f03d4e81da8e3b6d7f │ c8f03d4e81da8e3b6d7fb7f63c771147 │
│ Topic Graph Checksum │ no change │ edce22bce4f8de475a5e │ edce22bce4f8de475a5e58a2e45dcdc0 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, sounds good. Should we consider landing the performance fix as part of this PR though, otherwise we'll incur a regression?
} | ||
} | ||
|
||
protocol OptionallyWrappedCollectionWrappedDirectiveConvertible: OptionallyWrapped {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙃
problems: inout [Problem] | ||
) -> Bool | ||
|
||
static var keyPaths: [String : AnyKeyPath] { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be good to document why this is required
} | ||
|
||
@propertyWrapper | ||
public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this API and the ones below need to be public? If so, we're missing documentation for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They unfortunately do because you can't attach an internal property wrapper to a public property. This would actually be a good use case for the no documentation attribute @QuietMisdreavus is working on!
Curious on your thoughts here- I don't really think these should be considered truly public API but it also makes them much less user-friendly in the library itself if they're all underscored. Any suggestions? I suppose documenting them as-such is a reasonable option. 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I see, so you want the projected value of the property wrapper to be public, but not the property itself? It feels like we should just document what this API does for clients, with a note that we're not expecting them to use it.
Agreed! Planning on adding that as part of a future PR that will also add auto-generated user-facing docs for any defined directives. |
👀👀👀👀👀 that sounds really neat! |
@swift-ci please test |
I missed your comments regarding the performance regression: #362 (comment), which should be addressed before merging the PR.
77260cf looks to have resolved the regression.
|
@swift-ci please test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Super excited about this!
@swift-ci please test |
Bug/issue #: rdar://99065717
Summary
Introduces a new
AutomaticDirectiveConvertible
protocol along withseveral property wrappers to allow for declaring new markup directives
with built-in parsing and diagnostics. The general architecture is based
on
ArgumentParser
.Here’s an example directive:
This has some immediate benefits of reducing code duplication but
longer term the main goal is to create a single source of truth for
what Directives are allowed where and what arguments they expect.
The next step here will be to expose this information to DocC’s own
conceptual documentation so that directive documentation can be
auto-generated based on this metadata. Longer term, this information
can be used to power other integrations like code completion strategies.
Testing
The existing fairly extensive directive tests for each individually converted directive continue to pass. I've added some addition tests focused on the directive mirror as well.
Checklist
Make sure you check off the following items. If they cannot be completed, provide a reason.
./bin/test
script and it succeeded