Skip to content

Commit 44f3997

Browse files
authored
Fix @SupportedLanguage directive for articles (#1318)
The `@SupportedLanguage` directive allows specifying a language that an article is available in. It can be used within the `@Metadata` directive like in the below example, where the article is made available in both Swift and Objective-C: ``` @metadata { @SupportedLanguage(swift) @SupportedLanguage(objc) } ``` This directive is processed when creating the topic graph node for the article. The supported languages of an article need to be stored in the resolved topic reference that eventually gets serialised into the render node, which the navigator uses. When a catalog contains more than one module, any articles present are not registered in the documentation cache, since it is not possible to determine what module it is belongs to. In such cases, to correctly include the set of supported languages, they must be added to the reference in the topic graph node during creation, rather than creating the reference and later updating this information during registration. This patch adds the logic to store the set of supported languages during topic node creation for all articles, independent of the cache registration. rdar://160284853
1 parent 5f905e4 commit 44f3997

File tree

7 files changed

+156
-15
lines changed

7 files changed

+156
-15
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ public class DocumentationContext {
757757
let (url, analyzed) = analyzedDocument
758758

759759
let path = NodeURLGenerator.pathForSemantic(analyzed, source: url, bundle: inputs)
760-
let reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguage: .swift)
760+
var reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguage: .swift)
761761

762762
// Since documentation extensions' filenames have no impact on the URL of pages, there is no need to enforce unique filenames for them.
763763
// At this point we consider all articles with an H1 containing link a "documentation extension."
@@ -811,7 +811,11 @@ public class DocumentationContext {
811811

812812
insertLandmarks(tutorialArticle.landmarks, from: topicGraphNode, source: url)
813813
} else if let article = analyzed as? Article {
814-
814+
// If the article contains any `@SupportedLanguage` directives in the metadata,
815+
// include those languages in the set of source languages for the reference.
816+
if let supportedLanguages = article.supportedLanguages {
817+
reference = reference.withSourceLanguages(supportedLanguages)
818+
}
815819
// Here we create a topic graph node with the prepared data but we don't add it to the topic graph just yet
816820
// because we don't know where in the hierarchy the article belongs, we will add it later when crawling the manual curation via Topics task groups.
817821
let topicGraphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: url), title: article.title!.plainText)
@@ -1842,17 +1846,7 @@ public class DocumentationContext {
18421846
let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: inputs)
18431847

18441848
// Use the languages specified by the `@SupportedLanguage` directives if present.
1845-
let availableSourceLanguages = article.value
1846-
.metadata
1847-
.flatMap { metadata in
1848-
let languages = Set(
1849-
metadata.supportedLanguages
1850-
.map(\.language)
1851-
)
1852-
1853-
return languages.isEmpty ? nil : languages
1854-
}
1855-
?? availableSourceLanguages
1849+
let availableSourceLanguages = article.value.supportedLanguages ?? availableSourceLanguages
18561850

18571851
// If available source languages are provided and it contains Swift, use Swift as the default language of
18581852
// the article.

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
630630
node.hierarchyVariants = hierarchyVariants
631631

632632
// Emit variants only if we're not compiling an article-only catalog to prevent renderers from
633-
// advertising the page as "Swift", which is the language DocC assigns to pages in article only pages.
633+
// advertising the page as "Swift", which is the language DocC assigns to pages in article only catalogs.
634634
// (github.com/swiftlang/swift-docc/issues/240).
635635
if let topLevelModule = context.soleRootModuleReference,
636636
try! context.entity(with: topLevelModule).kind.isSymbol

Sources/SwiftDocC/Semantics/Article/Article.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
6666
return abstractSection?.paragraph
6767
}
6868

69+
/// The list of supported languages for the article, if present.
70+
///
71+
/// This information is available via `@SupportedLanguage` in the `@Metadata` directive.
72+
public var supportedLanguages: Set<SourceLanguage>? {
73+
guard let metadata = self.metadata else {
74+
return nil
75+
}
76+
77+
let langs = metadata.supportedLanguages.map(\.language)
78+
return langs.isEmpty ? nil : Set(langs)
79+
}
80+
6981
/// An optional custom deprecation summary for a deprecated symbol.
7082
private(set) public var deprecationSummary: MarkupContainer?
7183

