Skip to content

Commit 556de91

Browse files
committed
Merge branch 'main' into remove-redundant-bundle-params
2 parents 73ed112 + eeabad0 commit 556de91

26 files changed

+1257
-182
lines changed

Sources/SwiftDocC/Checker/Checkers/AbstractContainsFormattedTextOnly.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public import Markdown
1414
/**
1515
A document's abstract may only contain formatted text. Images and links are not allowed.
1616
*/
17-
@available(*, deprecated, message: "This check is no longer applicable. This deprecated API will be removed after 6.3 is released")
17+
@available(*, deprecated, message: "This check is no longer applicable. This deprecated API will be removed after 6.4 is released")
1818
public struct AbstractContainsFormattedTextOnly: Checker {
1919
public var problems: [Problem] = [Problem]()
2020
private var sourceFile: URL?
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
internal import Foundation
12+
internal import Markdown
13+
14+
/**
15+
Code blocks can have a `nocopy` option after the \`\`\`, in the language line.
16+
`nocopy` can be immediately after the \`\`\` or after a specified language and a comma (`,`).
17+
*/
18+
internal struct InvalidCodeBlockOption: Checker {
19+
var problems = [Problem]()
20+
21+
/// Parsing options for code blocks
22+
private let knownOptions = RenderBlockContent.CodeBlockOptions.knownOptions
23+
24+
private var sourceFile: URL?
25+
26+
/// Creates a new checker that detects documents with multiple titles.
27+
///
28+
/// - Parameter sourceFile: The URL to the documentation file that the checker checks.
29+
init(sourceFile: URL?) {
30+
self.sourceFile = sourceFile
31+
}
32+
33+
mutating func visitCodeBlock(_ codeBlock: CodeBlock) {
34+
let (lang, tokens) = RenderBlockContent.CodeBlockOptions.tokenizeLanguageString(codeBlock.language)
35+
36+
func matches(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) {
37+
guard token == .unknown, let value = value else { return }
38+
39+
let matches = NearMiss.bestMatches(for: knownOptions, against: value)
40+
41+
if !matches.isEmpty {
42+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(value.singleQuoted) in code block.")
43+
let possibleSolutions = matches.map { candidate in
44+
Solution(
45+
summary: "Replace \(value.singleQuoted) with \(candidate.singleQuoted).",
46+
replacements: []
47+
)
48+
}
49+
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: possibleSolutions))
50+
} else if lang == nil {
51+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(value.singleQuoted) in code block.")
52+
let possibleSolutions =
53+
Solution(
54+
summary: "If \(value.singleQuoted) is the language for this code block, then write \(value.singleQuoted) as the first option.",
55+
replacements: []
56+
)
57+
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [possibleSolutions]))
58+
}
59+
}
60+
61+
func validateArrayIndices(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) {
62+
guard token == .highlight || token == .strikeout, let value = value else { return }
63+
// code property ends in a newline. this gives us a bogus extra line.
64+
let lineCount: Int = codeBlock.code.split(omittingEmptySubsequences: false, whereSeparator: { $0.isNewline }).count - 1
65+
66+
let indices = RenderBlockContent.CodeBlockOptions.parseCodeBlockOptionsArray(value)
67+
68+
if !value.isEmpty, indices.isEmpty {
69+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Could not parse \(token.rawValue.singleQuoted) indices from \(value.singleQuoted). Expected an integer (e.g. 3) or an array (e.g. [1, 3, 5])")
70+
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: []))
71+
return
72+
}
73+
74+
let invalid = indices.filter { $0 < 1 || $0 > lineCount }
75+
guard !invalid.isEmpty else { return }
76+
77+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Invalid \(token.rawValue.singleQuoted) index\(invalid.count == 1 ? "" : "es") in \(value.singleQuoted) for a code block with \(lineCount) line\(lineCount == 1 ? "" : "s"). Valid range is 1...\(lineCount).")
78+
let solutions: [Solution] = {
79+
if invalid.contains(where: {$0 == lineCount + 1}) {
80+
return [Solution(
81+
summary: "If you intended the last line, change '\(lineCount + 1)' to \(lineCount).",
82+
replacements: []
83+
)]
84+
}
85+
return []
86+
}()
87+
problems.append(Problem(diagnostic: diagnostic, possibleSolutions: solutions))
88+
}
89+
90+
for (token, value) in tokens {
91+
matches(token: token, value: value)
92+
validateArrayIndices(token: token, value: value)
93+
}
94+
// check if first token (lang) might be a typo
95+
matches(token: .unknown, value: lang)
96+
}
97+
}

