Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to write diagnostics to a file to provide more information to tools #494

Merged
merged 18 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f24a83b
Deprecate using diagnostics as errors.
d-ronnqvist Feb 28, 2023
00dc703
Update calls to deprecated diagnostic-as-error properties
d-ronnqvist Feb 28, 2023
9e021f5
Add "features.json" to indicate feature availability to other tools
d-ronnqvist Feb 28, 2023
4b3f5b8
Add diagnostic consumer that writes to a file
d-ronnqvist Feb 28, 2023
b3f1cf5
Update calls to deprecated diagnostic properties
d-ronnqvist Feb 28, 2023
7c81333
Fix bug in preview test where some diagnostics were written twice
d-ronnqvist Feb 28, 2023
05b6fb8
Fix issue where preview tests would assert if one encountered an error
d-ronnqvist Feb 28, 2023
8c199f6
Separate formatting of diagnostics for tools and for people
d-ronnqvist Mar 1, 2023
1ffd90b
Rename 'formattedDescriptionFor(...)' to 'formattedDescription(...)'
d-ronnqvist Mar 1, 2023
2978be8
Add tests for DiagnosticFileWriter
d-ronnqvist Mar 1, 2023
bcf2152
Merge branch 'main' into diagnostics-file
d-ronnqvist Mar 2, 2023
2fd49c6
Use SemanticVersion type for DiagnosticFile version
d-ronnqvist Mar 2, 2023
8258eff
Use dedicated diagnostic file severity type
d-ronnqvist Mar 2, 2023
326a931
Rename 'formattedDescription(_:)' to 'formattedDescription(for:)'
d-ronnqvist Mar 2, 2023
0d08bf6
Document DiagnosticFileWriter API
d-ronnqvist Mar 2, 2023
83151b1
Correct install location of features.json file
d-ronnqvist Mar 3, 2023
9b00e9d
Merge branch 'main' into diagnostics-file
d-ronnqvist Mar 3, 2023
74ae8f9
Correct install location of features.json file
d-ronnqvist Mar 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

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

See https://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -192,7 +192,7 @@ public struct ConvertService: DocumentationService {

guard conversionProblems.isEmpty else {
throw ConvertServiceError.conversionError(
underlyingError: conversionProblems.localizedDescription)
underlyingError: DiagnosticConsoleWriter.formattedDescription(for: conversionProblems))
}

let references: RenderReferenceStore?
Expand Down
58 changes: 27 additions & 31 deletions Sources/SwiftDocC/Infrastructure/Diagnostics/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import SymbolKit
public typealias BasicDiagnostic = Diagnostic

/// A diagnostic explains a problem or issue that needs the end-user's attention.
public struct Diagnostic: DescribedError {

public struct Diagnostic {
/// The origin of the diagnostic, such as a file or process.
public var source: URL?

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

/// Provides the short, localized abstract provided by ``localizedExplanation`` in plain text if an
/// explanation is available.
///
/// At a bare minimum, all diagnostics must have at least one paragraph or sentence describing what the diagnostic is.
public var localizedSummary: String
/// A brief summary that describe the problem or issue.
public var summary: String

@available(*, deprecated, renamed: "summary")
public var localizedSummary: String {
return summary
}

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

@available(*, deprecated, renamed: "explanation")
public var localizedExplanation: String? {
return explanation
}

/// Extra notes to tack onto the editor for additional information.
///
Expand All @@ -79,8 +69,8 @@ public struct Diagnostic: DescribedError {
self.severity = severity
self.range = range
self.identifier = identifier
self.localizedSummary = summary
self.localizedExplanation = explanation
self.summary = summary
self.explanation = explanation
self.notes = notes
}
}
Expand All @@ -95,13 +85,19 @@ public extension Diagnostic {
range?.offsetWithRange(docRange)

}
}

// MARK: Deprecated