Sources/SwiftDocC/Semantics/Metadata/Metadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible {
105105

106106
func validate(source: URL?, problems: inout [Problem]) -> Bool {
107107
// Check that something is configured in the metadata block
108-
if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty {
108+
if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && supportedLanguages.isEmpty && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty {
109109
let diagnostic = Diagnostic(
110110
source: source,
111111
severity: .information,

Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,77 @@ Root
953953
"""
954954
)
955955
}
956+
957+
// Bug: rdar://160284853
958+
// The supported languages of an article need to be stored in the resolved
959+
// topic reference that eventually gets serialised into the render node,
960+
// which the navigator uses. This must be done when creating the topic
961+
// graph node, rather than updating the set of supported languages when
962+
// registering the article. If a catalog contains more than one module, any
963+
// articles present are not registered in the documentation cache, since it
964+
// is not possible to determine what module it is belongs to. This test
965+
// ensures that in such cases, the supported languages information is
966+
// correctly included in the render node, and that the navigator is built
967+
// correctly.
968+
func testSupportedLanguageDirectiveForStandaloneArticles() async throws {
969+
let catalog = Folder(name: "unit-test.docc", content: [
970+
InfoPlist(identifier: testBundleIdentifier),
971+
TextFile(name: "UnitTest.md", utf8Content: """
972+
# UnitTest
973+
974+
@Metadata {
975+
@TechnologyRoot
976+
@SupportedLanguage(data)
977+
}
978+
979+
## Topics
980+
981+
- <doc:Article>
982+
- ``Foo``
983+
"""),
984+
TextFile(name: "Article.md", utf8Content: """
985+
# Article
986+
987+
Just a random article.
988+
"""),
989+
// The correct way to configure a catalog is to have a single root module. If multiple modules,
990+
// are present, it is not possible to determine which module an article is supposed to be
991+
// registered with. We include multiple modules to prevent registering the articles in the
992+
// documentation cache, to test if the supported languages are attached prior to registration.
993+
JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo", symbols: [
994+
makeSymbol(id: "some-symbol", language: SourceLanguage.data, kind: .class, pathComponents: ["SomeSymbol"]),
995+
]))
996+
])
997+
998+
let (_, context) = try await loadBundle(catalog: catalog)
999+
1000+
let renderContext = RenderContext(documentationContext: context)
1001+
let converter = DocumentationContextConverter(context: context, renderContext: renderContext)
1002+
1003+
let targetURL = try createTemporaryDirectory()
1004+
let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier)
1005+
builder.setup()
1006+
1007+
for identifier in context.knownPages {
1008+
let entity = try context.entity(with: identifier)
1009+
let renderNode = try XCTUnwrap(converter.renderNode(for: entity))
1010+
try builder.index(renderNode: renderNode)
1011+
}
1012+
1013+
builder.finalize()
1014+
1015+
let navigatorIndex = try XCTUnwrap(builder.navigatorIndex)
1016+
1017+
let expectedNavigator = """
1018+
[Root]
1019+
┗╸UnitTest
1020+
┣╸Article
1021+
┗╸Foo
1022+
┣╸Classes
1023+
┗╸SomeSymbol
1024+
"""
1025+
XCTAssertEqual(navigatorIndex.navigatorTree.root.dumpTree(), expectedNavigator)
1026+
}
9561027

9571028
func testNavigatorIndexUsingPageTitleGeneration() async throws {
9581029
let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests")

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5772,6 +5772,48 @@ let expected = """
57725772
XCTAssertEqual(solution.replacements.first?.replacement, "")
57735773
}
57745774

5775+
func testSupportedLanguageDirectiveForStandaloneArticles() async throws {
5776+
let catalog = Folder(name: "unit-test.docc", content: [
5777+
TextFile(name: "Root.md", utf8Content: """
5778+
# Root
5779+
5780+
@Metadata {
5781+
@TechnologyRoot
5782+
@SupportedLanguage(objc)
5783+
@SupportedLanguage(data)
5784+
}
5785+
5786+
## Topics
5787+
5788+
- <doc:Article>
5789+
"""),
5790+
TextFile(name: "Article.md", utf8Content: """
5791+
# Article
5792+
5793+
@Metadata {
5794+
@SupportedLanguage(objc)
5795+
@SupportedLanguage(data)
5796+
}
5797+
"""),
5798+
// The correct way to configure a catalog is to have a single root module. If multiple modules,
5799+
// are present, it is not possible to determine which module an article is supposed to be
5800+
// registered with. We include multiple modules to prevent registering the articles in the
5801+
// documentation cache, to test if the supported languages are attached prior to registration.
5802+
JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo")),
5803+
])
5804+
5805+
let (bundle, context) = try await loadBundle(catalog: catalog)
5806+
5807+
XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))")
5808+
5809+
do {
5810+
let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Article", sourceLanguage: .data)
5811+
// Find the topic graph node for the article
5812+
let node = context.topicGraph.nodes.first { $0.key == reference }?.value
5813+
// Ensure that the reference within the topic graph node contains the supported languages
5814+
XCTAssertEqual(node?.reference.sourceLanguages, [.objectiveC, .data])
5815+
}
5816+
}
57755817
}
57765818

57775819
func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #filePath, line: UInt = #line) {

Tests/SwiftDocCTests/Semantics/ArticleTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,26 @@ class ArticleTests: XCTestCase {
249249
XCTAssertNil(semantic.metadata?.pageKind)
250250
XCTAssertNil(semantic.metadata?.titleHeading)
251251
}
252+
253+
func testSupportedLanguageDirective() async throws {
254+
let source = """
255+
# Root
256+
257+
@Metadata {
258+
@SupportedLanguage(swift)
259+
@SupportedLanguage(objc)
260+
@SupportedLanguage(data)
261+
}
262+
"""
263+
let document = Document(parsing: source, options: [.parseBlockDirectives])
264+
let (bundle, _) = try await testBundleAndContext()
265+
var problems = [Problem]()
266+
let article = Article(from: document, source: nil, for: bundle, problems: &problems)
267+
268+
XCTAssert(problems.isEmpty, "Unexpectedly found problems: \(DiagnosticConsoleWriter.formattedDescription(for: problems))")
269+
270+
XCTAssertNotNil(article)
271+
XCTAssertNotNil(article?.metadata, "Article should have a metadata container since the markup has a @Metadata directive")
272+
XCTAssertEqual(article?.metadata?.supportedLanguages.map(\.language), [.swift, .objectiveC, .data])
273+
}
252274
}

0 commit comments

Comments
 (0)