Skip to content

Commit 70d8cc1

Browse files
authored
Include suggested solution messages in main diagnostic message when writing diagnostics to console (#492)
* Fix warnings about unused variables in unit test * Rename TopicReferenceResolutionError to ...ErrorInfo - Add public member-wise initializer - Remove DescribedError conformance * Include disambiguation suffix in solution summary * Describe what's being replaced in near-miss link error messages. rdar://105985393 * Don't drop solution messages when writing diagnostics to console rdar://105985393 * Fix minor typo in unit test names * Resolve CI build error (unable to infer complex closure type)
1 parent 94d8aca commit 70d8cc1

17 files changed

+344
-201
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift

Lines changed: 3 additions & 22 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-2023 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
@@ -97,30 +97,11 @@ public extension Diagnostic {
9797
}
9898

9999
var localizedDescription: String {
100-
var result = ""
101-
102-
if let range = range, let url = self.source {
103-
result += "\(url.path):\(range.lowerBound.line):\(range.lowerBound.column): "
104-
} else if let url = self.source {
105-
result += "\(url.path): "
106-
}
107-
108-
result += "\(severity): \(localizedSummary)"
109-
110-
if let explanation = localizedExplanation {
111-
result += "\n\(explanation)"
112-
}
113-
114-
if !notes.isEmpty {
115-
result += "\n"
116-
result += notes.map { $0.description }.joined(separator: "\n")
117-
}
118-
119-
return result
100+
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
120101
}
121102

122103
var errorDescription: String {
123-
return localizedDescription
104+
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
124105
}
125106
}
126107

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 78 additions & 2 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-2023 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
@@ -35,7 +35,83 @@ public final class DiagnosticConsoleWriter: DiagnosticFormattingConsumer {
3535
}
3636

3737
public func receive(_ problems: [Problem]) {
38-
let text = problems.formattedLocalizedDescription(withOptions: formattingOptions).appending("\n")
38+
let text = Self.formattedDescriptionFor(problems, options: formattingOptions).appending("\n")
3939
outputStream.write(text)
4040
}
4141
}
42+
43+
// MARK: Formatted descriptions
44+
45+
extension DiagnosticConsoleWriter {
46+
47+
public static func formattedDescriptionFor<Problems>(_ problems: Problems, options: DiagnosticFormattingOptions = []) -> String where Problems: Sequence, Problems.Element == Problem {
48+
return problems.map { formattedDescriptionFor($0, options: options) }.joined(separator: "\n")
49+
}
50+
51+
public static func formattedDescriptionFor(_ problem: Problem, options: DiagnosticFormattingOptions = []) -> String {
52+
guard let source = problem.diagnostic.source, options.contains(.showFixits) else {
53+
return formattedDescriptionFor(problem.diagnostic)
54+
}
55+
56+
var description = formattedDiagnosticSummary(problem.diagnostic)
57+
58+
// Since solution summaries aren't included in the fixit string we include them in the diagnostic
59+
// summary so that the solution information isn't dropped.
60+
61+
if !problem.possibleSolutions.isEmpty, description.last?.isPunctuation == false {
62+
description += "."
63+
}
64+
for solution in problem.possibleSolutions {
65+
description += " \(solution.summary)"
66+
if description.last?.isPunctuation == false {
67+
description += "."
68+
}
69+
}
70+
71+
// Add explanations and notes
72+
description += formattedDiagnosticDetails(problem.diagnostic)
73+
74+
// Only one fixit (but multiple related replacements) can be a presented with each diagnostic
75+
if problem.possibleSolutions.count == 1, let solution = problem.possibleSolutions.first {
76+
description += solution.replacements.reduce(into: "") { accumulation, replacement in
77+
let range = replacement.range
78+
accumulation += "\n\(source.path):\(range.lowerBound.line):\(range.lowerBound.column)-\(range.upperBound.line):\(range.upperBound.column): fixit: \(replacement.replacement)"
79+
}
80+
}
81+
82+
return description
83+
}
84+
85+
public static func formattedDescriptionFor(_ diagnostic: Diagnostic, options: DiagnosticFormattingOptions = []) -> String {
86+
return formattedDiagnosticSummary(diagnostic) + formattedDiagnosticDetails(diagnostic)
87+
}
88+
89+
private static func formattedDiagnosticSummary(_ diagnostic: Diagnostic) -> String {
90+
var result = ""
91+
92+
if let range = diagnostic.range, let url = diagnostic.source {
93+
result += "\(url.path):\(range.lowerBound.line):\(range.lowerBound.column): "
94+
} else if let url = diagnostic.source {
95+
result += "\(url.path): "
96+
}
97+
98+
result += "\(diagnostic.severity): \(diagnostic.localizedSummary)"
99+
100+
return result
101+
}
102+
103+
private static func formattedDiagnosticDetails(_ diagnostic: Diagnostic) -> String {
104+
var result = ""
105+
106+
if let explanation = diagnostic.localizedExplanation {
107+
result += "\n\(explanation)"
108+
}
109+
110+
if !diagnostic.notes.isEmpty {
111+
result += "\n"
112+
result += diagnostic.notes.map { $0.description }.joined(separator: "\n")
113+
}
114+
115+
return result
116+
}
117+
}

Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift

Lines changed: 3 additions & 15 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-2023 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
@@ -31,23 +31,11 @@ public struct Problem {
3131

3232
extension Problem {
3333
var localizedDescription: String {
34-
return formattedLocalizedDescription(withOptions: [])
34+
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
3535
}
3636

3737
func formattedLocalizedDescription(withOptions options: DiagnosticFormattingOptions = []) -> String {
38-
let description = diagnostic.localizedDescription
39-
40-
guard let source = diagnostic.source, options.contains(.showFixits) else {
41-
return description
42-
}
43-
44-
let fixitString = possibleSolutions.reduce("", { string, solution -> String in
45-
return solution.replacements.reduce(string, {
46-
$0 + "\n\(source.path):\($1.range.lowerBound.line):\($1.range.lowerBound.column)-\($1.range.upperBound.line):\($1.range.upperBound.column): fixit: \($1.replacement)"
47-
})
48-
})
49-
50-
return description + fixitString
38+
return DiagnosticConsoleWriter.formattedDescriptionFor(self, options: options)
5139
}
5240
}
5341

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
123123
do {
124124
guard let unresolvedTopicURL = unresolvedReference.topicURL.components.url else {
125125
// Return the unresolved reference if the underlying URL is not valid
126-
return .failure(unresolvedReference, TopicReferenceResolutionError("URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid."))
126+
return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid."))
127127
}
128128
let metadata = try resolveInformationForTopicURL(unresolvedTopicURL)
129129
// Don't do anything with this URL. The external URL will be resolved during conversion to render nodes
@@ -136,7 +136,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
136136
)
137137
)
138138
} catch let error {
139-
return .failure(unresolvedReference, TopicReferenceResolutionError(error))
139+
return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo(error))
140140
}
141141
}
142142
}

Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift

Lines changed: 4 additions & 4 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) 2022 Apple Inc. and the Swift project authors
4+
Copyright (c) 2022-2023 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
@@ -129,7 +129,7 @@ final class DocumentationCacheBasedLinkResolver {
129129
// Ensure we are resolving either relative links or "doc:" scheme links
130130
guard unresolvedReference.topicURL.url.scheme == nil || ResolvedTopicReference.urlHasResolvedTopicScheme(unresolvedReference.topicURL.url) else {
131131
// Not resolvable in the topic graph
132-
return .failure(unresolvedReference, TopicReferenceResolutionError("Reference URL \(unresolvedReference.description.singleQuoted) doesn't have \"doc:\" scheme."))
132+
return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("Reference URL \(unresolvedReference.description.singleQuoted) doesn't have \"doc:\" scheme."))
133133
}
134134

135135
// Fall back on the parent's bundle identifier for relative paths
@@ -267,7 +267,7 @@ final class DocumentationCacheBasedLinkResolver {
267267
// Return the successful or failed externally resolved reference.
268268
return resolvedExternalReference
269269
} else if !context.registeredBundles.contains(where: { $0.identifier == bundleID }) {
270-
return .failure(unresolvedReference, TopicReferenceResolutionError("No external resolver registered for \(bundleID.singleQuoted)."))
270+
return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("No external resolver registered for \(bundleID.singleQuoted)."))
271271
}
272272
}
273273

@@ -295,7 +295,7 @@ final class DocumentationCacheBasedLinkResolver {
295295
// Give up: there is no local or external document for this reference.
296296

297297
// External references which failed to resolve will already have returned a more specific error message.
298-
return .failure(unresolvedReference, TopicReferenceResolutionError("No local documentation matches this reference."))
298+
return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("No local documentation matches this reference."))
299299
}
300300

301301

0 commit comments

Comments
 (0)