Sources/SwiftDocC/Indexing/Navigator/AvailabilityIndex+Ext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public struct InterfaceLanguage: Hashable, CustomStringConvertible, Codable, Equ
8383
/// > ``from(string:)`` function.
8484
public let id: String
8585

86-
/// A mask to use to identify the interface language..
86+
/// A mask to use to identify the interface language.
8787
public let mask: ID
8888

8989

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,12 @@ public class NavigatorIndex {
219219
}
220220

221221
/**
222-
Initialize an `NavigatorIndex` from a given path with an empty tree.
222+
Initialize a `NavigatorIndex` from a given path with an empty tree.
223223

224224
- Parameter url: The URL pointing to the path from which the index should be read.
225225
- Parameter bundleIdentifier: The name of the bundle the index is referring to.
226226

227-
- Note: Don't exposed this initializer as it's used **ONLY** for building an index.
227+
- Note: Don't expose this initializer as it's used **ONLY** for building an index.
228228
*/
229229
fileprivate init(withEmptyTree url: URL, bundleIdentifier: String) throws {
230230
self.url = url
@@ -364,14 +364,14 @@ public class NavigatorIndex {
364364
Read a tree on disk from a given path.
365365
The read is atomically performed, which means it reads all the content of the file from the disk and process the tree from loaded data.
366366
The queue is used to load the data for a given timeout period, after that, the queue is used to schedule another read after a given delay.
367-
This approach ensures that the used queue doesn't stall while loading the content from the disk keeping the used queue responsive.
367+
This approach ensures that the used queue doesn't stall while loading the content from the disk keeping the used queue responsive.
368368

369369
- Parameters:
370-
- timeout: The amount of time we can load a batch of items from data, once the timeout time pass,
370+
- timeout: The duration for which we can load a batch of items from data. Once the timeout duration passes,
371371
the reading process will reschedule asynchronously using the given queue.
372-
- delay: The delay to wait before schedule the next read. Default: 0.01 seconds.
372+
- delay: The duration to wait for before scheduling the next read. Default: 0.01 seconds.
373373
- queue: The queue to use.
374-
- broadcast: The callback to update get updates of the current process.
374+
- broadcast: The callback to receive updates on the status of the current process.
375375

376376
- Note: Do not access the navigator tree root node or the map from identifier to node from a different thread than the one the queue is using while the read is performed,
377377
this may cause data inconsistencies. For that please use the broadcast callback that notifies which items have been loaded.
@@ -455,6 +455,17 @@ extension NavigatorIndex {
455455
self.fragment = fragment
456456
self.languageIdentifier = languageIdentifier
457457
}
458+
459+
/// Compare an identifier with another one, ignoring the identifier language.
460+
///
461+
/// Used when curating cross-language references in multi-language frameworks.
462+
///
463+
/// - Parameter other: The other identifier to compare with.
464+
func isEquivalentIgnoringLanguage(to other: Identifier) -> Bool {
465+
return self.bundleIdentifier == other.bundleIdentifier &&
466+
self.path == other.path &&
467+
self.fragment == other.fragment
468+
}
458469
}
459470

460471
/**
@@ -884,7 +895,7 @@ extension NavigatorIndex {
884895
/// - emitJSONRepresentation: Whether or not a JSON representation of the index should
885896
/// be written to disk.
886897
///
887-
/// Defaults to `false`.
898+
/// Defaults to `true`.
888899
///
889900
/// - emitLMDBRepresentation: Whether or not an LMDB representation of the index should
890901
/// written to disk.
@@ -917,7 +928,7 @@ extension NavigatorIndex {
917928
let (nodeID, parent) = nodesMultiCurated[index]
918929
let placeholders = identifierToChildren[nodeID]!
919930
for reference in placeholders {
920-
if let child = identifierToNode[reference] {
931+
if let child = identifierToNode[reference] ?? externalNonSymbolNode(for: reference) {
921932
parent.add(child: child)
922933
pendingUncuratedReferences.remove(reference)
923934
if !multiCurated.keys.contains(reference) && reference.fragment == nil {
@@ -938,7 +949,7 @@ extension NavigatorIndex {
938949
for (nodeIdentifier, placeholders) in identifierToChildren {
939950
for reference in placeholders {
940951
let parent = identifierToNode[nodeIdentifier]!
941-
if let child = identifierToNode[reference] {
952+
if let child = identifierToNode[reference] ?? externalNonSymbolNode(for: reference) {
942953
let needsCopy = multiCurated[reference] != nil
943954
parent.add(child: (needsCopy) ? child.copy() : child)
944955
pendingUncuratedReferences.remove(reference)
@@ -969,14 +980,11 @@ extension NavigatorIndex {
969980
// page types as symbol nodes on the assumption that an unknown page type is a
970981
// symbol kind added in a future version of Swift-DocC.
971982
// Finally, don't add external references to the root; if they are not referenced within the navigation tree, they should be dropped altogether.
972-
if let node = identifierToNode[nodeID], PageType(rawValue: node.item.pageType)?.isSymbolKind == false , !node.item.isExternal {
983+
if let node = identifierToNode[nodeID], PageType(rawValue: node.item.pageType)?.isSymbolKind == false, !node.item.isExternal {
973984

974985
// If an uncurated page has been curated in another language, don't add it to the top-level.
975986
if curatedReferences.contains(where: { curatedNodeID in
976-
// Compare all the identifier's properties for equality, except for its language.
977-
curatedNodeID.bundleIdentifier == nodeID.bundleIdentifier
978-
&& curatedNodeID.path == nodeID.path
979-
&& curatedNodeID.fragment == nodeID.fragment
987+
curatedNodeID.isEquivalentIgnoringLanguage(to: nodeID)
980988
}) {
981989
continue
982990
}
@@ -1256,7 +1264,22 @@ extension NavigatorIndex {
12561264
problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
12571265
problems.append(problem)
12581266
}
1259-
1267+
1268+
/// Find an external node for the reference that is not of a symbol kind. The source language
1269+
/// of the reference is ignored during this lookup since the reference assumes the target node
1270+
/// to be of the same language as the page that it is curated in. This may or may not be true
1271+
/// since non-symbol kinds (articles, tutorials, etc.) are not tied to a language.
1272+
// This is a workaround for https://github.com/swiftlang/swift-docc/issues/240.
1273+
// FIXME: This should ideally be solved by making the article language-agnostic rather
1274+
// than accomodating the "Swift" language and special-casing for non-symbol nodes.
1275+
func externalNonSymbolNode(for reference: NavigatorIndex.Identifier) -> NavigatorTree.Node? {
1276+
identifierToNode
1277+
.first { identifier, node in
1278+
identifier.isEquivalentIgnoringLanguage(to: reference)
1279+
&& PageType.init(rawValue: node.item.pageType)?.isSymbolKind == false
1280+
&& node.item.isExternal
1281+
}?.value
1282+
}
12601283

12611284
/// Build the index using the render nodes files in the provided documentation archive.
12621285
/// - Returns: A list containing all the errors encountered during indexing.

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ public class DocumentationContext {
259259
MissingAbstract(sourceFile: source).any(),
260260
NonOverviewHeadingChecker(sourceFile: source).any(),
261261
SeeAlsoInTopicsHeadingChecker(sourceFile: source).any(),
262+
InvalidCodeBlockOption(sourceFile: source).any(),
262263
])
263264
checker.visit(document)
264265
diagnosticEngine.emit(checker.problems)
@@ -2414,7 +2415,6 @@ public class DocumentationContext {
24142415
}
24152416
}
24162417
}
2417-
24182418
/// A closure type getting the information about a reference in a context and returns any possible problems with it.
24192419
public typealias ReferenceCheck = (DocumentationContext, ResolvedTopicReference) -> [Problem]
24202420

@@ -2697,6 +2697,11 @@ public class DocumentationContext {
26972697
).isSymbol
26982698
}
26992699

2700+
/// Returns whether the given reference resolves to an external entity.
2701+
func isExternal(reference: ResolvedTopicReference) -> Bool {
2702+
externalCache[reference] != nil
2703+
}
2704+
27002705
// MARK: - Relationship queries
27012706

27022707
/// Fetch the child nodes of a documentation node with the given `reference`, optionally filtering to only children of the given `kind`.

0 commit comments

Comments
 (0)