Skip to content

Commit c66e840

Browse files
authored
Add option to write diagnostics to a file to provide more information to tools (#494)
* Deprecate using diagnostics as errors. The error formatting is implicitly tied to a specific diagnostic writer The diagnostics include more information that gets lost when they are raised as errors. * Update calls to deprecated diagnostic-as-error properties * Add "features.json" to indicate feature availability to other tools * Add diagnostic consumer that writes to a file rdar://105181169 * Update calls to deprecated diagnostic properties * Fix bug in preview test where some diagnostics were written twice * Fix issue where preview tests would assert if one encountered an error * Separate formatting of diagnostics for tools and for people * Rename 'formattedDescriptionFor(...)' to 'formattedDescription(...)' * Add tests for DiagnosticFileWriter * Use SemanticVersion type for DiagnosticFile version * Use dedicated diagnostic file severity type * Rename 'formattedDescription(_:)' to 'formattedDescription(for:)' * Document DiagnosticFileWriter API * Correct install location of features.json file * Correct install location of features.json file
1 parent c21f412 commit c66e840

File tree

45 files changed

+804
-210
lines changed

Some content is hidden

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

45 files changed

+804
-210
lines changed

Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift

Lines changed: 2 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
@@ -192,7 +192,7 @@ public struct ConvertService: DocumentationService {
192192

193193
guard conversionProblems.isEmpty else {
194194
throw ConvertServiceError.conversionError(
195-
underlyingError: conversionProblems.localizedDescription)
195+
underlyingError: DiagnosticConsoleWriter.formattedDescription(for: conversionProblems))
196196
}
197197

198198
let references: RenderReferenceStore?

Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import SymbolKit
1717
public typealias BasicDiagnostic = Diagnostic
1818

1919
/// A diagnostic explains a problem or issue that needs the end-user's attention.
20-
public struct Diagnostic: DescribedError {
21-
20+
public struct Diagnostic {
2221
/// The origin of the diagnostic, such as a file or process.
2322
public var source: URL?
2423

@@ -35,30 +34,21 @@ public struct Diagnostic: DescribedError {
3534
/// `org.swift.docc.SummaryContainsLink`
3635
public var identifier: String
3736

38-
/// Provides the short, localized abstract provided by ``localizedExplanation`` in plain text if an
39-
/// explanation is available.
40-
///
41-
/// At a bare minimum, all diagnostics must have at least one paragraph or sentence describing what the diagnostic is.
42-
public var localizedSummary: String
37+
/// A brief summary that describe the problem or issue.
38+
public var summary: String
39+
40+
@available(*, deprecated, renamed: "summary")
41+
public var localizedSummary: String {
42+
return summary
43+
}
4344

44-
/// Provides a markup document for this diagnostic in the end-user's most preferred language, the base language
45-
/// if one isn't available, or `nil` if no explanations are provided for this diagnostic's identifier.
46-
///
47-
/// - Note: All diagnostics *must have* an explanation. If a diagnostic can't be explained in plain language
48-
/// and easily understood by the reader, it should not be shown.
49-
///
50-
/// An explanation should have at least the following items:
51-
///
52-
/// - Document
53-
/// - Abstract: A summary paragraph; one or two sentences.
54-
/// - Discussion: A discussion of the situation and why it's interesting or a problem for the end-user.
55-
/// This discussion should implicitly justify the diagnostic's existence.
56-
/// - Heading, level 2, text: "Example"
57-
/// - Problem Example: Show an example of the problematic situation and highlight important areas.
58-
/// - Heading, level 2, text: "Solution"
59-
/// - Solution: Explain what the end-user needs to do to correct the problem in plain language.
60-
/// - Solution Example: Show the *Problem Example* as corrected and highlight the changes made.
61-
public var localizedExplanation: String?
45+
/// Additional details that explain the the problem or issue to the end-user in plain language.
46+
public var explanation: String?
47+
48+
@available(*, deprecated, renamed: "explanation")
49+
public var localizedExplanation: String? {
50+
return explanation
51+
}
6252

6353
/// Extra notes to tack onto the editor for additional information.
6454
///
@@ -79,8 +69,8 @@ public struct Diagnostic: DescribedError {
7969
self.severity = severity
8070
self.range = range
8171
self.identifier = identifier
82-
self.localizedSummary = summary
83-
self.localizedExplanation = explanation
72+
self.summary = summary
73+
self.explanation = explanation
8474
self.notes = notes
8575
}
8676
}
@@ -95,13 +85,19 @@ public extension Diagnostic {
9585
range?.offsetWithRange(docRange)
9686

9787
}
88+
}
89+
90+
// MARK: Deprecated
9891

92+
@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
93+
extension Diagnostic: DescribedError {
94+
@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
9995
var localizedDescription: String {
100-
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
96+
return DiagnosticConsoleWriter.formattedDescription(for: self)
10197
}
10298

103-
var errorDescription: String {
104-
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
99+
@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
100+
public var errorDescription: String {
101+
return DiagnosticConsoleWriter.formattedDescription(for: self)
105102
}
106103
}
107-

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,84 @@ public final class DiagnosticConsoleWriter: DiagnosticFormattingConsumer {
1717

1818
var outputStream: TextOutputStream
1919
public var formattingOptions: DiagnosticFormattingOptions
20+
private var diagnosticFormatter: DiagnosticConsoleFormatter
2021

2122
/// Creates a new instance of this class with the provided output stream and filter level.
2223
/// - Parameter stream: The output stream to which this instance will write.
2324
/// - Parameter filterLevel: Determines what diagnostics should be printed. This filter level is inclusive, i.e. if a level of ``DiagnosticSeverity/information`` is specified, diagnostics with a severity up to and including `.information` will be printed.
2425
@available(*, deprecated, message: "Use init(_:formattingOptions:) instead")
25-
public init(_ stream: TextOutputStream = LogHandle.standardError, filterLevel: DiagnosticSeverity = .warning) {
26-
outputStream = stream
27-
formattingOptions = []
26+
public convenience init(_ stream: TextOutputStream = LogHandle.standardError, filterLevel: DiagnosticSeverity = .warning) {
27+
self.init(stream, formattingOptions: [])
2828
}
2929

3030
/// Creates a new instance of this class with the provided output stream.
3131
/// - Parameter stream: The output stream to which this instance will write.
3232
public init(_ stream: TextOutputStream = LogHandle.standardError, formattingOptions options: DiagnosticFormattingOptions = []) {
3333
outputStream = stream
3434
formattingOptions = options
35+
diagnosticFormatter = Self.makeDiagnosticFormatter(options)
3536
}
3637

3738
public func receive(_ problems: [Problem]) {
38-
let text = Self.formattedDescriptionFor(problems, options: formattingOptions).appending("\n")
39+
// Add a newline after each formatter description, including the last one.
40+
let text = problems.map { diagnosticFormatter.formattedDescription(for: $0).appending("\n") }.joined()
3941
outputStream.write(text)
4042
}
43+
44+
public func finalize() throws {
45+
// The console writer writes each diagnostic as they are received.
46+
}
47+
48+
private static func makeDiagnosticFormatter(_ options: DiagnosticFormattingOptions) -> DiagnosticConsoleFormatter {
49+
if options.contains(.formatConsoleOutputForTools) {
50+
return IDEDiagnosticConsoleFormatter(options: options)
51+
} else {
52+
return DefaultDiagnosticConsoleFormatter(options: options)
53+
}
54+
}
4155
}
4256

4357
// MARK: Formatted descriptions
4458

4559
extension DiagnosticConsoleWriter {
4660

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")
61+
public static func formattedDescription<Problems>(for problems: Problems, options: DiagnosticFormattingOptions = []) -> String where Problems: Sequence, Problems.Element == Problem {
62+
return problems.map { formattedDescription(for: $0, options: options) }.joined(separator: "\n")
63+
}
64+
65+
public static func formattedDescription(for problem: Problem, options: DiagnosticFormattingOptions = []) -> String {
66+
let diagnosticFormatter = makeDiagnosticFormatter(options)
67+
return diagnosticFormatter.formattedDescription(for: problem)
4968
}
5069

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)
70+
public static func formattedDescription(for diagnostic: Diagnostic, options: DiagnosticFormattingOptions = []) -> String {
71+
let diagnosticFormatter = makeDiagnosticFormatter(options)
72+
return diagnosticFormatter.formattedDescription(for: diagnostic)
73+
}
74+
}
75+
76+
protocol DiagnosticConsoleFormatter {
77+
var options: DiagnosticFormattingOptions { get set }
78+
79+
func formattedDescription<Problems>(for problems: Problems) -> String where Problems: Sequence, Problems.Element == Problem
80+
func formattedDescription(for problem: Problem) -> String
81+
func formattedDescription(for diagnostic: Diagnostic) -> String
82+
}
83+
84+
extension DiagnosticConsoleFormatter {
85+
func formattedDescription<Problems>(for problems: Problems) -> String where Problems: Sequence, Problems.Element == Problem {
86+
return problems.map { formattedDescription(for: $0) }.joined(separator: "\n")
87+
}
88+
}
89+
90+
// MARK: IDE formatting
91+
92+
struct IDEDiagnosticConsoleFormatter: DiagnosticConsoleFormatter {
93+
var options: DiagnosticFormattingOptions
94+
95+
func formattedDescription(for problem: Problem) -> String {
96+
guard let source = problem.diagnostic.source else {
97+
return formattedDescription(for: problem.diagnostic)
5498
}
5599

56100
var description = formattedDiagnosticSummary(problem.diagnostic)
@@ -82,11 +126,11 @@ extension DiagnosticConsoleWriter {
82126
return description
83127
}
84128

85-
public static func formattedDescriptionFor(_ diagnostic: Diagnostic, options: DiagnosticFormattingOptions = []) -> String {
129+
public func formattedDescription(for diagnostic: Diagnostic) -> String {
86130
return formattedDiagnosticSummary(diagnostic) + formattedDiagnosticDetails(diagnostic)
87131
}
88132

89-
private static func formattedDiagnosticSummary(_ diagnostic: Diagnostic) -> String {
133+
private func formattedDiagnosticSummary(_ diagnostic: Diagnostic) -> String {
90134
var result = ""
91135

92136
if let range = diagnostic.range, let url = diagnostic.source {
@@ -95,23 +139,41 @@ extension DiagnosticConsoleWriter {
95139
result += "\(url.path): "
96140
}
97141

98-
result += "\(diagnostic.severity): \(diagnostic.localizedSummary)"
142+
result += "\(diagnostic.severity): \(diagnostic.summary)"
99143

100144
return result
101145
}
102146

103-
private static func formattedDiagnosticDetails(_ diagnostic: Diagnostic) -> String {
147+
private func formattedDiagnosticDetails(_ diagnostic: Diagnostic) -> String {
104148
var result = ""
105149

106-
if let explanation = diagnostic.localizedExplanation {
150+
if let explanation = diagnostic.explanation {
107151
result += "\n\(explanation)"
108152
}
109153

110154
if !diagnostic.notes.isEmpty {
111155
result += "\n"
112-
result += diagnostic.notes.map { $0.description }.joined(separator: "\n")
156+
result += diagnostic.notes.map { formattedDescription(for: $0) }.joined(separator: "\n")
113157
}
114158

115159
return result
116160
}
161+
162+
private func formattedDescription(for note: DiagnosticNote) -> String {
163+
let location = "\(note.source.path):\(note.range.lowerBound.line):\(note.range.lowerBound.column)"
164+
return "\(location): note: \(note.message)"
165+
}
166+
}
167+
168+
// FIXME: Improve the readability for diagnostics on the command line https://github.com/apple/swift-docc/issues/496
169+
struct DefaultDiagnosticConsoleFormatter: DiagnosticConsoleFormatter {
170+
var options: DiagnosticFormattingOptions
171+
172+
func formattedDescription(for problem: Problem) -> String {
173+
formattedDescription(for: problem.diagnostic)
174+
}
175+
176+
func formattedDescription(for diagnostic: Diagnostic) -> String {
177+
return IDEDiagnosticConsoleFormatter(options: options).formattedDescription(for: diagnostic)
178+
}
117179
}

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsumer.swift

Lines changed: 4 additions & 1 deletion
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
@@ -15,6 +15,9 @@ public protocol DiagnosticConsumer: AnyObject {
1515
/// Receive diagnostics encountered by a ``DiagnosticEngine``.
1616
/// - Parameter problems: The encountered diagnostics.
1717
func receive(_ problems: [Problem])
18+
19+
/// Inform the consumer that the engine has sent all diagnostics for this build.
20+
func finalize() throws
1821
}
1922

2023
/// A type that can format received diagnostics in way that's suitable for writing to a destination such as a file or `TextOutputStream`.

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticEngine.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ public final class DiagnosticEngine {
9595
}
9696
}
9797
}
98+
99+
public func finalize() {
100+
workQueue.async { [weak self] in
101+
// If the engine isn't around then return early
102+
guard let self = self else { return }
103+
for consumer in self.consumers.sync({ $0.values }) {
104+
try? consumer.finalize()
105+
}
106+
}
107+
}
98108

99109
/// Subscribes a given consumer to the diagnostics emitted by this engine.
100110
/// - Parameter consumer: The consumer to subscribe to this engine.

0 commit comments

Comments
 (0)