Skip to content

Commit 78bb328

Browse files
committed
Merge branch 'main' into parameters-and-returns-validation
# Conflicts: # Package.resolved # Sources/SwiftDocC/Utility/FeatureFlags.swift # Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift # Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift
2 parents a30d75e + 95a45d0 commit 78bb328

File tree

117 files changed

+7455
-1216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+7455
-1216
lines changed

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
/*
33
This source file is part of the Swift.org open source project
44

5-
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
66
Licensed under Apache License v2.0 with Runtime Library Exception
77

88
See https://swift.org/LICENSE.txt for license information
@@ -90,6 +90,7 @@ let package = Package(
9090
.target(
9191
name: "SwiftDocCTestUtilities",
9292
dependencies: [
93+
.target(name: "SwiftDocC"),
9394
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
9495
],
9596
swiftSettings: swiftSettings
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 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+
import Foundation
12+
import SymbolKit
13+
14+
/// A type that writes the auto-generated curation into documentation extension files.
15+
public struct GeneratedCurationWriter {
16+
let context: DocumentationContext
17+
let catalogURL: URL?
18+
let outputURL: URL
19+
let linkResolver: PathHierarchyBasedLinkResolver
20+
21+
public init(
22+
context: DocumentationContext,
23+
catalogURL: URL?,
24+
outputURL: URL
25+
) {
26+
self.context = context
27+
28+
self.catalogURL = catalogURL
29+
self.outputURL = outputURL
30+
31+
self.linkResolver = context.linkResolver.localResolver
32+
}
33+
34+
/// Generates the markdown representation of the auto-generated curation for a given symbol reference.
35+
///
36+
/// - Parameters:
37+
/// - reference: The symbol reference to generate curation text for.
38+
/// - Returns: The auto-generated curation text, or `nil` if this reference has no auto-generated curation.
39+
func defaultCurationText(for reference: ResolvedTopicReference) -> String? {
40+
guard let node = context.documentationCache[reference],
41+
let symbol = node.semantic as? Symbol,
42+
let automaticTopics = try? AutomaticCuration.topics(for: node, withTraits: [], context: context),
43+
!automaticTopics.isEmpty
44+
else {
45+
return nil
46+
}
47+
48+
let relativeLinks = linkResolver.disambiguatedRelativeLinksForDescendants(of: reference)
49+
50+
// Top-level curation has a few special behaviors regarding symbols with different representations in multiple languages.
51+
let isForTopLevelCuration = symbol.kind.identifier == .module
52+
53+
var text = ""
54+
for taskGroup in automaticTopics {
55+
if isForTopLevelCuration, let firstReference = taskGroup.references.first, context.documentationCache[firstReference]?.symbol?.kind.identifier == .typeProperty {
56+
// Skip type properties in top-level curation. It's not clear what's the right place for these symbols are since they exist in
57+
// different places in different source languages (which documentation extensions don't yet have a way of representing).
58+
continue
59+
}
60+
61+
let links: [(link: String, comment: String?)] = taskGroup.references.compactMap { (curatedReference: ResolvedTopicReference) -> (String, String?)? in
62+
guard let linkInfo = relativeLinks[curatedReference] else { return nil }
63+
// If this link contains disambiguation, include a comment with the full symbol declaration to make it easier to know which symbol the link refers to.
64+
var commentText: String?
65+
if linkInfo.hasDisambiguation {
66+
commentText = context.documentationCache[curatedReference]?.symbol?.declarationFragments?.map(\.spelling)
67+
// Replace sequences of whitespace and newlines with a single space
68+
.joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ")
69+
}
70+
71+
return ("\n- ``\(linkInfo.link)``", commentText.map { " <!-- \($0) -->" })
72+
}
73+
74+
guard !links.isEmpty else { continue }
75+
76+
text.append("\n\n### \(taskGroup.title ?? "<!-- This auto-generated topic has no title -->")\n")
77+
78+
// Calculate the longest link to nicely align all the comments
79+
let longestLink = links.map(\.link.count).max()! // `links` are non-empty so it's safe to force-unwrap `.max()` here
80+
for (link, comment) in links {
81+
if let comment = comment {
82+
text.append(link.padding(toLength: longestLink, withPad: " ", startingAt: 0))
83+
text.append(comment)
84+
} else {
85+
text.append(link)
86+
}
87+
}
88+
}
89+
90+
guard !text.isEmpty else { return nil }
91+
92+
var prefix = "<!-- The content below this line is auto-generated and is redundant. You should either incorporate it into your content above this line or delete it. -->"
93+
94+
// Add "## Topics" to the curation text unless the symbol already had some manual curation.
95+
let hasAnyManualCuration = symbol.topics?.taskGroups.isEmpty == false
96+
if !hasAnyManualCuration {
97+
prefix.append("\n\n## Topics")
98+
}
99+
return "\(prefix)\(text)\n"
100+
}
101+
102+
enum Error: DescribedError {
103+
case symbolLinkNotFound(TopicReferenceResolutionErrorInfo)
104+
105+
var errorDescription: String {
106+
switch self {
107+
case .symbolLinkNotFound(let errorInfo):
108+
var errorMessage = "'--from-symbol <symbol-link>' not found: \(errorInfo.message)"
109+
for solution in errorInfo.solutions {
110+
errorMessage.append("\n\(solution.summary.replacingOccurrences(of: "\n", with: ""))")
111+
}
112+
return errorMessage
113+
}
114+
}
115+
}
116+
117+
/// Generates documentation extension content with a markdown representation of DocC's auto-generated curation.
118+
/// - Parameters:
119+
/// - symbolLink: A link to the symbol whose sub hierarchy the curation writer will descend.
120+
/// - depthLimit: The depth limit of how far the curation writer will descend from its starting point symbol.
121+
/// - Returns: A collection of file URLs and their markdown content.
122+
public func generateDefaultCurationContents(fromSymbol symbolLink: String? = nil, depthLimit: Int? = nil) throws -> [URL: String] {
123+
// Used in documentation extension page titles to reference symbols that don't already have a documentation extension file.
124+
let allAbsoluteLinks = linkResolver.pathHierarchy.disambiguatedAbsoluteLinks()
125+
126+
guard var curationCrawlRoot = linkResolver.modules().first else {
127+
return [:]
128+
}
129+
130+
if let symbolLink = symbolLink {
131+
switch context.linkResolver.resolve(UnresolvedTopicReference(topicURL: .init(symbolPath: symbolLink)), in: curationCrawlRoot, fromSymbolLink: true, context: context) {
132+
case .success(let foundSymbol):
133+
curationCrawlRoot = foundSymbol
134+
case .failure(_, let errorInfo):
135+
throw Error.symbolLinkNotFound(errorInfo)
136+
}
137+
}
138+
139+
var contentsToWrite = [URL: String]()
140+
for (usr, reference) in context.symbolIndex {
141+
// Filter out symbols that aren't in the specified sub hierarchy.
142+
if symbolLink != nil || depthLimit != nil {
143+
guard reference == curationCrawlRoot || context.pathsTo(reference).contains(where: { path in path.suffix(depthLimit ?? .max).contains(curationCrawlRoot)}) else {
144+
continue
145+
}
146+
}
147+
148+
guard let absoluteLink = allAbsoluteLinks[usr], let curationText = defaultCurationText(for: reference) else { continue }
149+
if let catalogURL = catalogURL, let existingURL = context.documentationExtensionURL(for: reference) {
150+
let updatedFileURL: URL
151+
if catalogURL == outputURL {
152+
updatedFileURL = existingURL
153+
} else {
154+
var url = outputURL
155+
let relativeComponents = existingURL.standardizedFileURL.pathComponents.dropFirst(catalogURL.standardizedFileURL.pathComponents.count)
156+
for component in relativeComponents.dropLast() {
157+
url.appendPathComponent(component, isDirectory: true)
158+
}
159+
url.appendPathComponent(relativeComponents.last!, isDirectory: false)
160+
updatedFileURL = url
161+
}
162+
// Append to the end of the file. See if we can avoid reading the existing contents on disk.
163+
var contents = try String(contentsOf: existingURL)
164+
contents.append("\n")
165+
contents.append(curationText)
166+
contentsToWrite[updatedFileURL] = contents
167+
} else {
168+
let relativeReferencePath = reference.url.pathComponents.dropFirst(2).joined(separator: "/")
169+
let fileName = urlReadablePath("/" + relativeReferencePath)
170+
let newFileURL = NodeURLGenerator.fileSafeURL(outputURL.appendingPathComponent("\(fileName).md"))
171+
172+
let contents = """
173+
# ``\(absoluteLink)``
174+
175+
\(curationText)
176+
"""
177+
contentsToWrite[newFileURL] = contents
178+
}
179+
}
180+
181+
return contentsToWrite
182+
}
183+
}

Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -12,17 +12,6 @@ import Foundation
1212

1313
/// A container for a collection of data. Each data can have multiple variants.
1414
struct DataAssetManager {
15-
enum Error: DescribedError {
16-
case invalidImageAsset(URL)
17-
18-
var errorDescription: String {
19-
switch self {
20-
case .invalidImageAsset(let url):
21-
return "The dimensions of the image at \(url.path.singleQuoted) could not be computed because the file is not a valid image."
22-
}
23-
}
24-
}
25-
2615
var storage = [String: DataAsset]()
2716

2817
// A "file name with no extension" to "file name with extension" index
@@ -113,12 +102,10 @@ struct DataAssetManager {
113102
return (reference: dataReference, traits: traitCollection, metadata: metadata)
114103
}
115104

116-
/**
117-
Registers a collection of data and determines their trait collection.
118-
119-
Data objects which have a file name ending with '~dark' are associated to their light variant.
120-
- Throws: Will throw `Error.invalidImageAsset(URL)` if fails to read the size of an image asset (e.g. the file is corrupt).
121-
*/
105+
106+
/// Registers a collection of data and determines their trait collection.
107+
///
108+
/// Data objects which have a file name ending with '~dark' are associated to their light variant.
122109
mutating func register<Datas: Collection>(data datas: Datas, dataProvider: DocumentationContextDataProvider? = nil, bundle documentationBundle: DocumentationBundle? = nil) throws where Datas.Element == URL {
123110
for dataURL in datas {
124111
let meta = try referenceMetaInformationForDataURL(dataURL, dataProvider: dataProvider, bundle: documentationBundle)

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -27,18 +27,30 @@ public final class DiagnosticConsoleWriter: DiagnosticFormattingConsumer {
2727
/// - formattingOptions: The formatting options for the diagnostics.
2828
/// - baseUrl: A url to be used as a base url when formatting diagnostic source path.
2929
/// - highlight: Whether or not to highlight the default diagnostic formatting output.
30-
public init(
30+
public convenience init(
3131
_ stream: TextOutputStream = LogHandle.standardError,
3232
formattingOptions options: DiagnosticFormattingOptions = [],
3333
baseURL: URL? = nil,
3434
highlight: Bool? = nil
35+
) {
36+
self.init(stream, formattingOptions: options, baseURL: baseURL, highlight: highlight, fileManager: FileManager.default)
37+
}
38+
39+
@_spi(FileManagerProtocol)
40+
public init(
41+
_ stream: TextOutputStream = LogHandle.standardError,
42+
formattingOptions options: DiagnosticFormattingOptions = [],
43+
baseURL: URL? = nil,
44+
highlight: Bool? = nil,
45+
fileManager: FileManagerProtocol = FileManager.default
3546
) {
3647
outputStream = stream
3748
formattingOptions = options
3849
diagnosticFormatter = Self.makeDiagnosticFormatter(
3950
options,
4051
baseURL: baseURL,
41-
highlight: highlight ?? TerminalHelper.isConnectedToTerminal
52+
highlight: highlight ?? TerminalHelper.isConnectedToTerminal,
53+
fileManager: fileManager
4254
)
4355
}
4456

@@ -72,31 +84,43 @@ public final class DiagnosticConsoleWriter: DiagnosticFormattingConsumer {
7284
private static func makeDiagnosticFormatter(
7385
_ options: DiagnosticFormattingOptions,
7486
baseURL: URL?,
75-
highlight: Bool
87+
highlight: Bool,
88+
fileManager: FileManagerProtocol
7689
) -> DiagnosticConsoleFormatter {
7790
if options.contains(.formatConsoleOutputForTools) {
7891
return IDEDiagnosticConsoleFormatter(options: options)
7992
} else {
80-
return DefaultDiagnosticConsoleFormatter(baseUrl: baseURL, highlight: highlight, options: options)
93+
return DefaultDiagnosticConsoleFormatter(baseUrl: baseURL, highlight: highlight, options: options, fileManager: fileManager)
8194
}
8295
}
8396
}
8497

8598
// MARK: Formatted descriptions
8699

87100
extension DiagnosticConsoleWriter {
88-
89101
public static func formattedDescription<Problems>(for problems: Problems, options: DiagnosticFormattingOptions = []) -> String where Problems: Sequence, Problems.Element == Problem {
90-
return problems.map { formattedDescription(for: $0, options: options) }.joined(separator: "\n")
102+
formattedDescription(for: problems, options: options, fileManager: FileManager.default)
103+
}
104+
@_spi(FileManagerProtocol)
105+
public static func formattedDescription<Problems>(for problems: Problems, options: DiagnosticFormattingOptions = [], fileManager: FileManagerProtocol) -> String where Problems: Sequence, Problems.Element == Problem {
106+
return problems.map { formattedDescription(for: $0, options: options, fileManager: fileManager) }.joined(separator: "\n")
91107
}
92108

93109
public static func formattedDescription(for problem: Problem, options: DiagnosticFormattingOptions = []) -> String {
94-
let diagnosticFormatter = makeDiagnosticFormatter(options, baseURL: nil, highlight: TerminalHelper.isConnectedToTerminal)
110+
formattedDescription(for: problem, options: options, fileManager: FileManager.default)
111+
}
112+
@_spi(FileManagerProtocol)
113+
public static func formattedDescription(for problem: Problem, options: DiagnosticFormattingOptions = [], fileManager: FileManagerProtocol = FileManager.default) -> String {
114+
let diagnosticFormatter = makeDiagnosticFormatter(options, baseURL: nil, highlight: TerminalHelper.isConnectedToTerminal, fileManager: fileManager)
95115
return diagnosticFormatter.formattedDescription(for: problem)
96116
}
97117

98118
public static func formattedDescription(for diagnostic: Diagnostic, options: DiagnosticFormattingOptions = []) -> String {
99-
let diagnosticFormatter = makeDiagnosticFormatter(options, baseURL: nil, highlight: TerminalHelper.isConnectedToTerminal)
119+
formattedDescription(for: diagnostic, options: options, fileManager: FileManager.default)
120+
}
121+
@_spi(FileManagerProtocol)
122+
public static func formattedDescription(for diagnostic: Diagnostic, options: DiagnosticFormattingOptions = [], fileManager: FileManagerProtocol) -> String {
123+
let diagnosticFormatter = makeDiagnosticFormatter(options, baseURL: nil, highlight: TerminalHelper.isConnectedToTerminal, fileManager: fileManager)
100124
return diagnosticFormatter.formattedDescription(for: diagnostic)
101125
}
102126
}
@@ -205,18 +229,21 @@ final class DefaultDiagnosticConsoleFormatter: DiagnosticConsoleFormatter {
205229
private let baseUrl: URL?
206230
private let highlight: Bool
207231
private var sourceLines: [URL: [String]] = [:]
232+
private var fileManager: FileManagerProtocol
208233

209234
/// The number of additional lines from the source file that should be displayed both before and after the diagnostic source line.
210235
private static let contextSize = 2
211236

212237
init(
213238
baseUrl: URL?,
214239
highlight: Bool,
215-
options: DiagnosticFormattingOptions
240+
options: DiagnosticFormattingOptions,
241+
fileManager: FileManagerProtocol
216242
) {
217243
self.baseUrl = baseUrl
218244
self.highlight = highlight
219245
self.options = options
246+
self.fileManager = fileManager
220247
}
221248

222249
func formattedDescription<Problems>(for problems: Problems) -> String where Problems: Sequence, Problems.Element == Problem {
@@ -360,7 +387,7 @@ extension DefaultDiagnosticConsoleFormatter {
360387
// Example:
361388
// 9 | A line outside the diagnostic range.
362389
// 10 + A line inside the diagnostic range.
363-
result.append("\n\(linePrefix) \(separator) \(highlightedSource)")
390+
result.append("\n\(linePrefix) \(separator) \(highlightedSource)".removingTrailingWhitespace())
364391

365392
var suggestionsPerColumn = [Int: [String]]()
366393

@@ -466,8 +493,11 @@ extension DefaultDiagnosticConsoleFormatter {
466493
}
467494

468495
// TODO: Add support for also getting the source lines from the symbol graph files.
469-
guard let content = try? String(contentsOf: url)
470-
else { return [] }
496+
guard let data = fileManager.contents(atPath: url.path),
497+
let content = String(data: data, encoding: .utf8)
498+
else {
499+
return []
500+
}
471501

472502
let lines = content.splitByNewlines
473503
sourceLines[url] = lines

0 commit comments

Comments
 (0)