-
Notifications
You must be signed in to change notification settings - Fork 128
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
Add directive for linking symbols as alternate representations #1097
Conversation
Before I get into the code, some feedback on this syntax:
|
Sources/SwiftDocC/Semantics/General Purpose Analyses/HasExactlyOne.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/SourceLanguage+Directive.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/SourceLanguage+Directive.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/AlternativeDeclaration.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/AlternativeDeclaration.swift
Outdated
Show resolved
Hide resolved
for alternateDeclaration in entity.metadata?.alternateDeclarations ?? [] { | ||
guard case .resolved(.success(let counterpartReference)) = alternateDeclaration.counterpart, | ||
let counterpartEntity = try? self.entity(with: counterpartReference) else { | ||
continue |
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.
If this is the only place that checks that the link resolved we should emit a warning with the right link source range so that the developer knows that the link failed to resolve.
unresolvedReferenceProblem(...)
can create the same type of diagnostic as for other unresolved links by passing the TopicReferenceResolutionErrorInfo
from the TopicReferenceResolutionResult.failure(_:_:)
.
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.
We will already emit a diagnostic in the code below so I don't think it's also needed here, WDYT?
swift-docc/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Lines 614 to 621 in bc6df51
// Also resolve the node's alternate declaration. This isn't part of the node's 'semantic' value (resolved above). | |
documentationNode.metadata?.alternateDeclarations.forEach { synonym in | |
resolver.resolve( | |
synonym: synonym, | |
range: synonym.originalMarkup.range, | |
severity: .warning | |
) | |
} |
Since this ultimately ends up adding this diagnostic:
problems.append(unresolvedReferenceProblem(source: range?.source, range: range, severity: severity, uncuratedArticleMatch: uncuratedArticleMatch, errorInfo: error, fromSymbolLink: false)) |
(I can add a comment to explain why we don't need a diagnostic here)
abe7160
to
14d85bf
Compare
@swift-ci Please test |
@Synonym
directive for linking symbolsThere 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.
The implementation generally looks good but the tests are unrealistic and don't cover a handful of behaviors that should raise diagnostics. Also, I think much of the user-facing documentation and other user-facing text could benefit from some refinements.
Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift
Show resolved
Hide resolved
Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift
Outdated
Show resolved
Hide resolved
Tests/SwiftDocCTests/Semantics/MetadataAlternateRepresentationTests.swift
Outdated
Show resolved
Hide resolved
1f367f9
to
ad4d279
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.
The code looks good but I'd like to make one more iteration on the user facing documentation.
Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift
Outdated
Show resolved
Hide resolved
Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift
Outdated
Show resolved
Hide resolved
@@ -1852,23 +1852,50 @@ public struct RenderNodeTranslator: SemanticVisitor { | |||
private func variants(for documentationNode: DocumentationNode) -> [RenderNode.Variant] { | |||
let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL) | |||
|
|||
return documentationNode.availableSourceLanguages | |||
.sorted(by: { language1, language2 in | |||
func mapSourceLanguageToVariants(identifier: ResolvedTopicReference, sourceLanguages: Set<SourceLanguage>) -> [(SourceLanguage, [ResolvedTopicReference])] { |
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.
minor: I don't think this function is helping the readability of the rest of this code.
The first usage could be written as one these alternatives without the mapSourceLanguageToVariants
function
var allVariants: [SourceLanguage: ResolvedTopicReference] = documentationNode.availableSourceLanguages.reduce(into: [:]) { partialResult, language in
partialResult[language] = identifier
}
var allVariants = [SourceLanguage: ResolvedTopicReference]()
for language in documentationNode.availableSourceLanguages {
allVariants[language] = identifier
}
I find both alternatives to be more explicit in how the data is being built up.
Similarly, the second usage could be written s one these alternatives without the mapSourceLanguageToVariants
function
for language in alternateRepresentationReference.sourceLanguages.subtracting(allVariants.keys) {
allVariants[language] = alternateRepresentationReference
}
allVariants.merge(
alternateRepresentationReference.sourceLanguages.map { ($0, alternateRepresentationReference) }
) { existing, _ in existing }
Also, I don't see the reason to store an array of references for each language when the only value ever is a single element, so in the examples above I used [SourceLanguage: ResolvedTopicReference]
instead of [SourceLanguage: [ResolvedTopicReference]]
.
The only difference that I can see is that the creation of the RenderNode.Variant
towards the end would create the array inline from the only reference
RenderNode.Variant(
traits: [.interfaceLanguage(sourceLanguage.id)],
+ paths: [
- paths: references.map { reference in
generator.presentationURLForReference(reference).path
- }
+ ]
)
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.
Seems reasonable to me, I agree it makes it clearer to use a ResolvedTopicReference
over a [ResolvedTopicReference]
to emphasise there can only be one. The main reason I wasn't doing that originally is to remove the chances of unintentionally causing a regression in behaviour.
Previously, we weren't enforcing having only one path per trait, so even though I can't think of a case where there would have been multiple paths per trait, it could have theoretically happened. I'm not sure if enforcing it will have unintentional consequences, but I think it does make it clearer to enforce it here.
Made the suggested changes, thanks for the code suggestions!
149a977
to
9e3cb87
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.
Thanks for all the iterations.
This looks good to me.
Adds a new child directive to `@Metadata` which can be used in a symbol extension file by specifying the link to another symbol: ```swift @metadata { @AlternateRepresentation(``MyClass/property``) } ``` External links are also supported, as long as they're quoted: ```swift @metadata { @AlternateRepresentation("doc://com.example/documentation/MyClass/property") } ``` The intent of this directive is to define an alternate language representation for the current symbol, such that both symbols are considered to be alternate representations of the same symbol. Ideally two symbols which are equivalent would have the same USR and be resolved as the same symbol by the compiler, but this is not always the case. For the cases in which it is not feasible to change the source code to have them as one symbol, the `@AlternateRepresentation` directive can be used to manually link them as variants of each other. Discussion: ---------- A mutable topic reference type was chosen as the type for parsing&storing the link so that it can be parsed as an unresolved link at the directive parsing stage, and then later be updated to a resolved link at a later stage when the documentation context is resolving all links. A parsing failure diagnostic is returned if the link is invalid in any way: ``` AlternateRepresentation expects an argument for an unnamed parameter that's convertible to 'TopicReference' --> SynonymSample.docc/Symbol.md:4:31-4:37 2 | 3 | @metadata { 4 + @AlternateRepresentation("doc://") 5 | } 6 | ``` This commit adds the directive itself, but doesn't hook it up to other parts of SwiftDocC. Subsequent commits will add: - link resolution - rendering logic Alternatives considered: ----------------------- Considered other names such as `@Synonym`, `@Counterpart`, `@AlternativeDeclaration` and `@VariantOf`. In the end disqualified these as being confusing, and chose `@AlternateRepresentation` for being the one which strikes the best balance between readable and closeness to the technical term for this concept.
Adds logic in `DocumentationContext` which will resolve the references inside the alternate representation directive. The same logic is used as for resolving all other links. This is done outside the usual ReferenceResolver visit of the semantic object, because `Symbol` objects don't have access to the node metadata, where the unresolved link resides. If the link cannot be resolved, the usual diagnostic is emitted: ``` warning: 'MissingSymbol' doesn't exist at '/Synonyms' --> SynonymSample.docc/SymbolExtension2.md:4:19-4:32 2 | 3 | @metadata { 4 + @AlternateRepresentation(``Synonyms/MissingSymbol``) 5 | } ```
If an `@AlternateRepresentation` clashes with already available source languages, this will now be reported as diagnostics. These diagnostics are performed in the final stage of registering a bundle, during the global analysis of the topic graph, where all nodes are available and all links will have been resolved. This is so that we have all the information we need for detecting duplicates. The following cases are detected: - if the symbol the alternate representation is being defined for (the "original" symbol) was already available in one of the languages the counterpart symbol is available in - if the alternate representations have duplicate source languages in common, i.e. if counterpart1 is available in Objective-C and counterpart2 is **also** available in Objective-C. Suggestions will be provided depending on context: - which languages are duplicate - all the languages the symbol is already available in will be available as part of the diagnostic explanation - if the `@AlternateRepresentation` directive is a duplicate, a suggestion will be made to remove it, with a suitable replacement - if the `@AlternateRepresentation` directive is a duplicate, a note pointing to the original directive will be added Example diagnostics: ``` warning: An alternate representation for Swift already exists This node is already available in Swift and Objective-C. SynonymSample.docc/SymbolExtension2.md:4:5: An alternate representation for Swift has already been defined by an @AlternateRepresentation directive. --> SynonymSample.docc/SymbolExtension2.md:5:5-5:57 3 | @metadata { 4 | @AlternateRepresentation(``Synonyms/Synonym-5zxmc``) 5 + @AlternateRepresentation(``Synonyms/Synonym-5zxmc``) | ╰─suggestion: Remove this alternate representation 6 | } 7 | ``` ``` warning: This node already has a representation in Swift This node is already available in Swift. --> SynonymSample.docc/SynonymExtension.md:5:5-5:56 3 | @metadata { 4 | @AlternateRepresentation(``Synonyms/Synonym-1wqxt``) 5 + @AlternateRepresentation(``Synonyms/OtherSynonym``) | ╰─suggestion: Replace the counterpart link with a node which isn't available in Swift 6 | } 7 | ```
The `@AlternateRepresentation` directive is not expected for non-symbol pages, and we now emit diagnostics for this case. For example, if an `@AlternateDeclaration` directive is added to an article, the resulting diagnostic will be: ``` warning: Custom alternate representations are not supported for page kind 'Article' Alternate representations are only supported for symbols. --> ./SynonymSample.docc/Article.md:4:5-4:57 2 | 3 | @metadata { 4 + @AlternateRepresentation(``Synonyms/Synonym-5zxmc``) | ╰─suggestion: Remove this alternate representation 5 | } ``` And if a custom alternate declaration to an article is specified, the resulting dia gnostic will be: ``` warning: Page kind 'Article' is not allowed as a custom alternate language representation Symbols can only specify other symbols as custom language representations. --> ./SynonymSample.docc/Synonym-1wqxt.md:5:5-5:44 3 | @metadata { 4 | @AlternateRepresentation(``Synonyms/Synonym-5zxmc``) 5 + @AlternateRepresentation("doc:Article") | ╰─suggestion: Remove this alternate representation 6 | } ```
When rendering the variants of a node, use the topic references from the `@AlternateRepresentation` directives to populate more variants. There is no need to report diagnostics as they would have been reported during bundle registration. Link resolution would have already been performed at that point. Unresolved topic references are ignored, but all resolved references are added as variants. If there are multiple symbols per variant, Swift-DocC-Render prefers the first one that was added, which will always be the current node's symbol. There should be no breakage and change of behaviour for anyone not using `@AlternateRepresentation`, and the current symbol's variants will always be preferred over any other.
68adda3
to
74f3f2c
Compare
@swift-ci please test |
Bug/issue #, if applicable: rdar://109417745
Summary
Adds a directive which is meant to be used in markdown extension files for a specific symbol:
Where all variants of
MyApp/MyClass/property
will be added as variants of the current symbol.Its purpose is to be able to define an alternative language representation for a symbol, where the alternative symbol is an unrelated symbol as far as the compiler is aware (i.e. they have different USRs in the symbol graph).
This makes it possible to switch between both symbols through the language switcher:
Whenever possible, the alternative language representations should be defined in-source, by using in-source annotations such as the
@objc
and@_objcImplementation
attributes in Swift, or theNS_SWIFT_NAME
macro in Objective C.However, if this is not possible, this is an alternative to be able to render both symbols as counterparts of each other.
Diagnostics
Links within the directive are resolved, and emit a warning if the link cannot be resolved same as all other markup links:
Duplication of source language availability is also detected:
And suggestions will be provided depending on context:
@AlternateRepresentation
directive is a duplicate, a suggestion will be made to remove it, with a suitable replacement@AlternateRepresentation
directive is a duplicate, a note pointing to the original directive will be addedDependencies
None.
Testing
Tested by building and rendering the following archive locally, which uses the new directive:
SynonymSample.docc.zip
This contains two symbols which have been configured to be counterparts of each other.
You should be able to switch between both symbols using the language picker from either:
http://localhost:8080/documentation/synonyms/synonym-1wqxt?language=objc
or
http://localhost:8080/documentation/synonyms/synonym-5zxmc
Checklist
Make sure you check off the following items. If they cannot be completed, provide a reason.
./bin/test
script and it succeeded