@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
extension Diagnostic: DescribedError {
@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
var localizedDescription: String {
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
return DiagnosticConsoleWriter.formattedDescription(for: self)
}

var errorDescription: String {
return DiagnosticConsoleWriter.formattedDescriptionFor(self)
@available(*, deprecated, message: "Use 'DiagnosticConsoleWriter.formattedDescription(for:options:)' instead.")
public var errorDescription: String {
return DiagnosticConsoleWriter.formattedDescription(for: self)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,84 @@ public final class DiagnosticConsoleWriter: DiagnosticFormattingConsumer {

var outputStream: TextOutputStream
public var formattingOptions: DiagnosticFormattingOptions
private var diagnosticFormatter: DiagnosticConsoleFormatter

/// Creates a new instance of this class with the provided output stream and filter level.
/// - Parameter stream: The output stream to which this instance will write.
/// - 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.
@available(*, deprecated, message: "Use init(_:formattingOptions:) instead")
public init(_ stream: TextOutputStream = LogHandle.standardError, filterLevel: DiagnosticSeverity = .warning) {
outputStream = stream
formattingOptions = []
public convenience init(_ stream: TextOutputStream = LogHandle.standardError, filterLevel: DiagnosticSeverity = .warning) {
self.init(stream, formattingOptions: [])
}

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

public func receive(_ problems: [Problem]) {
let text = Self.formattedDescriptionFor(problems, options: formattingOptions).appending("\n")
// Add a newline after each formatter description, including the last one.
let text = problems.map { diagnosticFormatter.formattedDescription(for: $0).appending("\n") }.joined()
outputStream.write(text)
}

public func finalize() throws {
// The console writer writes each diagnostic as they are received.
}

private static func makeDiagnosticFormatter(_ options: DiagnosticFormattingOptions) -> DiagnosticConsoleFormatter {
if options.contains(.formatConsoleOutputForTools) {
return IDEDiagnosticConsoleFormatter(options: options)
} else {
return DefaultDiagnosticConsoleFormatter(options: options)
}
}
}

// MARK: Formatted descriptions

extension DiagnosticConsoleWriter {

public static func formattedDescriptionFor<Problems>(_ problems: Problems, options: DiagnosticFormattingOptions = []) -> String where Problems: Sequence, Problems.Element == Problem {
return problems.map { formattedDescriptionFor($0, options: options) }.joined(separator: "\n")
public static func formattedDescription<Problems>(for problems: Problems, options: DiagnosticFormattingOptions = []) -> String where Problems: Sequence, Problems.Element == Problem {
return problems.map { formattedDescription(for: $0, options: options) }.joined(separator: "\n")
}

public static func formattedDescription(for problem: Problem, options: DiagnosticFormattingOptions = []) -> String {
let diagnosticFormatter = makeDiagnosticFormatter(options)
return diagnosticFormatter.formattedDescription(for: problem)
}

public static func formattedDescriptionFor(_ problem: Problem, options: DiagnosticFormattingOptions = []) -> String {
guard let source = problem.diagnostic.source, options.contains(.showFixits) else {
return formattedDescriptionFor(problem.diagnostic)
public static func formattedDescription(for diagnostic: Diagnostic, options: DiagnosticFormattingOptions = []) -> String {
let diagnosticFormatter = makeDiagnosticFormatter(options)
return diagnosticFormatter.formattedDescription(for: diagnostic)
}
}

protocol DiagnosticConsoleFormatter {
var options: DiagnosticFormattingOptions { get set }

func formattedDescription<Problems>(for problems: Problems) -> String where Problems: Sequence, Problems.Element == Problem
func formattedDescription(for problem: Problem) -> String
func formattedDescription(for diagnostic: Diagnostic) -> String
}

extension DiagnosticConsoleFormatter {
func formattedDescription<Problems>(for problems: Problems) -> String where Problems: Sequence, Problems.Element == Problem {
return problems.map { formattedDescription(for: $0) }.joined(separator: "\n")
}
}

// MARK: IDE formatting

struct IDEDiagnosticConsoleFormatter: DiagnosticConsoleFormatter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Are we expecting diagnostics in this format to only be used by IDEs, or are there broader applications? If so, maybe there is a term that describes what the format provides rather than what we expect to use it for. For example "RichDiagnosticConsoleFormater

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intended to be parsed by IDEs and other tools or scripts who are interacting with the docc executable. The format (which is the same as today) contains some general information about the diagnostics in an easily parseable format but doesn't provide the full diagnostic information.

I think once we improve the default diagnostic format (#496) there shouldn't be many use cases for this format other than tools/scripts who don't need the full details of the diagnostic file.

var options: DiagnosticFormattingOptions

func formattedDescription(for problem: Problem) -> String {
guard let source = problem.diagnostic.source else {
return formattedDescription(for: problem.diagnostic)
}

var description = formattedDiagnosticSummary(problem.diagnostic)
Expand Down Expand Up @@ -82,11 +126,11 @@ extension DiagnosticConsoleWriter {
return description
}

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

private static func formattedDiagnosticSummary(_ diagnostic: Diagnostic) -> String {
private func formattedDiagnosticSummary(_ diagnostic: Diagnostic) -> String {
var result = ""

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

result += "\(diagnostic.severity): \(diagnostic.localizedSummary)"
result += "\(diagnostic.severity): \(diagnostic.summary)"

return result
}

private static func formattedDiagnosticDetails(_ diagnostic: Diagnostic) -> String {
private func formattedDiagnosticDetails(_ diagnostic: Diagnostic) -> String {
var result = ""

if let explanation = diagnostic.localizedExplanation {
if let explanation = diagnostic.explanation {
result += "\n\(explanation)"
}

if !diagnostic.notes.isEmpty {
result += "\n"
result += diagnostic.notes.map { $0.description }.joined(separator: "\n")
result += diagnostic.notes.map { formattedDescription(for: $0) }.joined(separator: "\n")
}

return result
}

private func formattedDescription(for note: DiagnosticNote) -> String {
let location = "\(note.source.path):\(note.range.lowerBound.line):\(note.range.lowerBound.column)"
return "\(location): note: \(note.message)"
}
}

// FIXME: Improve the readability for diagnostics on the command line https://github.com/apple/swift-docc/issues/496
struct DefaultDiagnosticConsoleFormatter: DiagnosticConsoleFormatter {
var options: DiagnosticFormattingOptions

func formattedDescription(for problem: Problem) -> String {
formattedDescription(for: problem.diagnostic)
}

func formattedDescription(for diagnostic: Diagnostic) -> String {
return IDEDiagnosticConsoleFormatter(options: options).formattedDescription(for: diagnostic)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

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

See https://swift.org/LICENSE.txt for license information
Expand All @@ -15,6 +15,9 @@ public protocol DiagnosticConsumer: AnyObject {
/// Receive diagnostics encountered by a ``DiagnosticEngine``.
/// - Parameter problems: The encountered diagnostics.
func receive(_ problems: [Problem])

/// Inform the consumer that the engine has sent all diagnostics for this build.
func finalize() throws
}

/// A type that can format received diagnostics in way that's suitable for writing to a destination such as a file or `TextOutputStream`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ public final class DiagnosticEngine {
}
}
}

public func finalize() {
workQueue.async { [weak self] in
// If the engine isn't around then return early
guard let self = self else { return }
for consumer in self.consumers.sync({ $0.values }) {
try? consumer.finalize()
}
}
}

/// Subscribes a given consumer to the diagnostics emitted by this engine.
/// - Parameter consumer: The consumer to subscribe to this engine.
Expand Down
Loading